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:35

UPDATE

Realized I could put the logic in SelectedItemChanged instead. A little cleaner solution.

Xaml

<TreeView Name="c_treeView"
          SelectedItemChanged="c_treeView_SelectedItemChanged">
    <TreeView.ItemContainerStyle>
        <Style TargetType="{x:Type TreeViewItem}">
            <Setter Property="IsSelected" Value="{Binding Path=IsSelected, Mode=TwoWay}" />
        </Style>
    </TreeView.ItemContainerStyle>
</TreeView>

Code behind. I have some classes that is my ItemsSource of the TreeView so I made an interface (MyInterface) that exposes the IsSelected property for all of them.

private MyInterface m_selectedTreeViewItem = null;
private void c_treeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
    if (m_selectedTreeViewItem != null)
    {
        if (e.NewValue == m_selectedTreeViewItem)
        {
            // Will only end up here when reversing item
            // Without this line childs can't be selected
            // twice if "No" was pressed in the question..   
            c_treeView.Focus();   
        }
        else
        {
            if (MessageBox.Show("Change TreeViewItem?",
                                "Really change",
                                MessageBoxButton.YesNo,
                                MessageBoxImage.Question) != MessageBoxResult.Yes)
            {
                EventHandler eventHandler = null;
                eventHandler = new EventHandler(delegate
                {
                    c_treeView.LayoutUpdated -= eventHandler;
                    m_selectedTreeViewItem.IsSelected = true;
                });
                // Will be fired after SelectedItemChanged, to early to change back here
                c_treeView.LayoutUpdated += eventHandler;
            }   
            else
            {
                m_selectedTreeViewItem = e.NewValue as MyInterface;
            }        
        }
    }
    else
    {
        m_selectedTreeViewItem = e.NewValue as MyInterface;
    }
}

I haven't found any situation where it doesn't revert back to the previous item upon pressing "No".

查看更多
放荡不羁爱自由
3楼-- · 2020-02-08 06:36

I had to solve the same problem, but in multiple treeviews in my application. I derived TreeView and added event handlers, partly using Meleak's solution and partly using the extension methods from this forum: http://forums.silverlight.net/t/65277.aspx/1/10

I thought I'd share my solution with you, so here is my complete reusable TreeView that handles "cancel node change":

public class MyTreeView : TreeView
{
    public static RoutedEvent PreviewSelectedItemChangedEvent;
    public static RoutedEvent SelectionCancelledEvent;

    static MyTreeView()
    {
        PreviewSelectedItemChangedEvent = EventManager.RegisterRoutedEvent("PreviewSelectedItemChanged", RoutingStrategy.Bubble,
                                                                           typeof(RoutedPropertyChangedEventHandler<object>), typeof(MyTreeView));

        SelectionCancelledEvent = EventManager.RegisterRoutedEvent("SelectionCancelled", RoutingStrategy.Bubble,
                                                                   typeof(RoutedEventHandler), typeof(MyTreeView));
    }

    public event RoutedPropertyChangedEventHandler<object> PreviewSelectedItemChanged
    {
        add { AddHandler(PreviewSelectedItemChangedEvent, value); }
        remove { RemoveHandler(PreviewSelectedItemChangedEvent, value); }
    }

    public event RoutedEventHandler SelectionCancelled
    {
        add { AddHandler(SelectionCancelledEvent, value); }
        remove { RemoveHandler(SelectionCancelledEvent, value); }
    }


    private object selectedItem = null;
    protected override void OnSelectedItemChanged(RoutedPropertyChangedEventArgs<object> e)
    {
        if (e.NewValue == selectedItem)
        {
            this.Focus();

            var args = new RoutedEventArgs(SelectionCancelledEvent);
            RaiseEvent(args);
        }
        else
        {
            var args = new RoutedPropertyChangedEventArgs<object>(e.OldValue, e.NewValue, PreviewSelectedItemChangedEvent);
            RaiseEvent(args);

            if (args.Handled)
            {
                EventHandler eventHandler = null;
                eventHandler = delegate
                {
                    this.LayoutUpdated -= eventHandler;

                    var treeViewItem = this.ContainerFromItem(selectedItem);
                    if (treeViewItem != null)
                        treeViewItem.IsSelected = true;
                };

                this.LayoutUpdated += eventHandler;
            }
            else
            {
                selectedItem = this.SelectedItem;
                base.OnSelectedItemChanged(e);
            }
        }
    }
}

public static class TreeViewExtensions
{
    public static TreeViewItem ContainerFromItem(this TreeView treeView, object item)
    {
        if (item == null) return null;

        var containerThatMightContainItem = (TreeViewItem)treeView.ItemContainerGenerator.ContainerFromItem(item);

        return containerThatMightContainItem ?? ContainerFromItem(treeView.ItemContainerGenerator, treeView.Items, item);
    }

    private static TreeViewItem ContainerFromItem(ItemContainerGenerator parentItemContainerGenerator, ItemCollection itemCollection, object item)
    {
        foreach (var child in itemCollection)
        {
            var parentContainer = (TreeViewItem)parentItemContainerGenerator.ContainerFromItem(child);
            var containerThatMightContainItem = (TreeViewItem)parentContainer.ItemContainerGenerator.ContainerFromItem(item);

            if (containerThatMightContainItem != null)
                return containerThatMightContainItem;

            var recursionResult = ContainerFromItem(parentContainer.ItemContainerGenerator, parentContainer.Items, item);
            if (recursionResult != null)
                return recursionResult;
        }
        return null;
    }
}

Here is an example of usage (codebehind for window containing a MyTreeView):

    private void theTreeView_PreviewSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
    {
        if (e.OldValue != null)
            e.Handled = true;
    }

    private void theTreeView_SelectionCancelled(object sender, RoutedEventArgs e)
    {
        MessageBox.Show("Cancelled");
    }

After choosing the first node in the treeview, all other node changes are cancelled and a message box is displayed.

查看更多
【Aperson】
4楼-- · 2020-02-08 06:39

Instead of selecting for Selected/Unselected, a better route might be to hook into PreviewMouseDown. The preblem with handling a Selected and Unselected event is that the event has already occurred when you receive the notification. There is nothing to cancel because it's already happened.

On the other hand, Preview events are cancelable. It's not the exact event you want but it does give you the oppuritunity to prevent the user from selecting a different node.

查看更多
劳资没心,怎么记你
5楼-- · 2020-02-08 06:39

Since the SelectedItemChanged event is triggered after the SelectedItem has already changed, you can't really cancel the event at this point.

What you can do is listen for mouse-clicks and cancel them before the SelectedItem gets changed.

查看更多
唯我独甜
6楼-- · 2020-02-08 06:50

You can't actually put your logic into the OnSelectedItemChanged Method, if the logic is there the Selected Item has actually already changed.

As suggested by another poster, the PreviewMouseDown handler is a better spot to implement the logic, however, a fair amount of leg work still needs to be done.

Below is my 2 cents:

First the TreeView that I have implemented:

public class MyTreeView : TreeView
{
    static MyTreeView( )
    {
        DefaultStyleKeyProperty.OverrideMetadata(
            typeof(MyTreeView),
            new FrameworkPropertyMetadata(typeof(TreeView)));
    }

    // Register a routed event, note this event uses RoutingStrategy.Tunnel. per msdn docs
    // all "Preview" events should use tunneling.
    // http://msdn.microsoft.com/en-us/library/system.windows.routedevent.routingstrategy.aspx
    public static RoutedEvent PreviewSelectedItemChangedEvent = EventManager.RegisterRoutedEvent(
        "PreviewSelectedItemChanged",
        RoutingStrategy.Tunnel,
        typeof(CancelEventHandler),
        typeof(MyTreeView));

    // give CLR access to routed event
    public event CancelEventHandler PreviewSelectedItemChanged
    {
        add
        {
            AddHandler(PreviewSelectedItemChangedEvent, value);
        }
        remove
        {
            RemoveHandler(PreviewSelectedItemChangedEvent, value);
        }
    }

    // override PreviewMouseDown
    protected override void OnPreviewMouseDown(MouseButtonEventArgs e)
    {
        // determine which item is going to be selected based on the current mouse position
        object itemToBeSelected = this.GetObjectAtPoint<TreeViewItem>(e.GetPosition(this));

        // selection doesn't change if the target point is null (beyond the end of the list)
        // or if the item to be selected is already selected.
        if (itemToBeSelected != null && itemToBeSelected != SelectedItem)
        {
            bool shouldCancel;

            // call our new event
            OnPreviewSelectedItemChanged(out shouldCancel);
            if (shouldCancel)
            {
                // if we are canceling the selection, mark this event has handled and don't
                // propogate the event.
                e.Handled = true;
                return;
            }
        }

        // otherwise we want to continue normally
        base.OnPreviewMouseDown(e);
    }

    protected virtual void OnPreviewSelectedItemChanged(out bool shouldCancel)
    {
        CancelEventArgs e = new CancelEventArgs( );
        if (PreviewSelectedItemChangedEvent != null)
        {
            // Raise our event with our custom CancelRoutedEventArgs
            RaiseEvent(new CancelRoutedEventArgs(PreviewSelectedItemChangedEvent, e));
        }
        shouldCancel = e.Cancel;
    }
}

some extension methods to support the TreeView finding the object under the mouse.

public static class ItemContainerExtensions
{
    // get the object that exists in the container at the specified point.
    public static object GetObjectAtPoint<ItemContainer>(this ItemsControl control, Point p)
        where ItemContainer : DependencyObject
    {
        // ItemContainer - can be ListViewItem, or TreeViewItem and so on(depends on control)
        ItemContainer obj = GetContainerAtPoint<ItemContainer>(control, p);
        if (obj == null)
            return null;

        // it is worth noting that the passed _control_ may not be the direct parent of the
        // container that exists at this point. This can be the case in a TreeView, where the
        // parent of a TreeViewItem may be either the TreeView or a intermediate TreeViewItem
        ItemsControl parentGenerator = obj.GetParentItemsControl( );

        // hopefully this isn't possible?
        if (parentGenerator == null)
            return null;

        return parentGenerator.ItemContainerGenerator.ItemFromContainer(obj);
    }

    // use the VisualTreeHelper to find the container at the specified point.
    public static ItemContainer GetContainerAtPoint<ItemContainer>(this ItemsControl control, Point p)
        where ItemContainer : DependencyObject
    {
        HitTestResult result = VisualTreeHelper.HitTest(control, p);
        DependencyObject obj = result.VisualHit;

        while (VisualTreeHelper.GetParent(obj) != null && !(obj is ItemContainer))
        {
            obj = VisualTreeHelper.GetParent(obj);
        }

        // Will return null if not found
        return obj as ItemContainer;
    }

    // walk up the visual tree looking for the nearest ItemsControl parent of the specified
    // depObject, returns null if one isn't found.
    public static ItemsControl GetParentItemsControl(this DependencyObject depObject)
    {
        DependencyObject obj = VisualTreeHelper.GetParent(depObject);
        while (VisualTreeHelper.GetParent(obj) != null && !(obj is ItemsControl))
        {
            obj = VisualTreeHelper.GetParent(obj);
        }

        // will return null if not found
        return obj as ItemsControl;
    }
}

and last, but not least the custom EventArgs that leverage the RoutedEvent subsystem.

public class CancelRoutedEventArgs : RoutedEventArgs
{
    private readonly CancelEventArgs _CancelArgs;

    public CancelRoutedEventArgs(RoutedEvent @event, CancelEventArgs cancelArgs)
        : base(@event)
    {
        _CancelArgs = cancelArgs;
    }

    // override the InvokeEventHandler because we are going to pass it CancelEventArgs
    // not the normal RoutedEventArgs
    protected override void InvokeEventHandler(Delegate genericHandler, object genericTarget)
    {
        CancelEventHandler handler = (CancelEventHandler)genericHandler;
        handler(genericTarget, _CancelArgs);
    }

    // the result
    public bool Cancel
    {
        get
        {
            return _CancelArgs.Cancel;
        }
    }
}
查看更多
smile是对你的礼貌
7楼-- · 2020-02-08 06:52

CAMS_ARIES:

XAML:

code :

  private bool ManejarSeleccionNodoArbol(Object origen)
    {
        return true;  // with true, the selected nodo don't change
        return false // with false, the selected nodo change
    }


    private void Arbol_PreviewMouseDown(object sender, MouseButtonEventArgs e)
    {            
        if (e.Source is TreeViewItem)
        {
            e.Handled = ManejarSeleccionNodoArbol(e.Source);
        }
    }

    private void Arbol_PreviewKeyDown(object sender, KeyEventArgs e)
    {
        if (e.Source is TreeViewItem)
        {
           e.Handled=ManejarSeleccionNodoArbol(e.Source);
        }
    }         
查看更多
登录 后发表回答