Select TreeView Node on right click before displaying ContextMenu
Asked Answered
F

11

110

I would like to select a WPF TreeView Node on right click, right before the ContextMenu displayed.

For WinForms I could use code like this Find node clicked under context menu, what are the WPF alternatives?

Fabyola answered 26/2, 2009 at 20:49 Comment(0)
F
143

Depending on the way the tree was populated, the sender and the e.Source values may vary.

One of the possible solutions is to use e.OriginalSource and find TreeViewItem using the 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;
}
Fabyola answered 27/2, 2009 at 13:6 Comment(5)
is this event for the TreeView or TreeViewItem?Forwarding
an Any idea how to unselect everything if the right click is on an empty location?Forwarding
The only one answer which helped out of 5 others... I am really doing something wrong with treeview population, thanks.Ferren
In answer to Louis Rhys's question: if (treeViewItem == null) treeView.SelectedIndex = -1 or treeView.SelectedItem = null. I believe either should work.Mint
This event handler is for the TreeView.Filmy
H
27

If you want a XAML-only solution you can use Blend Interactivity.

Assume the TreeView is data bound to a hierarchical collection of view-models having a Boolean property IsSelected and a String property Name as well as a collection of child items named 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>

There are two interesting parts:

  1. The TreeViewItem.IsSelected property is bound to the IsSelected property on the view-model. Setting the IsSelected property on the view-model to true will select the corresponding node in the tree.

  2. When PreviewMouseRightButtonDown fires on the visual part of the node (in this sample a TextBlock) the IsSelected property on the view-model is set to true. Going back to 1. you can see that the corresponding node that was clicked on in the tree becomes the selected node.

One way to get Blend Interactivity in your project is to use the NuGet package Unofficial.Blend.Interactivity.

Henceforward answered 15/1, 2013 at 22:40 Comment(3)
Great answer, thank you! It would be helpful to show what the i and ei namespace mappings resolve to though and which assemblies they can be found in. I assume: xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" and xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions" , which are found in the System.Windows.Interactivity and Microsoft.Expression.Interactions assemblies respectively.Featherbedding
This didn't help as the ChangePropertyAction is trying to set an IsSelected property of the bound data object, which is not part of the UI, so it does not have IsSelected property. Am I doing something wrong?Cetinje
@AntonínProcházka: My answer requires that your "data object" (or view model) has an IsSelected property as stated in the second paragraph of my answer: Assume the TreeView is data bound to a hierarchical collection of view-models having a Boolean property IsSelected... (my emphasis).Henceforward
E
17

Using item.Focus(); doesn't seems to work 100%, using item.IsSelected = true; does.

Earmark answered 13/10, 2009 at 9:59 Comment(1)
Good tip. I call Focus() first, and then set IsSelected = true.Scalade
I
14

Using the original idea from alex2k8, correctly handling non-visuals from Wieser Software Ltd, the XAML from Stefan, the IsSelected from Erlend, and my contribution of truly making the static method Generic:

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# code behind:

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: The previous code always worked fine for this scenario, but in another scenario VisualTreeHelper.GetParent returned null when LogicalTreeHelper returned a value, so fixed that.

Indefectible answered 12/9, 2012 at 19:42 Comment(1)
To further this, this answer implements this in a DependencyProperty extension: https://mcmap.net/q/196394/-select-node-in-treeview-on-right-click-mvvmHomocercal
T
13

In XAML, add a PreviewMouseRightButtonDown handler in 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>

Then handle the event like this:

    private void TreeViewItem_PreviewMouseRightButtonDown( object sender, MouseEventArgs e )
    {
        TreeViewItem item = sender as TreeViewItem;
        if ( item != null )
        {
            item.Focus( );
            e.Handled = true;
        }
    }
Toolis answered 26/2, 2009 at 21:18 Comment(5)
It does not work as expected, I always get the root element as a sender. I have found a similar solution one social.msdn.microsoft.com/Forums/en-US/wpf/thread/… Event handlers added this way works as expected. Any changes to your code to accept it? :-)Fabyola
It apparently depends on how you populate the tree view. The code I posted works, because that's the exact code I use in one of my tools.Toolis
Note if you set a debug point here you can see what type your sender is which will of course differ based on how you setup the treeMonachism
This seems like the simplest solution when it works. It worked for me. In fact, you should just cast sender as a TreeViewItem because if it's not, that's a bug.Uncharted
For some reason, setting e.Handled = true causes it to select the root. Removing it seems to fix the issue....Whistle
S
7

Almost Right, but you need to watch out for non visuals in the tree, (like a Run, for instance).

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; 
}
Stila answered 8/4, 2011 at 17:17 Comment(2)
this generic method seems a little bit strange how can I use it when i write TreeViewItem treeViewItem = VisualUpwardSearch<TreeViewItem>(e.OriginalSource as DependencyObject); it gives me conversion errorMalleus
TreeViewItem treeViewItem = VisualUpwardSearch<TreeViewItem>(e.OriginalSource as DependencyObject) as TreeViewItem;Stila
S
7

I think registering a class handler should do the trick. Just register a routed event handler on the TreeViewItem's PreviewMouseRightButtonDownEvent in your app.xaml.cs code file like this:

/// <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;
    }
}
Schulman answered 7/10, 2011 at 9:22 Comment(3)
Worked for me! And simple too.Muncey
Hello Nathan. It sounds like the code is global and will affect every TreeView. Wouldn't be better to have a solution that is local only ? It could create side effects ?Booma
This code is indeed global for the whole WPF application. In my case, this was required behavior so it was consistent for all treeviews used within the application. You can however register this event on a treeview instance itself so it is only applicable for that treeview.Schulman
B
2

Another way to solve it using MVVM is bind command for right click to your view model. There you can specify other logic as well as source.IsSelected = true. This uses only xmlns:i="http://schemas.microsoft.com/expression/2010/intera‌​ctivity" from System.Windows.Interactivity.

XAML for view:

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

View model:

    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;
        }
    }
Banister answered 22/3, 2017 at 9:14 Comment(0)
A
1

I was having a problem with selecting children with a HierarchicalDataTemplate method. If I selected the child of a node it would somehow select the root parent of that child. I found out that the MouseRightButtonDown event would get called for every level the child was. For example if you have a tree something like this:

Item 1
   - Child 1
   - Child 2
      - Subitem1
      - Subitem2

If I selected Subitem2 the event would fire three times and item 1 would be selected. I solved this with a boolean and an asynchronous call.

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

It feels a little cludgy but basically I set the boolean to true on the first pass through and have it reset on another thread in a few seconds (3 in this case). This means that the next passes through where it would try to move up the tree will get skipped leaving you with the correct node selected. It seems to work so far :-)

Accept answered 27/9, 2013 at 14:3 Comment(1)
The answer is to set MouseButtonEventArgs.Handled to true. Since the child is the first one to be called. Settings this property to true will disable other calls to the parent.Whatsoever
N
0

You can select it with the on mouse down event. That will trigger the select before the context menu kicks in.

Nevis answered 27/4, 2012 at 14:26 Comment(0)
F
0

If you want to stay within the MVVM pattern you could do the following:

View:

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

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

Now you could either react to the ClickedTreeElement property change or you could use a command which internally works with the ClickedTreeElement.

Extended View:

<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>
Festination answered 7/11, 2019 at 14:57 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.