|<<>>|1 of 306 Show listMobile Mode

And another thing about MVVM

Published by marco on

Updated by marco on

I recently wrote Real quick on MVVM and now I see that a good colleague and friend has written his own MVVM understandings by Austin Jones (Austin's Journey for Meaning). His piece got me thinking again about how the concept is a good start but isn’t really sufficient.

Justifying the view model

Somewhere near the beginning, he writes,

“The View Model’s function is separate from the Model. Abstraction requires discipline to not let two pieces of code that do the same thing become the same thing, purely out of convenience. Things that operate together should be functionally coupled, not just that same code.”

While I deeply appreciate the sentiment, I think that (A) most people are going to be unconvinced that they need additional complexity for such a vague goal, and (B) there are more concrete reasons to keep them separate. In Real quick on MVVM, I posited a simple example, repeated below.

record Person(
  string FirstName,
  string LastName,
  Company Company,
  DateTime BirthDate);

The view model might want to expose:

int Age => DateTime.Now.Year − _model.BirthDate.Year;

string FullName => $"{_model.FirstName} {_model.LastName}";

Company Company { get; }

IReadOnlyList<Company> AvailableCompanies { get; }

The AvailableCompanies is for the drop-down menu.

The data in the model is a different shape than that required by the view. This happens quite quickly and quite often. Anyone who tries to “cheat” by using a type as both a model and view model will quickly be writing spaghetti code.

It is the view-model’s job to marshal data to and from its own shape to that of the model. It is decidedly not the model’s job to do that, because it exposes data, while one or more views might display it in different ways. Perhaps another view is showing the birthdate directly, in which case that view model would simply pass the value through unmodified.

Thinking through an example

“Most logic seems to fall into the View Model as your business logic rules are often mirrored by presentation rules. E.g. a button has to be disabled if the user hasn’t met some requirement.”

It may seem too picky but I would instead use the verb reflect instead of mirror, to say that the view model exposes properties that reflect the state in the model. Just off the top of my head, I can imagine that each component of the architecture has unique duties, as illustrated in the example below,

  • A model contains several properties that must adhere to certain rules in order to be saved.
  • A validation service determines whether those rules have been satisfied, returning a list of zero or more validation results.
  • A view model exposes the most recent list of validations as a property, as well as a property called readyToSubmit. The view model triggers the service to calculate a new list of validations when the view notifies it that a relevant change has been made.
  • A view binds the validations as it sees fit—either attaching properties directly to the controls that will display their values and allow users to manipulate them, as well as exposing the list of validations to the user in some way—as well as binding the Enabled property of the submission button to the readyToSubmit property.

This is just a simple example but we can see that the model is just a data container. In classic OO, the service would have been part of those objects. However, it’s far more flexible to keep the model as a set of “dumb” DTOs and to keep the logic in the service. This makes it much easier to replace the validation logic in specific cases, without touching the data layer, which doesn’t need to change.

The view model does the work of managing calls to the validation service as well as retaining the results as long as the view needs them. The view model doesn’t know anything about buttons. It doesn’t need to know that they can be enabled or disabled. That’s the view’s job, which deals with the actual representations presented to the user.

This makes the view model, in turn, flexible enough to be used with alternate representations. For example, we can imagine a view or view model that simply auto-saves when readyToSubmit is true, so it would have been a shame to have named that property saveButtonEnabled because it would have been an awkward fit for the hypothetical second view.

Benefits

As you can well imagine, it’s incredibly easy to test systems built in this way, as you can very easily construct the data/model that you want and test something like the validation service. You can also very easily build on top of that to verify that the view model updates and notifies as expected. You can even bind to its properties to verify that a potential view would have received the expected notifications.

The view doesn’t have more logic in it than binding. Views are more finicky to test—although it’s not impossible or even especially difficult with practice, its it’s also not usually necessary. For most problems that crop up, your tests eliminate the possibility that the bug is anywhere other than the view, so you quickly find where the incorrect binding. Obviously, if errors like this are chronic—or if you have very complex views—then you’ll want to test the view with end-to-end tests. Just remember that testing the view usually requires the most effort, results in the slowest tests, and provides the least benefit, so you should really be doing those last, if at all.

Conclusion

My colleague’s example focuses more on how the service layer pertains to persistence, for loading and storing models. I wanted to provide an example that doesn’t have anything to do with persistence but shows that there is non-persistence logic that obviously—at least in hindsight—doesn’t belong anywhere but in the service layer.

I’ve been working with this type of abstraction since at least 2002, when I started working on the Atlas framework at Opus Software AG, which was written in Delphi Pascal. We didn’t call it MVVM but we had a very clear separation between the object model, the view model, and renderers.