100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
.NET

The ViewModel in MVVM

How the ViewModel exposes observable state and commands, manages lifecycle, and stays independently testable.

FoundationsIntermediate10 min readJul 10, 2026
Analogies

The ViewModel in MVVM

The ViewModel is the abstraction layer that represents 'what the View needs to display' and 'what the View can do,' expressed entirely as observable properties and invocable commands, with no dependency on any concrete UI control or framework beyond a binding/observation primitive. It pulls raw domain data from the Model via repositories or services, transforms and formats that data for presentation (turning a Decimal into a currency string, or a list of Order objects into a sorted, filtered list of OrderRowViewModel items), and exposes user-triggerable actions as commands the View can bind buttons or gestures to.

🏏

Cricket analogy: A cricket app's match-detail ViewModel takes the raw scorer's feed (Model) and exposes it as ready-to-bind properties like a formatted 'CRR: 6.42' string and a 'canRequestReview' boolean, so the screen never has to compute run rate itself.

Observable Properties

The core mechanism a ViewModel uses to expose state is a framework-specific observable primitive: in .NET, properties raise INotifyPropertyChanged events; in SwiftUI, @Published properties inside an ObservableObject automatically notify subscribers; in Jetpack Compose, mutableStateOf or StateFlow values trigger recomposition when changed; in Angular, RxJS Observables or, more recently, Signals serve the same purpose. Regardless of the specific API, the pattern is identical: the ViewModel mutates a property, the framework's binding layer detects the change, and every View element bound to that property re-renders automatically, without the ViewModel ever calling a method like updateLabel() itself.

🏏

Cricket analogy: Whichever broadcaster's graphics vendor you use — Hawk-Eye, Virtual Eye, or an in-house system — the underlying idea is the same: the scoring engine's internal number changes, and the graphics overlay auto-refreshes, regardless of which specific vendor API triggers it.

kotlin
// Jetpack Compose ViewModel with observable state
class CartViewModel(private val repository: CartRepository) : ViewModel() {
    private val _items = MutableStateFlow<List<CartItemUiState>>(emptyList())
    val items: StateFlow<List<CartItemUiState>> = _items.asStateFlow()

    val total: StateFlow<String> = items.map { list ->
        val sum = list.sumOf { it.price * it.quantity }
        "$%.2f".format(sum)
    }.stateIn(viewModelScope, SharingStarted.Lazily, "$0.00")

    fun removeItem(id: String) {
        viewModelScope.launch {
            repository.removeFromCart(id)
            _items.value = repository.getCartItems().map { it.toUiState() }
        }
    }
}

Commands and User Actions

Alongside observable properties, the ViewModel exposes commands: named, invocable actions that encapsulate what happens when a user interacts with a control, typically implemented as an ICommand object in WPF (with RelayCommand or DelegateCommand as common helper implementations), a plain method in SwiftUI or Compose that the View calls from a button action, or an Output event in a reactive framework. A command's real power is that it can carry its own 'can this currently execute' logic — a submitCommand might report CanExecute as false while a form is invalid — which lets the View disable the corresponding button purely by binding to that property, with zero conditional logic written in the View itself.

🏏

Cricket analogy: A 'Request Review' command in a broadcast app reports as unavailable once a team has used both of its DRS reviews, so the button greys out automatically without the broadcast graphics needing any if-statements of its own.

In WPF, the RelayCommand pattern is the classic way to implement this: a class wrapping an Action (what to do) and a Func<bool> (whether it can currently execute), which the View binds a Button's Command property to, letting the button's enabled state and click behavior both come purely from the ViewModel with no code-behind.

ViewModel Lifecycle and State Management

On mobile platforms, ViewModel lifetime is often intentionally decoupled from a single screen instance: Android's Jetpack ViewModel class is specifically designed to survive configuration changes like screen rotation, so in-flight network calls and loaded data aren't lost and re-fetched every time the device rotates, while the Activity or Fragment (View) is destroyed and recreated underneath it. This means the ViewModel is effectively where you should hold UI-relevant state that needs to outlive a single View instance's lifecycle, such as scroll position, form input the user hasn't submitted yet, or an in-progress upload's percentage, rather than storing that state in the View where it would be lost on recreation.

🏏

Cricket analogy: A cricket app's live-match ViewModel keeps polling and holding the current score even if you rotate your phone to landscape to watch a replay, so the score doesn't reset to zero and re-fetch from scratch on rotation.

Testing the ViewModel

Because the ViewModel depends only on the Model (typically through injected, mockable repository interfaces) and exposes plain observable properties and commands, it can be unit tested by instantiating it directly, injecting a fake or mock repository that returns known data, invoking a command or triggering a property change, and asserting on the resulting property values — all without a device, simulator, or UI test framework. This is where MVVM delivers its biggest practical return on investment: form validation, filtering, sorting, and error-state logic that would otherwise require slow UI tests can instead run in milliseconds as part of a normal unit test suite.

🏏

Cricket analogy: A cricket app's team can unit test 'does the reviewsRemainingText update to 0 of 2 after two failed DRS calls' by feeding a fake match-event stream into the ViewModel, with no actual live match or device needed.

A ViewModel that imports a UI framework type — a UIColor, a Color, a View struct, or a navigation controller reference — has broken MVVM's core contract, sometimes called the 'Massive View Model' problem when it grows to also absorb business logic that belongs in the Model. Keep imports UI-framework-agnostic wherever the platform allows it (e.g., depend only on Combine/ObservableObject in Swift, not SwiftUI itself, when feasible), and push navigation decisions out through an event or coordinator rather than a stored screen reference.

  • The ViewModel exposes 'what the View shows' and 'what the View can do' as observable properties and commands.
  • Observable property primitives vary by platform (INotifyPropertyChanged, @Published, StateFlow, Signals) but all trigger automatic View updates.
  • Commands encapsulate user actions and often carry can-execute logic that lets the View disable controls without its own conditional logic.
  • On mobile, ViewModel lifetime is often decoupled from a single View instance, surviving events like screen rotation.
  • The ViewModel is the right place to hold UI-relevant state that must outlive a single View instance, like unsaved form input.
  • ViewModel logic is unit-testable in isolation by injecting mock repositories and asserting on property values.
  • A ViewModel that imports UI framework types or absorbs Model-level business logic breaks MVVM's separation.

Practice what you learned

Was this page helpful?

Topics covered

#NET#MVVMDesignPatternStudyNotes#MicrosoftTechnologies#TheViewModelInMVVM#ViewModel#MVVM#Observable#Properties#StudyNotes#SkillVeris