vrijdag 24 april 2009

A Master-Detail ViewModel

Introduction

A while ago I attempted to write a Master/Detail viewmodel . The result you can find here. This article was a reaction on a video Beth Massi showed here. It’s a very nice tutorial on how to create a master-detail data entry form in WPF. However you have to type quite a lot of code each time you want to create your master-detail forms. So I thought I should write a viewmodel that does most of the work. My first attempt worked pretty good but since then I read some wonderful WPF articles like this one of Josh Smith. In it he describes some viewmodels that can be used as base classes for my own viewmodels.



The ViewModelBase base class

Below you see a diagram of all the viewmodels I use in this article.
image
The base class for all viewmodels is the ViewModelBase class. I changed the base class of Josh Smith a little because I don’t like using strings in my code. The base class uses the INotifyPropertyChanged interface and every property that changes its value can notify the change by calling a function like:


OnPropertyChanged("Position")
The code would look nicer and you wouldn’t get runtime errors if you typed a string wrong if the code would look like this.


RaisePropertyChanged(() => Position);
You can do this by using expression trees. I copied the code for this from an Italian blog by this guy, but more people posted code like this. First he uses an extension method:
    public static class PropertyExtensions
    {
        public static PropertyChangedEventArgs CreateChangeEventArgs<T>(this Expression<Func<T>> property)
        {
            var expression = property.Body as MemberExpression;
            var member = expression.Member;
            return new PropertyChangedEventArgs(member.Name);
        }
    }
 
The function below must now be added to the base class:
        protected void RaisePropertyChanged<T>(Expression<Func<T>> property)
        {
            PropertyChangedEventHandler handler = this.PropertyChanged;
            if (handler != null)
                handler(this, property.CreateChangeEventArgs());
        }
Every viewmodel that derives from the base class can now call the RaisePropertyChanged method with a property as its parameter. The property name will pop up now in the method parameter thanks to Intellisense.

NavigationViewModel

The Master-Detail viewmodel will derive from the NavigationViewModel and can be used by all kind of collections. I created the NavigationViewModel so that you can also use it as a base class for viewmodels that manipulate lists and observable collections. The NavigationViewModel is an abstract class that has all the commands to change and move through a collection. Also there’s a property for the position of the current item and the number of items in a collection. The NavigationViewModel creates a collection of CommandViewModels. I changed the CommandViewModel from Josh Smith’s article. My CommandViewModel now has properties for images and tooltips, and looks more like the RibbonCommand in the wpf toolkit. In the example program these extra properties aren’t used by the way. If you look at the code below you see that the property NavigationViewModels is a list of all the commands in the NavigationViewModel. If you look at the CreateCommands method you’ll notice that the third viewmodel in the command list is the NavigationViewModel itself. This is used in the WPF NavigationControl to show the position of the current selected item and the number of items.
    /// <summary>
    /// ViewModel for navigating and manipulating collections
    /// </summary>
    public abstract class NavigationViewModel : ViewModelBase, INavigationViewModel
    {
        ObservableCollection<ViewModelBase> _navigationViewModels;
 
        /// <summary>
        /// Initializes a new instance of the <see cref="NavigationViewModel"/> class.
        /// </summary>
        public NavigationViewModel()
        {
            VisibleCommandText = false;
        }
 
        /// <summary>
        /// Gets the viewmodels of the <see cref="NavigationViewModel"/>.
        /// </summary>
        /// <value>The <see cref="NavigationViewModel"/> viewmodels.</value>
        public ObservableCollection<ViewModelBase> NavigationViewModels
        {
            get
            {
                if (_navigationViewModels == null)
                {
                    List<ViewModelBase> cmds = this.CreateCommands();
                    _navigationViewModels = new ObservableCollection<ViewModelBase>(cmds);
 
                }
                return _navigationViewModels;
            }
        }
 
 
        /// <summary>
        /// Creates the CommandViewModels.
        /// </summary>
        /// <returns>a list of viewmodels</returns>
        List<ViewModelBase> CreateCommands()
        {
            return new List<ViewModelBase>
            {
                new CommandViewModel("First",FirstCommand,Properties.Resources.First_Small),
                new CommandViewModel("Previous",PreviousCommand,Properties.Resources.Previous_Small),
                this, // The navigation view model itself lies between the previous and next CommandViewModel
                new CommandViewModel("Next",NextCommand,Properties.Resources.Next_Small),
                new CommandViewModel("Last",LastCommand,Properties.Resources.Last_Small),
                new CommandViewModel("Delete",DeleteCommand,Properties.Resources.Cancel),
                new CommandViewModel("Add", AddCommand, Properties.Resources.Add_Small)
            };
        }
 
        #region INavigationViewModel Members
 
        RelayCommand _addCommand;
        /// <summary>
        /// Gets the add command.
        /// </summary>
        /// <value>The add command.</value>
        public ICommand AddCommand
        {
            get
            {
                if (_addCommand == null)
                {
                    _addCommand = new RelayCommand(param => this.Add(),
                        param => this.CanAdd);
                }
                return _addCommand;
            }
        }
 
        /// <summary>
        /// Adds an item to the collection.
        /// </summary>
        protected abstract void Add();
 
        /// <summary>
        /// Gets a value indicating whether this instance can add an item.
        /// </summary>
        /// <value><c>true</c> if this instance can add an item; otherwise, <c>false</c>.</value>
        protected abstract bool CanAdd { get; }
The rest of the class above looks more or less the same. All the command properties use a RelayCommand field with an Execute and CanExecute delegate. In the case of the AddCommand the Add method is the execute delegate and the CanAdd property is the CanExecute delegate. In the NavigationViewModel all of these delegates are abstract.

The NavigationControl

In Windows Forms there is a NavigationControl for bindable collections. WPF doesn’t have a control for this yet. Because the NavigationViewModel has a list of navigation commands, creating one now becomes very easy. I created a new UserControl that should have an object derived from the NavigationViewModel as its DataContext. This control only has one ListBox. The ItemsSource binds to the NavigationViewModels property of the NavigationViewModel . If an item of the NavigationViewModels is of the CommandViewModel type, a button is created that binds to the CommandViewModel’s Command property. If an item is of type NavigationViewModel, a string is created that shows the number of items and the position of the currently selected item. It’s easy to create a textbox for the position field, so that you can type in the position you want to move to, but I had trouble aligning the position textbox and the count string.
    <UserControl.Resources>
        <!--The standard DataTemplate for the CommandViewModel.
           This template has an image with underneath it the display name of the command-->
        <DataTemplate DataType="{x:Type vm:CommandViewModel}">
            <Button Command="{Binding Path=Command}">
                <StackPanel Orientation="Vertical">
                    <Image Height="16" Width="16" Source="{Binding Path=SmallImageSource}"/>
                    <TextBlock HorizontalAlignment="Center" Text="{Binding Path=DisplayName}" 
                               Visibility="{Binding Path=TextVisibility}"/>
                </StackPanel>
            </Button>
        </DataTemplate>
        <!--The standard DataTemplate for the NavigationViewModel.
           This template shows a text with the current position and the number of items of a collection-->
        <DataTemplate DataType="{x:Type vm:NavigationViewModel}">
            <TextBlock >
                <TextBlock.Text>
            <MultiBinding StringFormat=" {0:D} of {1:D}">
                <Binding Path="Position" Mode="OneWay" />
                <Binding Path="Count" Mode="OneWay"/>
            </MultiBinding>
                </TextBlock.Text>
            </TextBlock>
        </DataTemplate>
        <!--The Style for the ListBox.
           The commands are shown horizontally stacked-->
        <Style TargetType="{x:Type ListBox}">
            <Setter Property="ItemsPanel">
                <Setter.Value>
                    <ItemsPanelTemplate>
                        <StackPanel Orientation="Horizontal"
                          VerticalAlignment="Top"
                          HorizontalAlignment="Left"
                        />
                    </ItemsPanelTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </UserControl.Resources>
    <!--The listbox holding the viewmodels of the NavigationViewModel class-->
    <ListBox Name="commandListbox" ItemsSource="{Binding Path=NavigationViewModels}">
    </ListBox>
The navigation control now looks like this:

 Navigation

The MasterDetailViewModel

The MasterDetailViewModel uses the CollectionViewModel as its base class and implements the IMasterDetailViewModel interface. The CollectionViewModel is described in this article.
The interface tells that each master-detail viewmodel holds a list of other master-detail viewmodels. Also each viewmodel has a property called ViewSource of type CollectionViewSource.
All the abstract methods and properties in the NavigationViewModel must now be implemented in this class. Below you see that the MasterDetailViewModel is a generic class that uses the NavigationViewModel as its base class. The class has a variable called CurrentItem of type T that holds the current selected item. The viewmodel of that selected item is of type V and must have the IItemViewModel<T> interface implemented.
    /// <summary>
    /// interface for the master detail viewmodel.
    /// Each master or detail viewmodel can have a number of detail view models.
    /// Each viewmodel has a property of type CollectionViewSource.
    /// </summary>
    public interface IMasterDetailViewModel
    {
        List<IMasterDetailViewModel> Details { get; }
        Action OnMasterChanged { get; }
        CollectionViewSource ViewSource { get; }
 
    }
 
    /// <summary>
    /// Viewmodel for master detail collections
    /// </summary>
    /// <typeparam name="T">The type of items the collection holds</typeparam>
    /// <typeparam name="V">The viewmodel of the items</typeparam>
    public class MasterDetailViewModel<T, V> : CollectionViewModel<T, V>, IMasterDetailViewModel where V : IItemViewModel<T>, new()
The equivalent of the BindingSource class in windows forms is the CollectionViewSource in WPF. This class has a Source property that holds the collection. There’s a View property that enables collections to have the functionalities of current record management. The constructors for creating the master-detail relation look like this.
        /// <summary>
        /// Initializes a new instance of the <see cref="MasterDetailViewModel&lt;T, V&gt;"/> class.
        /// </summary>
        private MasterDetailViewModel()
            : base()
        {
            Details = new List<IMasterDetailViewModel>();
            OnMasterChanged = new Action(MasterChanged);
        }
 
        /// <summary>
        /// Initializes a new instance of the <see cref="MasterDetailViewModel&lt;T, V&gt;"/> class.
        /// </summary>
        /// <param name="master">The master collection</param>
        /// <param name="path">The path.</param>
        public MasterDetailViewModel(IMasterDetailViewModel master, string path)
            : this()
        {
            Binding b = new Binding();
            b.Source = master.ViewSource;
            b.Path = new PropertyPath(path);
            BindingOperations.SetBinding(ViewSource, CollectionViewSource.SourceProperty, b);
            master.Details.Add(this);
            ViewSourceChanged();
        }
 
        /// <summary>
        /// Adds new handlers to the new View's CurrentChanged event and calls View_CurrentChanged.
        /// </summary>
        protected override void ViewSourceChanged()
        {
            if (View == null) return;
            IView.CurrentChanged -= new EventHandler(View_CurrentChanged);
            IView.CurrentChanged += new EventHandler(View_CurrentChanged);
            View_CurrentChanged(null, EventArgs.Empty);
        }
You can’t just set a DataSource and a DataMember like with the BindingSource in windows forms. Instead you have to call this strange looking BindingOperations.SetBinding method to set the _viewSource field of type CollectionViewSource.
The details viewmodel is added to the master viewmodel with this method:

master.Details.Add(this);

The AddEventCallCurrentChanged method is called every time the view changes. If there’s an event handler it will be removed because you don’t want to keep adding multiple event handlers to each view. Every time the view changes position the View_CurrentChanged method is called:
        /// <summary>
        /// Handles the CurrentChanged event of the View control.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
        protected override void View_CurrentChanged(object sender, EventArgs e)
        {
            base.View_CurrentChanged(sender, e);
            foreach (IMasterDetailViewModel detail in Details)
                this._currentDispatcher.BeginInvoke(detail.OnMasterChanged, DispatcherPriority.Render);
        }
 
If the viewmodel is a master table with detail tables the OnMasterChanged action is called for each detail table. This action again calls the MasterChanged method that updates the handlers of the CurrentChanged event of the View. The MasterChanged method needs to be called with an BeginInvoke because otherwise the details of the old View property are shown instead of the current view.
The View_CurrentChanged method that is called in the CollectionViewModel base class looks like this:
       /// <summary>
        /// Handles the CurrentChanged event of the IView control.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
        protected virtual void View_CurrentChanged(object sender, EventArgs e)
        {
            if (IView == null)
                return;
            RaisePropertyChanged(() => Position);
            CurrentViewModel.Item = CurrentItem;
            RaisePropertyChanged(() => CurrentItem);
            RaisePropertyChanged(() => CurrentViewModel);
            EventHandler<CurrentChangedArgs<T, V>> temp = CurrentChanged;
            if (temp != null)
                temp(null, new CurrentChangedArgs<T, V>(CurrentItem, CurrentViewModel));
        }
In this method the CurrentItem of the CurrentViewModel are set, and all changed properties will notify the view that they have changed.
        /// <summary>
        /// Called by the master viewmodel. Adds new handlers to the View's CurrentChanged event and
        /// Notifies the Count and View properties have changed.
        /// </summary>
        void MasterChanged()
        {
            AddEventCallCurrentChanged();
            RaisePropertyChanged(() => Count);
            RaisePropertyChanged(() => View);
        }
The Add method and the CanAdd property implementation are shown below:
        /// <summary>
        /// Adds an item to the collection.
        /// </summary>
        protected override void Add()
        {
            T item = (T)View.AddNew();
            View.CommitNew();
            RaisePropertyChanged(() => Count);
        }
 
        /// <summary>
        /// Gets a value indicating whether this instance can add an item.
        /// </summary>
        /// <value>
        ///     <c>true</c> if this instance can add an item; otherwise, <c>false</c>.
        /// </value>
        protected override bool CanAdd
        {
            get
            {
                return View != null && View.CanAddNew;
            }
        }
The AddNew method of the View is called in the Add method. This creates a new item. If you want to change the item after its creation, you can add an handler to the ItemAddedEvent. The rest of the class overrides all the other abstract methods and properties of the NavigationViewModel.

Implementation

The database has a Customer, Order and an OrderDetails table. For each table a viewmodel must be created. You can also define a viewmodel for the currently selected items in the table. Below the viewmodel for an order is defined.
    /// <summary>
    /// Viewmodel of an Order item. Just to show the use of the IItemViewModel interface this one
    /// has the WorkSpaceViewModel as its base class. 
    /// </summary>    
    public class OrderWorkspaceViewModel : WorkspaceViewModel, IItemViewModel<Order>
    {
        #region IItemViewModel<Order> Members
 
        public Order Item
        {
            get;
            set;
        }
 
        #endregion
    }
 
The OrderWorkSpaceViewModel has the WorkSpaceViewModel of Josh Smith as its base class. This definition has no use in the example application but it shows how you can derive from another viewmodel and still add the needed behaviour of the IItemViewModel interface. The viewmodel for the orders table now looks like this: 
    public class OrdersViewModel : MasterDetailViewModel<Order, OrderWorkspaceViewModel>
    {
        public OrdersViewModel(CustomersViewModel vm)
            : base(vm, "Orders")
        { }
    }
The master table is the customers table. The Orders table becomes the detail table.
In Xaml all you have to do to add a navigation control and a table view to a grid is type the following two lines:
        <vw:NavigationControl DataContext="{Binding Path=Orders}" Grid.Row="2" />
        <my:DataGrid Grid.Row="3" ItemsSource="{Binding Orders.View}"/>
 
The DataContext of the navigation control is bound to the orders viewmodel. The ItemsSource of the DataGrid is bound to View property of type BindingListCollectionView of the orders viewmodel. The creation of a view for the other tables is not very different. You can view all details in the source code.

Conclusion/Remarks

I think having a master/detail viewmodel reduces the amount of code you have to type considerably. It also reduces the complexity for the end user.
The navigation control and viewmodel work good but can have some added functionality.
Also I would like to add some custom sorting, filtering, and grouping. The View property of CollectionViewSource has functionality for filtering but it doesn’t work in the sample application.
So if you have any comments or know how to improve the code please contact me. You can find the source code here.
Shout it kick it on DotNetKicks.com

1 opmerking:

Anoniem zei

Great article.