Seleccione el nodo TreeView al hacer clic con el botón derecho antes de mostrar ContextMenu

Respuestas:

130

Dependiendo de la forma en que se llenó el árbol, el remitente y los valores de e.Source pueden variar .

Una de las posibles soluciones es usar e.OriginalSource y 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
fuente
¿Es este evento para TreeView o TreeViewItem?
Louis Rhys
1
an ¿Alguna idea de cómo anular la selección de todo si el clic derecho está en una ubicación vacía?
Louis Rhys
La única respuesta que ayudó a otras 5 ... Realmente estoy haciendo algo mal con la población de treeview, gracias.
3
En respuesta a la pregunta de Louis Rhys: if (treeViewItem == null) treeView.SelectedIndex = -1o treeView.SelectedItem = null. Creo que cualquiera debería funcionar.
James M
24

Si desea una solución solo de XAML, puede usar Blend Interactivity.

Suponga que los TreeViewdatos están vinculados a una colección jerárquica de modelos de vista que tienen una Booleanpropiedad IsSelectedy una Stringpropiedad Name, así como una colección de elementos secundarios nombrados 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>

Hay dos partes interesantes:

  1. La TreeViewItem.IsSelectedpropiedad está vinculada a la IsSelectedpropiedad en el modelo de vista. Establecer la IsSelectedpropiedad en el modelo de vista en verdadero seleccionará el nodo correspondiente en el árbol.

  2. Cuando se PreviewMouseRightButtonDowndispara en la parte visual del nodo (en este ejemplo a TextBlock), la IsSelectedpropiedad del modelo de vista se establece en verdadera. Volviendo a 1., puede ver que el nodo correspondiente en el que se hizo clic en el árbol se convierte en el nodo seleccionado.

Una forma de obtener Blend Interactivity en su proyecto es usar el paquete NuGet Unofficial.Blend.Interactivity .

Martin Liversage
fuente
2
¡Gran respuesta, gracias! Sin embargo, sería útil mostrar en qué se resuelven las asignaciones de espacio de nombres iy eiy en qué ensamblados se pueden encontrar. Supongo: xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"y xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions", que se encuentran en los ensamblados System.Windows.Interactivity y Microsoft.Expression.Interactions respectivamente.
prlc
Esto no ayudó, ya que ChangePropertyActionestá intentando establecer una IsSelectedpropiedad del objeto de datos vinculado, que no forma parte de la interfaz de usuario, por lo que no tiene IsSelectedpropiedad. ¿Estoy haciendo algo mal?
Antonín Procházka
@ AntonínProcházka: Mi respuesta requiere que su "objeto de datos" (o modelo de vista) tenga una IsSelectedpropiedad como se indica en el segundo párrafo de mi respuesta: Suponga que los TreeViewdatos están vinculados a una colección jerárquica de modelos de vista que tienen una propiedad booleanaIsSelected ... (mi énfasis).
Martin Liversage
16

Usando "item.Focus ();" no parece funcionar al 100%, usando "item.IsSelected = true;" hace.

Erlend
fuente
Gracias por este consejo. Me ayudó.
i8abug
Buen consejo. Primero llamo a Focus () y luego configuro IsSelected = true.
Jim Gomes
12

En XAML, agregue un controlador 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>

Luego maneja el evento de esta manera:

    private void TreeViewItem_PreviewMouseRightButtonDown( object sender, MouseEventArgs e )
    {
        TreeViewItem item = sender as TreeViewItem;
        if ( item != null )
        {
            item.Focus( );
            e.Handled = true;
        }
    }
Stefan
fuente
2
No funciona como se esperaba, siempre obtengo el elemento raíz como remitente. Encontré una solución similar en social.msdn.microsoft.com/Forums/en-US/wpf/thread/… Los controladores de eventos agregados de esta manera funcionan como se esperaba. ¿Algún cambio en su código para aceptarlo? :-)
alex2k8
Aparentemente, depende de cómo llene la vista de árbol. El código que publiqué funciona, porque ese es el código exacto que uso en una de mis herramientas.
Stefan
Tenga en cuenta que si establece un punto de depuración aquí, puede ver de qué tipo es su remitente, que, por supuesto, diferirá en función de cómo configure el árbol
Esta parece la solución más simple cuando funciona. Funcionó para mí. De hecho, debería convertir el remitente como un TreeViewItem porque si no lo es, es un error.
craftworkgames
12

Usando la idea original de alex2k8, manejando correctamente los elementos no visuales de Wieser Software Ltd, el XAML de Stefan, el IsSelected de Erlend, y mi contribución de hacer verdaderamente genérico el método estático:

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 # detrá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;
}

Editar: el código anterior siempre funcionó bien para este escenario, pero en otro escenario VisualTreeHelper.GetParent devolvió nulo cuando LogicalTreeHelper devolvió un valor, así que lo solucionó.

Sean Hall
fuente
1
Para promover esto, esta respuesta implementa esto en una extensión DependencyProperty: stackoverflow.com/a/18032332/84522
Terrence
7

Casi correcto , pero debes tener cuidado con los elementos no visuales en el árbol (como Run, por ejemplo).

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
fuente
este método genérico parece un poco extraño cómo puedo usarlo cuando escribo TreeViewItem treeViewItem = VisualUpwardSearch <TreeViewItem> (e.OriginalSource como DependencyObject); me da un error de conversión
Rati_Ge
TreeViewItem treeViewItem = VisualUpwardSearch <TreeViewItem> (e.OriginalSource como DependencyObject) como TreeViewItem;
Anthony Wieser
6

Creo que registrar un controlador de clase debería ser suficiente. Simplemente registre un controlador de eventos enrutados en PreviewMouseRightButtonDownEvent de TreeViewItem en su archivo 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
fuente
¡Trabajó para mi! Y también simple.
dvallejo
2
Hola Nathan. Parece que el código es global y afectará a todos los TreeView. ¿No sería mejor tener una solución que sea solo local? ¿Podría crear efectos secundarios?
Eric Ouellet
Este código es de hecho global para toda la aplicación WPF. En mi caso, esto era un comportamiento obligatorio, por lo que era coherente para todas las vistas de árbol utilizadas dentro de la aplicación. Sin embargo, puede registrar este evento en una instancia de vista de árbol, por lo que solo es aplicable para esa vista de árbol.
Nathan Swannet
2

Otra forma de resolverlo usando MVVM es el comando de enlace para hacer clic derecho en su modelo de vista. Allí puede especificar otra lógica además de source.IsSelected = true. Esto usa solo xmlns:i="http://schemas.microsoft.com/expression/2010/intera‌​ctivity"de System.Windows.Interactivity.

XAML para ver:

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

Tenía un problema al seleccionar niños con un método HierarchicalDataTemplate. Si selecciono al hijo de un nodo, de alguna manera seleccionaría al padre raíz de ese hijo. Descubrí que el evento MouseRightButtonDown se llamaría para todos los niveles que tenía el niño. Por ejemplo, si tienes un árbol parecido a esto:

Elemento 1
   - Niño 1
   - Niño 2
      - Subitem1
      - Subitemm2

Si seleccioné Subitem2, el evento se dispararía tres veces y se seleccionaría el elemento 1. Resolví esto con una llamada booleana y asincrónica.

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

Se siente un poco complicado, pero básicamente configuro el booleano en verdadero en la primera pasada y lo restablezco en otro hilo en unos segundos (3 en este caso). Esto significa que el siguiente pasa por donde intentaría moverse hacia arriba, el árbol se saltará, dejándolo con el nodo correcto seleccionado. Parece funcionar hasta ahora :-)

Zoey
fuente
La respuesta es establecer MouseButtonEventArgs.Handleden true. Dado que el niño es el primero en ser llamado. Si configura esta propiedad en true, se inhabilitarán otras llamadas al padre.
Basit Anwer
0

Puede seleccionarlo con el evento on mouse down. Eso activará la selección antes de que se active el menú contextual.

Scott Thurlow
fuente
0

Si desea permanecer dentro del patrón MVVM, puede hacer lo siguiente:

Ver:

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

Ahora puede reaccionar al cambio de propiedad de ClickedTreeElement o puede usar un comando que funcione internamente con ClickedTreeElement.

Vista extendida:

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