I'm struggling to find an elegant and idiomatic way of coding the following scenario using the MVVM paradigm in WPF and was wondering how other people would approach it.
I have a UserControl
in my WPF
application which I want to reuse in a number of places. The control is a filtered ComboBox
setup allowing users to refine their selections. My example is Department> Team> Person.
Filtered ComboBoxes being used in three different windows
In each scenario I might have the control configured in a slightly different manner. e.g. Window 1
might have all the departments, teams, and people; Window 2
might only display a subset of all departments; Window 3
might be locked to the user's department and team.
First (and probably worst) Solution: Give the UserControl
its own ViewModel
This works as far as I can drop the control on each window and it appears to immediately require no further work. The filtering logic is the control's ViewModel
as is the loading of all the lookup values. The problem comes when I then want to get the values and when I want to configure it slightly differently for each scenario, largely because I've broken the DataContext inheritance chain. I ended up having the control's ViewModel
subscribe to messages for configuration settings and send messages for reporting value selections and it feels like I'm fighting MVVM/WPF rather than working with it.
Second Solution: No ViewModel
for the UserControl
and Rely on the Window's ViewModel
This has the advantage of being easy to interact with the UserControl
through the Window's ViewModel
but it feels like I'm duplicating a lot of the loading of lookup values logic as well as the filtering logic.
I feel like there's an elegant solution of code-behind and MVVM but I can't seem to find it! How would you go about solving this requirement?
-
Can you rewrite the lookup and filtering logic (or some part of it) in an abstract way against one or more of IEnumerable<YourType> (or IQueriable<YourType)? If this approach doesn't introduce more problems than it solves, than you could reuse (most) of that logic and still have the ViewModel/DataContext be pluggable. For parts that you can't reuse, design a suitable interface (or overridable methods) to serve as extension points.Filip Milovanović– Filip Milovanović2020年04月16日 19:32:41 +00:00Commented Apr 16, 2020 at 19:32
5 Answers 5
To begin with, this answer seriously lacks theoretical support (i.e. explaining why). I would very much second this answer instead. It is much under-voted because it was posted more than a bit later.
Your approach should definitely also be guided by a bit of philosophy.
MVVM (or How to separate business logic from presentation logic)
Let's see what you have come up with. You have a UserControl
with DependencyProperty
-ies named Departments
, People
, Teams
, etc. How is this UserControl
not exposing business logic when it's crowded with Domain-specific nouns? Programming is a lot more about names than you may think. Contrast: TextBox.Text
and TextBox.Address
as names for properties. The second case suddenly introduces a pre-disposition. An expectation that the value of this property serves a very specific purpose. Never underestimate the names of members and the conceptual communication weight involved.
What is a UserControl?
Based on your linked answer:
A UserControl is simply an easy way to create a Control using composition. UserControls are still Controls, and therefore should solely be concerned with matters of UI.
But, is it? Based on this explanation, a proper PersonPicker
control would have been:
<PersonPicker
FirstItemsSource="{Binding PersonPickerModel.Departments}"
SecondItemsSource="{Binding PersonPickerModel.Teams}"
ThirdItemsSource="{Binding PersonPickerModel.People}"
FirstSelectedItem="{Binding PersonPickerModel.SelectedDepartment}"
SecondSelectedItem="{Binding PersonPickerModel.SelectedTeam}"
ThirdSelectedItem="{Binding PersonPickerModel.SelectedPerson}"
IsFirstItemSelectionEnabled="{Binding PersonPickerModel.IsDepartmentSelectionEnabled}"
IsSecondItemSelectionEnabled="{Binding PersonPickerModel.IsDepartmentSelectionEnabled}"
</PersonPicker>
Oh, by the way, the control should also have been named something like ThreeComboBoxControl
or ThreeHierarchiesControl
. You named it PersonPicker
because that is what you want it for. While fully re-usable, the control is not a UserControl
... it is a facility. If you need a facility for your very own needs, you sometimes have to concede: Do you really want a reusable UserControl
, or a reusable business tool? If you decide that it is a business tool, it makes perfect sense to have a specific viewmodel for it.
View-models are over-interpreted
Binding, in WPF, does not care about class types, only about property names. As long as an object of type PersonPickerModel
exists in your bound DataContext
, in your example, and it carries properties with the bound names (and, of course, the property types are compatible), everything will work properly, regardless of whether your PersonPickerModel
is named as such, or any other way. You can even define it as an object.
In short, what you have come up with is an intermediate between a UserControl
and a business tool. Even if only for the correctness of it, I suggest you reconsider, what it is that you need between the two. If you need a business tool, keep the names, create a tailor-made viewmodel. Think about this... you have a Window
or something else, and:
<PersonPicker DataContext="{Binding PersonPickerViewModel}"/>
The bindings can be placed inside the control's xaml code because you are only ever going to use it for the very specific reason of picking people. Why do you have to be so verbose in every place where you are going to reuse your control? Just pass it the ViewModel and leave the rest to that.
But there is a really clearer reason why this is better than what you are already have.
Your solution is imparting a false sense of reusability.
Designing a truly abstract "proper" UI control is hard and not to be underestimated. Look what you have come up with, three adjacent combo-boxes. What do they represent? Departments? Teams? No, it's collections. Why three? Why not four, or, even better, a variable number. A proper control pretending to cover your specific needs would have to offer a variable number of collection handling. What are these collections? Simple strings? More complicated viewmodels?
So far, we have pretty much come up with a MultiComboBox
control. Think about that, a variable number of ComboBoxes, their ItemsSource bound to a different collection, as per your needs, ViewModels can be used for that. You know how to use a ComboBox
and you definitely understand its conceptual representation, so you will not have a hard time juggling multiple of these. Thereafter, you can expose them in the code-behind through collections of ItemsSource
and SelectedItem
properties, accessible by index, maybe? You have dozens of possibilities (and loads of responsibilities, of course) because a UserControl
is abstract.
Designing a UserControl
means that nobody cares that you have Departments, Teams or People. Creating "non-generally-but-partially-reusable" UserControl
s is a bad practice but a good workaround so use that to your benefit when MVVM-ing around.
EDIT:
I'm not certain... but it feels like the selection in one ComboBox affecting the items of another could be considered a pure UI concern and something which belongs in the code-behind- depending on the selection logic, I guess.
Tomorrow, your requirements suddenly change! You don't want ComboBox
es, you want ListBox
es. Think about it, more beautiful. You select a Department in the first list, second list immediately shows you the Teams, you avoid an additional click (at the cost of some more UI space). So you decide to build another control, one with three list boxes (or ListView
s for that matter, or anything else). Do you write the filtering logic again? Copy-paste it, maybe?
Whatever it is that you are referring to as "filtering logic" is emphatically not a UI concern. Most basic UserControl
s are that basic for a good reason: they try to avoid assumptions about what you would want to do, as much as possible. They simply try to convey the user's reactions to you, the programmer. Any additional "initiative" only robs you of flexibility.
A Button
is the abstraction for "producing clicks". A ComboBox
is an abstraction for making a selection from a collection. A ListBox
too. A ListBox
does not offer anything more than a ComboBox
in terms of abstractions, it only has a different fanciness in terms of presentation. That is your keyword, there! Presentation (as in Windows Presentation Foundation).
Whenever you compose a UserControl
from these basic UserControls, you are practically risking breaking this elegantly meaningless abstraction. Your UserControls
should only "chain" abstractions together. In that sense, the UserControl
you are trying to create offers the abstraction of making three selections at once. A filtering logic is, again, emphatically NOT a UI concern. My simple way to argue about this was that you might need to change the UI and the filtering logic would have to be re-written, instead of simply being attached. That's your other keyword there. Attached, like ViewModels attach to controls.
The only job of your controls should be to model and abstract away user interaction, and user interaction does not include filtering lists anymore than the remote control of a TV includes logic for producing two-digit values (as when you click two numbers in sequence). You press the number 1 twice in a row, within 3-4 seconds, which takes you to channel 11. Do you think this wait for a second input within 3-4 seconds is "coded" into the remote control, so that the number 11 is sent to the TV after 3 seconds? Or... the signal for number 1 actually simply travels in two occurrences, 3-4 seconds apart, with the TV deciding what to do next, instead?
Well, your UserControl
is the remote control, the TV is your model. It receives two notifications about the pressing of button 1, one now, one in three seconds. The UserControl
has to ask you for collections and convey potential selections from items of the collections to you, the programmer. This is, pretty much, where its responsibilities end. The rest is... philosophy ;)
-
11/3 Thanks very much for your detailed answer - it's given me a lot to think about. I would certainly concede I have not produced a general purpose
UserControl
and if I were intending to do so then naming it, as you suggested, (ThreeComboFilter
,TriumverateFilter
, etc.) would have made more sense. I also accept, following this logic, three is an arbitrary number of filters and that supporting 2..n filters would be more generally useful.Pseudonymous– Pseudonymous2020年04月18日 16:49:21 +00:00Commented Apr 18, 2020 at 16:49 -
12/3 I don't know if I'm arguing semantics here but does talking about a
UserControl
necessarily imply that level of generality? Perhaps the link I cited does indicate that. However, I had the requirement for reusable "person picker" control and not (as yet, at least) a 2..n filter control. The solution I created is reusable within the scope of my project. I grant it might not be terribly useful to anyone else but then I don't need it to be and to expend the effort of making it more general would have meant undertaking additional effort to create something for which there is no current need.Pseudonymous– Pseudonymous2020年04月18日 16:50:25 +00:00Commented Apr 18, 2020 at 16:50 -
13/3 My other thought is with accepting a
ViewModel
for this control (whether the theoretical general purpose one or my own) I feel like I'm putting UI logic in aViewModel
. I'm not certain... but it feels like the selection in oneComboBox
affecting the items of another could be considered a pure UI concern and something which belongs in the code-behind- depending on the selection logic, I guess. Again, thanks for taking the time to leave such a comprehensive and thoughtful answer.Pseudonymous– Pseudonymous2020年04月18日 16:50:39 +00:00Commented Apr 18, 2020 at 16:50 -
These are interesting comments and concerns. I will try to briefly comment on those within the answer.Vector Zita– Vector Zita2020年04月18日 20:05:12 +00:00Commented Apr 18, 2020 at 20:05
-
@CptCoathanger I have updated the answer based on your thoughts. I tried to be as pragmatic as possible, but I am afraid I have strayed into philosophy dangerously much. Let me know if what I have written makes any sense!Vector Zita– Vector Zita2020年04月19日 08:16:52 +00:00Commented Apr 19, 2020 at 8:16
The third solution you are looking for might be DependencyProperty
https://docs.microsoft.com/en-us/dotnet/framework/wpf/advanced/dependency-properties-overview
You can use these to do all sorts of cleverness, although how elegant they are is another question.
I think the correct solution, unless you are making a super generic control, however is to have a view model. I'm not sure why its causing you problems. Its not a static shared viewmodel, you can have an instance per window and bind events accordingly
public class Window1VM
{
public UserControlVM ucvm {get;set;}
}
-
Thanks for your comment. I did end up using dependency properties in my approach I took. I'm about to add it as an answer, I'd be interested in your thoughts on it, if you have any.Pseudonymous– Pseudonymous2020年04月18日 09:27:13 +00:00Commented Apr 18, 2020 at 9:27
-
pff its been a while since i used em. my impression is that they are pretty vital to make "proper" controls. ie the kind of thing you might buy off the shelf. But the implementation has so much boilerplate and is so 'ugly' that it feels like some sort of secret underbellyEwan– Ewan2020年04月18日 09:43:07 +00:00Commented Apr 18, 2020 at 9:43
-
1We definitely have the same opinion of dependency properties: they're fundamental to WPF but as soon as you use a few of them you generate a lot of boiler plate and they're not immediately intuitive!Pseudonymous– Pseudonymous2020年04月18日 10:09:54 +00:00Commented Apr 18, 2020 at 10:09
Unless the logic is extremely intricate, it may just be easier to reimplement the functionality three times for each screen, rather than try and find a common design that integrates all three.
The danger in future is that if you have a fourth screen which requires this control but has its own special requirements, you may have to rework the logic of the common control yet again, but now you've got 4 screens to (re-)test instead of one, and you have to rework the logic in a way that is compatible with all 4 screens instead of simply specialised to each one.
I'm not sure what the name is for this pattern of behaviour, but although it seems like the code is reusable or more manageable for maintenance, that repetition is eliminated, or that it contains fewer conceptual elements, in fact what it produces is a highly complex and over-specialised element which is difficult to devise, difficult to reason about, and can rarely be reused for any later purpose without yet further adjustment.
-
Thanks for your comment. I definitely shared your concern about ending up with a overly-complex control to handle two or three simple scenarios - and the ongoing maintenance cost for additional functionality. I'm about to add the approach I took as an answer, if you've any thoughts on that.Pseudonymous– Pseudonymous2020年04月18日 09:26:06 +00:00Commented Apr 18, 2020 at 9:26
Your first and worst idea is actually the best and the most MVVM-like approach.
With a proper MVVM framework like caliburn micro you just point out the sub viewmodel on the parent model and the correct view will be rendered for that view model.
You need to make sure that viewmodel and view does not get messy though to support all cases. It can be better to have smaller modules like in this case 3 modules for each aggregate. And then combine these as you like in a more composition like way.
-
Thanks you for you comment. I did take a somewhat similar approach to this but using a POCO model, as opposed to a ViewModel. I'm about to add an answer with the approach I took and would appreciate any thoughts you have.Pseudonymous– Pseudonymous2020年04月18日 09:28:37 +00:00Commented Apr 18, 2020 at 9:28
The approach I took was largely informed by this answer to a SO question: https://stackoverflow.com/a/28815689/2571982. While user1228 is... passionate... I found their argument convincing.
The thrust of the SO answer was that a UserControl
such as I'm creating should be thought of as no different to any other framework control (DatePicker
, ComboBox
, ListBox
, etc.), i.e. interaction with it should be through dependency properties; the UI logic should be contained in the code-behind file of the control. It was also recommended that the creation of a UserControl
should be about rendering out a model, in my case a simple POCO.
<PersonPicker
Departments="{Binding PersonPickerModel.Departments}"
Teams="{Binding PersonPickerModel.Teams}"
People="{Binding PersonPickerModel.People}"
SelectedDepartment="{Binding PersonPickerModel.SelectedDepartment}"
SelectedTeam="{Binding PersonPickerModel.SelectedTeam}"
SelectedPerson="{Binding PersonPickerModel.SelectedPerson}"
IsDepartmentSelectionEnabled="{Binding PersonPickerModel.IsDepartmentSelectionEnabled}"
IsTeamSelectionEnabled="{Binding PersonPickerModel.IsDepartmentSelectionEnabled}">
</PersonPicker>
public class PersonPickerModel
{
public IEnumerable<Department> Departments { get; }
public IEnumerable<Team> Teams { get; }
...
public bool IsTeamSelectionEnabled { get; }
}
public class Window1ViewModel
{
public PersonPickerModel PersonPickerModel { get; set; }
public WindowViewModel(IDepartmentsQuery departmentsQuery)
{
PersonPickerViewModel = new PersonPickerViewModel
{
Departments = departmentsQuery.Execute(),
...
IsTeamSelectionEnabled = true;
}
}
}
I create a PersonPickerModel
in each of ViewModel and populate it accordingly. The filtering logic lives in the code-behind of the control.
This approach allows me to drop the control on any page and I get the filtering logic without additional work, I just need to construct a model to bind for configuring the particular instance of the control.