MVVM and Presentation Model: Data Binding, Commands and the ViewModel
MVVM is Fowler's Presentation Model built on data binding: the ViewModel carries view state and view logic, binding keeps it in sync with the view, and user actions become commands with CanExecute. We write the order form's ViewModel in C# with the CommunityToolkit.Mvvm 8.4 source generators and in Angular 22 with signals, test both without a view, and look at binding's silent failures with real output.
Version note: The examples in this post were tested with Angular 22 and TypeScript, and the comparison examples with .NET 10.
Guess First
The MVP (Model-View-Presenter) presenter in the previous post told the view three things on every change: showError, showTotal, setPlaceOrderEnabled. When the user typed "3" into the quantity input, the presenter made these three calls in order, and if you forgot one the screen kept its old value.
Now imagine the presenter not knowing the view at all. There is no view interface and no show... methods. The class in the presenter's place only carries properties: QuantityText, Total, Error. The screen, the UI (User Interface), is bound to these properties. When the user types "3", who will tell the screen that the Total property changed? And if you write the code that computes Total but forget the notification part, what happens: an exception, a compile error, nothing?
Presentation Model = MVVM
In his 2004 Presentation Model essay, Martin Fowler describes the pattern like this: represent the state and behavior of the view in a class independent of the GUI (Graphical User Interface) controls on the screen. The view reads from and writes to this class; the class does not know the view. Where the MVP presenter tells the view "show this", the Presentation Model says nothing; it only updates its own state.
A year later, in October 2005, John Gossman announced the same idea for WPF (Windows Presentation Foundation) under the name MVVM (Model-View-ViewModel). Two things stand out in Gossman's post. The first is the meaning of the name ViewModel: "Model of a View". The second is that the pattern relies on a general data binding mechanism. Fowler also notes on his page that Presentation Model is increasingly known as MVVM. The two names describe the same idea; the difference is that MVVM hands the synchronization code to the platform's binding.
Let's place the roles on the order form. The model is the domain: Pricing.PriceOrder, Catalog, the order API (Application Programming Interface). The view is the XAML (Extensible Application Markup Language) or HTML (HyperText Markup Language) template: it only says what is bound to what. The ViewModel carries the view state we separated in the first post (the raw text in the input, the "saving" flag) and the view logic ("disable the button if the quantity is invalid"). In Gossman's words, state such as selection and modes also lives in the ViewModel.
A C# ViewModel: CommunityToolkit.Mvvm 8.4
In the WPF and .NET MAUI (Multi-platform App UI) world, the practical standard for MVVM is Microsoft's CommunityToolkit.Mvvm package. Its 8.x versions generate the repetitive code needed for INotifyPropertyChanged and ICommand with source generators. The order form's ViewModel:
using System.Globalization;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace Bookstore.Desktop;
public interface IOrderApi
{
Task<int> PlaceOrderAsync(int bookId, int quantity, CancellationToken cancellationToken);
}
public sealed partial class OrderFormViewModel(IOrderApi api) : ObservableObject
{
public IReadOnlyList<Book> Books { get; } = Catalog.Books;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(Total), nameof(Error))]
[NotifyCanExecuteChangedFor(nameof(PlaceOrderCommand))]
public partial string QuantityText { get; set; } = "1";
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(Total))]
public partial Book SelectedBook { get; set; } = Catalog.Books[0];
[ObservableProperty]
public partial string? Confirmation { get; set; }
public string Total => TryGetQuantity(out int quantity)
? string.Create(CultureInfo.InvariantCulture, $"Total: {Pricing.PriceOrder(SelectedBook, quantity):0.00}")
: "";
public string Error => TryGetQuantity(out _) ? "" : "Quantity must be between 1 and 10";
[RelayCommand(CanExecute = nameof(CanPlaceOrder))]
private async Task PlaceOrderAsync(CancellationToken cancellationToken)
{
_ = TryGetQuantity(out int quantity);
int orderId = await api.PlaceOrderAsync(SelectedBook.Id, quantity, cancellationToken);
Confirmation = $"Order #{orderId} placed";
}
private bool CanPlaceOrder() => TryGetQuantity(out _);
private bool TryGetQuantity(out int quantity) =>
int.TryParse(QuantityText, NumberStyles.None, CultureInfo.InvariantCulture, out quantity)
&& quantity is >= 1 and <= 10;
}
Four attributes do most of the work. [ObservableProperty] generates the property's setter so that it raises PropertyChanged when the value changes. [NotifyPropertyChangedFor] also sends notifications for Total and Error when QuantityText changes, because those two are computed properties without setters of their own. [NotifyCanExecuteChangedFor] tells the command the button is bound to "ask for your state again". [RelayCommand] generates an IAsyncRelayCommand named PlaceOrderCommand from the PlaceOrderAsync method and wires its CanExecute to CanPlaceOrder.
The examples on Microsoft Learn show [ObservableProperty] on a private field; version 8.4 also supports partial properties. With C# 14 and .NET 10, the public partial string QuantityText { get; set; } = "1"; form above compiles without any extra language setting. When written to disk with EmitCompilerGeneratedFiles, the code the generator produces looks like this:
public partial string QuantityText
{
get => field;
set
{
if (!global::System.Collections.Generic.EqualityComparer<string>.Default.Equals(field, value))
{
OnQuantityTextChanging(value);
OnQuantityTextChanging(default, value);
OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.QuantityText);
OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Total);
OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Error);
field = value;
OnQuantityTextChanged(value);
OnQuantityTextChanged(default, value);
OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.QuantityText);
OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Total);
OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Error);
PlaceOrderCommand.NotifyCanExecuteChanged();
}
}
}
The backing field uses the C# 14 field keyword, and both the dependent notifications and the command update are written inside the setter. This is exactly the code you would write by hand, and could forget.
Commands and CanExecute
In MVVM, the work the user triggers is exposed as commands, not methods. WPF's ICommand interface has three members: Execute, CanExecute and the CanExecuteChanged event. When a button's Command property is bound to a command, WPF disables the button on its own as long as CanExecute returns false. This is the most common UI use of the Command pattern from the "Command Pattern: Turning Requests into Objects, Undo/Redo and Queuing" post of the Design Patterns series.
Let's drive the ViewModel from a console application, without any view. We imitate what the view's binding does: listen to the PropertyChanged and CanExecuteChanged events and print them.
using Bookstore.Desktop;
OrderFormViewModel viewModel = new(new SlowOrderApi());
// we imitate what the view does: binding listens to PropertyChanged
viewModel.PropertyChanged += (_, e) => Console.WriteLine($"PropertyChanged: {e.PropertyName}");
viewModel.PlaceOrderCommand.CanExecuteChanged += (_, _) =>
Console.WriteLine($"CanExecuteChanged -> {viewModel.PlaceOrderCommand.CanExecute(null)}");
Console.WriteLine("> QuantityText = \"3\"");
viewModel.QuantityText = "3";
Console.WriteLine($"Total = {viewModel.Total}");
Console.WriteLine("> QuantityText = \"abc\"");
viewModel.QuantityText = "abc";
Console.WriteLine($"Error = {viewModel.Error}");
Console.WriteLine("> QuantityText = \"2\", PlaceOrderCommand");
viewModel.QuantityText = "2";
Task running = viewModel.PlaceOrderCommand.ExecuteAsync(null);
Console.WriteLine($"While running, CanExecute = {viewModel.PlaceOrderCommand.CanExecute(null)}");
await running;
Console.WriteLine($"Confirmation = {viewModel.Confirmation}");
sealed class SlowOrderApi : IOrderApi
{
public async Task<int> PlaceOrderAsync(int bookId, int quantity, CancellationToken cancellationToken)
{
await Task.Delay(100, cancellationToken);
return 42;
}
}
> QuantityText = "3"
PropertyChanged: QuantityText
PropertyChanged: Total
PropertyChanged: Error
CanExecuteChanged -> True
Total = Total: 37.80
> QuantityText = "abc"
PropertyChanged: QuantityText
PropertyChanged: Total
PropertyChanged: Error
CanExecuteChanged -> False
Error = Quantity must be between 1 and 10
> QuantityText = "2", PlaceOrderCommand
PropertyChanged: QuantityText
PropertyChanged: Total
PropertyChanged: Error
CanExecuteChanged -> True
CanExecuteChanged -> False
While running, CanExecute = False
PropertyChanged: Confirmation
Confirmation = Order #42 placed
CanExecuteChanged -> True
The whole MVVM mechanism is visible in the output. A single property assignment produces three PropertyChanged notifications and one CanExecuteChanged; on a screen with binding, that means the input, the total, the error message and the button update together. While the command runs, CanExecute returns false: by default AsyncRelayCommand does not allow a second concurrent execution. The package does the job of the #saving flag we wrote by hand in the presenter in the previous post. This test needed no mock framework, no UI thread and no WPF; even though WPF runs only on Windows, this ViewModel compiled and ran on macOS.
The Same ViewModel in Angular
In Angular you do not need a separate ViewModel class: the component class is the ViewModel. The template is the view, the signals in the component class are view state, the computed() values are view logic. You know signals themselves from the "What's New in Angular 16: Signals, Required Inputs and takeUntilDestroyed" post of the Angular series; what we look at here is how the roles are distributed.
import { Component, computed, inject, signal } from '@angular/core';
import { catalog, parseQuantity, priceOrder } from '../domain/pricing';
import { OrderApi } from './order-api';
@Component({
selector: 'app-order-form',
template: `
<select #book (change)="selectedBookId.set(+book.value)">
@for (b of books; track b.id) {
<option [value]="b.id">{{ b.title }}</option>
}
</select>
<input #qty [value]="quantityText()" (input)="quantityText.set(qty.value)">
<p class="total">{{ total() }}</p>
<p class="error">{{ error() }}</p>
<button [disabled]="!canPlaceOrder()" (click)="placeOrder()">Place order</button>
<p class="confirmation">{{ confirmation() }}</p>
`,
})
export class OrderForm {
readonly #api = inject(OrderApi);
// view state: meaningful only while the screen is open
readonly books = catalog;
readonly selectedBookId = signal(1);
readonly quantityText = signal('1');
readonly saving = signal(false);
readonly confirmation = signal('');
// view logic: values derived from state
readonly #check = computed(() => parseQuantity(this.quantityText()));
readonly total = computed(() => {
const check = this.#check();
const book = catalog.find(b => b.id === this.selectedBookId())!;
return check.ok ? `Total: ${priceOrder(book, check.quantity).toFixed(2)}` : '';
});
readonly error = computed(() => {
const check = this.#check();
return check.ok ? '' : check.error;
});
readonly canPlaceOrder = computed(() => this.#check().ok && !this.saving());
// command: work the user triggers
async placeOrder(): Promise<void> {
const check = this.#check();
if (!check.ok || this.saving()) return;
this.saving.set(true);
try {
const { orderId } = await this.#api.placeOrder(this.selectedBookId(), check.quantity);
this.confirmation.set(`Order #${orderId} placed`);
} finally {
this.saving.set(false);
}
}
}
The order API is the counterpart of IOrderApi in the C# version; a thin service wrapping HttpClient:
import { HttpClient } from '@angular/common/http';
import { Injectable, inject } from '@angular/core';
import { firstValueFrom } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class OrderApi {
readonly #http = inject(HttpClient);
placeOrder(bookId: number, quantity: number): Promise<{ orderId: number }> {
return firstValueFrom(this.#http.post<{ orderId: number }>('/api/orders', { bookId, quantity }));
}
}
It maps to the C# version line by line. The quantityText signal is the QuantityText property, the total and error computeds are the computed properties notified via NotifyPropertyChangedFor, the canPlaceOrder computed is CanExecute, the placeOrder method is the command. The difference is how notifications are produced. In C# you state with attributes which property depends on which; in Angular, computed() tracks which signals it reads at runtime. The moment you read selectedBookId() inside total, the dependency is set up; there is nothing to forget.
On the template side the bindings are one-way: [value] from the signal to the input, (input) from the input to the signal. The Angular counterpart of WPF's two-way binding is [(ngModel)] or Signal Forms, which became stable in Angular 22; the details of both are in the "Data Binding in Angular: Interpolation, Property, Event and Two-Way Binding" and "Angular 22: OnPush by Default, Stable Signal Forms and a File-by-File Comparison with Angular 12" posts of the Angular series. In this example I wrote the two directions separately on purpose so the flow is visible.
ng test runs the tests with Vitest. The first test uses the component like a ViewModel and never looks at the DOM (Document Object Model); the second verifies that the same state is bound to the template:
import { TestBed } from '@angular/core/testing';
import { OrderForm } from './order-form';
import { OrderApi } from './order-api';
describe('OrderForm', () => {
let placeOrder: ReturnType<typeof vi.fn>;
beforeEach(() => {
placeOrder = vi.fn().mockResolvedValue({ orderId: 42 });
TestBed.configureTestingModule({ providers: [{ provide: OrderApi, useValue: { placeOrder } }] });
});
it('works as a ViewModel without touching the DOM', async () => {
const vm = TestBed.createComponent(OrderForm).componentInstance;
vm.quantityText.set('3');
expect(vm.total()).toBe('Total: 37.80');
vm.quantityText.set('abc');
expect(vm.error()).toBe('Quantity must be between 1 and 10');
expect(vm.canPlaceOrder()).toBe(false);
vm.quantityText.set('2');
const running = vm.placeOrder();
expect(vm.canPlaceOrder()).toBe(false);
await running;
expect(vm.confirmation()).toBe('Order #42 placed');
expect(placeOrder).toHaveBeenCalledWith(1, 2);
});
it('binds the template to the same state', async () => {
const fixture = TestBed.createComponent(OrderForm);
const root = fixture.nativeElement as HTMLElement;
const input = root.querySelector('input')!;
input.value = '3';
input.dispatchEvent(new Event('input'));
await fixture.whenStable();
expect(root.querySelector('.total')!.textContent).toBe('Total: 37.80');
expect(root.querySelector('button')!.disabled).toBe(false);
});
});
$ npx ng test --watch=false --reporters=verbose --include src/app/orders/order-form.spec.ts
✓ bookstore-ui src/app/orders/order-form.spec.ts > OrderForm > works as a ViewModel without touching the DOM 12ms
✓ bookstore-ui src/app/orders/order-form.spec.ts > OrderForm > binds the template to the same state 6ms
Test Files 1 passed (1)
Tests 2 passed (2)
The Angular Style Guide recommends protected for members read only by the template. Here I left the signals public because the test reads them too; we will treat the component's public API as a contract in the eighth post of the series.
The Price of Binding: Silent Failures and "Magic"
Back to the question from Guess First: what happens if you forget the notification part? In the C# ViewModel I changed the attribute on QuantityText to [NotifyPropertyChangedFor(nameof(Error))], removing Total from the list. The project built with 0 warnings and 0 errors, and the output was:
> QuantityText = "3"
PropertyChanged: QuantityText
PropertyChanged: Error
CanExecuteChanged -> True
Total = Total: 37.80
The Total property returns the right value (37.80), but the PropertyChanged: Total line is missing. On a screen with binding the total keeps its old value; no exception, no compiler warning, and a test only catches it if it checks the notification explicitly. In WPF, mistyping a binding path in XAML is silent in a similar way: the application runs, the field stays empty, and the error appears only as a trace line in Visual Studio's output window.
Most criticism of MVVM comes from this point. Binding itself is declarative and short, but when a value on screen does not update, you have to rebuild the notification chain in your head to find out why. Gossman himself later admitted that MVVM can be overkill for simple interfaces.
Angular catches both of these mistakes somewhere else. Because dependency tracking is automatic, there is no "forgetting the notification". A typo in the template is caught at compile time; in Angular 22 strictTemplates is on by default. Writing totl() instead of total() in the template gives:
$ npx ng build
✘ [ERROR] TS2551: Property 'totl' does not exist on type 'OrderForm'. Did you mean 'total'? [plugin angular-compiler]
src/app/orders/order-form.ts:14:24:
14 │ <p class="total">{{ totl() }}</p>
╵ ~~~~
Common Pitfalls
Making the ViewModel depend on the view. The moment a Window, HTMLElement or ElementRef gets into the ViewModel, the whole advantage of Presentation Model is gone: the ViewModel can no longer be tested without the view. The ViewModel's import list should contain no UI types.
Piling everything into the ViewModel. The ViewModel is the home of view state and view logic; the discount rule, stock checks or order number generation belong to the domain. In the examples the calculation is in Pricing.PriceOrder; the ViewModel only calls it.
Copying computed properties into fields. Keeping Total in a field and updating it by hand when QuantityText changes creates two sources of truth. A value that can be derived should be derived: a computed property with NotifyPropertyChangedFor in C#, computed() in Angular.
Writing derived state with effect() in Angular. effect(() => this.total.set(...)) seems to work but does the job of computed() in a more fragile way; the Angular documentation also recommends effects for side effects toward the outside world, not for propagating state. We will cover one-way flow in detail in the seventh post.
When Not to Use It
For a one-off dialog with a few fields, a separate ViewModel class with commands and notification attributes can be longer than the screen itself; even in WPF, code-behind is a legitimate choice for such a dialog. Forcing MVVM on a platform without binding means writing your own binding; there MVP is the more honest choice. And in large applications where many independent screens share the same state and a change in one updates the others in a chain, the two-way binding web itself can become the problem; that is the starting point of Flux in the sixth post.
A Small Experiment
★☆☆ Add the attribute to the C# ViewModel that makes PlaceOrderCommand re-query when SelectedBook changes. Is it really needed in this scenario? Verify with the console output.
★★☆ Add a discountApplied computed to the Angular component and bind the "10% off" badge to it in the template. Write its test without looking at the DOM, only by reading the signal.
★★★ Set up an xUnit test project for the C# ViewModel and write a test verifying that PropertyChanged is raised for Total when QuantityText changes. Then remove Total from the attribute and watch the test break; the earlier "silent failure" is no longer silent.
For four posts we have built the patterns one by one. In the next post we turn the question around: which of these patterns do the frameworks we use, Angular, React, Blazor, WPF and ASP.NET Core, correspond to, and what is the real answer to "Is Angular MVC (Model-View-Controller) or MVVM?"