💡 Introduction
The Model-View-ViewModel (MVVM) pattern has been around since the early days of WPF, and yet, decades later, many developers still misunderstand its real purpose. It's not a WPF-exclusive magic trick. MVVM is a UI architecture pattern — and like all patterns, it's portable.
Source code :
GitHub Repo: MVVM-WPF-BLAZOR-WINFORMS
Source code :
GitHub Repo: WPF Clean Architecture - Master Detail App
This is a fully working WPF Master-Details application built with CommunityToolkit.Mvvm. It features:
✅ An editable DataGrid
✅ Clean MVVM architecture
✅ Repository pattern for data access
✅ Async-ready CRUD operations (New, Save, Delete)
✅ Selection handling with detail binding
✅ ViewModel-first approach, testable and scalable
This solution also contains the foundation for future extensions in Blazor and WinForms, following the same MVVM logic.
In the next session, we’ll walk through the entire application step by step — from project setup to advanced interactions like validation, nested ViewModels, and UI enhancements.
In this article, we’ll:
Clarify what MVVM really is.
- Show why MVVM still makes sense in modern projects.
- Debunk myths that MVVM only applies to WPF.
- See how MVVM is useful in Blazor and even WinForms.
📀 What is MVVM (Really)?
MVVM separates your application into three distinct layers:
- Model – The business logic and data.
- View – The UI (buttons, textboxes, grids).
- ViewModel – A binding-friendly abstraction that connects the View with the Model.
The key feature? Binding and separation of concerns.
🧐 Why Do People Still Think MVVM = WPF?
Because WPF made MVVM famous. With its powerful data binding and command system, WPF was a natural fit. Frameworks like Prism, Caliburn.Micro, and MVVM Light reinforced that.
But now we’re seeing new UI platforms — like Blazor — and old ones like WinForms still in use. And here’s the truth:
MVVM is not tied to any UI platform. It’s a way of thinking.
🎲 MVVM in WPF — The Classic Example
Let’s build a basic counter app using MVVM in WPF.
ViewModel
public class CounterViewModel : INotifyPropertyChanged
{
private int _count;
public int Count
{
get => _count;
set { _count = value; OnPropertyChanged(); }
}
public ICommand IncrementCommand { get; }
public CounterViewModel()
{
IncrementCommand = new RelayCommand(_ => Count++);
}
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
RelayCommand Helper
public class RelayCommand : ICommand
{
private readonly Action<object?> _execute;
private readonly Func<object?, bool>? _canExecute;
public RelayCommand(Action<object?> execute, Func<object?, bool>? canExecute = null)
{
_execute = execute;
_canExecute = canExecute;
}
public bool CanExecute(object? parameter) => _canExecute?.Invoke(parameter) ?? true;
public void Execute(object? parameter) => _execute(parameter);
public event EventHandler? CanExecuteChanged;
}
View (XAML)
<Window x:Class="WpfAppMVVM.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfAppMVVM"
mc:Ignorable="d"
DataContext="CounterViewModel"
Title="MainWindow" Height="450" Width="800">
<Grid>
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
<TextBlock Text="{Binding Count}" FontSize="24" HorizontalAlignment="Center" />
<Button Content="Increment"
Command="{Binding IncrementCommand}"
Margin="0,10,0,0"
Width="100" />
</StackPanel>
</Grid>
</Window>
🎲 MVVM in WPF — using CommunityToolkit.Mvvm
✅ 3. App.xaml.cs – HostBuilder with DI
public partial class App : Application
{
public static IHost AppHost { get; private set; } = null!;
public App()
{
AppHost = Host.CreateDefaultBuilder()
.ConfigureServices((context, services) =>
{
// Register services
services.AddSingleton<ICustomerRepository, InMemoryCustomerRepository>();
// Register viewmodels
services.AddTransient<CustomerGridViewModel>();
// Register windows
services.AddTransient<MainWindow>();
})
.Build();
}
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
AppHost.Start();
// Resolve and show the main window
var mainWindow = AppHost.Services.GetRequiredService<MainWindow>();
mainWindow.Show();
}
protected override void OnExit(ExitEventArgs e)
{
base.OnExit(e);
AppHost.StopAsync().GetAwaiter().GetResult();
AppHost.Dispose();
}
✅ 4. MainWindow.xaml.cs
public partial class MainWindow : Window
{
public MainWindow(CustomerGridViewModel vm)
{
InitializeComponent();
DataContext = vm;
}
}
✅ 5. MainWindow.xaml
<Window x:Class="WpfAppCommunityMVVM.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Customers" Height="400" Width="600">
<DockPanel Margin="10">
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="0,0,0,10">
<Button Content="New" Command="{Binding NewCommand}" Margin="0,0,5,0" />
<Button Content="Save" Command="{Binding SaveCommand}" Margin="0,0,5,0" />
<Button Content="Delete" Command="{Binding DeleteCommand}" />
</StackPanel>
<DataGrid ItemsSource="{Binding Customers}"
SelectedItem="{Binding SelectedCustomer, Mode=TwoWay}"
AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Name" Binding="{Binding Name}" Width="*" />
<DataGridTextColumn Header="Email" Binding="{Binding Email}" Width="*" />
</DataGrid.Columns>
</DataGrid>
</DockPanel>
</Window>
✅ 6. ViewModel
public partial class CustomerGridViewModel : ObservableObject
{
private readonly ICustomerRepository _repository;
public CustomerGridViewModel(ICustomerRepository repository)
{
_repository = repository;
Customers = new();
LoadCommand.Execute(null);
}
[ObservableProperty]
private ObservableCollection<Customer> customers;
[ObservableProperty]
private Customer? selectedCustomer;
[RelayCommand]
private async Task LoadAsync()
{
var list = await _repository.GetAllAsync();
Customers = new ObservableCollection<Customer>(list);
}
[RelayCommand]
private void New()
{
var newCustomer = new Customer();
Customers.Add(newCustomer);
SelectedCustomer = newCustomer;
}
[RelayCommand]
private async Task SaveAsync()
{
if (SelectedCustomer == null) return;
if (SelectedCustomer.Id == 0)
await _repository.AddAsync(SelectedCustomer);
else
await _repository.UpdateAsync(SelectedCustomer);
}
[RelayCommand]
private async Task DeleteAsync()
{
if (SelectedCustomer == null || SelectedCustomer.Id == 0) return;
await _repository.DeleteAsync(SelectedCustomer.Id);
Customers.Remove(SelectedCustomer);
SelectedCustomer = null;
}
}
🚄 MVVM in WinForms
While WinForms lacks strong data binding support compared to WPF, MVVM is still possible. Here's a simplified version using a BindingSource.
ViewModel
public class CounterViewModel : INotifyPropertyChanged
{
private int _count;
public int Count
{
get => _count;
set { _count = value; OnPropertyChanged(); }
}
public void Increment() => Count++;
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string name = "") =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
WinForms View Code (Form1.cs)
public partial class Form1 : Form
{
private readonly CounterViewModel _viewModel = new();
public Form1()
{
InitializeComponent();
var bindingSource = new BindingSource { DataSource = _viewModel };
labelCount.DataBindings.Add("Text", bindingSource, "Count", true, DataSourceUpdateMode.OnPropertyChanged);
buttonIncrement.Click += (_, __) => _viewModel.Increment();
}
}
🛠️ MVVM in Blazor
Blazor supports data binding and component-based UI, which makes MVVM a natural fit.
ViewModel
public class CounterViewModel : INotifyPropertyChanged
{
private int _count;
public int Count
{
get => _count;
set { _count = value; OnPropertyChanged(); }
}
public void Increment() => Count++;
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string name = "") =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
Razor Component
@inject CounterViewModel VM
<h3>Count: @VM.Count</h3>
<button @onclick="VM.Increment">Increment</button>
Add CounterViewModel as a scoped service in Program.cs:
builder.Services.AddScoped<CounterViewModel>();
✅ CommunityToolkit.Mvvm absolutely works with Blazor!
Even though it’s more commonly used in WPF, MAUI, or WinUI, it works perfectly in Blazor too. You can use:
- ObservableObject
- ObservableProperty
- RelayCommand
and even source generators like [RelayCommand] or [ObservableProperty]
✅ Example: Blazor + CommunityToolkit.Mvvm
- Install the NuGet Package
dotnet add package CommunityToolkit.Mvvm
- Create a ViewModel
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
public partial class CounterViewModel : ObservableObject
{
[ObservableProperty]
private int count;
[RelayCommand]
private void Increment() => Count++;
}
- Use it in a Blazor Component
@page "/counter"
@inject CounterViewModel ViewModel
<h3>Counter</h3>
<p>Current count: @ViewModel.Count</p>
<button class="btn btn-primary" @onclick="ViewModel.IncrementCommand.Execute">Increment</button>
📄 Blazor + CommunityToolkit.Mvvm Login Example
Let’s build a more advanced login form in Blazor Server using CommunityToolkit.Mvvm, validation, and clean MVVM separation
LoginViewModel.cs
/// <summary>
/// ViewModel for the login form. Uses CommunityToolkit.Mvvm to expose observable properties and commands.
/// Inherits from ObservableValidator to enable DataAnnotations validation.
/// </summary>
public partial class LoginViewModel : ObservableValidator
{
[ObservableProperty]
[Required(ErrorMessage = "Username is required")]
private string username = string.Empty;
[ObservableProperty]
[Required(ErrorMessage = "Password is required")]
private string password = string.Empty;
[ObservableProperty]
private string message;
/// <summary>
/// Command bound to the login button. Validates the form and performs mock login logic.
/// </summary>
[RelayCommand]
private void Login()
{
ValidateAllProperties();
if (HasErrors)
{
Message = "Please correct the errors above.";
return;
}
if (Username == "admin" && Password == "1234")
{
Message = "Login successful!";
}
else
{
Message = "Invalid credentials.";
}
}
}
🧩 Register ViewModel in Program.cs
builder.Services.AddScoped<LoginViewModel>();
🖼 Login.razor
@page "/login"
@inject LoginViewModel Vm
@implements IDisposable
<h3>Login</h3>
<EditForm Model="Vm" OnValidSubmit="Vm.LoginCommand.Execute">
<DataAnnotationsValidator />
<ValidationSummary />
<div class="mb-3">
<label>Username</label>
<InputText class="form-control" @bind-Value="Vm.Username" />
<ValidationMessage For="@(() => Vm.Username)" />
</div>
<div class="mb-3">
<label>Password</label>
<InputText class="form-control" type="password" @bind-Value="Vm.Password" />
<ValidationMessage For="@(() => Vm.Password)" />
</div>
<button type="submit" class="btn btn-primary">Login</button>
</EditForm>
@if (!string.IsNullOrWhiteSpace(Vm.Message))
{
<div class="alert alert-info mt-3">@Vm.Message</div>
}
@code {
protected override void OnInitialized()
{
Vm.PropertyChanged += OnViewModelPropertyChanged;
}
private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
InvokeAsync(StateHasChanged);
}
public void Dispose()
{
Vm.PropertyChanged -= OnViewModelPropertyChanged;
}
}
🪨 The Real Benefits
- Testability: ViewModels can be unit tested without the UI.
- Reusability: Business logic and UI state live outside the View.
- Maintainability: Smaller, cleaner Views. Focused ViewModels.
- Scalability: Especially in component-based systems like Blazor.
📚 References
John Gossman. Introduction to Model-View-ViewModel pattern for WPF
Microsoft Docs. Data binding overview (WPF)
Microsoft Docs. Commanding Overview (WPF)
Steve Sanderson. Blazor: Reusable web UI with C#
Microsoft Docs. Data Binding in Windows Forms
🙏 Special Thanks
A heartfelt thank you to my former colleague Andreas Meyer for his invaluable guidance and patience. His deep knowledge of WPF and MVVM helped me move beyond the basics and truly understand how powerful and elegant this pattern can be when applied correctly.
✅ Final Thoughts
MVVM is alive and well in 2025 — not because it’s trendy, but because it still solves real problems. Whether you're building a Blazor dashboard, maintaining a WinForms enterprise app, or architecting a WPF monster, MVVM keeps your code testable, modular, and clean.
So next time someone tells you “MVVM is just for WPF,” remind them:
"MVVM is a pattern, not a prison."
Top comments (4)
It’s inspiring to see the relevance of MVVM even in modern frameworks like Blazor and WinForms. The clear separation of concerns and testability it offers are timeless benefits. I particularly appreciated the point about adapting MVVM patterns to work seamlessly in web and desktop applications.
Glad you like it.. appreciate it.
I used both the WinForms framework and WPF. Ultimately, I ended up with Avalonia UI, which is 99% WPF but adds cross-platform capabilities to it.
For Blazor, when I wanted to try it, I wanted to use MVVM as I am already comfortable with it, I did a quick search online found this:
"The MVVM Pattern can be implemented in Blazor, though it's not as straightforward as in WPF. You can create ViewModels as separate classes and inject them into components; however, Blazor's component model often makes this approach feel less natural than in desktop applications."
Also, the tutorials that I found are most in not all of them using the Component-Based Architecture.
The main point of the article isn't to argue which platform offers better support for MVVM, but rather to highlight how design patterns , like MVVM—can help us write cleaner, more maintainable code regardless of the platform.
I’ve worked with WinForms and WPF, and eventually transitioned to Avalonia UI, which mirrors WPF in many ways but brings cross-platform support. When exploring Blazor, I naturally looked for a way to apply MVVM, since I was already comfortable with it.
While it’s true that MVVM isn’t as seamless in Blazor as in WPF, it’s still entirely possible. You can separate logic into ViewModels and use dependency injection—though most Blazor tutorials lean toward component-based patterns instead.
What surprised me the most, however, was how many experienced developers still seem unfamiliar with the basics of dependency injection or dismiss MVVM outside of desktop apps. Design patterns aren’t tied to frameworks , they’re tools we can use to improve code quality across the board.
Some comments may only be visible to logged-in visitors. Sign in to view all comments.