Oct 25, 2011

Reusable WPF

One of the immediate benefits I saw in moving to WPF was the ability to design a small (or large) user control and reuse it, both multiple times in the same application and in some cases across multiple applications. I have now done both, and even though I'm a relative newcomer to WPF, I feel there are a few simple techniques worth documenting that make the process of reusing components easier and cleaner.

Who's Got the DataContext

The biggest hurdle for me was binding to the proper data context for the control. Even in the same application there is no guarantee that a user control will be nested in the same chain. Obviously across applications you can't rely on the same data context being present. This means in general you don't want the actual data context to be set anywhere in the view, i.e. not in the xaml and not in the code behind. This doesn't mean you can't bind to the properties by name. In my case I create a View/ViewModel pairing for every reusable control. The two go together everywhere just like an old married couple. The property names will never change. Not having a DataContext does mean that tools like Resharper will put a info icon under the property letting you know it couldn't find a DataContext to resolve the property against. Not that big a deal, and if you want, you can always set the design time DataContext to make sure you got the names right. You'll probably want to remove that design time context once you plan on reusing the control across multiple solutions.  Here's how one tab in the final solution was structured.

A Real World Example



Working with the Reusable Control

image The line item navigation box for this order entry system needs to be used on multiple tabs. On some tabs it will be nested directly within the the GroupBox that holds the line items on other tabs (like the one shown) it may be a level deeper inside a grouping of controls for the line items. The user control takes up only about 40 lines of xaml (included below)  and does nothing more than allow the user to jump to a particular line item (there can be hundreds) and scroll that line item into view, or bring up a dialog allowing the user to search by the line item description and jump to that line item again bringing it into view. There is NOTHING in the code behind except the default call to  InitializeComponent method call. All the logic for the control is in the LineItemNavViewModel.  The question is where do I put a property that returns the LineItemNavViewModel such that the control can find it.

The Navigation

While the line item navigation box may be in one of several container controls, and those controls may themselves be nested in other controls, they all only make sense in the context of an actual order. Therefore I decided to add a property at the OrderEntryViewModel that holds my LineItemNavViewModel.

The question becomes: how do I reach up from inside a control that already has an assigned data context to a parent control an unknown number of levels above that has a different data context? It turns out it's not that hard. The syntax appears a little convoluted but the RelativeSource attribute is very flexible.

For the sake of completeness, let me specify how the parent container is used. The LineItemNavView sits in another control LineItemActionsView (the red outlined box in the image).  The code for that row looks like this:

<Order:LineItemActionsView Grid.Row="0"
                       DataContext="{Binding LineItemActionVm}" />

You can see the DataContext is set to LineItemActionVm and not to the parent OrderEntryViewModel.   In the LineItemActionsView.xaml I specify the  LineItemNavView control as follows:

<Shared:LineItemNavView Grid.Column="1"
                       DataContext="{Binding RelativeSource={RelativeSource
    FindAncestor,AncestorType={x:Type Order:OrderTabLineItemsView}}, Path=DataContext.LineItemNavVm}" />

Shared:  and Order: are  just  xmlns paths to the clr-namespaces that hold my xaml code for the respective user controls. The RelativeSource is specifying that somewhere up the chain of controls holding the LineItemActionsView is a user control of type OrderTabLineItemsView (the View that holds the entire tab). The Path portion of the binding says that I want to reuse the DataContext of  that view, and in that DataContext there will be a property named LineItemNavVm that holds my LineItemNavViewModel. 

There were two things that took me a while to discover, one was just the rather strange double use of RelativeSource, and the second was discovering that I had to explicitly state in the path, that I wanted the DataContext. It seemed obvious once I discovered it but without a rather casual reference to it on StackOverflow I might still be struggling. The Ancestor I specified is the "View". The view doesn't have any of my properties since in MVVM I have nothing in the code behind. The data context associated with the view holds the View Model and it does have the properties I'm interested in.  So explicitly prefixing the property with the DataContext makes it look for the property on my view model.

In my case everywhere I reuse this control I'll be able to use this same Binding but that's only because in this application I know there will always be an ancestor Order:OrderTabLineItemsView. If there weren't it wouldn't be a problem, it just means I need to specify a RelativeSource that makes sense for the application where I want to use the control. Remember the reusable control knows nothing about its DataContext, it just references the properties on its ViewModel. As long as you specify where that ViewModel can be found, the control will work. In fact if I saw this particular control as being useful across multiple applications I would move it into its own assembly, or at least into an assembly with other related reusable controls.

There are other ways to use the RelativeSource binding. You can specify another control explicitly using an x:Type of UserControl and the AncestorLevel attribute to specify the level where the UserControl of interest can be found. This struck me as a rather fragile way to do things but nonetheless might be prove to be a useful technique to know about. Below as promised is the entire XAML of the little user control LineItemNavView. Those three properties, MaxLineItem, CurrentLineItem and FindLineItemCommand are all in the LineItemNavViewModel class (they are shown in red because the xaml can't resolve them because it doesn't know the data context.).


LineItemNavView xaml
  1. <UserControl x:Class="HssOrderTracker.View.OrderEntry.Shared.LineItemNavView"
  2.              xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3.              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  4.              xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  5.              xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  6.              xmlns:extToolkit="http://schemas.microsoft.com/winfx/2006/xaml/presentation/toolkit/extended"
  8.              mc:Ignorable="d"
  9.              d:DesignHeight="300" d:DesignWidth="300">
  11.     <GroupBox Header="Line Item Nav."
  12.               Style="{StaticResource gbBackground}"
  13.               Foreground="Black">
  14.         <Grid Margin="0,0,0,2">
  15.             <Grid.ColumnDefinitions>
  16.                 <ColumnDefinition Width="auto" />
  17.                 <ColumnDefinition Width="auto" />
  18.             </Grid.ColumnDefinitions>
  20.             <extToolkit:IntegerUpDown Grid.Column="0"
  21.                                       Style="{StaticResource nudSpacingTight}"
  22.                                       Minimum="1"
  23.                                       Maximum="{Binding MaxLineItem}"
  24.                                       Value="{Binding CurrentLineItem}"
  25.                                       UseLayoutRounding="False"
  26.                                       IsTabStop="False" />
  28.             <Button Style="{StaticResource btnStyleNav}"
  29.                     Grid.Column="1"
  30.                     Command="{Binding FindLineItemCommand}"
  31.                     IsTabStop="False">
  32.                 <StackPanel Orientation="Horizontal">
  33.                     <Image Source="/Artwork/Find.png"
  34.                            Style="{StaticResource btnImage}" />
  36.                 </StackPanel>
  37.             </Button>
  38.         </Grid>
  40.     </GroupBox>
  41. </UserControl>

For the insatiably curious here is the LineItemNavViewModel code:

  1. using System.Windows;
  2. using System.Windows.Input;
  3. using HssOrderTracker.View.Dialogs;
  4. using JetBrains.Annotations;
  5. using Syncor.MvvmLib;
  6. using Syncor.Ninject;
  8. namespace HssOrderTracker.ViewModel.OrderEntry
  9. {
  10.     [IoC]
  11.     public class LineItemNavViewModel : ViewModelBase
  12.     {
  13.         private readonly OrderEntryViewModel _orderEntryViewModel;
  15.         public LineItemNavViewModel(OrderEntryViewModel orderEntryViewModel)
  16.         {
  17.             _orderEntryViewModel = orderEntryViewModel;
  18.         }
  21.         //Take and int and pass it to the OrderEntryViewModel which can turn it into a OrderLineItemViewModel
  22.         //and raise the property change to the view.
  23.         private int _currentLineItem;
  24.         [UsedImplicitly]
  25.         public int CurrentLineItem
  26.         {
  27.             get { return _currentLineItem; }
  28.             set
  29.             {
  30.                 if (value == _currentLineItem) return;
  31.                 _currentLineItem = value;
  32.                 _orderEntryViewModel.SetCurrentLineItem(_currentLineItem-1);
  33.                 RaisePropertyChanged("CurrentLineItem");
  34.             }
  35.         }
  37.         [UsedImplicitly]
  38.         public int MaxLineItem
  39.         {
  40.             get { return _orderEntryViewModel.LineItems.Count; }
  41.         }
  43.         #region FindLineItemCommand
  45.         private RelayCommand _findLineItemCommand;
  47.         [UsedImplicitly]
  48.         public ICommand FindLineItemCommand
  49.         {
  50.             get { return _findLineItemCommand ?? (_findLineItemCommand = new RelayCommand(x => FindLineItemCommandExecute(), x => FindLineItemCommandCanExecute)); }
  51.         }
  53.         public bool FindLineItemCommandCanExecute
  54.         {
  55.             get { return _orderEntryViewModel.LineItems.Count > 1; }
  56.         }
  58.         private void FindLineItemCommandExecute()
  59.         {
  60.             FindLineItemDlogView find_dlog_view = new FindLineItemDlogView(_orderEntryViewModel.LineItems);
  61.             find_dlog_view.Owner=Application.Current.MainWindow;
  62.             if ( find_dlog_view.ShowDialog() == true)
  63.             {
  64.                 if (find_dlog_view.SelectedLineNumber != -1)
  65.                 {
  66.                     CurrentLineItem = find_dlog_view.SelectedLineNumber + 1;
  67.                 }
  68.             }
  70.         }
  71.         #endregion
  73.     }
  74. }

About Me

My photo
Tod Gentille (@todgentille) is now a Curriculum Director for Pluralsight. He's been programming professionally since well before you were born and was a software consultant for most of his career. He's also a father, husband, drummer, and windsurfer. He wants to be a guitar player but he just hasn't got the chops for it.