DEV Community

Arlo
Arlo

Posted on • Updated on

Trickle-down MVVM: High maintainability via careful data handling

MVVM is highly refined and standardized, but there's rarely any good guidance on how to actually architect your app around it.

As a result, apps are often architectured with MVVM in mind, but in ways that are hard to extend or maintain, and don't take full advantage of your data's structure.

Years of making and paying for architectural mistakes in both new and existing apps have conditioned me to think extremely carefully about the best way to create or refactor a new or existing codebase.

The result is Trickle-down MVVM, an architectural pattern which aims to maintain the same reusability, structure, and scope allowed by your data, throughout all aspects of your app.

When using this pattern, Views, Models and ViewModels are always at least as modular and capable as the data that power them.

Refresher on MVVM

The core concept of MVVM is split into 3 parts:

  1. Model - The data that powers your app. Interacts with REST APIs, a local database, etc.
  2. View - What the user sees and interacts with. Can be WinUI, WPF, Xamarin, etc.
  3. ViewModel - Wraps your models and makes them easily consumable from any View. Uses INotifyPropertyChanged, ICommand, etc.

In a nutshell:

A ViewModel loosely couples a UI (View) to the Model that it depends on to function.

A well-crafted Model can be reused with a non-MVVM pattern, in a completely different UI framework, or with no UI at all.

A well-crafted ViewModel can be reused in different controls, or even completely different UI Frameworks.

There is more to MVVM, such as Behaviors and the Messenger pattern, but we only need to know these things for now. A search engine is a programmer's best friend if you want to know more.


The pitfalls that led to this

Before we discuss trickle-down MVVM, let's quickly address the common pitfalls that led to its inception.

The bulk of my experience is with WinUI, so that's what examples will use, but the wisdom should be applicable to other UI frameworks as well.

❌ Implementing controls using ViewModels.

A very common pitfall that I see is using a ViewModel for the internal implementation of a control.

A control firmly falls into the "View" part of MVVM. You can probably force a single ViewModel as the base for every control you make, but it creates an unnessecary split in the code that operates your control* and severely limits what data you can pass in from XAML.

Custom dependency properties provide data binding support, while also allowing you to pass data into your control, with the added benefit of being able to update the property by binding it externally. Very important!

Further, these values can be easily set in the Control's Style setters and accessed in the Control's template.

// In MyFancyControl
public static readonly DependencyProperty LabelProperty =
       DependencyProperty.Register(nameof(Label),
                                   typeof(string), 
                                   typeof(MyFancyControl),
                                   new PropertyMetadata("Default value"));

public string Label
{
    get => (string)GetValue(LabelProperty);
    set => SetValue(LabelProperty, value);
}
Enter fullscreen mode Exit fullscreen mode
<!-- In the MyFancyControl style setters -->
<Setter Property="Label" Value="New default value" />

<!-- In the MyFancyControl style template  -->
<TextBlock Text="{TemplateBinding Label}" />

<!-- Or, for binding to a subproperty -->
<TextBlock Text="{x:Bind Label.Length}" />
<TextBlock Text="{Binding Label.Length, RelativeSource={RelativeSource TemplatedParent}}" />
Enter fullscreen mode Exit fullscreen mode
<!-- Somewhere else -->
<local:MyFancyControl Label="{x:Bind TheLabel}" />
Enter fullscreen mode Exit fullscreen mode

Using a ViewModel to implement a Control is an abstraction that (usually) adds complexity with no benefit. It can make reusing, refactoring, or extending your Control much more difficult.

If you're making a custom control, the goal isn't to write it using MVVM, but rather to make sure that it works cleanly when used with MVVM.

Typically, this is as easy as sticking to Dependency Properties when creating a Control.

Visual Studio can quickly generate these for you, via the propdp snippet.

...

*While avoiding this unnecessary split is important, being able to recognize when it might be necessary is just as important.

Your ViewModels usually are more than enough to create your control, but sometimes the data structure doesn't quite fit the needs of your View.

For example, you might need to generate a color for each item in an ItemsControl. Normally you'd create and bind to a Dependency Property, but these aren't accessible from inside the Items template.

Creating a custom ViewModel wrapper over your data, and adding what you need is an easy way to solve this problem without creating an entire new Control.

This is architecturally sound because it:

  • Doesn't interfere with other controls
  • Doesn't ruin reusability
  • Doesn't mix UI into your data.

Just make sure it's only used by the control it was specifically created for.

❌ Mixing frontend/View into ViewModels

Control behavior is the responsibility of the control. Backend behavior is the responsibility of the backend. ViewModels do nothing but loosely couple them together. All too often, this separation is broken for one reason or another.

Let's draw up an example. Say you're creating a ViewModel, and a control template that uses it. You've just wired everything up and tested that it shows data in the View.

But now, you need to hide something in the UI until the user clicks a button, so you add a Visibility property and an ICommand to your ViewModel, and set it up so the command inverts the Visibility. Then you bind to them in the template.

ViewModels are meant to be reused in multiple possible views, and you've just added code specific to one View to a reusable ViewModel. Worse, if you leave it like that, others may take it as guidance and do it more. Eventually you'll have a bunch of UI code that you can't reuse anywhere else, and you'll need to either refactor it or the redo the whole ViewModel. I've seen this happen more than I care to admit.

Luckily, discouraging this is made easier by keeping your backend ViewModels in a .NET Standard library, where you can't access UI-dependent code.

To summarize:

  • Backend behavior is the responsibility of the backend code.
  • Control behavior is the responsibility of the control code.
  • ViewModels do nothing but loosely couple these two together.

Be very careful not to mix them together, it's a painful web to unweave.

❌ Mixing services into ViewModels.

In MVVM, services belong in Models. Change my mind.

As covered above, ViewModels in their purest form have one purpose: to wrap around models and makes them easily consumable from any View.

Models are just the C# version of some contract, usually an API. If an object from that API can return other objects, so should your models. This is the "trickle-down" part of Trickle Down MVVM. ViewModels simply wrap around this, but instead of returning and exposing Models, it wraps them in ViewModels.

In an ideal codebase, the only dependency a ViewModel has is the model being wrapped. The behavior of methods, properties and events is decided 100% by the model.

Keeping services out of ViewModels is an important step towards maximizing code portability and maintainability. We'll cover how to do this in the next part.


Trickle-down MVVM

By default, data from databases, APIs, file systems, and backends in general, are always:

  • Modular and reusable anywhere.
  • Object-oriented (models hold data and other models)
  • Easily scoped to a context (login credentials, API keys, etc)

Far too often, one or more of these things are lost due to limitations of the architecture built to interact with it.

Trickle-down MVVM is an architectural pattern which aims to maintain the same reusability, object structure, and scope allowed by your backend throughout the rest of your app.

To take full advantage of the Trickle-Down pattern, we need

  • Models to be just as flexible as your backend APIs
  • ViewModels to be just as flexible as your Models.
  • Views to be just as flexible as your ViewModels.

Step 1: Create application models

The application and the backend that provides it data are two very distinct projects with their own needs and development trajectories.

If you plan on maintaining a project long-term, it's smart to create a separation between your "Application" models and the deserialized models coming from your backend.

But let's assume the two (Application and backend) were explicitly made for each other, you find no issue with tying your entire applications' data structure to your API's models.

At best, it forces you to implement your application logic in your ViewModels and ties you to MVVM indefinitely. This means ViewModels depend on Services, and can't be instantiated without those dependencies, making them harder to use.

At worse, it dictates that an API change requires refactoring all Models, ViewModels, and Views, and requires that app and backend must ship at the exact same time, since they're all tied together.


This step is required for the full trickle-down pattern. If an object returned from an API links to other objects, the model should have a method to get those (and would do it via an injected service).

Data literally trickles down in a tree, keeping the configuration scope and reusability of the root object. Without this, you can't do the same thing in ViewModels or in the View.


It's also important to never mix View or ViewModel code in here. You should be able to remove the View and ViewModels and still interact with your application's data (HTTP, Database, whatever) using the C# objects you created.

When all is said and done, you should have a "root" object which serves as the entry point for your application's data. All configuration should be done here, and all dependencies should be passed down via constructors.

Congratulations! Your applications' data is now just as modular and portable as your API(s), and as normal C# objects, isn't tied to MVVM or any UI framework.

Step 2: Create ViewModels

This is possibly the easiest part. If you followed the previous advice, then all ViewModels should have only 1 dependency: the Model.

Take the model in via the constructor, store it in a field or property, and relay all the members with MVVM patterns like Commands, INotifyPropertyChanged, and ObservableCollection.

I highly recommend the MVVM Toolkit for this, as it contains insanely valuable things like IAsyncRelayCommand, source generators, and was overall built with performance and ease of use in mind.

Step 3: Create your View

Now that your Models and ViewModels are modular and reusable, to take full advantage of the trickle-down pattern, your Views needs to do the same.

Luckily, in most UI Frameworks like WinUI and MAUI, Controls are reusable and trickle-down by default. It's in the name, Visual Tree. As long as we avoid the pitfalls mentioned above, it doesn't take much.

  1. Create controls that use dependency properties to ingest ViewModels.
  2. Bind to that ViewModel in your templates.
  3. Use other custom controls in your template, and pass ViewModels into them.
  4. Start with your root ViewModel and repeat step 1-3 until you have an app.

A minor oversimplification, but that's really all it takes!

Once configured:

  • The root of your app uses the ViewModel which wraps the root application model, and creates the topmost part of your visual tree. This can be first time setup, a changelog screen, or the actual meat of your application.
  • Data is populated and "trickled" down the Visual Tree to other Controls via Dependency Properties.
  • The user interacts with the data via Commands, and as things happen in our application models / backend, new data may arrive in the ViewModels.
  • As new data arrives, our View can respond in the template or using custom Control logic.

Rinse and repeat, that's the "View" part of trickle-down MVVM.

Wrap up

There are more advanced MVVM techniques that you can use to further decouple and enhance your app's architecture, like Behaviors and using the messenger pattern for app navigation.

These are easily the 2 most useful things nobody told me about. I'll likely cover these later on, but they can be researched independently in the meantime.


Trickle-down MVVM is an architectural pattern for maintaining the same reusability, object structure, and scope allowed by your APIs throughout the rest of your app.

The advantages of this pattern are absolutely indispensable. When fully using the trickle-down pattern, you can easily:

  • Multi-instance your application.
  • Migrate your backend with little to no refactoring.
  • Port your app to another UI framework or non-MVVM pattern.
  • Switch to an entirely different UI template without reloading any data.

If you haven't given trickle-down MVVM a try, I cannot recommend it enough.

Top comments (1)

Collapse
 
randypj92 profile image
Randy

Thanks for this information.