Playing with ViewModel in WPF
Please don’t read this post as some sort of lesson in best-practice for WPF. It’s purely about my (mis)adventures with the Model-View-ViewModel (MVVM) pattern so far, in which I totally misuse and abuse the pattern to get something working.
We’ve been working on a WPF navigation-style application that’s essentially a big load of CRUD. It has a bunch of pages, and they all have a very similar layout:
After creating the seventh or eighth version of this page, I started wondering whether we couldn’t just have one “view” page with this layout, and a bunch of “ViewModel” classes that provide the page with its logic. We could use WPF’s DataTemplate system to customize what the list displayed, and its Command system for the buttons and “extra” commands.
I found this article by Josh Smith on MSDN and gave it a quick skim. My one big take-away from it was his RelayCommand class, which is a lightweight, lambda-based ICommand implementation. So I stole that and started spiking.
So here’s my ViewModel class in all its glory. As you can see, it has a command for each button (Find, New, Edit and Delete). They’re instances of RelayCommand which are tied to some virtual methods on the class. When I want a new ViewModel I simply derive from this class and override the methods like CanDelete(object item) or CreateNew().
Notice that the class has a property called SearchCriteriaControl. This is of type UserControl, and represents the “Search criteria” section in my sketched-out page above. That way a ViewModel can customize how the user searches for its items.
The other thing I’ve done here is given my ViewModel SelectedValue and SelectedValuePath properties. These are dependency properties, and are created with FrameworkPropertyMetadata.Journal = true, so when you navigate away from the page and then back again, it remembers which item you last selected in the ListBox.
Along the way I have also added two properties to RelayCommand. One is “Text” so that I can customize what the buttons show (certain objects might show “Disable” rather than “Delete”, for example). The other is a boolean “IsAvailable” property which I set to false if I want the button to disappear completely. Certain objects can be created and deleted but never edited, and it would be frustrating for a user to see an Edit button that’s always disabled.
Here’s a snippet of XAML from my view page:
<StackPanel> <ContentControl x:Name="searchCriteriaControl" Content="{Binding SearchCriteriaControl}" Margin="0,0,0,5" TabIndex="0" IsTabStop="False" Keyboard.PreviewKeyDown="searchCriteriaControl_PreviewKeyDown" /> <Button Command="{Binding FindCommand}" Width="75" HorizontalAlignment="Right" Content="_Find" TabIndex="1" /> </StackPanel>
So you can see that I’m presenting the user with the customized Search Criteria UI, and a “Find” button that tells the ViewModel to perform the search. The PreviewKeyDown event handler is so the user can always press Enter to perform the search. Here’s the strip of buttons along the bottom:
<DockPanel DockPanel.Dock="Bottom" LastChildFill="False" Margin="10"> <DockPanel.Resources> <BooleanToVisibilityConverter x:Key="b2v"/> <Style TargetType="Button"> <Setter Property="DockPanel.Dock" Value="Right"/> <Setter Property="Margin" Value="5,0,0,0"/> <Setter Property="Width" Value="75"/> <Setter Property="Content"
Value="{Binding Command.Text,RelativeSource={x:Static RelativeSource.Self}}" /> <Setter Property="Visibility"
Value="{Binding Command.IsAvailable,RelativeSource={x:Static RelativeSource.Self},Converter={StaticResource b2v}}" /> </Style> </DockPanel.Resources> <Button Command="{Binding DeleteCommand}" CommandParameter="{Binding SelectedItem,ElementName=listBox1}" TabIndex="5"/> <Button Command="{Binding EditCommand}" CommandParameter="{Binding SelectedItem,ElementName=listBox1}" TabIndex="4" /> <Button Command="{Binding NewCommand}" TabIndex="3" /> </DockPanel>
So each button is bound back to its corresponding command, and also pulls its visibility and content from the command’s IsAvailable and Text properties, respectively.
Lastly, here’s the list of “extra” commands at the bottom-left of the page:
<GroupBox DockPanel.Dock="Bottom" Header="Extras" Padding="5"> <GroupBox.Style> <Style TargetType="{x:Type GroupBox}"> <Style.Triggers> <DataTrigger Binding="{Binding ExtraCommands.Count}" Value="0"> <Setter Property="Visibility" Value="Collapsed"/> </DataTrigger> </Style.Triggers> </Style> </GroupBox.Style> <ItemsControl DockPanel.Dock="Bottom" ItemsSource="{Binding ExtraCommands}"> <ItemsControl.ItemTemplate> <DataTemplate> <TextBlock Margin="0,3">
<Hyperlink Command="{Binding}"><TextBlock Text="{Binding Text}"/></Hyperlink>
</TextBlock> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl> </GroupBox>
So we simply display the extra commands as a list of hyperlinks inside a GroupBox, and hide the entire thing if there aren’t any extra commands.
Now the big reveal. Here’s what my page looks like with a derived ViewModel driving it:
So the “Edit” and “Delete” buttons are disabled because the selected item is read-only (a property on the model). You can see that I’ve provided my own Search Criteria control (a label and a date TextBox). The big “Entries” label at the top is bound to the “Title” property on the ViewModel so it changes depending on what type you’re dealing with.
This is working pretty well in my spike. I haven’t yet hit any major stumbling blocks, and I’ve managed to get a few different view models working, including one that swaps out the search criteria for a simple CheckBox (“active only”).
One thing that’s still fuzzy right now is how much the ViewModels should know about my application. For example, when the user clicks the “New” button, the ViewModel will have to navigate to another page; but should it know which page it needs to navigate to, or should I be “injecting” that into the ViewModel with a Func<Page> or something?
More to come! Leave me a comment and tell me where you think I’m going wrong!
Comments
# Michael A. Smith
28/05/2009 9:58 PM
I like your solution; it's very good.
The way I like to do things isn't usually popular, but I have been preferring an approach of, rather than creating specific view models to hold commands and the like, I prefer to just augment [bindable] versions of the models with AttachedBehaviours which provide the properties for commands I want to bind to. I prefer to live in a Dynamic, composable land rather than a Static, pseudo-composable one.
For your solution, the approach I'd take for handling the "New" command depends on how the ViewModel gets the instance of the command. If the command is injected, the problem exists in a different place than if the command's implementation is local to the ViewModel.
# mabster
28/05/2009 10:17 PM
Thanks for the reply, Mike.
My "New" command is sort of stuck between two worlds. For example, if I'm in the "FooViewModel" then the "New" command has to:
1. Instantiate a new Foo, and populate it with meaningful values
2. Navigate to a page so that the user can edit the Foo and save it.
So #1 there is not UI-related, but #2 is. That's why I'm considering "injecting" a Func<Page> so that #2 can be performed in a UI-independent way - the ViewModel wouldn't have to know which page it's navigating to in order to edit the item it has just created.
I might be totally barking up the wrong tree - perhaps it's totally ok for the ViewModel to have intimate knowledge of its host application. Certainly my reasons for doing this are more about saving from having to create the same page over and over rather than anything like separation of concerns.
# Michael A. Smith
28/05/2009 10:46 PM
One of the "beautiful" things about MVVM approaches is that there is no "wrong" tree to bark up (yet). MVVM is, to me, opinionated
I'd say that your application does need to have some "opinionation" with respect to how MVVM is going to be conducted. This is to say, you might deciced to treat your VM's like bindable Presenters, or have them more lightweight as View or model-specific bindable data structures (and have presentation/control logic in a Presenter). Try it one way or both ways and you'll develop experience and preferences. In the case of a given view, however, complexity will relegate you to conform to at least one consistent way.
I'd say that you'll likely get more favourable results for this scenario (and in general) by keeping your ViewModel as dumb and lightweight as possible, and have some other coordinator, like a presenter, set it up. When the presenter is directed to show a particular type of collection, it might resolve some sort of handler from a factory for the type of list you're about to show. This handler can provide the actual implementation of the commands for the type of data you are showing.
Or maybe I'm making things too complex? I do have a tendency to do that ;)
# jman
12/10/2009 12:59 AM
I am curious as to how you were able to pass your command parameters over through the RelayCommand. I have seen a few posts about it, but none of them seem to work.
# mabster
14/10/2009 12:41 PM
Not sure, jman - it has just worked for me. The RelayCommand constructor takes Action<object> lambdas for its Execute and CanExecute handlers, so the command parameters are passed directly into there via that object parameter. Has that not been working for you?
# FrancescoG.
14/04/2010 8:17 PM
Great post Matt.
I have similar considerations like you. I see this problem like viewstate in ASP.NET pages. But in web pages viewstate can store "application state" too while in WPF application it seems to be stored only the real "VIEW" state. An Example. Two related combobox, with countries and cities. The city selection depends on the country selection. In this model, if I the two sources are not mapped to the two combos, I need to story the "Application" state too, and not only the "View" state. It sounds like "event history". That do you think?