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?
- Does it have I/O, Network, or data access related code (including abstractions like Entity Framework)?
- Does it have a lot of logic that has nothing to do with data-binding or view concerns?
- Does it actually model your view?
- Is all the command logic in the view model?
- 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
PropertyChanged
e
- 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.