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:

A common page in my app

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.

ViewModel<T>

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:

The working view page with an "Entries" ViewModel

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!