Model-View-ViewModel (MVVM) Design Pattern with WPF Docking

by Weifen Luo (DevZest) 10. February 2010 13:37

Download Source: MvvmDemoApp.zip (183KB)

MvvmDemoApp 

Today an evaluation user asked how to implement MVVM design pattern with WPF Docking. Here is a sample application modified from the MSDN Magazine article WPF Apps With The Model-View-ViewModel Design Pattern(by Josh Smith): the application can contain any number of "workspaces," each of which the user can open by clicking on a command link in the navigation area on the left. The user can close a workspace by clicking the Close button on that workspace's tab item. The application has two available workspaces: "All Customers" and "New Customer." Josh implemented all workspaces in a TabControl on the main content area. Of course, we need to change the view to our DockControl, without changing the underlying Model and View-Model.

Josh defines class MainWindowViewModel which contains a Workspaces property and presented by data binding to TabControl.ItemsSource property:

ObservableCollection<WorkspaceViewModel> _workspaces;
/// <summary>
/// Returns the collection of available workspaces to display.
/// A 'workspace' is a ViewModel that can request to be closed.
/// </summary>
public ObservableCollection<WorkspaceViewModel> Workspaces
{
    get
    {
        if (_workspaces == null)
        {
            _workspaces = new ObservableCollection<WorkspaceViewModel>();
            _workspaces.CollectionChanged += this.OnWorkspacesChanged;
        }
        return _workspaces;
    }
}

Now we need to present this property with our DockControl. Since DockControl is a sophisticated control and does not directly support binding with a collection of workspaces, we need to write some code for it:

public class MyDockControl : DockControl
{
    public static readonly DependencyProperty WorkspacesProperty = DependencyProperty.Register("Workspaces", typeof(ObservableCollection<WorkspaceViewModel>), typeof(MyDockControl),
        new FrameworkPropertyMetadata(null, new PropertyChangedCallback(_OnWorkspacesChanged)));

    ICollectionView _workspacesView;
    Dictionary<WorkspaceViewModel, DockItem> _dockItems = new Dictionary<WorkspaceViewModel,DockItem>();

    public ObservableCollection<WorkspaceViewModel> Workspaces
    {
        get { return (ObservableCollection<WorkspaceViewModel>)GetValue(WorkspacesProperty); }
        set { SetValue(WorkspacesProperty, value); }
    }

    static void _OnWorkspacesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        ((MyDockControl)d).OnWorkspacesChanged((ObservableCollection<WorkspaceViewModel>)e.OldValue, (ObservableCollection<WorkspaceViewModel>)e.NewValue);
    }

    void OnWorkspacesChanged(ObservableCollection<WorkspaceViewModel> oldValue, ObservableCollection<WorkspaceViewModel> newValue)
    {
        // For simplicity, we assume Workspaces property changed only once
        _workspacesView = CollectionViewSource.GetDefaultView(this.Workspaces);
        _workspacesView.CollectionChanged += this.OnWorkspacesChanged;
        _workspacesView.CurrentChanged += this.OnActiveWorkspaceChanged;
    }

    void OnWorkspacesChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        if (e.NewItems != null && e.NewItems.Count != 0)
            foreach (WorkspaceViewModel workspace in e.NewItems)
            {
                DockItem dockItem = new DockItem();
                dockItem.HideOnPerformClose = false;
                dockItem.Content = workspace;
                dockItem.AllowedDockTreePositions = AllowedDockTreePositions.Document;
                dockItem.TabText = workspace.DisplayName;
                dockItem.Show(this, DockPosition.Document);
                _dockItems.Add(workspace, dockItem);
            }

        if (e.OldItems != null && e.OldItems.Count != 0)
            foreach (WorkspaceViewModel workspace in e.OldItems)
            {
                _dockItems.Remove(workspace);
            }
    }

    void OnActiveWorkspaceChanged(object sender, EventArgs e)
    {
        WorkspaceViewModel activeWorkspace = (WorkspaceViewModel)_workspacesView.CurrentItem;
        if (activeWorkspace != null && _dockItems.ContainsKey(activeWorkspace))
            _dockItems[activeWorkspace].Activate();
    }

    protected override void OnDockItemStateChanged(DockItemStateEventArgs e)
    {
        base.OnDockItemStateChanged(e);
        WorkspaceViewModel workspace = e.DockItem.Content as WorkspaceViewModel;
        if (workspace != null)
        {
            if (e.NewDockPosition == DockPosition.Unknown)  // DockItem closed
                workspace.CloseCommand.Execute(null);
            else if (e.NewDockPosition == DockPosition.Document && e.DockItem.IsSelected)
                _workspacesView.MoveCurrentTo(workspace);
        }
    }
}

We derive MyDockControl class from DockControl, add a Workspaces property for data binding, and listens the collection change event to show DockItem objects. When DockItem object is closed, we remove the corresponding workspace from the model view.

Finally, we put MyDockControl into the main window to replace Josh’s TabControl (MainWindow.xaml):

<Window 
  x:Class="DemoApp.MainWindow"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:vm="clr-namespace:DemoApp.ViewModel"
  xmlns:local="clr-namespace:DemoApp"
  xmlns:dz="http://schemas.devzest.com/presentation/docking"
  FontSize="13" 
  FontFamily="Verdana"
  MinWidth="650" MinHeight="420"
  Title="{Binding Path=DisplayName}"
  Width="650" Height="420"
  WindowStartupLocation="CenterScreen"
  >
  <Window.Resources>
    <ResourceDictionary Source="MainWindowResources.xaml" />
  </Window.Resources>

  <DockPanel>
    <DockPanel DockPanel.Dock="Top" KeyboardNavigation.TabNavigation="None">
      <Menu KeyboardNavigation.TabNavigation="Cycle">
        <MenuItem Header="_File">
          <MenuItem Header="E_xit" Command="{Binding Path=CloseCommand}" />
        </MenuItem>
        <MenuItem Header="_Edit" />
        <MenuItem Header="_Options" />
        <MenuItem Header="_Help" />
      </Menu>
    </DockPanel>

    <local:MyDockControl Workspaces="{Binding Workspaces}">
        <local:MyDockControl.DockItems>
                <dz:DockItem Title="Control Panel" TabText="Control Panel">
                    <dz:DockItem.FirstShowAction>
                        <dz:ShowAsDockPositionAction DockPosition="Left" />
                    </dz:DockItem.FirstShowAction>
                    <HeaderedContentControl
                      Content="{Binding Path=Commands}"
                      ContentTemplate="{StaticResource CommandsTemplate}"
                      Header="Control Panel"
                      Style="{StaticResource MainHCCStyle}"
                      />
                </dz:DockItem>
            </local:MyDockControl.DockItems>
    </local:MyDockControl>

    <!--
    <Grid Margin="4">
      <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto" />
        <ColumnDefinition Width="4" />
        <ColumnDefinition Width="*" />
      </Grid.ColumnDefinitions>

      <Border 
        Grid.Column="0" 
        Style="{StaticResource MainBorderStyle}"
        Width="170"
        >
        <HeaderedContentControl
          Content="{Binding Path=Commands}"
          ContentTemplate="{StaticResource CommandsTemplate}"
          Header="Control Panel"
          Style="{StaticResource MainHCCStyle}"
          />
      </Border>

      <Border
        Grid.Column="2" 
        Style="{StaticResource MainBorderStyle}"
        >
        <HeaderedContentControl 
          Content="{Binding Path=Workspaces}"
          ContentTemplate="{StaticResource WorkspacesTemplate}"
          Header="Workspaces"
          Style="{StaticResource MainHCCStyle}"
          />
      </Border>
    </Grid>
    -->
  </DockPanel>
</Window>

Since we don’t change anything other than the main window view, you can switch between the TabControl and MyDockControl by commenting out the appropriate UI element, and everything else such as unit testing works without any problem.

Tags: ,

.Net | Code Sample | WPF

Comments are closed

Copyright DevZest, 2008 - 2017