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?
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?
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;
}
if (treeViewItem == null) treeView.SelectedIndex = -1
or treeView.SelectedItem = null
. I believe either should work. –
Mint 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:
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.
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.
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 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 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 Using item.Focus();
doesn't seems to work 100%, using item.IsSelected = true;
does.
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.
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;
}
}
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;
}
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;
}
}
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/interactivity"
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;
}
}
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 :-)
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 You can select it with the on mouse down event. That will trigger the select before the context menu kicks in.
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>
© 2022 - 2024 — McMap. All rights reserved.