💡 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."
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.