wpf mvvm treeview

WPF MVVM TreeView SelectedItem



(6)

Esto no puede ser tan difícil. El TreeView en WPF no le permite establecer el SelectedItem, diciendo que la propiedad es ReadOnly. Tengo la TreeView poblando, incluso actualizando cuando se produce un cambio en la colección de datos.

Solo necesito saber qué elemento se selecciona. Estoy usando MVVM, por lo que no hay código subyacente o variable para hacer referencia a la vista de árbol por. Esta es la única solución que he encontrado, pero es un truco obvio, crea otro elemento en XAML que usa el enlace ElementName para establecerse en el elemento seleccionado treeviews, que también debe vincular a su Viewmodel. Several otras questions sobre esto, pero no se dan otras soluciones de trabajo.

He visto esta pregunta , pero usar la respuesta dada me da errores de compilación, por alguna razón no puedo agregar una referencia a la combinación de System.Windows.Windows.Interactivity a mi proyecto. Dice "sistema de error desconocido.windows no ha sido precargado" y aún no he descubierto cómo superarlo.

Para puntos de bonificación: ¿por qué diablos hizo Microsoft la propiedad SelectedItem de este elemento ReadOnly?


Decidí usar una combinación de código detrás y código de modelo de vista. el xaml es así:

<TreeView Name="tvCountries" ItemsSource="{Binding Path=Countries}" ItemTemplate="{StaticResource ResourceKey=countryTemplate}" SelectedValuePath="Name" SelectedItemChanged="tvCountries_SelectedItemChanged">

Código detrás

private void tvCountries_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e) { var vm = this.FindResource("vm") as ViewModels.CoiEditorViewModel; if (vm != null) { var treeItem = sender as TreeView; vm.TreeItemSelected = treeItem.SelectedItem; } }

Y en el modelo de vista hay un objeto TreeItemSelected al que puede acceder en el modelo de vista.


Puede crear una propiedad adjunta que sea vinculable y que tenga un getter y un setter:

public class TreeViewHelper { private static Dictionary<DependencyObject, TreeViewSelectedItemBehavior> behaviors = new Dictionary<DependencyObject, TreeViewSelectedItemBehavior>(); public static object GetSelectedItem(DependencyObject obj) { return (object)obj.GetValue(SelectedItemProperty); } public static void SetSelectedItem(DependencyObject obj, object value) { obj.SetValue(SelectedItemProperty, value); } // Using a DependencyProperty as the backing store for SelectedItem. This enables animation, styling, binding, etc... public static readonly DependencyProperty SelectedItemProperty = DependencyProperty.RegisterAttached("SelectedItem", typeof(object), typeof(TreeViewHelper), new UIPropertyMetadata(null, SelectedItemChanged)); private static void SelectedItemChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e) { if (!(obj is TreeView)) return; if (!behaviors.ContainsKey(obj)) behaviors.Add(obj, new TreeViewSelectedItemBehavior(obj as TreeView)); TreeViewSelectedItemBehavior view = behaviors[obj]; view.ChangeSelectedItem(e.NewValue); } private class TreeViewSelectedItemBehavior { TreeView view; public TreeViewSelectedItemBehavior(TreeView view) { this.view = view; view.SelectedItemChanged += (sender, e) => SetSelectedItem(view, e.NewValue); } internal void ChangeSelectedItem(object p) { TreeViewItem item = (TreeViewItem)view.ItemContainerGenerator.ContainerFromItem(p); item.IsSelected = true; } } }

Agregue la declaración del espacio de nombres que contiene esa clase a su XAML y agréguelo de la siguiente manera (local es la forma en que denominé la declaración del espacio de nombres):

<TreeView ItemsSource="{Binding Path=Root.Children}" local:TreeViewHelper.SelectedItem="{Binding Path=SelectedItem, Mode=TwoWay}"/>

Ahora puede vincular el elemento seleccionado y también configurarlo en su modelo de vista para cambiarlo programáticamente, en caso de que surja ese requisito. Esto es, por supuesto, asumiendo que implemente INotifyPropertyChanged en esa propiedad en particular.


Siempre puede crear una DependencyProperty que use ICommand y escuche el evento SelectedItemChanged en TreeView. Esto puede ser un poco más fácil que el enlace IsSelected, pero imagino que terminará vinculando IsSelected de todos modos por otros motivos. Si solo desea enlazar IsSelected, siempre puede hacer que su elemento envíe un mensaje cada vez que IsSelected cambie. Entonces puede escuchar esos mensajes en cualquier lugar de su programa.


Una forma muy inusual pero bastante efectiva de resolver esto de una manera MVVM aceptable es la siguiente:

  1. Cree un ContentControl comprimido de visibilidad en la misma Vista que TreeView. Nómbrelo apropiadamente y enlace su Contenido a alguna propiedad SelectedSomething en viewmodel. Este ContentControl "mantendrá" el objeto seleccionado y manejará su enlace, OneWayToSource;
  2. Escuche SelectedItemChanged en TreeView y agregue un controlador en código subyacente para configurar ContentControl.Content para el elemento recién seleccionado.

XAML:

<ContentControl x:Name="SelectedItemHelper" Content="{Binding SelectedObject, Mode=OneWayToSource}" Visibility="Collapsed"/> <TreeView ItemsSource="{Binding SomeCollection}" SelectedItemChanged="TreeView_SelectedItemChanged">

Código detrás:

private void TreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e) { SelectedItemHelper.Content = e.NewValue; }

ViewModel:

public object SelectedObject // Class is not actually "object" { get { return _selected_object; } set { _selected_object = value; RaisePropertyChanged(() => SelectedObject); Console.WriteLine(SelectedObject); } } object _selected_object;


Use el modo de enlace OneWayToSource . Esto no funciona Ver editar.

Editar : parece que se trata de un error o comportamiento "por diseño" de Microsoft, según esta pregunta ; aunque hay algunas soluciones publicadas. ¿Alguno de esos funciona para tu TreeView?

El problema de Microsoft Connect: https://connect.microsoft.com/WPF/feedback/details/523865/read-only-dependency-properties-does-not-support-onewaytosource-bindings

Publicado por Microsoft el 1/10/2010 a las 2:46 p.m.

No podemos hacer esto en WPF hoy, por la misma razón que no podemos soportar enlaces en propiedades que no son DependencyProperties. El estado de tiempo de ejecución por instancia de un enlace se mantiene en BindingExpression, que almacenamos en EffectiveValueTable para el objetivo DependencyObject. Cuando la propiedad de destino no es un DP o el DP es de solo lectura, no hay lugar para almacenar BindingExpression.

Es posible que algún día optemos por extender la funcionalidad de enlace a estos dos escenarios. Nos preguntan sobre ellos con bastante frecuencia. En otras palabras, su solicitud ya está en nuestra lista de funciones para considerar en futuras versiones.

Gracias por sus comentarios.


En realidad, no debería tener que ocuparse de la propiedad SelectedItem directamente, vincular IsSelected a una propiedad en su viewmodel y realizar un seguimiento del elemento seleccionado allí.

Un bosquejo:

<TreeView ItemsSource="{Binding TreeData}"> <TreeView.ItemContainerStyle> <Style TargetType="{x:Type TreeViewItem}"> <Setter Property="IsSelected" Value="{Binding IsSelected}" /> </Style> </TreeView.ItemContainerStyle> </TreeView>

public class TViewModel : INotifyPropertyChanged { private static object _selectedItem = null; // This is public get-only here but you could implement a public setter which // also selects the item. // Also this should be moved to an instance property on a VM for the whole tree, // otherwise there will be conflicts for more than one tree. public static object SelectedItem { get { return _selectedItem; } private set { if (_selectedItem != value) { _selectedItem = value; OnSelectedItemChanged(); } } } static virtual void OnSelectedItemChanged() { // Raise event / do other things } private bool _isSelected; public bool IsSelected { get { return _isSelected; } set { if (_isSelected != value) { _isSelected = value; OnPropertyChanged("IsSelected"); if (_isSelected) { SelectedItem = this; } } } } public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged(string propertyName) { var handler = this.PropertyChanged; if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName)); } }