Selecione TreeView Node com o botão direito antes de exibir ContextMenu

100

Gostaria de selecionar um Nó TreeView WPF com um clique direito, logo antes do ContextMenu ser exibido.

Para WinForms, eu poderia usar um código como este nó Find clicado no menu de contexto , quais são as alternativas do WPF?

alex2k8
fonte

Respostas:

130

Dependendo da forma como a árvore foi preenchida, os valores do remetente e da e.Source podem variar .

Uma das soluções possíveis é usar e.OriginalSource e encontrar TreeViewItem usando 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
fonte
este evento é para TreeView ou TreeViewItem?
Louis Rhys de
1
an Alguma ideia de como desmarcar tudo se o botão direito estiver em um local vazio?
Louis Rhys
A única resposta que ajudou entre 5 outras ... Estou realmente fazendo algo errado com a população de visualização em árvore, obrigado.
3
Em resposta à pergunta de Louis Rhys: if (treeViewItem == null) treeView.SelectedIndex = -1ou treeView.SelectedItem = null. Eu acredito que qualquer um deve funcionar.
James M
24

Se você deseja uma solução apenas XAML, pode usar o Blend Interactivity.

Suponha que os TreeViewdados sejam vinculados a uma coleção hierárquica de modelos de visualização tendo uma Booleanpropriedade IsSelectede uma Stringpropriedade Name, bem como uma coleção de itens filhos nomeados 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>

Existem duas partes interessantes:

  1. A TreeViewItem.IsSelectedpropriedade está ligada à IsSelectedpropriedade no modelo de exibição. Definir a IsSelectedpropriedade no modelo de visualização como true selecionará o nó correspondente na árvore.

  2. Quando PreviewMouseRightButtonDowndispara na parte visual do nó (neste exemplo a TextBlock), a IsSelectedpropriedade no modelo de visualização é definida como verdadeira. Voltando a 1. você pode ver que o nó correspondente que foi clicado na árvore se torna o nó selecionado.

Uma maneira de obter o Blend Interactivity em seu projeto é usar o pacote NuGet Unofficial.Blend.Interactivity .

Martin Liversage
fonte
2
Ótima resposta, obrigado! Seria útil mostrar como os mapeamentos ie einamespace resolvem embora e em quais assemblies eles podem ser encontrados. Suponho que: xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"e xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions", que são encontrados nos assemblies System.Windows.Interactivity e Microsoft.Expression.Interactions respectivamente.
prlc
Isso não ajudou, pois o ChangePropertyActionestá tentando definir uma IsSelectedpropriedade do objeto de dados vinculado, que não faz parte da interface do usuário, portanto, não tem IsSelectedpropriedade. Estou fazendo algo errado?
Antonín Procházka
@ AntonínProcházka: Minha resposta requer que seu "objeto de dados" (ou modelo de visualização) tenha uma IsSelectedpropriedade conforme declarado no segundo parágrafo da minha resposta: Suponha que os TreeViewdados sejam vinculados a uma coleção hierárquica de modelos de visualização com uma propriedade booleanaIsSelected ... (ênfase minha).
Martin Liversage
16

Usando "item.Focus ();" não parece funcionar 100%, usando "item.IsSelected = true;" faz.

Erlend
fonte
Obrigado por esta dica. Me ajudou.
18abug
Boa dica. Eu chamo Focus () primeiro e, em seguida, defino IsSelected = true.
Jim Gomes
12

Em XAML, adicione um manipulador PreviewMouseRightButtonDown em 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>

Em seguida, trate o evento assim:

    private void TreeViewItem_PreviewMouseRightButtonDown( object sender, MouseEventArgs e )
    {
        TreeViewItem item = sender as TreeViewItem;
        if ( item != null )
        {
            item.Focus( );
            e.Handled = true;
        }
    }
Stefan
fonte
2
Não funciona como esperado, sempre recebo o elemento raiz como remetente. Eu encontrei uma solução semelhante um social.msdn.microsoft.com/Forums/en-US/wpf/thread/… Manipuladores de eventos adicionados dessa forma funcionam conforme o esperado. Alguma alteração em seu código para aceitá-lo? :-)
alex2k8
Aparentemente, depende de como você preenche a visualização em árvore. O código que postei funciona, porque é o código exato que uso em uma de minhas ferramentas.
Stefan
Observe que, se você definir um ponto de depuração aqui, poderá ver que tipo é o seu remetente, o que irá diferir com base em como você configurou a árvore
Esta parece ser a solução mais simples quando funciona. Funcionou para mim Na verdade, você deve apenas converter o remetente como um TreeViewItem, porque se não for, isso é um bug.
craftworkgames de
12

Usando a ideia original de alex2k8, manipulando corretamente os elementos não visuais da Wieser Software Ltd, o XAML de Stefan, o IsSelected de Erlend e minha contribuição de realmente tornar o método estático Genérico:

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>

Código C # por trás:

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: O código anterior sempre funcionou bem para este cenário, mas em outro cenário VisualTreeHelper.GetParent retornou null quando LogicalTreeHelper retornou um valor, então corrigiu isso.

Sean Hall
fonte
1
Para promover isso, esta resposta implementa isso em uma extensão DependencyProperty: stackoverflow.com/a/18032332/84522
Terrence
7

Quase certo , mas você precisa estar atento para não visuais na árvore, (como um Run, por exemplo).

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
fonte
este método genérico parece um pouco estranho, como posso usá-lo ao escrever TreeViewItem treeViewItem = VisualUpwardSearch <TreeViewItem> (e.OriginalSource as DependencyObject); dá-me um erro de conversão
Rati_Ge
TreeViewItem treeViewItem = VisualUpwardSearch <TreeViewItem> (e.OriginalSource como DependencyObject) como TreeViewItem;
Anthony Wieser
6

Acho que registrar um manipulador de classe deve resolver o problema. Basta registrar um manipulador de eventos roteados no PreviewMouseRightButtonDownEvent do TreeViewItem em seu arquivo de código app.xaml.cs como este:

/// <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
fonte
Funcionou para mim! E simples também.
dvallejo
2
Olá Nathan. Parece que o código é global e afetará cada TreeView. Não seria melhor ter uma solução apenas local? Isso pode criar efeitos colaterais?
Eric Ouellet
Este código é realmente global para todo o aplicativo WPF. No meu caso, esse era o comportamento necessário, portanto, era consistente para todas as visualizações de árvore usadas no aplicativo. No entanto, você pode registrar este evento em uma instância de treeview, portanto, só é aplicável para essa treeview.
Nathan Swannet
2

Outra maneira de resolver isso usando MVVM é o comando de ligação para clicar com o botão direito em seu modelo de visualização. Lá você também pode especificar outra lógica source.IsSelected = true. Isso usa apenas xmlns:i="http://schemas.microsoft.com/expression/2010/intera‌​ctivity"de System.Windows.Interactivity.

XAML para visualização:

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

Ver modelo:

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

Eu estava tendo problemas ao selecionar filhos com um método HierarchicalDataTemplate. Se eu selecionasse o filho de um nó, de alguma forma, ele selecionaria o pai raiz desse filho. Eu descobri que o evento MouseRightButtonDown seria chamado para todos os níveis em que a criança estava. Por exemplo, se você tiver uma árvore parecida com esta:

Item 1
   - Criança 1
   - Criança 2
      - Subitem1
      - Subitem2

Se eu selecionasse o Subitem2, o evento seria disparado três vezes e o item 1 seria selecionado. Resolvi isso com uma chamada booleana e assíncrona.

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

Parece um pouco confuso, mas basicamente eu defini o booleano como verdadeiro na primeira passagem e o redefini em outro encadeamento em alguns segundos (3 neste caso). Isso significa que a próxima passagem por onde tentaria subir na árvore será ignorada, deixando você com o nó correto selecionado. Parece que funciona até agora :-)

Zoey
fonte
A resposta é definir MouseButtonEventArgs.Handledcomo true. Pois a criança é a primeira a ser chamada. Configurar esta propriedade como true irá desativar outras chamadas para o pai.
Basit Anwer
0

Você pode selecioná-lo com o evento ao pressionar o mouse. Isso irá acionar o select antes de o menu de contexto entrar em ação.

Scott Thurlow
fonte
0

Se você deseja permanecer dentro do padrão MVVM, você pode fazer o seguinte:

Visão:

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

Código por trás:

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

ViewModel:

private YourTreeElementClass _clickedTreeElement;

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

Agora você pode reagir à alteração da propriedade ClickedTreeElement ou pode usar um comando que funcione internamente com ClickedTreeElement.

Visão estendida:

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