If someone asked you, what are the building blocks of MVVM, what would you say? Models, Views, and ViewModels, of course. It’s in the name. But really, there are two others:
If Commands and Events aren’t strongly represented in your application, I’d say there is a strong chance it isn’t factored very well.
One problem I see quite often is the Fat ViewModel: a long, bloated class that violates SRP by presenting complex application Commands to the view, and implementing them too.
You can tell you have a Fat ViewModel if any of the following are true:
- Instead of having a separate class for each Command, your ViewModel uses DelegateCommands (aka RelayCommands) to invoke methods on itself.
- Your ViewModel has three properties, two commands, and is 300 lines long.
- Your ViewModel has a large number of services injected into it.
- Your ViewModel tests include assertions for both presentation and application behaviour.
- Testing simple UI behaviour — e.g. a button should be disabled after it has been clicked — requires excessive mocking of dependencies.
- Your ViewModel tests frequently call xyzCommand.Execute().
- Commands cannot easily be re-used by other ViewModels. Your application is not modular.
- ViewModels update their state directly, instead of listening for global application Events.
Instead, if you extract Commands into separate classes, that publish their results via Events, you will benefit by having:
- Skinny and light ViewModels.
- Commands that can be developed and tested in isolation.
- Commands that encapsulate and mask complex logic.
- Separation of presentation logic (ViewModels) from application logic (Commands).
- A catalog of clearly defined and reusable Commands that be composed into new ViewModels.
From the cases I have seen, I have no doubt that DelegateCommands are the primary cause of Fat ViewModels — they encourage developers to implement Commands using local ViewModel methods, which results in poorly-factored code. For this reason, I consider DelegateCommands to be in the same bad code smell category as ServiceLocator: as an anti-pattern, with few legitimate uses. (One is when the Command is entirely self-contained within the ViewModel (i.e. does not collaborate with any other object), the other when genuinely delegating to another object. But in that case, the other object should probably be a Command anyway.)