Gestion du SelectedItem d'un TreeView en MVVM

La propriété SelectedItem d'un contrôle TreeView est en lecture seule, ce qui pose un défi pour l'implémentation du pattern MVVM où les interactions utilisateur doivent être liées aux propriétés du ViewModel. Pour surmonter cette limitation, deux approches pricnipales peuvent être adoptées.

  1. Utilisation d'un Comportement Personnalisé (Custom Behavior) Cette méthode implique la création d'une classe dérivée de Behavior<TreeView> qui gère explicitement la synchronisation entre la propriété SelectedItem du TreeView et une propriété correspondante dans le ViewModel. Vous devrez ajouter une référence à l'assemb System.Windows.Interactivity.dll.
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interactivity;

public class TreeViewSelectedItemBehavior : Behavior<TreeView>
{
    // Propriété pour lier le SelectedItem du TreeView
    public object SelectedItem
    {
        get { return GetValue(SelectedItemProperty); }
        set { SetValue(SelectedItemProperty, value); }
    }

    public static readonly DependencyProperty SelectedItemProperty =
        DependencyProperty.Register("SelectedItem", typeof(object), typeof(TreeViewSelectedItemBehavior), new PropertyMetadata(null, OnSelectedItemPropertyChanged));

    // Gère le changement de la propriété SelectedItem dans le ViewModel
    private static void OnSelectedItemPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var behavior = d as TreeViewSelectedItemBehavior;
        if (behavior != null && behavior.AssociatedObject != null)
        {
            // Tente de sélectionner l'élément correspondant dans le TreeView
            SetTreeViewSelectedItem(behavior.AssociatedObject, e.NewValue);
        }
    }

    // Méthode récursive pour trouver et sélectionner un TreeViewItem
    private static void SetTreeViewSelectedItem(TreeView treeView, object itemToSelect)
    {
        if (treeView == null || itemToSelect == null) return;

        // Parcours des éléments de premier niveau
        for (int i = 0; i < treeView.Items.Count; i++)
        {
            var treeViewItem = treeView.ItemContainerGenerator.ContainerFromIndex(i) as TreeViewItem;
            if (FindAndSelectTreeViewItem(treeViewItem, itemToSelect))
            {
                return; // Élément trouvé et sélectionné
            }
        }
    }

    // Méthode récursive pour trouver et sélectionner un TreeViewItem spécifique
    private static bool FindAndSelectTreeViewItem(TreeViewItem currentItem, object itemToSelect)
    {
        if (currentItem == null) return false;

        if (currentItem.DataContext == itemToSelect)
        {
            currentItem.IsSelected = true;
            currentItem.BringIntoView(); // Assure que l'élément est visible
            return true;
        }

        // Exploration des enfants si l'élément actuel n'est pas celui recherché
        for (int i = 0; i < currentItem.Items.Count; i++)
        {
            var childItem = currentItem.ItemContainerGenerator.ContainerFromIndex(i) as TreeViewItem;
            if (childItem != null && FindAndSelectTreeViewItem(childItem, itemToSelect))
            {
                return true; // Trouvé dans les sous-éléments
            }
        }
        return false; // Non trouvé dans cette branche
    }

    protected override void OnAttached()
    {
        base.OnAttached();
        // S'abonne à l'événement SelectedItemChanged du TreeView
        AssociatedObject.SelectedItemChanged += OnTreeViewSelectedItemChanged;
    }

    protected override void OnDetaching()
    {
        base.OnDetaching();
        // Se désabonne de l'événement pour éviter les fuites de mémoire
        AssociatedObject.SelectedItemChanged -= OnTreeView রোগেরSelectedItemChanged;
    }

    // Gère le changement du SelectedItem du TreeView et met à jour le ViewModel
    private void OnTreeViewSelectedItemChanged(object sender, RoutedEventArgs e)
    {
        var treeView = sender as TreeView;
        if (treeView != null)
        {
            // Met à jour la propriété SelectedItem du ViewModel avec le nouveau SelectedItem du TreeView
            SelectedItem = treeView.SelectedItem;
        }
    }
}

Utilisation dans le XAML :

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition/>
        <ColumnDefinition/>
    </Grid.ColumnDefinitions>
    <TreeView ItemsSource="{Binding Nodes}">
        <TreeView.ItemTemplate>
            <HierarchicalDataTemplate ItemsSource="{Binding Children}">
                <TextBlock Text="{Binding Name}"/>
            </HierarchicalDataTemplate>
        </TreeView.ItemTemplate>
        <i:Interaction.Behaviors>
            <local:TreeViewSelectedItemBehavior SelectedItem="{Binding SelectedItem, Mode=TwoWay}"/>
        </i:Interaction.Behaviors>
    </TreeView>
    <Label Grid.Column="1" Content="{Binding SelectedItem.Name}"/>
    <Button Grid.Column="1" Margin="100" Content="Action" Click="PerformAction"/>
</Grid>

  1. Utilisation d'une Propriété Attachée et d'un Gestionnaire d'Événement Routé Cette approche consiste à créer une classe d'assistance qui enregistre un gestionnaire pour l'événement TreeView.SelectedItemChangedEvent. Une propriété attachée est définie pour permettre la liaison bidirectionnelle.
using System.Windows;
using System.Windows.Controls;

public static class TreeViewAttachedProperties
{
    // Propriété attachée pour lier le SelectedItem
    public static readonly DependencyProperty SelectedItemProperty =
        DependencyProperty.RegisterAttached("SelectedItem", typeof(object), typeof(TreeViewAttachedProperties), new PropertyMetadata(null, OnSelectedItemPropertyChanged));

    public static object GetSelectedItem(DependencyObject obj)
    {
        return obj.GetValue(SelectedItemProperty);
    }

    public static void SetSelectedItem(DependencyObject obj, object value)
    {
        obj.SetValue(SelectedItemProperty, value);
    }

    // Constructeur statique pour enregistrer le gestionnaire d'événements
    static TreeViewAttachedProperties()
    {
        // Enregistre un gestionnaire pour l'événement SelectedItemChanged au niveau de la classe TreeView
        EventManager.RegisterClassHandler(typeof(TreeView), TreeView.SelectedItemChangedEvent, new RoutedEventHandler(OnTreeViewSelectedItemChangedGlobal));
    }

    // Callback appelé lorsque la propriété attachée SelectedItem change
    private static void OnSelectedItemPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        // Tente de sélectionner l'élément correspondant dans le TreeView
        SetTreeViewSelectedItem(d as TreeView, e.NewValue);
    }

    // Méthode pour trouver et sélectionner un TreeViewItem (identique à la méthode du Behavior)
    private static void SetTreeViewSelectedItem(TreeView treeView, object itemToSelect)
    {
        if (treeView == null || itemToSelect == null) return;

        for (int i = 0; i < treeView.Items.Count; i++)
        {
            var treeViewItem = treeView.ItemContainerGenerator.ContainerFromIndex(i) as TreeViewItem;
            if (FindAndSelectTreeViewItem(treeViewItem, itemToSelect))
            {
                return;
            }
        }
    }

    // Méthode récursive pour trouver et sélectionner un TreeViewItem spécifique (identique à la méthode du Behavior)
    private static bool FindAndSelectTreeViewItem(TreeViewItem currentItem, object itemToSelect)
    {
        if (currentItem == null) return false;

        if (currentItem.DataContext == itemToSelect)
        {
            currentItem.IsSelected = true;
            currentItem.BringIntoView();
            return true;
        }

        for (int i = 0; i < currentItem.Items.Count; i++)
        {
            var childItem = currentItem.ItemContainerGenerator.ContainerFromIndex(i) as TreeViewItem;
            if (childItem != null && FindAndSelectTreeViewItem(childItem, itemToSelect))
            {
                return true;
            }
        }
        return false;
    }

    // Gestionnaire global appelé lorsque le SelectedItem d'un TreeView change
    private static void OnTreeViewSelectedItemChangedGlobal(object sender, RoutedEventArgs e)
    {
        var treeView = sender as TreeView;
        if (treeView != null)
        {
            // Met à jour la propriété attachée SelectedItem du TreeView
            SetSelectedItem(treeView, treeView.SelectedItem);
        }
    }
}

Utilisation dans le XAML :

<Window x:Class="WPF_TreeViewMVVM.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WPF_TreeViewMVVM"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <TreeView ItemsSource="{Binding Nodes}"
                  local:TreeViewAttachedProperties.SelectedItem="{Binding SelectedItem, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
            <TreeView.ItemTemplate>
                <HierarchicalDataTemplate ItemsSource="{Binding Children}">
                    <TextBlock Text="{Binding Name}"/>
                </HierarchicalDataTemplate>
            </TreeView.ItemTemplate>
        </TreeView>
        <Label Grid.Column="1" Content="{Binding SelectedItem.Name}"/>
        <Button Grid.Column="1" Margin="100" Content="Action" Click="PerformAction"/>
    </Grid>
</Window>

Les deux méthodes atteignent l'objectif de permettre une liaison bidirectionnelle pour la propriété SelectedItem d'un TreeView en environnement MVVM, offrant ainsi une meilleure intégrasion avec le pattern.

Étiquettes: WPF MVVM TreeView SelectedItem Behavior

Publié le 10 juillet à 23h28