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.
I am only just approaching these questions myself but I will give the best answers I can and make some observations.
We are told that iOS' MVC expects us to break our program up into models, views and controllers which link specific views to specific models. Sadly, in practice we will have a number of views and a number of models but only one (View)Controller taking care of all the linkages. This is what leads to massive ViewControllers.
Adopting MVVM in this context means breaking the ViewController up into two parts. The part that deals directly with Views (which stays in the ViewController class proper,) and the part that deals directly with Models (which is moved into the ModelView class/struct.)
This means that only the ViewController should be sending/receiving messages from/to the Views and the only non-view object the VC should be conversing with is the ModelView. While the ModelView should be conversing with all the various Model objects and should never converse with a View.
The above notion of the ViewController/ViewModel split leeds to the following answers to your questions:
No, your MVVM implementation is not correct. Your ViewModel methods have a fundamental flaw. They should not be talking directly with any view objects so they aren't in charge of showing the user any alerts. That's the ViewController's job. The ViewModel also isn't in charge of transitioning between ViewControllers.
The ViewController should not be dealing with any model objects. It should only be conversing with Views and the ModelView. Therefore, the ViewController should not be pulling the username and password out of it's views and passing them to the ViewModel's login function. That would require the ViewController to understand something about the differences between Usernames and Passwords and when it is appropriate to use them.
Instead, the ViewController should inform the ViewModel when changes occur in the username and password fields. It should inform the ViewModel when the login button is pressed. It shouldn't have any knowledge about what model objects the viewModel needs in any particular method.
- I would expect the
validate()
function to return a Bool (better to call it isValid
and make it a computed property.) I would expect the login()
function to accept two blocks (one for pass and one for fail.) It can communicate what happened by calling the correct block. Another option would be to have the function accept one block as a parameter with a success
boolean. So something like this:
-
struct LoginViewModel {
var username: String = ""
var password: String = ""
init(username: String, password: String) {
self.username = username
self.password = password
}
var isValid: Bool {
return !username.isEmpty && !password.isEmpty
}
func login(callback: (error: NSError?) -> Void) {
let api = ApiHandler()
api.login(username, password: password, success: { (data) -> Void in
callback(error: nil)
}) { (error) -> Void in
callback(error: error)
}
}
}
- In the MVVM paradigm, ReactiveCocoa (and RxSwift is another library) are used to communicate changes in the ModelView back to the ViewController, but let me ask you this... Does your ModelView ever change in any substantive way that your ViewController doesn't know about? So far, I have not been in a situation where any other object is sending messages to my ModelView other than the ViewController it is attached to, so there isn't any way for the ModelView to mutate without the ViewController knowing it's at least possible. Because of this, I generally have an
updateUI()
function in my ViewController. Every time my ViewController sends a potentially mutating message to the ModelView, it then calls updateUI()
which examines the ViewModel and updates the UI accordingly. No need for any tricks.
-
Hope this helps you and hopefully my answer will stimulate some discussion on this question.
Best Answer
The View is the place which has intimate knowledge of your graphical runtime environment, of how color is expressed in it, and even of the fact that you are actually running within a graphical runtime environment and not as part of some unit test. (Assuming that you unit-test your Model, and your View Model, but not your View.)
So, I would think that the View is the best place for converting the "over the limit" flag to a color, so that your unit tests only need to ensure that under the right conditions, the "over-the-limit" flag is raised in your View Model.
On the other hand, if you want to give the user the ability to choose the color to display in case of an "over the limit" situation, then the color becomes part of your Model.
However, even in that case, the color chosen by the user in the event of an "over-the-limit" situation can be presented by the View Model as a completely separate entity from the "over-the-limit" flag itself, and in a completely abstracted fashion, (say, a string containing a color name,) rather than as the actual RGBA value which your runtime environment requires.
Let the View read from the View Model the "over-the-limit" flag, and if it is set, then let it also read the abstracted color which is to be displayed in this situation, and let it worry about converting the abstracted color to an RGBA value.