Intro

MVC and MVVM are UI architecture patterns that separate data, presentation, and interaction logic to improve testability and maintainability. Both solve the same problem — keeping UI code from tangling with business logic — but they differ in how the View and the logic layer communicate. MVC uses a Controller that handles requests and selects views; MVVM uses a ViewModel that exposes observable state the View binds to directly.

MVC — Model-View-Controller

Roles:

  • Model — domain data and business rules. Independent of UI.
  • View — renders the UI from the model/view-model data.
  • Controller — handles incoming requests, invokes application logic, selects the response view.

Flow: Request → Controller → Model (read/write) → Controller selects View → View renders with data.

In ASP.NET Core MVC:

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

The Controller is the entry point. It is thin — it delegates to services and passes data to the view. Business logic lives in the service/domain layer, not the controller.

MVVM — Model-View-ViewModel

Roles:

  • Model — domain data and business rules. Same as MVC.
  • View — UI that binds to ViewModel properties and commands. Framework adapter code can live in code-behind, but business rules and parallel state synchronization should not.
  • ViewModel — exposes observable state (INotifyPropertyChanged) and commands (ICommand). The View binds to it; the ViewModel does not reference the View.

Flow: User action → View binding → ViewModel command → Model update → ViewModel notifies View via binding.

In WPF (.NET):

public class ProductDetailsViewModel : INotifyPropertyChanged
{
    private string _name = string.Empty;
    public string Name
    {
        get => _name;
        set { _name = value; OnPropertyChanged(); }
    }
 
    public ICommand LoadCommand { get; }
 
    public ProductDetailsViewModel(IProductService service)
    {
        LoadCommand = new AsyncRelayCommand(async () =>
        {
            var product = await service.GetByIdAsync(42);
            Name = product.Name; // binding updates the View automatically
        });
    }
 
    public event PropertyChangedEventHandler? PropertyChanged;
    private void OnPropertyChanged([CallerMemberName] string? name = null)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}

The View (XAML) binds to Name and LoadCommand. Small view-only event adapters can remain in code-behind; presentation state and business decisions stay in the ViewModel or domain service so they remain testable without a UI.

Comparison

AspectMVCMVVM
CommunicationController pulls data, pushes to ViewView binds to ViewModel; ViewModel notifies View
View couplingController selects View (loose)View binds directly to ViewModel (tight binding)
TestabilityController testable; View requires integration testViewModel fully unit-testable without UI
Data bindingManual (controller passes model to view)Automatic (two-way binding via INotifyPropertyChanged)
Primary platformServer-side web (ASP.NET Core MVC)Desktop/mobile UI with binding infrastructure (WPF, MAUI)
BoilerplateLowHigher (binding infrastructure, ICommand)

Decision Rule

Use MVC for server-rendered web applications (ASP.NET Core MVC, Razor Pages). The request/response model maps naturally to Controller → View. No persistent UI state between requests.

Use MVVM for stateful UI applications such as WPF and MAUI where binding infrastructure connects observable state to the UI. Blazor is primarily a component model; it can use view-models, but its event/render cycle and common one-way state flow are closer to component or MVU-style design than classic WPF MVVM.

When to switch: if WPF/MAUI code-behind starts duplicating presentation state or business decisions, move that behavior behind a ViewModel. View-only wiring does not justify a rewrite. If ASP.NET Core controllers are growing large with UI logic, consider Razor Pages or move behavior to application/domain services.

The visual is a responsibility map, not a maturity ladder. Presentation Architecture Variants compares MVP, MVU, MVVM-C coordinators, and VIPER, including the conditions that justify their extra seams. Across all variants, domain behavior must remain independent of UI frameworks.

Pitfalls

Massive Controllers (MVC)

What goes wrong: the Controller accumulates business logic, provider calls, retry policy, and notification side effects instead of delegating to application/domain services. The flow becomes difficult to test as one unit and uncertain retries can duplicate effects.

Why it happens: it is the path of least resistance. The controller already has access to the request, the response, and the DI container.

Mitigation: keep controllers thin. A controller action should: validate input, call one service method, map the result to a view model, and return a response. All business logic belongs in the service or domain layer.

Fat ViewModels (MVVM)

What goes wrong: the ViewModel accumulates business logic, data access, and navigation logic. It becomes a second controller.

Why it happens: the ViewModel is the natural place to put ‘everything the View needs,’ and that scope creeps.

Mitigation: ViewModels should expose observable state and commands. Business logic belongs in services injected into the ViewModel. Navigation belongs in a navigation service, not the ViewModel itself.

Questions

References