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.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:
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.
OnPropertyChanged("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:
RaisePropertyChanged(() => Position);
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);
}
}
protected void RaisePropertyChanged<T>(Expression<Func<T>> property)
{
PropertyChangedEventHandler handler = this.PropertyChanged;
if (handler != null)
handler(this, property.CreateChangeEventArgs());
}
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 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 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()
/// <summary>
/// Initializes a new instance of the <see cref="MasterDetailViewModel<T, V>"/> class.
/// </summary>
private MasterDetailViewModel()
: base()
{
Details = new List<IMasterDetailViewModel>();
OnMasterChanged = new Action(MasterChanged);
}
/// <summary>
/// Initializes a new instance of the <see cref="MasterDetailViewModel<T, V>"/> 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);
}
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);
}
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));
}
/// <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);
}
/// <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;
}
}
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
}
public class OrdersViewModel : MasterDetailViewModel<Order, OrderWorkspaceViewModel>
{
public OrdersViewModel(CustomersViewModel vm)
: base(vm, "Orders")
{ }
}
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}"/>
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.
1 opmerking:
Great article.
Een reactie posten