wpf treeview savechanges

¿Cómo puedo cancelar el clic de WPF TreeView de un usuario?



savechanges (9)

Dado que el evento SelectedItemChanged se desencadena después de que SelectedItem ya haya cambiado, no se puede cancelar el evento en este momento.

Lo que puede hacer es escuchar los clics del mouse y cancelarlos antes de que se SelectedItem .

Tengo una aplicación WPF con control Treeview.

Cuando el usuario hace clic en un nodo en el árbol, otros controles de TextBox, ComboBox, etc. en la página se llenan con los valores apropiados.

El usuario puede realizar cambios en esos valores y guardar sus cambios haciendo clic en el botón Guardar.

Sin embargo, si el usuario selecciona un nodo Treeview diferente sin guardar sus cambios, quiero mostrar una advertencia y la oportunidad de cancelar esa selección.

MessageBox: ¿Continuar y descartar los cambios no guardados? Aceptar / Cancelar 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

¿Cómo puedo cancelar esos eventos deseleccionar / seleccionar?

Actualización: he hecho una pregunta de seguimiento ...
¿Cómo manejo adecuadamente un evento PreviewMouseDown con una confirmación MessageBox?


En lugar de seleccionar para Seleccionado / No seleccionado, una mejor ruta podría ser enganchar en PreviewMouseDown. El problema con el manejo de un evento Seleccionado y No Seleccionado es que el evento ya ocurrió cuando recibes la notificación. No hay nada que cancelar porque ya ha sucedido.

Por otro lado, los eventos de vista previa son cancelables. No es el evento exacto que desea, pero le brinda la oportunidad de evitar que el usuario seleccione un nodo diferente.


No puede cancelar el evento como puede, por ejemplo, un evento de cierre. Pero puede deshacerlo si almacena en caché el último valor seleccionado. El secreto es que debes cambiar la selección sin volver a activar el evento SelectionChanged. Aquí hay un ejemplo:

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; }


CAMS_ARIES:

XAML:

código:

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); } }


ACTUALIZAR

Me di cuenta de que podía poner la lógica en SelectedItemChanged en su lugar. Una solución un poco más limpia.

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>

Código detrás. Tengo algunas clases que son mis ItemsSource de TreeView así que hice una interfaz (MyInterface) que expone la propiedad IsSelected para todos ellos.

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; } }

No he encontrado ninguna situación en la que no vuelva al elemento anterior al presionar "No".


Puede crear su control personalizado derivado de TreeView y luego anular el método OnSelectedItemChanged.

Antes de llamar a la base, primero puede activar un evento personalizado con un parámetro CancelEventArgs. Si el parámetro.Cancel se convierte en verdadero, no llame a la base, sino que seleccione el elemento anterior en su lugar (tenga cuidado de que se vuelva a llamar el OnSelectedItemChanged).

No es la mejor solución, pero al menos mantiene la lógica dentro del control de árbol, y no hay posibilidad de que el evento de cambio de selección dispare más de lo necesario. Además, no es necesario que se preocupe si el usuario hizo clic en el árbol, utilizó el teclado o tal vez la selección cambió programáticamente.


Tuve que resolver el mismo problema, pero en múltiples vistas de árbol en mi aplicación. Obtuve TreeView y agregué controladores de eventos, en parte usando la solución de Meleak y en parte utilizando los métodos de extensión de este foro: http://forums.silverlight.net/t/65277.aspx/1/10

Pensé que compartiría mi solución contigo, así que aquí está mi TreeView completamente reutilizable que maneja "cancelar el cambio de nodo":

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; } }

Aquí hay un ejemplo de uso (codebehind para la ventana que contiene 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"); }

Después de elegir el primer nodo en la vista de árbol, todos los demás cambios de nodo se cancelan y se muestra un cuadro de mensaje.


En realidad, no puede poner su lógica en el método OnSelectedItemChanged, si la lógica está allí, el elemento seleccionado ya ha cambiado.

Según lo sugerido por otro afiche, el controlador PreviewMouseDown es un mejor lugar para implementar la lógica, sin embargo, todavía es necesario realizar una gran cantidad de trabajo de campo.

Debajo están mis 2 centavos:

Primero el TreeView que he implementado:

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; } }

algunos métodos de extensión para admitir que TreeView encuentre el objeto debajo del 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; } }

y por último, pero no menos importante, los EventArgs personalizados que aprovechan el subsistema RoutedEvent.

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; } } }


Resolví este problema para 1 vista de árbol y visualización de 1 documento a la vez. Esta solución se basa en un comportamiento conectable que se puede adjuntar a una vista de árbol normal:

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

y el código para el comportamiento es este:

/// <summary> /// Source: /// http://.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://.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 }

En este ejemplo, estoy mostrando un cuadro de mensaje de bloqueo para bloquear el evento PreviewMouseDown cuando ocurre. El evento se maneja para indicar que la selección se cancela o no se maneja para permitir que la vista en árbol maneje el evento seleccionando el elemento que está a punto de ser seleccionado.

El comportamiento invoca entonces un comando vinculado en el modelo de vista si el usuario decide continuar de todos modos (el comportamiento adjunto no controla el evento PreviewMouseDown y se invoca el comando vinculado.

Supongo que el cuadro de mensaje que se muestra podría hacerse de otras formas, pero creo que aquí es esencial bloquear el evento cuando sucede, ya que de lo contrario no es posible cancelarlo (?). Por lo tanto, la única mejora que podría pensar acerca de este código es enlazar algunas cadenas para que el mensaje mostrado sea configurable.

He escrito un artículo que contiene una muestra descargable, ya que este es un área difícil de explicar (uno tiene que hacer muchas suposiciones sobre las partes que faltan y que pueden no ser compartidas por todos los lectores).

Aquí hay un artículo que contiene mis resultados: http://www.codeproject.com/Articles/995629/Cancelable-TreeView-Navigation-for-Documents-in-WP

Comente esta solución y avíseme si puede mejorar.