How can I cancel a user's WPF TreeView click?

2020-02-08 06:35发布

I've got a WPF application with a Treeview control.

When the user clicks a node on the tree, other TextBox, ComboBox, etc. controls on the page are populated with appropriate values.

The user can then make changes to those values and save his or her changes by clicking a Save button.

However, if the user selects a different Treeview node without saving his or her changes, I want to display a warning and an opportunity to cancel that selection.

MessageBox: Continue and discard your unsaved changes? OK/Cancel http://img522.imageshack.us/img522/2897/discardsj3.gif

XAML...

<TreeView Name="TreeViewThings"
    ...
    TreeViewItem.Unselected="TreeViewThings_Unselected"
    TreeViewItem.Selected="TreeViewThings_Selected" >

Visual Basic...

Sub TreeViewThings_Unselected(ByVal sender As System.Object, _
                              ByVal e As System.Windows.RoutedEventArgs)
    Dim OldThing As Thing = DirectCast(e.OriginalSource.DataContext, Thing)
    If CancelDueToUnsavedChanges(OldThing) Then
        'put canceling code here
    End If
End Sub

Sub TreeViewThings_Selected(ByVal sender As System.Object, _
                            ByVal e As System.Windows.RoutedEventArgs)
    Dim NewThing As Thing = DirectCast(e.OriginalSource.DataContext, Thing)
    PopulateControlsFromThing(NewThing)
End Sub

How can I cancel those unselect/select events?


Update: I've asked a follow-up question...
How do I properly handle a PreviewMouseDown event with a MessageBox confirmation?

9条回答
家丑人穷心不美
2楼-- · 2020-02-08 06:55

You can't cancel the event like you can, for example, a Closing event. But you can undo it if you cache the last selected value. The secret is you have to change the selection without re-firing the SelectionChanged event. Here's an example:

    private object _LastSelection = null;
    private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        if (IsUpdated)
        {
            MessageBoxResult result = MessageBox.Show("The current record has been modified. Are you sure you want to navigate away? Click Cancel to continue editing. If you click OK all changes will be lost.", "Warning", MessageBoxButton.OKCancel, MessageBoxImage.Hand);
            switch (result)
            {
                case MessageBoxResult.Cancel:
                    e.Handled = true;
                    // disable event so this doesn't go into an infinite loop when the selection is changed to the cached value
                    PersonListView.SelectionChanged -= new SelectionChangedEventHandler(OnSelectionChanged);
                    PersonListView.SelectedItem = _LastSelection;
                    PersonListView.SelectionChanged += new SelectionChangedEventHandler(OnSelectionChanged);
                    return;
                case MessageBoxResult.OK:
                    // revert the object to the original state
                    LocalDataContext.Persons.GetOriginalEntityState(_LastSelection).CopyTo(_LastSelection);
                    IsUpdated = false;
                    Refresh();
                    break;
                default:
                    throw new ApplicationException("Invalid response.");
            }
        }

        // cache the selected item for undo
        _LastSelection = PersonListView.SelectedItem;
    }
查看更多
Melony?
3楼-- · 2020-02-08 06:55

I solved this problem for 1 tree view and display of 1 document at a time. This solution is based on an attachable behavior that can be attached to a normal treeview:

<TreeView Grid.Column="0"
       ItemsSource="{Binding TreeViewItems}"
       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>

and the code for the behavior is this:

/// <summary>
/// Source:
/// http://stackoverflow.com/questions/1034374/drag-and-drop-in-mvvm-with-scatterview
/// http://social.msdn.microsoft.com/Forums/de-DE/wpf/thread/21bed380-c485-44fb-8741-f9245524d0ae
/// 
/// Attached behaviour to implement the SelectionChanged command/event via delegate command binding or routed commands.
/// </summary>
public static class TreeViewSelectionChangedBehavior
{
#region fields
/// <summary>
/// Field of attached ICommand property
/// </summary>
private static readonly DependencyProperty ChangedCommandProperty = DependencyProperty.RegisterAttached(
  "ChangedCommand",
  typeof(ICommand),
  typeof(TreeViewSelectionChangedBehavior),
  new PropertyMetadata(null, OnSelectionChangedCommandChange));

/// <summary>
/// Implement backing store for UndoSelection dependency proeprty to indicate whether selection should be
/// cancelled via MessageBox query or not.
/// </summary>
public static readonly DependencyProperty UndoSelectionProperty =
  DependencyProperty.RegisterAttached("UndoSelection",
  typeof(bool),
  typeof(TreeViewSelectionChangedBehavior),
  new PropertyMetadata(false, OnUndoSelectionChanged));
#endregion fields

#region methods
#region ICommand changed methods
/// <summary>
/// Setter method of the attached ChangedCommand <seealso cref="ICommand"/> property
/// </summary>
/// <param name="source"></param>
/// <param name="value"></param>
public static void SetChangedCommand(DependencyObject source, ICommand value)
{
  source.SetValue(ChangedCommandProperty, value);
}

/// <summary>
/// Getter method of the attached ChangedCommand <seealso cref="ICommand"/> property
/// </summary>
/// <param name="source"></param>
/// <returns></returns>
public static ICommand GetChangedCommand(DependencyObject source)
{
  return (ICommand)source.GetValue(ChangedCommandProperty);
}
#endregion ICommand changed methods

#region UndoSelection methods
public static bool GetUndoSelection(DependencyObject obj)
{
  return (bool)obj.GetValue(UndoSelectionProperty);
}

public static void SetUndoSelection(DependencyObject obj, bool value)
{
  obj.SetValue(UndoSelectionProperty, value);
}
#endregion UndoSelection methods

/// <summary>
/// This method is hooked in the definition of the <seealso cref="ChangedCommandProperty"/>.
/// It is called whenever the attached property changes - in our case the event of binding
/// and unbinding the property to a sink is what we are looking for.
/// </summary>
/// <param name="d"></param>
/// <param name="e"></param>
private static void OnSelectionChangedCommandChange(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
  TreeView uiElement = d as TreeView;  // Remove the handler if it exist to avoid memory leaks

  if (uiElement != null)
  {
      uiElement.SelectedItemChanged -= Selection_Changed;

      var command = e.NewValue as ICommand;
      if (command != null)
      {
          // the property is attached so we attach the Drop event handler
          uiElement.SelectedItemChanged += Selection_Changed;
      }
  }
}

/// <summary>
/// This method is called when the selection changed event occurs. The sender should be the control
/// on which this behaviour is attached - so we convert the sender into a <seealso cref="UIElement"/>
/// and receive the Command through the <seealso cref="GetChangedCommand"/> getter listed above.
/// 
/// The <paramref name="e"/> parameter contains the standard EventArgs data,
/// which is unpacked and reales upon the bound command.
/// 
/// This implementation supports binding of delegate commands and routed commands.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private static void Selection_Changed(object sender, RoutedPropertyChangedEventArgs<object> e)
{
  var uiElement = sender as TreeView;

  // Sanity check just in case this was somehow send by something else
  if (uiElement == null)
      return;

  ICommand changedCommand = TreeViewSelectionChangedBehavior.GetChangedCommand(uiElement);

  // There may not be a command bound to this after all
  if (changedCommand == null)
      return;

  // Check whether this attached behaviour is bound to a RoutedCommand
  if (changedCommand is RoutedCommand)
  {
      // Execute the routed command
      (changedCommand as RoutedCommand).Execute(e.NewValue, uiElement);
  }
  else
  {
      // Execute the Command as bound delegate
      changedCommand.Execute(e.NewValue);
  }
}

/// <summary>
/// Executes when the bound boolean property indicates that a user should be asked
/// about changing a treeviewitem selection instead of just performing it.
/// </summary>
/// <param name="d"></param>
/// <param name="e"></param>
private static void OnUndoSelectionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
  TreeView uiElement = d as TreeView;  // Remove the handler if it exist to avoid memory leaks

  if (uiElement != null)
  {
      uiElement.PreviewMouseDown -= uiElement_PreviewMouseDown;

      var command = (bool)e.NewValue;
      if (command == true)
      {
          // the property is attached so we attach the Drop event handler
          uiElement.PreviewMouseDown += uiElement_PreviewMouseDown;
      }
  }
}

/// <summary>
/// Based on the solution proposed here:
/// Source: http://stackoverflow.com/questions/20244916/wpf-treeview-selection-change
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
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;
  }
}
#endregion methods
}

In this example I am showing a blocking message box in order to block the PreviewMouseDown event when it occurs. The event is then handled to signal that selection is cancelled or it is not handled to let the treeview itself handle the event by selecting the item that is about to be selected.

The behavior then invokes a bound command in the viewmodel if the user decides to continue anyway (PreviewMouseDown event is not handled by attached behavior and bound command is invoked.

I guess the message box showing could be done in other ways but I think its essential here to block the event when it happens since its otherwise not possible to cancel it(?). So, the only improve I could possible think off about this code is to bind some strings to make the displayed message configurable.

I have written an article that contains a downloadable sample since this is otherwise a difficult area to explain (one has to make many assumptions about missing parts that and the may not always be shared by all readers)

Here is an article that contains my results: http://www.codeproject.com/Articles/995629/Cancelable-TreeView-Navigation-for-Documents-in-WP

Please comment on this solution and let me know if you see room for improvement.

查看更多
走好不送
4楼-- · 2020-02-08 06:56

You could create your custom control that derives from TreeView and then override the OnSelectedItemChanged method.

Before calling the base, you could first fire a custom event with a CancelEventArgs parameter. If the parameter.Cancel become true, then don't call the base, but select the old item instead (be careful that the OnSelectedItemChanged will be called again).

Not the best solution, but at least this keeps the logic inside the tree control, and there is not chance that the selection change event fires more than it's needed. Also, you don't need to care if the user clicked the tree, used the keyboard or maybe the selection changed programatically.

查看更多
登录 后发表回答