maandag 1 december 2008

Creating a master/detail viewmodel

UPDATE 4-6-2009: An article about the latest version of this program can be found here. The viewmodel described in this article has changed a lot.


I'm just starting with wpf and I read Beth Massi's code that belonged to this video. It's about creating a master/detail view in wpf. I also saw Jason Dollingers's video about creating a viewmodel: Jason Dolinger on Model-View-ViewModel I though I could give this viewmodel thing a try using as a starting point Beth Massi's code.

The Master ViewModel

First I created a generic class called CollectionModelView:


public class CollectionViewModel<T> : DependencyObject //,ICollectionViewModel
{
protected IEnumerable<T> data;
protected CollectionViewSource masterViewSource = new CollectionViewSource();

public CollectionViewModel(IEnumerable<T> list):this()
{
Data = list;
}

public CollectionViewModel()
{
AddCommand = new CommandAdd(this);
DeleteCommand = new CommandDelete(this);
PreviousCommand = new CommandPrevious(this);
NextCommand = new CommandNext(this);
}

public IEnumerable<T> Data { get { return data; }
set
{
data = value;
masterViewSource.Source = data;
MasterView = (BindingListCollectionView)masterViewSource.View;
}
}

public BindingListCollectionView MasterView
{
get { return (BindingListCollectionView)GetValue(MasterViewProperty); }
set { SetValue(MasterViewProperty, value); }
}

// Using a DependencyProperty as the backing store for MasterView.  This enables animation, styling, binding, etc...
public static readonly DependencyProperty MasterViewProperty =
DependencyProperty.Register("MasterView", typeof(BindingListCollectionView), 
typeof(CollectionViewModel<T>), new UIPropertyMetadata(null));

public CollectionViewSource MasterViewSource { get { return masterViewSource; } }

public T CurrentItem
{
get
{
return (T)GetValue(CurrentItemProperty);
}
set
{
SetValue(CurrentItemProperty, value);
}
}

// Using a DependencyProperty as the backing store for CurrentItem.  This enables animation, styling, binding, etc...
public static readonly DependencyProperty CurrentItemProperty =
DependencyProperty.Register("CurrentItem", typeof(T), typeof(CollectionViewModel<T>), 
new UIPropertyMetadata(null, new PropertyChangedCallback(OnCurrentItemChanged)));

private static void OnCurrentItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
/*CollectionViewModel<T> mv = (CollectionViewModel<T>)d;
            if (mv.CurrentViewModel != null)
                mv.CurrentViewModel.Item = (T)e.NewValue;*/
}

//public ItemViewModel<T> CurrentViewModel { get; set; }

public ICommand AddCommand { get; private set; }
public ICommand DeleteCommand { get; private set; }
public ICommand PreviousCommand { get; private set; }
public ICommand NextCommand { get; private set; }

// Not all commands are shown because they are pretty simple



/// <summary>
/// Private implementation of the Add Command
/// </summary>
private class CommandAdd : MasterBaseCommand<T>
{
public CommandAdd(CollectionViewModel<T> viewmodel): base(viewmodel) { }
            #region ICommand Members

public override void Execute(object parameter)
{
_vm.MasterView.AddNew();
_vm.MasterView.CommitNew();
}

            #endregion
}

/// <summary>
/// Private implementation of the Delete Command
/// </summary>
private class CommandPrevious : MasterBaseCommand<T>
{

public CommandPrevious(CollectionViewModel<T> viewmodel) : base(viewmodel) { }
            #region ICommand Members

public override bool CanExecute(object parameter)
{
return _vm.MasterView.CurrentPosition > 0;
}

public override void Execute(object parameter)
{
_vm.MasterView.MoveCurrentToPrevious();
}

            #endregion
}

}

The generic base class for all commands looks like this:





public abstract class MasterBaseCommand<T> : ICommand
{
protected CollectionViewModel<T> _vm;
public MasterBaseCommand(CollectionViewModel<T> viewModel)
{
_vm = viewModel;
}

        #region ICommand Members

public virtual bool CanExecute(object parameter)
{
return true;
}

public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}

public abstract void Execute(object parameter);
        #endregion
}

Now you already can create your own master/detail viewmodel:


public class OrdersViewModel : CollectionViewModel<Order>
{
public OrdersViewModel(IEnumerable<Order> orders) :
base(orders) { }
}

Create a OrdersViewModel and assigning it to the DataContext of the window. Here's an example how you can create master/detail
tables in xaml now:



        <my:DataGrid Name="dataGrid1" ItemsSource="{Binding MasterView}" 
 SelectedValue="{Binding Path=CurrentItem, Mode=OneWayToSource}" IsSynchronizedWithCurrentItem="True" Grid.Row="1" />
        <my:DataGrid Grid.Row="2" Height="Auto" Margin="0" Name="dataGrid2" VerticalAlignment="Stretch"
 ItemsSource="{Binding CurrentItem.OrderDetails}"/>
In this piece of xaml code the SelectedValue property of the datagrid binds to the CurrentItem property of the viewmodel.
The second datagrid binds to the OrderDetails.

The Details ViewModel

By deriving from the generic CollectionVewModel class we can create a DetailViewModel class.
public class DetailViewModel<V> : CollectionViewModel<V>
{
private string path;

public DetailViewModel(CollectionViewSource master, string path)
: base()
{
this.path = path;
Binding b = new Binding();
b.Source = master;
b.Path = new PropertyPath(path);
BindingOperations.SetBinding(masterViewSource, CollectionViewSource.SourceProperty, b);
MasterView = (BindingListCollectionView)masterViewSource.View;
master.View.CurrentChanged += new EventHandler(View_CurrentChanged);
}

public string Path
{
get { return path; }
}

void View_CurrentChanged(object sender, EventArgs e)
{
MasterView = (BindingListCollectionView)masterViewSource.View;
}

}
The DetailViewModel is like the CollectionViewModel except that a child table name should be assigned in the constructor.
We can now create an OrdersViewModel and an OrderDetailsViewmodel like below:
public class OrdersViewModel : CollectionViewModel<Order>
{
public OrdersViewModel(IEnumerable<Order> orders) :
base(orders) { }
}

public class OrderDetailsViewModel : DetailViewModel<OrdersViewModel>
{
public OrderDetailsViewModel(OrdersViewModel vm)
: base(vm.MasterViewSource, "OrderDetails")
{

}
}

public class DataModule
{
private OMSDataContext db = new OMSDataContext();

public OrdersViewModel Orders { get; private set; }

public OrderDetailsViewModel OrderDetails { get; private set; }

public ViewModels()
{
Orders = new OrdersViewModel(db.Orders);
OrderDetails = new OrderDetailsViewModel(Orders);
}
}

  All you have to do now is create a DataModule and connect it to the DataContext of the main window: 

this.DataContext = new DataModule();

The xaml code for the main window now looks like this:


<Window x:Class="MasterDetailViewModel.MasterDetailWindow2"
 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 Title="Window1" Height="302" Width="430" Loaded="Window_Loaded" xmlns:my="http://schemas.microsoft.com/wpf/2008/toolkit">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="36" />
            <RowDefinition Height="131*" />
            <RowDefinition Height="95*" />
        </Grid.RowDefinitions>
        <StackPanel Name="StackPanel3" Orientation="Horizontal" Grid.Row="0">
            <Button Height="25" Name="btnAdd" Width="Auto" Margin="3" Command="{Binding Orders.AddCommand}">Add Order</Button>
            <Button Height="25" Name="btnDelete" Width="Auto" Margin="3" Command="{Binding Orders.DeleteCommand}">Delete Order</Button>
            <Button Height="25" Name="btnPrevious" Width="75" Margin="3" Command="{Binding Orders.PreviousCommand}">Previous</Button>
            <Button Height="25" Name="btnNext" Width="75" Margin="3" Command="{Binding Orders.NextCommand}">Next</Button>
            <!-- The save button can work if I also allow the dataconnection in the ViewModel
            and add a save command
            <Button Height="25" Name="btnSave" Width="75" Margin="3" Command="{Binding SaveCommand}">Save</Button>-->
        </StackPanel>
        <my:DataGrid Name="dataGrid1" ItemsSource="{Binding Orders.MasterView}" IsSynchronizedWithCurrentItem="True" Grid.Row="1"/>
        <my:DataGrid Grid.Row="2" Height="Auto" Margin="0" Name="dataGrid2" VerticalAlignment="Stretch" 
 ItemsSource="{Binding OrderDetails.MasterView}" IsSynchronizedWithCurrentItem="True"/>
    </Grid>
</Window>

That's all! Creating master/details tables is now a very easy job with the the help of the two generic classes described above!
Any comments are very welcome, this is my first attempt in publishing so I would love some feedback!
Shout it kick it on DotNetKicks.com

Geen opmerkingen: