web-dev-qa-db-fra.com

WPF MVVM TreeView SelectedItem

Cela ne peut pas être aussi difficile. TreeView dans WPF ne vous permet pas de définir le SelectedItem, en disant que la propriété est en lecture seule. J'ai le peuplement TreeView, même la mise à jour lorsque sa collection de données change.

J'ai juste besoin de savoir quel article est sélectionné. J'utilise MVVM, donc il n'y a pas de code ou de variable pour référencer l'arborescence. C'est la seule solution J'ai trouvé, mais c'est un hack évident, il crée un autre élément en XAML qui utilise la liaison ElementName pour se définir sur l'élément sélectionné dans les arborescences, que vous devez ensuite lier à votre Viewmodel aussi . Plusieurs autre questions sont posées à ce sujet, mais aucune autre solution de travail n'est donnée.

J'ai vu cette question , mais l'utilisation de la réponse donnée me donne des erreurs de compilation, pour une raison quelconque, je ne peux pas ajouter une référence au mélange sdk System.Windows.Interactivity à mon projet. Il indique "erreur inconnue system.windows n'a pas été préchargée" et je n'ai pas encore compris comment surmonter cela.

Pour les points bonus: pourquoi diable Microsoft a-t-il créé la propriété SelectedItem de cet élément en lecture seule?

30
Kyeotic

Vous ne devriez pas vraiment avoir besoin de traiter directement la propriété SelectedItem, de lier IsSelected à une propriété de votre viewmodel et de garder une trace de l'élément sélectionné.

Un croquis:

<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));
    }
}
47
H.B.

Vous pouvez créer une propriété attachée qui peut être liée et possède un getter et 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;
        }
    }
}

Ajoutez la déclaration d'espace de noms contenant cette classe à votre XAML et liez comme suit (local est la façon dont j'ai nommé la déclaration d'espace de noms):

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

Vous pouvez maintenant lier l'élément sélectionné et le définir également dans votre modèle de vue pour le modifier par programme, si cette exigence devait se produire. Ceci, bien sûr, en supposant que vous implémentez INotifyPropertyChanged sur cette propriété particulière.

12
Bas

Un moyen très inhabituel mais assez efficace pour résoudre ce problème d'une manière acceptable par MVVM est le suivant:

  1. Créez un ContentControl à visibilité réduite sur la même vue que TreeView. Nommez-le de manière appropriée et liez son contenu à une propriété SelectedSomething dans viewmodel. Ce ContentControl "tiendra" l'objet sélectionné et gérera sa liaison, OneWayToSource;
  2. Écoutez le SelectedItemChanged dans TreeView et ajoutez un gestionnaire en code-behind pour définir votre ContentControl.Content sur l'élément nouvellement sélectionné.

XAML:

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

Code derrière:

    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;
6
heltonbiker

Utilisez le mode de liaison OneWayToSource . Ça ne marche pas. Voir modifier.

Edit : Il semble que ce soit un bug ou un comportement "par conception" de Microsoft, selon cette question ; il existe cependant quelques solutions de contournement. Est-ce que certains fonctionnent pour votre TreeView?

Le problème Microsoft Connect: https://connect.Microsoft.com/WPF/feedback/details/523865/read-only-dependency-properties-does-not-support-onewaytosource-bindings

Publié par Microsoft le 10/01/2010 à 14h46

Nous ne pouvons pas le faire dans WPF aujourd'hui, pour la même raison, nous ne pouvons pas prendre en charge les liaisons sur des propriétés qui ne sont pas DependencyProperties. L'état d'exécution par instance d'une liaison est conservé dans une BindingExpression, que nous stockons dans la EffectiveValueTable pour le DependencyObject cible. Lorsque la propriété cible n'est pas un DP ou que le DP est en lecture seule, il n'y a pas de place pour stocker la BindingExpression.

Il est possible que nous choisissions un jour d'étendre la fonctionnalité de liaison à ces deux scénarios. On nous pose souvent des questions à leur sujet. En d'autres termes, votre demande figure déjà sur notre liste de fonctionnalités à prendre en compte dans les futures versions.

Merci pour vos commentaires.

4
Aphex

J'ai décidé d'utiliser une combinaison de code derrière et de code viewmodel. le xaml est comme ceci:

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

Code derrière

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

Et dans le viewmodel, il y a un objet TreeItemSelected auquel vous pouvez ensuite accéder dans le viewmodel.

2
allan

Vous pouvez toujours créer un DependencyProperty qui utilise ICommand et écouter l'événement SelectedItemChanged sur TreeView. Cela peut être un peu plus facile que de lier IsSelected, mais j'imagine que vous finirez par lier IsSelected de toute façon pour d'autres raisons. Si vous souhaitez simplement vous lier sur IsSelected, vous pouvez toujours demander à votre élément d'envoyer un message chaque fois que IsSelected change. Ensuite, vous pouvez écouter ces messages n'importe où dans votre programme.

1
stricq