Presentation patterns separate rendering from the state and decisions that drive it. They differ in who receives input, who owns presentation state, how the view is updated, and where navigation lives. Pick the smallest pattern that keeps domain behavior outside the UI framework; the patterns are alternatives for different interaction models, not a maturity ladder.

Responsibility map

PatternState and decisionsView updateNavigationGood fit
MVCController handles a request and selects a response view; domain state stays in the modelController passes data to the viewRouting and controller resultServer-rendered request/response applications
MVVMView-model exposes observable presentation state and commandsBinding updates the viewView, service, or coordinatorStateful desktop/mobile UI with strong binding infrastructure
MVPPresenter coordinates a passive view interfacePresenter calls the viewPresenter or injected navigatorUI toolkits without strong binding
MVUImmutable model plus update(message, model)Render function derives the viewMessage interpreted by update/runtimeUnidirectional component UIs and deterministic state transitions
MVVM-CView-model owns screen state; coordinator owns flowBindingCoordinatorStateful clients with non-trivial navigation graphs
VIPERInteractor owns use cases; presenter maps display statePresenter calls a view interfaceRouterLarge client modules where independent seams repay the ceremony

MVC

MVC maps naturally to a server request: the controller accepts input, invokes application behavior, and selects a view. The model remains independent of HTTP and rendering.

public sealed class ProductsController(IProductService service) : Controller
{
    public async Task<IActionResult> Details(int id)
    {
        var product = await service.GetByIdAsync(id);
        if (product is null)
        {
            return NotFound();
        }
 
        var model = new ProductDetailsVm(
            product.Id,
            product.Name,
            product.Price);
 
        return View(model);
    }
}

The controller translates the request and result. Pricing rules, retries, and side effects belong in application or domain services; putting them in the controller makes HTTP concerns the accidental business boundary.

MVVM

MVVM fits a long-lived view whose controls bind to observable state and commands. The view-model exposes presentation behavior without referencing the view.

public sealed class ProductDetailsViewModel : INotifyPropertyChanged
{
    private string _name = string.Empty;
    private string? _error;
 
    public ProductDetailsViewModel(IProductService service)
    {
        LoadCommand = new AsyncRelayCommand(async () =>
        {
            var product = await service.GetByIdAsync(42);
            if (product is null)
            {
                Name = string.Empty;
                Error = "Product not found.";
                return;
            }
 
            Error = null;
            Name = product.Name;
        });
    }
 
    public string? Error
    {
        get => _error;
        private set
        {
            _error = value;
            PropertyChanged?.Invoke(
                this,
                new PropertyChangedEventArgs(nameof(Error)));
        }
    }
 
    public string Name
    {
        get => _name;
        private set
        {
            _name = value;
            PropertyChanged?.Invoke(
                this,
                new PropertyChangedEventArgs(nameof(Name)));
        }
    }
 
    public ICommand LoadCommand { get; }
 
    public event PropertyChangedEventHandler? PropertyChanged;
}

Small view-only adapters can remain in code-behind. Presentation state and business decisions stay behind the binding boundary so they can be tested without constructing the UI.

MVC and MVVM compared

DimensionMVCMVVM
CommunicationThe controller receives input, pulls or changes model data, and pushes a response model to a selected viewThe view sends actions through commands and observes state exposed by the view-model
BindingUsually explicit: the controller constructs data for each rendered responseUsually automatic: bindings react to property-change notifications and can write values back
Test seamController actions can be tested without rendering the view, although HTTP result types and routing remain part of the boundaryView-model commands and state transitions can be tested as plain objects without constructing controls
Primary platformServer-rendered request/response applications such as ASP.NET Core MVCLong-lived desktop and mobile clients with binding infrastructure such as WPF and .NET MAUI
BoilerplateLower for stateless flows; request mapping and response selection are explicitHigher because observable properties, commands, validation, and binding diagnostics need infrastructure

The binding convenience in MVVM is not free. Two-way bindings can hide control flow, and notification mistakes leave the screen stale without a compile-time failure. MVC keeps the request path explicit, but a controller that performs business decisions, provider calls, and response composition becomes a hard-to-test transaction script. In both patterns, the useful seam is the boundary around presentation behavior, not the pattern name.

Additional variants

Use MVP when a passive view interface is the natural test seam. Use MVU when explicit state transitions and one-way flow matter more than binding convenience. Add a coordinator when navigation has branching logic that should not live in a view-model. Use VIPER only when a large client module benefits from independently testable view, presentation, use-case, and routing boundaries.

For checkout, MVU makes the transition table explicit:

update(Submit, Editing) -> Submitting
update(PaymentDeclined, Submitting) -> Declined(reason)
update(PaymentCaptured(orderId), Submitting) -> Completed(orderId)

MVVM-C keeps the bound screen state in CheckoutViewModel but reports CheckoutCompleted(orderId) to a coordinator, which chooses confirmation, authentication, or recovery. The view-model remains testable without knowing routes.

Blazor supports binding, but its component state, event callbacks, and render cycle are closer to a component model with unidirectional-flow options than to classic WPF MVVM. A component can use a view-model without making MVVM the framework’s required architecture.

Decision rule

Use MVC for server-rendered request/response applications. Use MVVM for stateful clients whose binding infrastructure already supplies observable state and commands. Use MVP for a passive-view seam, MVU for deterministic state transitions, and a coordinator for navigation with its own branching policy. Do not add VIPER-sized separation to a small form whose state and navigation are already legible.

Switch because a boundary has failed, not because a file crossed a size threshold. In WPF or .NET MAUI, move code-behind behavior into a view-model when it starts duplicating presentation state, coordinating asynchronous operations, or making business decisions; event wiring that only adapts a control can stay in the view. In ASP.NET Core MVC, a growing controller usually does not require a new presentation pattern: move application behavior and domain rules into services first. Consider Razor Pages or a different UI shape when controller-and-view routing itself adds ceremony to page-focused interactions.

Pitfalls

Massive controllers

A controller becomes difficult to test when it accumulates provider calls, retry policy, and notification side effects. Keep it to input validation, one application operation, result mapping, and response selection.

Fat view-models

A view-model becomes a second controller when it owns data access, domain rules, and navigation. Keep observable state and commands there; inject application services for business behavior and a coordinator or navigation service for flow.

Questions

References