ModelingEvolution.Observable.Blazor 1.1.0

dotnet add package ModelingEvolution.Observable.Blazor --version 1.1.0
                    
NuGet\Install-Package ModelingEvolution.Observable.Blazor -Version 1.1.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="ModelingEvolution.Observable.Blazor" Version="1.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="ModelingEvolution.Observable.Blazor" Version="1.1.0" />
                    
Directory.Packages.props
<PackageReference Include="ModelingEvolution.Observable.Blazor" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add ModelingEvolution.Observable.Blazor --version 1.1.0
                    
#r "nuget: ModelingEvolution.Observable.Blazor, 1.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package ModelingEvolution.Observable.Blazor@1.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=ModelingEvolution.Observable.Blazor&version=1.1.0
                    
Install as a Cake Addin
#tool nuget:?package=ModelingEvolution.Observable.Blazor&version=1.1.0
                    
Install as a Cake Tool

Minimal MVVM for Blazor

  • Observable
  • ObservableForEach

ObservableForEach: keeping item identity (Key)

By default ObservableForEach renders its loop body unkeyed, so Blazor matches items by position. If the collection slides — items dropped at the front, appended at the back — every surviving item lands at a new index and its subtree is re-hosted onto a different item. In a browser that tears down and rebuilds the whole list: a text selection inside a surviving row is lost and keyboard focus drops to the document body.

Set Key to match items by identity instead:

<ObservableForEach ItemSource="@Vm.Messages" Key="@(m => m.Id)" IsNotifyPropertyChangedEnabled="true">
    <MessageCard Model="@context" />
</ObservableForEach>
  • Survivors keep their component instance and DOM nodes across inserts, removals, moves, a snapshot swap and a filter change; only items that actually entered or left are created or disposed.
  • The keys must be unique among the rendered items (after Filter) — see Duplicate keys below.
  • Leaving Key unset renders exactly as before.
  • Cost: see What keying costs — free in the notify branch, not free in the plain one.
  • A @key written inside the child content does not work: an invoked RenderFragment<TItem> is emitted inside a render-tree region, and Blazor only matches keys among direct siblings, so such a key never matches across a positional shift. The key has to be applied by ObservableForEach.

Duplicate keys: an error naming a type you never wrote

If two rendered items produce the same key, Blazor throws InvalidOperationException, and the message names a component from this library rather than anything in your code:

More than one sibling of component 'ModelingEvolution.Observable.Blazor.KeyedItem'
has the same key value, 'dup'. Key values must be unique.
More than one sibling of component
'ModelingEvolution.Observable.Blazor.Observable`1[YourApp.RowVm]'
has the same key value, 'dup'. Key values must be unique.

(The first is the plain branch, the second the IsNotifyPropertyChangedEnabled branch.) It is not a bug in the library. It means two items in your ItemSource returned equal values from your Key, and the quoted key value tells you which.

Picking a key for rows that are records or structs: record types have value equality, so Key="@(r => r)" — or any key built only from the displayed fields — makes two rows that happen to hold the same data collide, and a screen that renders fine today starts throwing. Key on something guaranteed unique per row instead:

@* good: a real identity *@
<ObservableForEach ItemSource="@Vm.Invoices" Key="@(i => i.InvoiceId)"> ... </ObservableForEach>

@* also fine: a composite that is unique by construction *@
<ObservableForEach ItemSource="@Vm.Lines" Key="@(l => (l.DocumentId, l.LineNo))"> ... </ObservableForEach>

@* bad: two lines with the same amount now collide *@
<ObservableForEach ItemSource="@Vm.Lines" Key="@(l => l.Amount)"> ... </ObservableForEach>

If your rows genuinely have no unique identity, leave Key unset — unkeyed rendering never throws.

Do not refresh by clearing in place

This is the commonest refresh idiom, and Key does nothing for it:

_items.Clear();                       // raises Reset -> renders an EMPTY list, disposes every row
foreach (var x in fresh) _items.Add(x);   // rows are rebuilt from scratch

ObservableCollection.Clear() raises Reset by itself. The component re-renders at that moment with an empty list and disposes every row; only then do the re-added items arrive. No key can preserve a row across a render in which the row was not present — so a screen that adopts Key while clearing in place gets no benefit and no error telling it why.

Either assign a new collection:

var next = new ObservableCollection<RowVm>();
foreach (var x in fresh) next.Add(x);
Items = next;              // ItemSource changes once; rows present in both keep their identity

or mutate the existing collection in place (add, remove, move the individual rows that changed) so no Reset is raised at all. Both are covered by tests in this repo.

What keying costs

With Key unset you pay nothing. No KeyedItem is ever constructed, in either branch.

In the notify branch (IsNotifyPropertyChangedEnabled="true") keying is free. The key goes on the <Observable> wrapper the component already renders, so no component is added.

In the plain branch it is not free. Keying adds one KeyedItem per row, and at a thousand rows that is visible on first render:

1000 rows, first render, plain branch time allocations
unkeyed 15 ms 3,362 KB
keyed 46 ms 5,864 KB

Roughly 3× the time and 1.7× the allocations — and it buys the opposite on every subsequent update, because surviving rows are no longer torn down and rebuilt. So keying pays for lists that change — a feed, a sliding window, a grid the user filters and sorts — and is not worth it for a large table that renders once and sits still.

Those numbers are one measurement, on one machine, under bUnit rather than a browser. Treat them as a guide to the shape of the trade-off, not as a guarantee; if it matters at your row counts, measure your own screen.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (7)

Showing the top 5 NuGet packages that depend on ModelingEvolution.Observable.Blazor:

Package Downloads
ModelingEvolution.Ide.Blazor

Package Description

MicroPlumberd.Services.Identity.Blazor

Blazor components for MicroPlumberd.Services.Identity - User and Role management UI

ModelingEvolution.Blazor.LogViewer

Reusable Blazor component for viewing logs from processes and Docker containers in real-time using XtermBlazor terminal.

ModelingEvolution.WeldingMachine.Fronius.Ui

Fronius welder controls (View + ViewModel) for TPS 5000 and iWave. Consumed verbatim by rocket-welder2 and the App test-host.

ModelingEvolution.FileSystem.Blazor

Blazor FileExplorer component with MudBlazor for browsing local file systems. Uses IFileSystem abstraction for all operations.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.1.0 79 9/19/2026
1.0.0 247 8/24/2026
0.0.13 107 8/24/2026
0.0.12 243 8/12/2026
0.0.11 206 7/18/2026
0.0.10 1,603 3/24/2026
0.0.9 206 3/3/2026
0.0.8.2 173 2/18/2026
0.0.8.1 132 2/18/2026
0.0.8 205 2/18/2026
0.0.7 158 2/8/2026
0.0.6.6 142 2/7/2026
0.0.6.5 137 2/7/2026
0.0.6.4 496 1/14/2026
0.0.5.4 2,721 4/30/2024
0.0.4.4 199 4/30/2024
0.0.2.4 208 4/30/2024