I have a ViewModel on top of a WPF TreeView control. I want the ViewModel to be able to set and read the SelectedItem from the TreeView. However, the SelectedItem property of the TreeView is not bindable.
I am able to set and get the selected item in the code behind (using the ItemContainerGenerator and TreeViewItem.IsSelected = true) but this leads to some ugly communication between the code behind and the ViewModel.
Does anyone have a clean solution for this?
I can provide an example.
What I do is setting the IsSelected property of a TreeViewItem (not the TreeView itself) in the view model, because you can bind to this.
In my view model I have a property ElementInViewModel which is a data structure that forms a tree itself.
I use a HierarchicalDataTemplate in my Xaml to display it.
The data object itself is of type YourDomainType and its child elements (of the same type) are in its ChildElements property.
In the view model, I set the IsExpanded and IsSelected property of my data class YourDomainType. Because of the style defined below, they will pass this setting to the TreeViewItem.
Does this work for you?
<UserControl>
<UserControl.Resources>
<CollectionViewSource Source="{Binding Path=ElementInViewModel}" x:Key="Cvs">
</CollectionViewSource>
<HierarchicalDataTemplate DataType="{x:Type DomainModel:YourDomainType}"
ItemsSource="{Binding Path=ChildElements}">
<TextBlock Text="{Binding Path=Name}"/>
</HierarchicalDataTemplate>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
</Setter>
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
</Setter>
</Style>
</UserControl.Resources>
<DockPanel>
<TreeView ItemsSource="{Binding Source={StaticResource Cvs}}"/>
</DockPanel>
</UserControl>
You can use some kind of proxy class to bind SelectedItem property to In property and Out property bind to your ViewModel:
public class Proxy : FrameworkElement
{
public static readonly DependencyProperty InProperty;
public static readonly DependencyProperty OutProperty;
public Proxy()
{
Visibility = Visibility.Collapsed;
}
static Proxy()
{
var inMetadata = new FrameworkPropertyMetadata(
delegate(DependencyObject p, DependencyPropertyChangedEventArgs args)
{
if (null != BindingOperations.GetBinding(p, OutProperty))
{
var proxy = p as Proxy;
if (proxy != null)
proxy.Out = args.NewValue;
}
});
inMetadata.BindsTwoWayByDefault = false;
inMetadata.DefaultUpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
InProperty = DependencyProperty.Register("In",
typeof (object),
typeof (Proxy),
inMetadata);
var outMetadata = new FrameworkPropertyMetadata(
delegate(DependencyObject p, DependencyPropertyChangedEventArgs args)
{
ValueSource source = DependencyPropertyHelper.GetValueSource(p, args.Property);
if (source.BaseValueSource != BaseValueSource.Local)
{
var proxy = p as Proxy;
if (proxy != null)
{
var expected = proxy.In;
if (!ReferenceEquals(args.NewValue, expected))
{
Dispatcher.CurrentDispatcher.BeginInvoke(
DispatcherPriority.DataBind, new Action(delegate
{
proxy.Out = proxy.In;
}));
}
}
}
});
outMetadata.BindsTwoWayByDefault = true;
outMetadata.DefaultUpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
OutProperty = DependencyProperty.Register("Out",
typeof (object),
typeof (Proxy),
outMetadata);
}
public object In
{
get { return GetValue(InProperty); }
set { SetValue(InProperty, value); }
}
public object Out
{
get { return GetValue(OutProperty); }
set { SetValue(OutProperty, value); }
}
}
<Proxy In="{Binding ElementName=Tree, Path=SelectedItem}" Out="{Binding SelectedItem, UpdateSourceTrigger=PropertyChanged}"/>
<TreeView x:Name="Tree" ItemsSource="{Binding Path=Items}"/>
Related
I want make TreeView with editable nodes. I googled this good, as I think, article:
http://www.codeproject.com/Articles/31592/Editable-TextBlock-in-WPF-for-In-place-Editing
But I have a problems. My TreeView formed dinamically, not statically as in the arcticle. Like that
<TreeView Name="_packageTreeView" Margin="5" ItemsSource="{Binding PackageExtendedList}">
<TreeView.InputBindings>
<KeyBinding Key="C" Command="{Binding *TestCommand*}" CommandParameter="{Binding}" />
</TreeView.InputBindings>
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectedItemChanged">
<i:InvokeCommandAction Command="{Binding PackageTreeItemChangeCommand}" CommandParameter="{Binding ElementName=_packageTreeView, Path=SelectedItem}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsSelected" Value="{Binding Path=IsSelected, Mode=TwoWay}" />
</Style>
</TreeView.ItemContainerStyle>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate DataType="{x:Type MasterBuisnessLogic:RootDocPackage}" ItemsSource="{Binding Path=Childs}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition>
</ColumnDefinition>
<ColumnDefinition>
</ColumnDefinition>
</Grid.ColumnDefinitions>
<Image Source="/Resources/DocGroup.png"></Image>
<Etb:EditableTextBlock Margin="5,0,0,0" Grid.Column="1" Text="{Binding Path=Name}"></Etb:EditableTextBlock>
</Grid>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
PackageExtendedList - List of DocPackageExtended.
So, first question - how can I get TreeViewItem instance in TestCommand? Not instance DocPackageExtended class! I want to get instance selected TreeViewItem like in the article.
And second question - After I get instance TreeViewItem, how can I get EditableTextBlock from the TreeView item's DataTemplate.
added answer
I already tried it. Cause in MVVM ViewModel cannot has any link to View object like TreeView, I make handler in code-behind, like that
private void TreeViewItemSelected(object sender, RoutedEventArgs e)
{
// Already have TreeViewItem instance without of ItemContainerGenerator help
var tvi = e.OriginalSource as TreeViewItem;
if (tvi == null)
return;
var etb = VisualTreeLib.VisualTreeLib.GetVisualChild<EditableTextBlock>(tvi);
if (etb == null)
return;
// Do what I want
etb.IsEditable = true;
}
Unfortunately, this has no any affect :(
I also tried that approach, but also failed.
in DocPackageExtended type I define property
public bool IsEditable
{
get { return _isEditable; }
set
{
_isEditable = value;
OnPropertyChanged(new PropertyChangedEventArgs("IsEditable"));
}
}
than change in XAML:
<Etb:EditableTextBlock Margin="5,0,0,0" Grid.Column="1" Text="{Binding Path=Name}" *IsEditable="{Binding Path=IsEditable}"*/>
and in ViewModel
private void TestCommandMethod(object obj)
{
var dpe = obj as DocPackageExtended;
if (dpe == null)
return;
dpe.IsEditable = true;
}
Doesn't work too :(
Any ideas?
This might help you.
private void Button_Click(object sender, RoutedEventArgs e)
{
TreeViewItem treeViewItemFound = GetItem(MyTreeview, MyTreeview.SelectedItem);
ContentPresenter header = treeViewItemFound.Template.FindName("PART_Header", treeViewItemFound) as ContentPresenter;
if (header != null)
{
TextBox myTextBox = (TextBox)header.ContentTemplate.FindName("MyTextBox", header);
}
}
public TreeViewItem GetItem(ItemsControl container, object itemToSelect)
{
foreach (object item in container.Items)
{
if (item == itemToSelect)
{
return (TreeViewItem)container.ItemContainerGenerator.ContainerFromItem(item);
}
else
{
ItemsControl itemContainer = (ItemsControl)container.ItemContainerGenerator.ContainerFromItem(item);
if (itemContainer.Items.Count > 0)
{
TreeViewItem treeViewItemFound = GetItem(itemContainer, itemToSelect);
if (treeViewItemFound != null)
return treeViewItemFound;
}
}
}
return null;
}
First question: Since it seems that you can select multiple entries, you need to filter all selected entries in TestCommand's executed method:
IEnumerable<DocPackageExtended> selectedEntries = PackageExtendedList.Where(d => d.IsSelected);
If multiple selection is disabled, you could bind the TreeView's selected item to a property in your VM and access this property in TestCommand's method.
Second question: You get the dataitem's container through var container = YourTreeViewInstance.ItemContainerGenerator.ContainerFromItem(dataInstance);. Now you have to go through this container with the help of the VisualTreeHelper until it finds a control of type EditableTextBlock. But I wouldn't do this in a ViewModel, rather in a helper-class or with the help of attached properties.
EDIT: You're binding the IsEditable property of the instances in the Childs property of your DocPackageExtended class to your EditableTextBox, but in your TestCommandMethod you're manipulating the IsEditableproperty of a DocPackageExtended instance directly. You could do the following:
private void TestCommandMethod(object obj)
{
var dpe = obj as DocPackageExtended;
if (dpe == null)
return;
dpe.IsEditable = true;
foreach (RootDocPackage rdp in dpe.Childs)
{
rdp.IsEditable = true;
}
}
I'm a bit new to Silverlight and now I'm developing a map app. I have a collection of custom controls (map markers, POIs, etc). Every control has a property "Location" of the type Point, where Location.X means Canvas.Left of the control, Location.Ymeans Canvas.Top of the control.
I'm trying to refactor my interface to MVVM pattern. I want to do something like this:
Say my controls are in Canvas. I want to have something like:
<Canvas DataContext="{StaticResource myModel}" ItemsSource="{Binding controlsCollection}">
<Canvas.ItemTemplate> ... </Canvas.ItemTemplate>
</Canvas>
In my custom control I want to have something like:
<myCustomControl DataContext="{StaticResource myControlModel}" Canvas.Left="{Binding Location.X}" Canvas.Top="{Binding Location.Y}" />
Is it possible? Maybe there's a better way?
I would think it is possible to use the ItemsControl control for this.
Let's say you create a holder-control that holds the position of the control + more info that you choose.
I call it "ControlDefinition.cs":
public class ControlDefinition : DependencyObject, INotifyPropertyChanged
{
public static readonly DependencyProperty TopProperty = DependencyProperty.Register("Top", typeof(Double), typeof(ControlDefinition), new PropertyMetadata(0d));
public static readonly DependencyProperty LeftProperty = DependencyProperty.Register("Left", typeof(Double), typeof(ControlDefinition), new PropertyMetadata(0d));
public static readonly DependencyProperty ModelProperty = DependencyProperty.Register("Model", typeof(Object), typeof(ControlDefinition), new PropertyMetadata(null));
public Double Top
{
get { return (Double)GetValue(TopProperty); }
set
{
SetValue(TopProperty, value);
NotifyPropertyChanged("Top");
}
}
public Double Left
{
get { return (Double)GetValue(LeftProperty); }
set
{
SetValue(LeftProperty, value);
NotifyPropertyChanged("Left");
}
}
public Object Model
{
get { return (Object)GetValue(ModelProperty); }
set
{
SetValue(ModelProperty, value);
NotifyPropertyChanged("Model");
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String aPropertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(aPropertyName));
}
}
Then, in my MODEL (ViewModel.cs), I create an ObservableCollection of this class:
public static readonly DependencyProperty ControlsProperty = DependencyProperty.Register("Controls", typeof(ObservableCollection<ControlDefinition>), typeof(MainWindow), new PropertyMetadata(null));
public new ObservableCollection<ControlDefinition> Controls
{
get { return (ObservableCollection<ControlDefinition>)GetValue(ControlsProperty); }
set
{
SetValue(ControlsProperty, value);
NotifyPropertyChanged("Controls");
}
}
Then, in the same MODEL, I initialize the collection and adds 4 dummy controls:
this.Controls = new ObservableCollection<ControlDefinition>();
this.Controls.Add(new ControlDefinition() { Top = 10, Left = 10, Model = "One" });
this.Controls.Add(new ControlDefinition() { Top = 50, Left = 10, Model = "Two" });
this.Controls.Add(new ControlDefinition() { Top = 90, Left = 10, Model = "Three" });
And I would have my VIEW (View.xaml) something like this:
<ItemsControl ItemsSource="{Binding Path=Controls}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas Background="Beige" IsItemsHost="True" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=Model, Mode=OneWay}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemContainerStyle>
<Style>
<Setter Property="Canvas.Top" Value="{Binding Path=Top, Mode=OneWay}" />
<Setter Property="Canvas.Left" Value="{Binding Path=Left, Mode=OneWay}" />
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
NOTE that I show a "TextBlock" control inside my "DataTemplate", in lack of your control.
And I show the "Model" property (which I have defined as "String") in the TextBlock's "Text" property. You can assign the "Model" property to your control's "DataContext" property as in your example.
Hope it helps!
So, after all times of googling, asking and reading, here I found a solution:
There should be a model implementing INotifyPropertyChanged interface. DependencyProperties are not necessary. Here's an example:
public class MyItemModel: INotifyPropertyChanged
{
// INotifyPropertyChanged implementation
...
//
private Point _location;
public Point Location
{
get { return Location; }
set { _location = value; NotifyPropertyChanged("Location"); }
}
// any other fields
...
//
}
Then, say we have a model with a collection of MyItemModels:
public class MyModel: INotifyPropertyChanged
{
// INotifyPropertyChanged implementation
...
//
private ObservableCollection<MyItemModel> _myCollection;
public ObservableCollection<MyItemModel> MyCollection
{
get { return _myCollection; }
set { _myCollection = value; NotifyPropertyChanged("MyCollection"); }
}
}
Then, in XAML, we should use ItemsControl like this:
<ItemsControl x:Name="LayoutRoot" DataContext="{StaticResource model}"
ItemsSource="{Binding MyCollection}"
HorizontalAlignment="Left" VerticalAlignment="Top">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas x:Name="host"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid x:Name="item" Background="Transparent">
<Grid.RenderTransform>
<TranslateTransform X="{Binding Location.X}" Y="{Binding Location.Y}"/>
</Grid.RenderTransform>
// other stuff here
...
//
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Works like a charm :) Thanks everyone.
I'm writing a value input control can be used everywhere. The control itself has a view model which set to its DataContext as usual. But when I use the control in a parent control like:
<UserControl x:Class="X.Y.Z.ParentControl">
...
<local:ValueInput Value="{Binding Path=MyValue}" />
...
</UserControl>
I'm going to bind the MyValue property of ParentControl's DataContext to the ValueInput control, but WPF tell me it cannot find the MyValue property in ValueInputViewModel class, which is the view model of ValueInput control itself. Why WPF is looking for the value from child's DataContext?
I just want to write a control which can be used like this:
<telerik:RadNumericUpDown Value="{Binding Path=NumberValue}" />
The NumberValue property is defined in in the parent's DataContext, not in the control's. This pattern works for teleriks control but not for my control.
What should I do?
For any FrameworkElement, there can be only 1 DataContext.
If UserControl has its own DataContext, it cannot use parent's DataContext.
However you can walk up to parent and get its DataContext (each time you need to reference Parent's DataContext) using RelativeSource
Binding="{Binding RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type Window}}, Path=DataContext.NumberValue}"
For this example to work, Parent (root at any level) should be Window. If it is a UserControl,
Binding="{Binding RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type UserControl}}, Path=DataContext.NumberValue}"
The code is from this link provided by fiq
My friend told me not to use DataContext as the view model in a standalone control since DataContext would be easily overridden - define a ViewModel property and bind in the XAML could solve the problem. Here's an example:
View model class:
public class MyValueInputViewModel
{
public string MyText { get; set; }
}
Code behind:
public partial class MyValueInput : UserControl
{
public MyValueInput()
{
InitializeComponent();
this.ViewModel = new MyValueInputViewModel
{
MyText = "Default Text"
};
}
public static readonly DependencyProperty ViewModelProperty =
DependencyProperty.Register("ViewModel", typeof(MyValueInputViewModel), typeof(MyValueInput));
public MyValueInputViewModel ViewModel
{
get
{
return (MyValueInputViewModel)this.GetValue(ViewModelProperty);
}
private set
{
this.SetValue(ViewModelProperty, value);
}
}
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register("Value", typeof(string), typeof(MyValueInput), new PropertyMetadata(OnValuePropertyChanged));
private static void OnValuePropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs args)
{
var input = (MyValueInput)o;
input.ViewModel.MyText = input.Value;
}
public string Value
{
get { return (string)this.GetValue(ValueProperty); }
set { this.SetValue(ValueProperty, value); }
}
}
XAML:
<UserControl x:Class="..." x:Name="Self" ...>
<Grid>
<TextBox Text="{Binding ViewModel.MyText, ElementName=Self, UpdateSourceTrigger=PropertyChanged}" />
</Grid>
</UserControl>
Scenario: I have a ListBox and the ListBoxItems have a DataTemplate. What I want to do is put a ContextMenu in the DataTemplate. The catch is that I want this ContextMenu ItemsSource to be different depending on certain properties in the window. My initial thought is that I could just bind the ItemsSource to a Property in the window and that would return an ItemsSource; however, I cant seem to bind to this property correctly. I believe this is because I am in the DataTemplate and consequently the DataContext (I believe that is the right word) is of that ListBoxItem and not of the window.
How could I get the ContextMenu that is inside a DataTemplate to bind to a Property outside of the DataTemplate.
You can get the DataContext from your window by using the RelativeSource FindAncestor syntax
<DataTemplate>
<TextBlock Text="{Binding MyInfo}">
<TextBlock.ContextMenu>
<Menu ItemsSource="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=DataContext.MyContextMenuItems}"/>
</TextBlock.ContextMenu>
</TextBlock>
</DataTemplate>
Not totally sure, but the binding is correct...
If your DataContext is on another object type, you just have to change the AncestorType (eg. by UserControl).
This might be a good candidate for an AttachedProperty. Basically what you would do is wrap your ContextMenu in a UserControl and then add a Dependency Property to the UserControl. For example:
MyContextMenu.xaml
<UserControl x:Class="MyContextMenu" ...>
<UserControl.Template>
<ContextMenu ItemSource="{Binding}" />
</UserControl.Template>
</UserControl>
MyContextMenu.xaml.cs
public static readonly DependencyProperty MenuItemsSourceProperty = DependencyProperty.RegisterAttached(
"MenuItemsSource",
typeof(Object),
typeof(MyContextMenu),
new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender)
);
public static void SetMenuItemsSource(UIElement element, Boolean value)
{
element.SetValue(MenuItemsSourceProperty, value);
// assuming you want to change the context menu when the mouse is over an element.
// use can use other events. ie right mouse button down if its a right click menu.
// you may see a perf hit as your changing the datacontext on every mousenter.
element.MouseEnter += (s, e) => {
// find your ContextMenu and set the DataContext to value
var window = element.GetRoot();
var menu = window.GetVisuals().OfType<MyContextMenu>().FirstOrDefault();
if (menu != null)
menu.DataContext = value;
}
}
public static Object GetMenuItemsSource(UIElement element)
{
return element.GetValue(MenuItemsSourceProperty);
}
Window1.xaml
<Window ...>
<Window.Resources>
<DataTemplate TargetType="ListViewItem">
<Border MyContextMenu.MenuItemsSource="{Binding Orders}">
<!-- Others -->
<Border>
</DataTemplate>
</Window.Resources>
<local:MyContextMenu />
<Button MyContextMenu.MenuItemsSource="{StaticResource buttonItems}" />
<ListView ... />
</Window>
VisualTreeHelpers
public static IEnumerable<DependencyObject> GetVisuals(this DependencyObject root)
{
int count = VisualTreeHelper.GetChildrenCount(root);
for (int i = 0; i < count; i++)
{
var child = VisualTreeHelper.GetChild(root, i);
yield return child;
foreach (var descendants in child.GetVisuals())
{
yield return descendants;
}
}
}
public static DependencyObject GetRoot(this DependencyObject child)
{
var parent = VisualTreeHelper.GetParent(child)
if (parent == null)
return child;
return parent.GetRoot();
}
This example is un-tested I'll take a look later tonight and make sure its accurate.
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 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.
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 :-)
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>