I have used ValueConverters
in some cases and put the logic in the ViewModel
in others. My feeling is that a ValueConverter
becomes part of the View
layer, so if the logic is really part of the View
then put it there, otherwise put it in the ViewModel
.
Personally I don't see a problem with a ViewModel
dealing with View
-specific concepts like Brush
es because in my applications a ViewModel
only exists as a testable and bindable surface for the View
. However, some people put a lot of business logic in the ViewModel
(I do not) and in that case the ViewModel
is more like a part of their business layer, so in that case I wouldn't want WPF-specific stuff in there.
I prefer a different separation:
View
- WPF stuff, sometimes untestable (like XAML and code-behind) but also ValueConverter
s
ViewModel
- testable and bindable class that is also WPF-specific
EditModel
- part of the business layer that represents my model during manipulation
EntityModel
- part of the business layer that represents my model as persisted
Repository
- responsible for persistence of the EntityModel
to the database
So, the way I do it, I have little use for ValueConverter
s
The way I got away from some of your "Con's" is to make my ViewModel
's very generic. For instance, one ViewModel
I have, called ChangeValueViewModel
implements a Label property and a Value property. On the View
there's a Label
that binds to the Label property and a TextBox
that binds to the Value property.
I then have a ChangeValueView
which is a DataTemplate
keyed off of the ChangeValueViewModel
type. Whenever WPF sees that ViewModel
it applies that View
. The constructor of my ChangeValueViewModel
takes the interaction logic it needs to refresh its state from the EditModel
(usually just passing in a Func<string>
) and the action it needs to take when the user edits the Value (just an Action
that executes some logic in the EditModel
).
The parent ViewModel
(for the screen) takes an EditModel
in its constructor and just instantiates the appropriate elementary ViewModel
s such as ChangeValueViewModel
. Since the parent ViewModel
is injecting the action to take when the user makes any change, it can intercept all of these actions and take other actions. Therefore, the injected edit action for a ChangeValueViewModel
might look like:
(string newValue) =>
{
editModel.SomeField = newValue;
foreach(var childViewModel in this.childViewModels)
{
childViewModel.RefreshStateFromEditModel();
}
}
Obviously the foreach
loop can be refactored elsewhere, but what this does is take the action, apply it to the model, then (assuming the model has updated its state in some unknown way), tells all the child ViewModel
s to go and get their state from the model again. If the state has changed, they are responsible for executing their PropertyChanged
events, as necessary.
That handles the interaction between, say, a list box and a details panel quite nicely. When the user selects a new choice, it updates the EditModel
with the choice, and the EditModel
changes the values of the properties exposed for the detail panel. The ViewModel
children that are responsible for displaying the detail panel information automatically get notified that they need to check for new values, and if they've changed, they fire their PropertyChanged
events.
It's the same approach as with any other major refactoring.
Built unit tests to validate the behavior of the existing code.
Identify and isolate small sections of code that can be refactored.
Refactor one section at a time.
Verify that all unit tests (see step 1) continue to pass. If they fail, fix your refactoring.
Rinse and repeat until you regret the day you ever decided to refactor the code base.
Eventually you'll complete the refactoring. Swear off code-behind and promise yourself that you'll never, ever make those mistakes again.
As far as how to separate out business logic from UI logic within MVVM. For the most part, all of your business logic should live at your Model layer or lower. All UI related logic, including properties to be bound to, will live at the View and ViewModel layer.
A good rule of thumb on "Is it business logic or UI logic" is the following. If the behavior of the code should persist regardless of what the UI is, that's business logic. If the code's behavior is tied to the presentation of the data, that's UI logic.
Best Answer
To answer the question, Yes, each view should have its own View Model. But there is no need to model the entire hierarchy. Only what the view needs.
The problem I had with most online resources regarding MVVM:
In most examples, the View is almost 1-to-1 mapping of the Model. But in my scenario, where there are different views for different facets of the same Model, I find myself stuck between two choices:
One monolithic view model that is used by all other view models
Or one view model for each view
But both are not ideal.
The Model-oriented View Model (MVM), while low in code duplication, is a nightmare to maintain
The View-oriented View Model (VVM) produces highly-specialised classes for each view, but contains duplicates.
In the end, I decided that having one VM per View is easier to maintain and code for, so I went with the VVM approach.
Once the code is working, I began refactoring all common properties and operations into its current, final form:
In this final form, the common view model class is composed into each VVM.
Of course, I still have to decide what is considered common/specialised. And when a view is added/merged/deleted, this balance changes.
But the nice thing about this is, I am now able to push up/down members from common to VVM and vice versa easily.
And a quick note regarding keeping the objects in-sync:
Having a Common View Model takes care of most of this. Each VVM can simply have a reference to the same Common View Model.
I also tend to start with simple callback methods, and evolving to event/observer if the need for multiple listeners arise.
And for really complex events (ie, unexpected cascading updates), I would switch over to using a Mediator.
I do not shy away from code where a child has a back reference to its parent. Anything to get the code working.
And if the opportunity to refactor arise, I would take it.
The lessons I learnt: