Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Cancelable TreeView Navigation of Views in WPF/MVVM

0.00/5 (No votes)
23 Oct 2015 1  
Selecting treeviewitems based on document states associated with each item.

Introduction

The application design approach for touch enabled applications sometimes generates the requirement that displayed views should be navigateable in a treeview like structure.


I have constructed a small sample application to illustrate this advanced concept that is very similar to the Windows Explorer window but is certainly not limited to displaying drives, folders, and their content. There are some hints towards an all in one solution spilled on the net [1] but I required an extra day to complete a real working sample solution. This article documents my approach in the hope that it could be useful to others. I also write this information down in the hope that people may even give feedback if they see obvious problems and improvements.

I also published a rather complex application since publishing this article. Have a look at Locult (http://locult.codeplex.com/) in case you are looking for something more real and complex to motivate this article.

Background

The screenshot in the above image shows the basic concept of the application. It should have a treeview to navigate different items and display document content based on the selected item. Changing a selection in the treeview should not be allowed if a document is in a (dirty) state that does not allow a change.

You can envision the application like an application with a Project Explorer but the ability to display only 1 document at a time. Or this could also be an application similar to the Window Control Panel.

 

The Cancel Selection checkbox in the upper part of the MainWindow shown above is bound to the ApplicationViewModel.CancelTreeVieSelection property. This represents an application wide property that determines whether the currently selected treeview item can be changed or not.

The above Cancel Selection checkbox can be set directly by the user or implicetly by setting the Document IsDirty checkbox of the current document. The Document IsDirty checkbox is bound to the DocumentViewModelBase.IsDirty property.

public bool IsDirty
{
  get
  {
    return _IsDirty;
  }

  set
  {
    if (_IsDirty != value)
    {
      if (DirtyFlagChangedEvent != null)
        DirtyFlagChangedEvent(this, new DocumentDirtyChangedEventArgs(_IsDirty, value));

      _IsDirty = value;
      RaisePropertyChanged(() => IsDirty);
    }
  }
}

Every change of this property fires a DocumentViewModelBase.DirtyFlagChangedEvent that executes in turn the ApplicationViewModel.CurrentDocument_DirtyFlagChangedEvent method.

private void CurrentDocument_DirtyFlagChangedEvent(object sender, DocumentDirtyChangedEventArgs e)
{
  this.CancelTreeVieSelection = e.IsDirtyNewValue;
}

This design allows us to have a general application wide property CancelTreeVieSelection to determine if change of selection should be queried or not. This property can override the document IsDirty property but is always setup to be in sync when a new document is selected.

Here is the intended behavior when a user selects a treeview item while the Cancel Selection checkbox is set:

The new treeview item selection should take place if the user selects Yes in the displayed messabox and it should stay on Child 2 if the user select No. The next chapter explains the code structure necessary to archive this behavior with an MVVM architecture.

Using the code

The ViewModels of the Document Views

The document views in this class are controlled by a viewmodel class that is either of the RootViewModel or DocumentViewModel type. These types can be used to differentciate between a root node and all other document nodes. Both classes are based on the DocumentViewModelBase class which implements all properties and events that are relevant for this article. It is, however, easy to imagine that a RootViewModel class could, for example, implement additional commands and properties to let users interact with that type of content.

The DocumentDirtyChangedEventArgs event is also implemented by the DocumentViewModelBase class. It is therefore, available in the DocumentViewModel and RootViewModel classes to signal when the dirty state of that document has changed.

The next section explains how the above viewmodel classes are integrated into the view part of the proposed WPF/MVVM sample application.

The View and Bindings

Lets explore the application in a top down approach starting at the MainWindow and exploring all other items from there.

<Grid Grid.Row="2">
  <Grid.Resources>
      <!--
      These datatemplates map a type of viewmodel into their view.
      This map definition is used below in the ContentPresenter to
      show the correct page for each type of view.
      -->
      <DataTemplate DataType="{x:Type vm:DocumentViewModel}">
        <StackPanel Margin="3">
          <TextBlock Text="This is a Document View!"  Margin="3"/>
          <TextBlock Text="{Binding DocumentTitle, StringFormat={} Document Title:{0}}"  Margin="3"/>
          <CheckBox Content="Document IsDirty" IsChecked="{Binding IsDirty}"  Margin="3"/>
        </StackPanel>
      </DataTemplate>
      <DataTemplate DataType="{x:Type vm:RootViewModel}">
        <StackPanel Margin="3">
          <TextBlock Text="This is a ROOT View!"  Margin="3"/>
            <TextBlock Text="{Binding DocumentTitle, StringFormat={} Document Title:{0}}"  Margin="3"/>
            <CheckBox Content="ROOT view document IsDirty" IsChecked="{Binding IsDirty}"  Margin="3"/>
        </StackPanel>
      </DataTemplate>

  </Grid.Resources>
  <Grid.ColumnDefinitions>
      <ColumnDefinition Width="*" />
      <ColumnDefinition Width="*" />
  </Grid.ColumnDefinitions>
  <TreeView Grid.Column="0"
            ItemsSource="{Binding TreeViewItems}"
            behav:TreeViewSelectionChangedBehavior.UndoSelection="{Binding CancelTreeVieSelection}"
            behav:TreeViewSelectionChangedBehavior.ChangedCommand="{Binding SelectItemChangedCommand}"
             >
    <TreeView.ItemTemplate>
      <HierarchicalDataTemplate ItemsSource="{Binding Children}">
         <StackPanel Orientation="Horizontal">
            <TextBlock Text="{Binding Name}"
                        ToolTipService.ShowOnDisabled="True"
                        VerticalAlignment="Center" Margin="3" />
            </StackPanel>
        </HierarchicalDataTemplate>
    </TreeView.ItemTemplate>
    <TreeView.ItemContainerStyle>
        <Style TargetType="{x:Type TreeViewItem}">
            <Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
            <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
        </Style>
    </TreeView.ItemContainerStyle>
  </TreeView>

  <ContentControl Grid.Column="1" Content="{Binding CurrentDocument}" Margin="3" />
</Grid>

The above XAML shows the definition items for the TreeView in Grid.Column=0 and a ContentControl in Column=1. It is important to understand that the DataTemplate definitions  at the top of the listed XAMl determine the view being shown in the ContentControl.

The core of the solution is the TreeViewSelectionChangedBehavior that is attached to the TreeView. This behavior executes the SelectItemChangedCommand in the ApplicationViewModel whenever the SelectItemChanged event is raised in the the TreeVew.

The SelectItemChanged event can be cancelled via the PreviewMouseDown event that is hooked/unhooked in the OnUndoSelectionChanged method of the TreeViewSelectionChangedBehavior. This method in turn is executed when the UndoSelection dependency property is changed via the CancelTreeViewSelection property in the ApplicationViewModel.

It is important that the application starts up CancelTreeViewSelection = false since the binding and generation of the the bound items can also generate the above events and cause additional problems ranging from a freezing application to no response at all. Make sure this property is set to false whenever the ItemsSource of the TreeView is manipulated programmatically.

The uiElement_PreviewMouseDown method in the TreeViewSelectionChangedBehavior is executed if, and only if, the UndoSelection dependency property is set to true. Otherwise, the corresponding event is not even evaluated.

private static void uiElement_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
  // first did the user click on a tree node?
  var source = e.OriginalSource as DependencyObject;
  while (source != null && !(source is TreeViewItem))
      source = VisualTreeHelper.GetParent(source);

  var itemSource = source as TreeViewItem;
  if (itemSource == null)
      return;

  var treeView = sender as TreeView;
  if (treeView == null)
      return;

  bool undoSelection = TreeViewSelectionChangedBehavior.GetUndoSelection(treeView);
  if (undoSelection == false)
      return;

  // Cancel the attempt to select an item.
  var result = MessageBox.Show("The current document has unsaved data. Do you want to continue without saving data?", "Are you really sure?",
                               MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No);

  if (result == MessageBoxResult.No)
  {
    // Cancel the attempt to select a differnet item.
    e.Handled = true;
  }
  else
  {
    // Lets disable this for a moment, otherwise, we'll get into an event "recursion"
    treeView.PreviewMouseDown -= uiElement_PreviewMouseDown;

    // Select the new item - make sure a SelectedItemChanged event is fired in any case
    // Even if this means that we have to deselect/select the one and the same item
    if (itemSource.IsSelected == true )
        itemSource.IsSelected = false;

    itemSource.IsSelected = true;

    // Lets enable this to get back to business for next selection
    treeView.PreviewMouseDown += uiElement_PreviewMouseDown;
  }
}

The core of the solution is at line where the MessageBox is shown (and below). The canceling of the selection event is equivalent to claiming that we handled this, while implementing the event requires us to raise the SelectedItemChanged event via manipulation of the IsSelected property of the TreeViewItem.

Summary

The proposed solution requires an application wide property that determines whether cancelling of an item selection should be possible or not. The attached behavior implements the cancel behavior logic and implements a Command binding to signal the application viewmodel when a selected item has changed.

public ICommand SelectItemChangedCommand
{
  get
  {
    if (_SelectItemChangedCommand == null)
    {
      _SelectItemChangedCommand = new RelayCommand<object>((p) =>
      {
        var param = p as ItemViewModel;
    
        if (param != null)
        {
          this.SelectedTreeViewItem = param;
    
          if (this.CurrentDocument != null)
             this.CurrentDocument.DirtyFlagChangedEvent -= CurrentDocument_DirtyFlagChangedEvent;
    
          if (param.Name == "Root")
          {
              this.CurrentDocument = new RootViewModel();
          }
          else
             this.CurrentDocument = new DocumentViewModel();
    
          this.CurrentDocument.DocumentTitle = param.Name;
          this.CancelTreeVieSelection = this.CurrentDocument.IsDirty;
          this.CurrentDocument.DirtyFlagChangedEvent += CurrentDocument_DirtyFlagChangedEvent;
        }
      });
    }
    
    return _SelectItemChangedCommand;
  }
}

The application viewmodel can react to this and make sure the corresponding document associated to an item is shown. Please play around with the attached sample and let me know if you find problems, or even better, see room for improvements.

References

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here