Sélectionnez TreeView Node sur un clic droit avant d'afficher ContextMenu

Réponses:

130

Selon la façon dont l'arborescence a été peuplée, les valeurs de l'expéditeur et de l'e.Source peuvent varier .

L'une des solutions possibles consiste à utiliser e.OriginalSource et à rechercher TreeViewItem à l'aide de VisualTreeHelper:

private void OnPreviewMouseRightButtonDown(object sender, MouseButtonEventArgs e)
{
    TreeViewItem treeViewItem = VisualUpwardSearch(e.OriginalSource as DependencyObject);

    if (treeViewItem != null)
    {
        treeViewItem.Focus();
        e.Handled = true;
    }
}

static TreeViewItem VisualUpwardSearch(DependencyObject source)
{
    while (source != null && !(source is TreeViewItem))
        source = VisualTreeHelper.GetParent(source);

    return source as TreeViewItem;
}
alex2k8
la source
est cet événement pour TreeView ou TreeViewItem?
Louis Rhys
1
Une idée comment tout désélectionner si le clic droit est sur un emplacement vide?
Louis Rhys
La seule réponse qui a aidé sur 5 autres ... Je fais vraiment quelque chose de mal avec la population treeview, merci.
3
En réponse à la question de Louis Rhys: if (treeViewItem == null) treeView.SelectedIndex = -1ou treeView.SelectedItem = null. Je pense que l'un ou l'autre devrait fonctionner.
James M
24

Si vous souhaitez une solution XAML uniquement, vous pouvez utiliser Blend Interactivity.

Supposons que les TreeViewdonnées soient liées à une collection hiérarchique de modèles de vue ayant une Booleanpropriété IsSelectedet une Stringpropriété Nameainsi qu'une collection d'éléments enfants nommés Children.

<TreeView ItemsSource="{Binding Items}">
  <TreeView.ItemContainerStyle>
    <Style TargetType="TreeViewItem">
      <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}"/>
    </Style>
  </TreeView.ItemContainerStyle>
  <TreeView.ItemTemplate>
    <HierarchicalDataTemplate ItemsSource="{Binding Children}">
      <TextBlock Text="{Binding Name}">
        <i:Interaction.Triggers>
          <i:EventTrigger EventName="PreviewMouseRightButtonDown">
            <ei:ChangePropertyAction PropertyName="IsSelected" Value="true" TargetObject="{Binding}"/>
          </i:EventTrigger>
        </i:Interaction.Triggers>
      </TextBlock>
    </HierarchicalDataTemplate>
  </TreeView.ItemTemplate>
</TreeView>

Il y a deux parties intéressantes:

  1. La TreeViewItem.IsSelectedpropriété est liée à la IsSelectedpropriété sur le modèle de vue. La définition de la IsSelectedpropriété sur le modèle de vue sur true sélectionnera le nœud correspondant dans l'arborescence.

  2. Lorsque se PreviewMouseRightButtonDowndéclenche sur la partie visuelle du nœud (dans cet exemple a TextBlock), la IsSelectedpropriété du modèle de vue est définie sur true. En revenant à 1. vous pouvez voir que le nœud correspondant sur lequel on a cliqué dans l'arborescence devient le nœud sélectionné.

Une façon d'obtenir l'interactivité de fusion dans votre projet consiste à utiliser le package NuGet Unofficial.Blend.Interactivity .

Martin Liversage
la source
2
Excellente réponse, merci! Il serait utile de montrer ce que les mappages d'espace de noms iet eirésolvent et dans quels assemblys ils peuvent être trouvés. Je suppose: xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"et xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions", qui se trouvent respectivement dans les assemblys System.Windows.Interactivity et Microsoft.Expression.Interactions.
prlc
Cela n'a pas aidé car le ChangePropertyActiontente de définir une IsSelectedpropriété de l'objet de données lié, qui ne fait pas partie de l'interface utilisateur, donc il n'a pas de IsSelectedpropriété. Est-ce que je fais quelque chose de mal?
Antonín Procházka
@ AntonínProcházka: Ma réponse exige que votre "objet de données" (ou modèle de vue) ait une IsSelectedpropriété comme indiqué dans le deuxième paragraphe de ma réponse: Supposons que les TreeViewdonnées soient liées à une collection hiérarchique de modèles de vue ayant une propriété booléenneIsSelected ... (je souligne).
Martin Liversage
16

Utilisation de "item.Focus ();" ne semble pas fonctionner à 100%, en utilisant "item.IsSelected = true;" Est-ce que.

Erlend
la source
Merci pour cette astuce. M'a aidé.
i8abug
Bon conseil. J'appelle d'abord Focus (), puis je fixe IsSelected = true.
Jim Gomes
12

En XAML, ajoutez un gestionnaire PreviewMouseRightButtonDown en XAML:

    <TreeView.ItemContainerStyle>
        <Style TargetType="{x:Type TreeViewItem}">
            <!-- We have to select the item which is right-clicked on -->
            <EventSetter Event="TreeViewItem.PreviewMouseRightButtonDown" Handler="TreeViewItem_PreviewMouseRightButtonDown"/>
        </Style>
    </TreeView.ItemContainerStyle>

Puis gérez l'événement comme ceci:

    private void TreeViewItem_PreviewMouseRightButtonDown( object sender, MouseEventArgs e )
    {
        TreeViewItem item = sender as TreeViewItem;
        if ( item != null )
        {
            item.Focus( );
            e.Handled = true;
        }
    }
Stefan
la source
2
Cela ne fonctionne pas comme prévu, j'obtiens toujours l'élément racine en tant qu'expéditeur. J'ai trouvé une solution similaire un social.msdn.microsoft.com/Forums/en-US/wpf/thread/... Les gestionnaires d'événements ajoutés de cette façon fonctionnent comme prévu. Des modifications à votre code pour l'accepter? :-)
alex2k8
Cela dépend apparemment de la façon dont vous remplissez l'arborescence. Le code que j'ai publié fonctionne, car c'est le code exact que j'utilise dans l'un de mes outils.
Stefan
Notez que si vous définissez un point de débogage ici, vous pouvez voir quel type est votre expéditeur qui sera bien sûr différent en fonction de la façon dont vous configurez l'arborescence
Cela semble être la solution la plus simple quand cela fonctionne. Cela a fonctionné pour moi. En fait, vous devriez simplement convertir l'expéditeur en TreeViewItem car si ce n'est pas le cas, c'est un bogue.
craftworkgames
12

En utilisant l'idée originale de alex2k8, en gérant correctement les non-visuels de Wieser Software Ltd, le XAML de Stefan, le IsSelected d'Erlend et ma contribution à vraiment rendre la méthode statique générique:

XAML:

<TreeView.ItemContainerStyle> 
    <Style TargetType="{x:Type TreeViewItem}"> 
        <!-- We have to select the item which is right-clicked on --> 
        <EventSetter Event="TreeViewItem.PreviewMouseRightButtonDown"
                     Handler="TreeViewItem_PreviewMouseRightButtonDown"/> 
    </Style> 
</TreeView.ItemContainerStyle>

Code C # derrière:

void TreeViewItem_PreviewMouseRightButtonDown(object sender, MouseButtonEventArgs e)
{
    TreeViewItem treeViewItem = 
              VisualUpwardSearch<TreeViewItem>(e.OriginalSource as DependencyObject);

    if(treeViewItem != null)
    {
        treeViewItem.IsSelected = true;
        e.Handled = true;
    }
}

static T VisualUpwardSearch<T>(DependencyObject source) where T : DependencyObject
{
    DependencyObject returnVal = source;

    while(returnVal != null && !(returnVal is T))
    {
        DependencyObject tempReturnVal = null;
        if(returnVal is Visual || returnVal is Visual3D)
        {
            tempReturnVal = VisualTreeHelper.GetParent(returnVal);
        }
        if(tempReturnVal == null)
        {
            returnVal = LogicalTreeHelper.GetParent(returnVal);
        }
        else returnVal = tempReturnVal;
    }

    return returnVal as T;
}

Edit: Le code précédent fonctionnait toujours très bien pour ce scénario, mais dans un autre scénario, VisualTreeHelper.GetParent retournait null lorsque LogicalTreeHelper renvoyait une valeur, donc corrigé cela.

Salle Sean
la source
1
Pour approfondir cela, cette réponse implémente ceci dans une extension DependencyProperty: stackoverflow.com/a/18032332/84522
Terrence
7

Presque à droite , mais vous devez faire attention aux éléments non visuels dans l'arborescence (comme un Run, par exemple).

static DependencyObject VisualUpwardSearch<T>(DependencyObject source) 
{
    while (source != null && source.GetType() != typeof(T))
    {
        if (source is Visual || source is Visual3D)
        {
            source = VisualTreeHelper.GetParent(source);
        }
        else
        {
            source = LogicalTreeHelper.GetParent(source);
        }
    }
    return source; 
}
Anthony Wieser
la source
cette méthode générique semble un peu étrange comment puis-je l'utiliser lorsque j'écris TreeViewItem treeViewItem = VisualUpwardSearch <TreeViewItem> (e.OriginalSource comme DependencyObject); il me donne une erreur de conversion
Rati_Ge
TreeViewItem treeViewItem = VisualUpwardSearch <TreeViewItem> (e.OriginalSource en tant que DependencyObject) en tant que TreeViewItem;
Anthony Wieser
6

Je pense que l'enregistrement d'un gestionnaire de classe devrait faire l'affaire. Enregistrez simplement un gestionnaire d'événements routés sur le PreviewMouseRightButtonDownEvent de TreeViewItem dans votre fichier de code app.xaml.cs comme ceci:

/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
    protected override void OnStartup(StartupEventArgs e)
    {
        EventManager.RegisterClassHandler(typeof(TreeViewItem), TreeViewItem.PreviewMouseRightButtonDownEvent, new RoutedEventHandler(TreeViewItem_PreviewMouseRightButtonDownEvent));

        base.OnStartup(e);
    }

    private void TreeViewItem_PreviewMouseRightButtonDownEvent(object sender, RoutedEventArgs e)
    {
        (sender as TreeViewItem).IsSelected = true;
    }
}
Nathan Swannet
la source
A travaillé pour moi! Et simple aussi.
dvallejo
2
Bonjour Nathan. Il semble que le code est global et affectera chaque TreeView. Ne serait-il pas préférable d'avoir une solution uniquement locale? Cela pourrait créer des effets secondaires?
Eric Ouellet
Ce code est en effet global pour l'ensemble de l'application WPF. Dans mon cas, c'était un comportement obligatoire, il était donc cohérent pour toutes les arborescences utilisées dans l'application. Vous pouvez cependant enregistrer cet événement sur une instance d'arborescence elle-même afin qu'il ne s'applique qu'à cette arborescence.
Nathan Swannet
2

Une autre façon de le résoudre à l'aide de MVVM est la commande de liaison pour un clic droit sur votre modèle de vue. Là, vous pouvez spécifier une autre logique ainsi que source.IsSelected = true. Cela utilise uniquement à xmlns:i="http://schemas.microsoft.com/expression/2010/intera‌​ctivity"partir de System.Windows.Interactivity.

XAML pour la vue:

<TreeView ItemsSource="{Binding Items}">
  <TreeView.ItemContainerStyle>
    <Style TargetType="TreeViewItem">
      <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}"/>
    </Style>
  </TreeView.ItemContainerStyle>
  <TreeView.ItemTemplate>
    <HierarchicalDataTemplate ItemsSource="{Binding Children}">
      <TextBlock Text="{Binding Name}">
        <i:Interaction.Triggers>
          <i:EventTrigger EventName="PreviewMouseRightButtonDown">
            <i:InvokeCommandAction Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=DataContext.TreeViewItemRigthClickCommand}" CommandParameter="{Binding}" />
          </i:EventTrigger>
        </i:Interaction.Triggers>
      </TextBlock>
    </HierarchicalDataTemplate>
  </TreeView.ItemTemplate>
</TreeView>

Voir le modèle:

    public ICommand TreeViewItemRigthClickCommand
    {
        get
        {
            if (_treeViewItemRigthClickCommand == null)
            {
                _treeViewItemRigthClickCommand = new RelayCommand<object>(TreeViewItemRigthClick);
            }
            return _treeViewItemRigthClickCommand;
        }
    }
    private RelayCommand<object> _treeViewItemRigthClickCommand;

    private void TreeViewItemRigthClick(object sourceItem)
    {
        if (sourceItem is Item)
        {
            (sourceItem as Item).IsSelected = true;
        }
    }
Benderto
la source
1

J'avais un problème avec la sélection d'enfants avec une méthode HierarchicalDataTemplate. Si je sélectionnais l'enfant d'un nœud, cela sélectionnerait en quelque sorte le parent racine de cet enfant. J'ai découvert que l'événement MouseRightButtonDown serait appelé pour chaque niveau de l'enfant. Par exemple, si vous avez un arbre quelque chose comme ceci:

Rubrique 1
   - Enfant 1
   - Enfant 2
      - Sous
      -élément1 - Sous -élément2

Si je sélectionnais Subitem2, l'événement se déclencherait trois fois et l'élément 1 serait sélectionné. J'ai résolu cela avec un appel booléen et asynchrone.

private bool isFirstTime = false;
    protected void TaskTreeView_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
    {
        var item = sender as TreeViewItem;
        if (item != null && isFirstTime == false)
        {
            item.Focus();
            isFirstTime = true;
            ResetRightClickAsync();
        }
    }

    private async void ResetRightClickAsync()
    {
        isFirstTime = await SetFirstTimeToFalse();
    }

    private async Task<bool> SetFirstTimeToFalse()
    {
        return await Task.Factory.StartNew(() => { Thread.Sleep(3000); return false; });
    }

Cela semble un peu brouillon, mais en gros, je règle le booléen sur true lors du premier passage et le réinitialise sur un autre thread en quelques secondes (3 dans ce cas). Cela signifie que le prochain passage par où il essaierait de monter dans l'arborescence sera ignoré, vous laissant avec le bon nœud sélectionné. Cela semble fonctionner jusqu'à présent :-)

Zoey
la source
La réponse est de régler MouseButtonEventArgs.Handledsur true. Puisque l'enfant est le premier à être appelé. Si cette propriété est définie sur true, les autres appels au parent seront désactivés.
Basit Anwer
0

Vous pouvez le sélectionner avec l'événement on mouse down. Cela déclenchera la sélection avant que le menu contextuel ne démarre.

Scott Thurlow
la source
0

Si vous souhaitez rester dans le modèle MVVM, vous pouvez effectuer les opérations suivantes:

Vue:

<TreeView x:Name="trvName" ItemsSource="{Binding RootElementListView}" Tag="{Binding ClickedTreeElement, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
    <TreeView.ItemTemplate>
        <HierarchicalDataTemplate DataType="{x:Type models:YourTreeElementClass}" ItemsSource="{Binding Path=Subreports}">
            <TextBlock Text="{Binding YourTreeElementDisplayProperty}" PreviewMouseRightButtonDown="TreeView_PreviewMouseRightButtonDown"/>
        </HierarchicalDataTemplate>
    </TreeView.ItemTemplate>
</TreeView>

Code derrière:

private void TreeView_PreviewMouseRightButtonDown(object sender, MouseButtonEventArgs e)
{
    if (sender is TextBlock tb && tb.DataContext is YourTreeElementClass te)
    {
        trvName.Tag = te;
    }
}

VoirModèle:

private YourTreeElementClass _clickedTreeElement;

public YourTreeElementClass ClickedTreeElement
{
    get => _clickedTreeElement;
    set => SetProperty(ref _clickedTreeElement, value);
}

Vous pouvez maintenant réagir à la modification de propriété ClickedTreeElement ou utiliser une commande qui fonctionne en interne avec ClickedTreeElement.

Vue étendue:

<UserControl ...
             xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity">
    <TreeView x:Name="trvName" ItemsSource="{Binding RootElementListView}" Tag="{Binding ClickedTreeElement, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
        <i:Interaction.Triggers>
            <i:EventTrigger EventName="MouseRightButtonUp">
                <i:InvokeCommandAction Command="{Binding HandleRightClickCommand}"/>
            </i:EventTrigger>
        </i:Interaction.Triggers>
        <TreeView.ItemTemplate>
            <HierarchicalDataTemplate DataType="{x:Type models:YourTreeElementClass}" ItemsSource="{Binding Path=Subreports}">
                <TextBlock Text="{Binding YourTreeElementDisplayProperty}" PreviewMouseRightButtonDown="TreeView_PreviewMouseRightButtonDown"/>
            </HierarchicalDataTemplate>
        </TreeView.ItemTemplate>
    </TreeView>
</UserControl>
RonnyR
la source