MVVM in Blazor
Blazor's component model (a .razor file combining markup and a @code block) doesn't enforce MVVM the way WPF's XAML/DataContext pairing does, but the pattern still applies well: a Blazor component can hold a reference to a ViewModel instance (often injected via DI or created in OnInitializedAsync), bind its markup to the ViewModel's properties, and delegate button clicks to ViewModel methods, keeping business logic out of the component's @code block just as it would be kept out of WPF code-behind. Because Blazor components already re-render reactively when their own state changes (via StateHasChanged), a common approach is for the ViewModel to implement INotifyPropertyChanged and the component to subscribe to PropertyChanged in OnInitialized, calling StateHasChanged() whenever the ViewModel raises it, bridging the ViewModel's notification model into Blazor's rendering model explicitly since Blazor has no built-in binding engine like WPF's.
Cricket analogy: It's like a broadcast director cutting to a specific camera the instant the scorer's system flags a boundary — the underlying camera feed (component render) reacts to an explicit trigger from the scoring system (PropertyChanged), rather than continuously refreshing itself on a fixed schedule.
Binding Syntax and Component Lifecycle
Blazor's @bind directive provides one-way and two-way binding syntax similar in spirit to XAML — '@bind-Value="ViewModel.OrderNumber"' on an InputText component keeps the input and the ViewModel property synchronized — but it operates at the component-render level rather than through a persistent binding object, meaning the value is read fresh on each render and written back on the relevant DOM event (oninput or onchange). Component lifecycle methods (OnInitializedAsync for setup, OnParametersSetAsync when parent-supplied parameters change, OnAfterRenderAsync for post-render DOM interaction, IDisposable.Dispose for cleanup) replace WPF's page-navigation lifecycle hooks, and it's in OnInitializedAsync that a component typically resolves its ViewModel from DI and kicks off an initial data load, then in Dispose that it unsubscribes from the ViewModel's PropertyChanged to avoid the same leaked-subscriber problem messaging systems face.
Cricket analogy: It's like a stadium's giant screen refreshing the batsman's strike rate the instant a new ball is bowled and the run total changes, recalculating fresh each time rather than maintaining some persistent live wire to the scorer's pencil.
@page "/orders/{OrderId:int}"
@inject IOrderService OrderService
@implements IDisposable
<h3>Order @ViewModel.OrderNumber</h3>
<InputText @bind-Value="ViewModel.OrderNumber" />
<button class="btn btn-primary" @onclick="ViewModel.SaveCommand.Execute">
Save
</button>
@code {
[Parameter] public int OrderId { get; set; }
private OrderViewModel ViewModel { get; set; } = default!;
protected override async Task OnInitializedAsync()
{
ViewModel = new OrderViewModel(OrderService);
ViewModel.PropertyChanged += (_, _) => InvokeAsync(StateHasChanged);
await ViewModel.LoadOrderAsync(OrderId);
}
public void Dispose()
{
ViewModel.PropertyChanged -= (_, _) => InvokeAsync(StateHasChanged);
}
}Blazor Server vs. WebAssembly Considerations
MVVM ViewModels behave slightly differently depending on Blazor's hosting model: in Blazor Server, the component and its ViewModel live on the server and every StateHasChanged triggers a diff sent over a persistent SignalR connection to the browser, so a ViewModel holding a large in-memory collection is relatively cheap for the client but adds server memory pressure per connected user and requires care around thread-affinity (UI updates from background threads must be marshaled via InvokeAsync). In Blazor WebAssembly, the ViewModel runs entirely client-side inside the browser's WASM runtime, so there's no SignalR round-trip for UI updates, but network calls to a backend API are genuinely async over HTTP, and dependency injection is configured once at WASM host startup rather than per-circuit as it effectively is in Blazor Server, which matters when deciding whether a service should be Scoped (per-user-circuit in Server) or effectively Singleton (per-browser-tab in WebAssembly).
Cricket analogy: It's like the difference between a stadium's live TV broadcast (Server model, where the production truck does all the work and sends a finished feed to your home) versus recording the match on a personal device and replaying it locally (WebAssembly, where all the processing happens on your own hardware) — same match, very different resource distribution.
In Blazor Server, never mutate ViewModel state or call StateHasChanged from a background thread (e.g., inside a Task.Run continuation or a timer callback) without wrapping the call in InvokeAsync — the SignalR circuit's rendering is not thread-safe, and doing so directly risks exceptions or corrupted UI state.
- Blazor has no built-in binding engine like WPF's, so bridging a ViewModel's PropertyChanged into StateHasChanged must be done explicitly.
- The @bind directive synchronizes a component's markup with a ViewModel property, reading and writing fresh on each relevant DOM event.
- Blazor's component lifecycle (OnInitializedAsync, OnParametersSetAsync, Dispose) replaces WPF's page-navigation hooks for ViewModel setup and teardown.
- Always unsubscribe from PropertyChanged in Dispose to avoid leaked subscriptions, mirroring the same problem seen in messaging systems.
- Blazor Server runs ViewModels on the server over a SignalR circuit; Blazor WebAssembly runs them entirely client-side in the browser.
- UI updates from background threads in Blazor Server must be marshaled via InvokeAsync since the rendering pipeline isn't thread-safe.
- Service lifetime choices (Scoped vs Singleton) mean different things per hosting model: per-circuit in Server, effectively per-tab in WebAssembly.
Practice what you learned
1. Why must a Blazor component explicitly subscribe to a ViewModel's PropertyChanged event and call StateHasChanged?
2. What does the @bind directive do in a Blazor component?
3. Which Blazor lifecycle method is most analogous to where a ViewModel is typically resolved and initial data loading begins?
4. In Blazor Server, why must UI updates triggered from a background thread be wrapped in InvokeAsync?
5. How does service lifetime semantics differ between Blazor Server and Blazor WebAssembly?
Was this page helpful?
You May Also Like
Navigation in MVVM
How MVVM apps navigate between screens through an INavigationService abstraction, pass parameters and receive results, and support deep linking without ViewModels ever referencing Views.
MVVM in WPF
How WPF's data binding engine, INotifyPropertyChanged, ICommand, and value converters combine to deliver the canonical MVVM implementation the pattern was originally designed for.
Dependency Injection in MVVM
How MVVM ViewModels receive their collaborators — data services, navigation, dialogs — via constructor injection instead of creating them directly, and why that makes apps testable and swappable.
Related Reading
Related Study Notes in Microsoft Technologies
Browse all study notesWindows 10 / UWP Development Study Notes
.NET · 30 topics
Microsoft TechnologiesWindows Batch Scripting Study Notes
Batch · 30 topics
Microsoft TechnologiesMFC (Microsoft Foundation Classes) Study Notes
C++ · 30 topics
Microsoft TechnologiesSilverlight Study Notes
.NET · 30 topics
Microsoft TechnologiesXAML Study Notes
.NET · 30 topics
Microsoft TechnologiesWPF (Windows Presentation Foundation) Study Notes
.NET · 30 topics