8

I wondered what is the "correct" or "intended" interpretation of MVVM? I worked mainly with WPF but I guess it is still relevant to all platforms embracing MVVM architectural pattern.

1. ViewModel as application

  • Model is representation of data+logic for retrieving the data
  • ViewModel is all the application logic and is UI agnostic, where you could in principle have Console application as your View, where concepts of windows, dialogs, docks and other UI elements do not make sense
  • View - any UI, graphical or text-based

2. Model as Application

  • Model is not only representation of data+logic for retrieving the data, but also all specific application logic, from there you just add a bit of work to create a console version of your application
  • ViewModel all the view logic, but abstracted away from the specific UI classes, this will allow the ViewModel to be unit tested without calling UI as you could mock the UIElements
  • View - specific UI layer, preferably "dumb"

I would say that until now I interpreted MVVM as in the first approach, but I increasingly see the evidence that most people use second approach. It seems from very superficial look at AvalonDock for example that they perhaps favour 2nd approach and similarly every developer who implements view-specific command (e.g. opening a dialog from ViewModel), but abstracted away from UI/View classes also uses 2nd approach.

I know that I should use whatever approach is most feasible, but I have a feeling that I am missing something. Maybe 1st approach is a different pattern altogether and I also wonder what creators of MVVM thought. Also I may face in foreseeable future that I would need to port my WPF application to another platform and can see that 2nd approach could make porting more difficult.

I've also recently realised that understanding original intentions behind designing a framework or pattern will help with clean design of software. E.g where I started to embrace WPF concepts in more detail, the quality of code and design improved. So I want to have clear understanding of intentions behind MVVM and hopefully my design will improve.

1
  • 1
    MVVM is originally WPF-centric variant of what Fowler calls Presentation Model, where Model is some part of the internal (non-presentation) layer code associated with this view (or views) or use case, while the ViewModel is the Presentation Model, residing in the presentation layer. Commented Mar 3, 2022 at 14:55

2 Answers 2

8

To determine the best course of action, one only needs to examine basic principles of OOP, SOLID and Separation of Concerns. Once that is done, the natural order of MVVM becomes obvious.

Applying those principles to MVVM leads one to several natural conclusions:

  1. UI structures that can be represented as XAML should be written as such. Your View should be comprised primarily of XAML. Avoid building your View using code-behind, if you can.

  2. Custom code having to do with direct user interaction can go into the view's code-behind. This includes things like mouse clicks, keystrokes and wheel movements that can not be accommodated automatically by WPF in XAML. However ...

  3. Most UI interaction should get pushed back into the View Model, in the form of Command and Data Bindings, and handled there. This code tends to be more stable and less complex than code-behind, and produces less "spaghetti code."

  4. If it's anything else, it gets handled in the Model.

Model classes tend to be ordinary C# classes, because they don't require data binding properties or tight coupling to UI structures. This makes them much easier to mock, unit test, and apply SOLID principles to improve their overall organizational structure.

So in summary:

  1. Push as much logic as you can out of the View (code-behind) and into the View Model.
  2. Push as much logic as you can out of the View Model into the Model.

That's it.

The most common mistake I see in large WPF programs is putting too much of the program's logic into the View Models. This makes the View Models large, multi-responsibility classes that violate SRP. Programmers who write classes like this often have to use #region directives to manage the sheer amount of code.

How much code is too much code in a View Model? A good View Model should have less than 800 lines of code in it, and typically much less than that. If your program logic doesn't have directly to do with the data bindings in your View Model, write model classes and call methods on those classes from your data bindings.

3
  • Thank you for the fresh take on that. I am definitely guilty of having large ViewModels and the idea of binding directly to the Model is something new to me and I definitely need to rethink how I design my WPF applications Commented Mar 7, 2022 at 9:28
  • 3
    Your UI (the View) will bind directly to the View Model. The View Model will call ordinary methods in the Model. As such, I wouldn't call it "direct binding" to the model as such, unless by that you mean making calls to the model directly from the View Model. Commented Mar 7, 2022 at 11:25
  • Ok that's closer to what I'm doing anyway Commented Mar 7, 2022 at 11:56
9

Over ten years I have developed using MVVM and WPF I agree with Robert Harvey.

In the beginning, and I think most developers fall into this pattern, I started with very large view models. In time I realized this was because of the widely used recommendation of delegate commands (e.g. DelegateCommand, ActionCommand) and placing command logic in the view models.

The result of this is that developers also end up writing model code inside the view model, and not purposefully, and it is not good when your view models become many thousands of lines of code with far too many responsibilities.

As time went on and so did my discovery and mastery of MVVM, I realized how important commands are, how important the model layer is, and how important INotifyPropertyChanged is, and how less relevant the view model is.

The largest application I have developed with MVVM is approximately 53,000 lines of code (excluding Xaml) and contains numerous "modules", "areas", services. It is built on top of an extensive framework I developed with many practices at play; Domain Driven Design (domain entities, domain services), imaging code (files, TIF, PDF, TWAIN), extensive data access (SQL Server), and so on. You can infer that this application is part of a larger enterprise application and its framework are much more extensive.

Simply put, from an MVVM perspective, you have a view, a view model both with clear concerns, and everything else is a model. Read this, over, and over, and over, and over until it clicks.

I think a good metric for your mastery of MVVM is to look at any of your view models and answer these questions?

  1. Does it have I/O, Network, or data access related code (including abstractions like Entity Framework)?
  2. Does it have a lot of logic that has nothing to do with data-binding or view concerns?
  3. Does it actually model your view?
  4. Is all the command logic in the view model?
  5. Is it a lot of code?

For me, I could answer a lot of these questions with 'Yes' for a lot of MVVM applications I have seen, including ones I built. If you were to remove the bulk of this code and push it into separated command classes, you can find that your view models become thin and almost empty of real logic. Then, you look to your commands and say, these can be simplified too, leading to conclude the construction of new model classes.

In the end, you will find your views, view models, and commands are very lightweight and easy to maintain, and your ratio of application code becomes something like 10/5/5/80 (view, view model, command, model).

Let's revisit your original question "In MVVM, how much of the business logic should reside in the view model, and how much should reside in the Model?".

A: Possibly near 0, and possibly near 100%, if you can achieve both.

View models are nearly empty and it is almost entirely the commands that do the real work interacting with the model and view model. Now commands are "part of the view model", even still, this is important to understand.

Here is an architectural diagram from my application.

                                         +-------------------------------------------------------------------+
                                         |                                                                   |
                                         |                                                                   |
                                         |                                                                   |
                                         |                                                                   |
                                         |                        ShellTask SubSystem                        |
                                         |                                                                   |
                                         |                                                                   |
                                         |                                                                   |
                                         |                                                                   |
                                         +------------+------------------------------------------------------+
                                                      ^
                                                      |
                                                      |
                                         +------------+-------------+              +-------------------------+
                                         |                          |              |                         |
                                         |                          |              |                         |
                 +---------------------+ |        ShellTask         | <----------+ |        ICommand         |
                 |                       |                          |              |                         |
                 |                       |                          |              |                         |
                 |                       |                          |              |                         |
                 |                       +--------------------------+              +------------+------------+
                 |                                                                              ^
                 |                                                                              |
    +---------------------------------------------------------------+              +-------------------------+
    |            |                                                  |              |            |            |
    |            |           ShellViewModel                         | data-binding |  +---------+---------+  |
    |            |                                                  | +----------> |  |                   |  |
    |  +---------------------------------------------------------+  |              |  |      Button       |  |
    |  |         |                                               |  |              |  |                   |  |
    |  |         |               Shell                           |  |              |  +-------------------+  |
    |  |         v                                               |  |              |       MainWindow        |
    |  |   +-----+------+ +---------------+ +----------------+   |  |              |                         |
    |  |   | TaskRunner | | Configuration | | ServiceLocator |   |  |              |                         |
    |  |   +------------+ +---------------+ +----------------+   |  |              |                         |
    |  |                                                         |  | data-binding |                         |
    |  +---------------------------------------------------------+  | <----------+ |                         |
    |                                                               |              |                         |
    +---------------------------------------------------------------+              +-------------------------+

Now, what is missing from this diagram is the model layer and you see there is a task system. This is simply because I created a special task system to manage the large amount of asynchrony this application requires. But the tasks are models, and they along with commands and other models themselves have the majority of source code for the application.

The average view model in this application has less than 500 lines of code and zero view models greater than 1000 lines of code.

None of the view models have any:

  • I/O, Network, or data access related code or even references to
  • Business logic
  • Command logic
  • Large number of models

If I were to describe their responsibility it would be:

  • Model the view
  • Take dependencies
  • Initialize commands (with dependencies)
  • Use PropertyChangede
  • Have mostly read and write properties so commands can interact

Here is a typical view model.

/// <summary>
/// Class that models the twain options view.
/// </summary>
public class TwainOptionsViewModel : ViewModel
{
    private bool hasBlankPageDetection;
    private bool hasDuplex;
    private bool hasFeeder;
    private bool hasFlatbed;
    private bool hasScanners;
    private bool hasSetupDialog;
    private bool isScannerDropDownOpen;
    private TwainOptions twainOptions;

    /// <summary>
    /// Initilializes a new instance of the <see cref="TwainOptionsViewModel"/> class.
    /// </summary>
    /// <param name="options">The <see cref="TwainOptions"/> object of twain specific options.</param>
    /// <param name="scanners">A collection of <see cref="Scanner"/> objects available for selection.</param>
    /// <param name="windows">The <see cref="IShellWindows"/> object used to display windows to the user.</param>
    /// <param name="shell">The <see cref="Shell"/> to be used to perform asynchronous tasks.</param>
    public TwainOptionsViewModel(TwainOptions options, IEnumerable<Scanner> scanners, IShellWindows windows, Shell shell)
    {
        Options = options;
        
        ColorFormats       = new ObservableCollection<string>();
        ScannerResolutions = new ObservableCollection<int>();
        ScannerSources     = new ObservableCollection<string>();
        Scanners           = new ObservableCollection<Scanner>(scanners);

        ((ObservableCollection<Scanner>)Scanners).CollectionChanged += OnScannersCollectionChanged;

        HasScanners = Scanners.Any();

        ClearTwainLogCommand   = new TwainClearLogCommand(shell, windows);
        FindScannersCommand    = new FindScannersCommand(shell, Scanners);
        OpenTwainLogCommand    = new TwainOpenLogCommand(shell);
        SetupDialogCommand     = new TwainSetupDialogCommand(shell, this);
        SetTwainDsmCommand     = new TwainSetDsmCommand(shell, this);
        SetTwainLogCommand     = new TwainSetLogCommand(shell, this);
        SetTwainOptionCommand  = new TwainSetOptionCommand(shell, this);
        SetTwainScannerCommand = new TwainSetScannerCommand(shell, this);
    }

    /// <summary>
    /// Gets the command that is responsible for clearing the twain log.
    /// </summary>
    public ICommand ClearTwainLogCommand
    {
        get;
    }

    /// <summary>
    /// Gets a collection view of color formats.
    /// </summary>
    public ICollection<string> ColorFormats
    {
        get;
    }

    /// <summary>
    /// Gets the command that is responsible for detecting TWAIN compatible scan devices.
    /// </summary>
    public IAsyncCommand FindScannersCommand
    {
        get;
    }

    /// <summary>
    /// Gets a value indicating whether the twain device supports blank page detection.
    /// </summary>
    public bool HasBlankPageDetection
    {
        get => hasBlankPageDetection;
        set
        {
            hasBlankPageDetection = value;
            NotifyPropertyChanged();
        }
    }

    /// <summary>
    /// Gets a value indicating whether the twain device supports duplex scanning.
    /// </summary>
    public bool HasDuplex
    {
        get => hasDuplex;
        set
        {
            hasDuplex = value;
            NotifyPropertyChanged();
        }
    }

    /// <summary>
    /// Gets a value indicating whether the selected scanner has a feeder source.
    /// </summary>
    public bool HasFeeder
    {
        get => hasFeeder;
        set
        {
            hasFeeder = value;
            NotifyPropertyChanged();
        }
    }

    /// <summary>
    /// Gets a value indicating whether the selected scanner has a flatbed source.
    /// </summary>
    public bool HasFlatbed
    {
        get => hasFlatbed;
        set
        {
            hasFlatbed = value;
            NotifyPropertyChanged();
        }
    }

    /// <summary>
    /// Gets a value indicating whether the are scanners available for selection.
    /// </summary>
    public bool HasScanners
    {
        get => hasScanners;
        private set
        {
            hasScanners = value;
            NotifyPropertyChanged();
        }
    }

    /// <summary>
    /// Gets a value indicating whether the twain devices supports a setup dialog from this application.
    /// </summary>
    public bool HasSetupDialog
    {
        get => hasSetupDialog;
        set
        {
            hasSetupDialog = value;
            NotifyPropertyChanged();
        }
    }

    /// <summary>
    /// Gets or sets a value that indicates whether the scanner drop down should be open.
    /// </summary>
    public bool IsScannerDropDownOpen
    {
        get => isScannerDropDownOpen;
        set
        {
            isScannerDropDownOpen = value;
            NotifyPropertyChanged();
        }
    }

    /// <summary>
    /// Gets the command that is responsible for opening the twain log.
    /// </summary>
    public ICommand OpenTwainLogCommand
    {
        get;
    }

    /// <summary>
    /// Gets or sets the twain specific application options.
    /// </summary>
    public TwainOptions Options
    {
        get => twainOptions;
        set
        {
            twainOptions = value;
            NotifyPropertyChanged();
        }
    }

    /// <summary>
    /// Gets the view for the scanner resolutions collection.
    /// </summary>
    public ICollection<int> ScannerResolutions
    {
        get;
    }

    /// <summary>
    /// Gets a collection view for the available scanner devices.
    /// </summary>
    public ICollection<Scanner> Scanners
    {
        get;
    }

    /// <summary>
    /// Gets a collection view of the current scanner's available acquisition sources.
    /// </summary>
    public ICollection<string> ScannerSources
    {
        get;
    }

    /// <summary>
    /// Gets the command that is responsible for showing the twain device setup dialog.
    /// </summary>
    public IAsyncCommand SetupDialogCommand
    {
        get;
    }

    /// <summary>
    /// Gets the command responsible for changing the TWAIN DSM.
    /// </summary>
    public IAsyncCommand SetTwainDsmCommand
    {
        get;
    }

    /// <summary>
    /// Gets the command responsible for enabling or disabling twain device logging.
    /// </summary>
    public IAsyncCommand SetTwainLogCommand
    {
        get;
    }

    /// <summary>
    /// Gets the command responsible for changing a twain option.
    /// </summary>
    public IAsyncCommand SetTwainOptionCommand
    {
        get;
    }

    /// <summary>
    /// Gets the command responsible for changing a twain option.
    /// </summary>
    public IAsyncCommand SetTwainScannerCommand
    {
        get;
    }

    private void OnScannersCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        HasScanners = Scanners.Count > 0;

        IsScannerDropDownOpen = HasScanners && Options.Device == null;
    }
}

It exposes one model, TwainOptions to which the view binds directly to its values because this model is a) part of this application codebase b) supports change notification c) does not require a data template for its type and concluding d) there is no reason to make the view model provide a facade for it or create another view model.

Considering that "commands are part of the view model", but simply encapsulated into their own classes, they do the bulk of work reading and writing property values of both the view model and model classes. I hope you see the prospect here; that the alternative is to have all your command logic in the view model, then it is massive, but rather we have it separated out into various command classes, thus have a maintainable codebase.

To conclude, and I feel my answer is still very bad, I feel that most developers do MVVM wrong. That if your view models are "fat" the application becomes very complex and difficult to maintain.

MVVM is a wonderful pattern, and it still comes with many complexities, but I think the pattern has lost its original definition and meaning as it seems to be very difficult to find good information on it, and thus people still ask this question to this very day.

In short, the view model is simply there to "model the view" and provide a data-binding source that you can interact with from code, but the bulk of your application logic, business logic, data access logic should entirely be in the model layer. How this model layer looks and is organized is up to you.

I will further add that view models have another important purpose, at least in WPF for data templates. You should generally have a view model for each type of data template or visa-vera (unless they're shared) because view models are the construct the view understands. In my application, it is also less common for the view to bind directly to models.

Because: you may or may not control the model. They may not support change notification, therein lies another purpose of the view model, to abstract and provide this. You want views and view models to change together. You want views/view models and models to change independently of one another.

MVVM also gets difficult when you introduce asynchronous programming and multithreading, and you must decide how to handle this, but there are practices for that also such as true asynchronous methods and synchronous library code and letting the UI decide when to run a task (e.g. Task.Run).

The question and answers are complex, I feel I could author an entire book to answer this one simple question you asked.

I hope I provided at least some valuable perspective for you and I hope it helps you greatly.

1
  • 3
    great answer, very informative thank you! Commented Jun 9, 2022 at 13:35

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.