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;
}
}
Related
I have a listbox that i've bound its ItemsSource and SelectedIndex to objects in the ViewModel. I'm also using an extender to auto scroll to the selected item. When I create the list there is some initial data, then I as i move through the list more items are added. The initial data shows up fine and I can scroll through it as I would expect. the problem is that once I add more items the new items don't show up in the listbox. When I move to the new items the list box stops actually scrolling to the selected item, but starts again when I move back. When I look at the ItemsSource in Snoop it shows all of the items, but they aren't displayed. The only items that are display are the original items. So I guess my question is why is this happening?
here is the xaml I'm using for the list box
<ListBox ItemsSource="{Binding Path=Items}"
Width="{Binding Path=Size.SizeW}"
Height="{Binding Path=Size.SizeH}"
SelectedIndex="{Binding Path=FocusedRow, Mode=OneWay}"
ScrollViewer.HorizontalScrollBarVisibility="Hidden"
ScrollViewer.VerticalScrollBarVisibility="Hidden"
IsSynchronizedWithCurrentItem="True"
extenders:ListBoxExtenders.AutoScrollToCurrentItem="True">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="Height"
Value="{Binding Path=RowHeight}" />
<Setter Property="Width"
Value="{Binding Path=RowWidth}" />
<Setter Property="Margin"
Value="{Binding Path=RowSpacing}" />
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<m:CanvasItemsControl ItemTemplateSelector="{StaticResource ResourceKey=listViewItemTemplateSelector}"
Visibility="Visible"
ItemsSource="{Binding Path=ListItems}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas Height="{Binding Path=RowHeight}"
Width="{Binding Path=RowWidth}"
ClipToBounds="True" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</m:CanvasItemsControl>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
here is the code for the extender
public class ListBoxExtenders : DependencyObject
{
public static readonly DependencyProperty AutoScrollToCurrentItemProperty = DependencyProperty.RegisterAttached("AutoScrollToCurrentItem", typeof(bool), typeof(ListBoxExtenders), new UIPropertyMetadata(default(bool), OnAutoScrollToCurrentItemChanged));
public static bool GetAutoScrollToCurrentItem(DependencyObject obj)
{
return (bool)obj.GetValue(AutoScrollToCurrentItemProperty);
}
public static void SetAutoScrollToCurrentItem(DependencyObject obj, bool value)
{
obj.SetValue(AutoScrollToCurrentItemProperty, value);
}
public static void OnAutoScrollToCurrentItemChanged(DependencyObject s, DependencyPropertyChangedEventArgs e)
{
var listBox = s as ListBox;
if (listBox != null)
{
var listBoxItems = listBox.Items;
if (listBoxItems != null)
{
var newValue = (bool)e.NewValue;
var autoScrollToCurrentItemWorker = new EventHandler((s1, e2) => OnAutoScrollToCurrentItem(listBox, listBox.Items.CurrentPosition));
if (newValue)
{ listBoxItems.CurrentChanged += autoScrollToCurrentItemWorker; }
else
{ listBoxItems.CurrentChanged -= autoScrollToCurrentItemWorker; }
}
}
}
public static void OnAutoScrollToCurrentItem(ListBox listBox, int index)
{
if (listBox != null && listBox.Items != null && listBox.Items.Count > index && index >= 0)
{
listBox.ScrollIntoView(listBox.Items[index]);
}
}
}
I'm not sure what other information would be helpful so if there is something else you need to see let me know and I'll add it.
Listbox SelectedIndex is bound to FocusedRow
private uint focusedRow;
public uint FocusedRow
{
get
{ return focusedRow; }
set
{
if (value == focusedRow)
{ return; }
focusedRow = value;
base.RaisePropertyChanged("FocusedRow");
}
}
ItemsSource is bound to Items
private ObservableCollection<DisplayList> items;
public ObservableCollection<DisplayList> Items
{
get
{ return items; }
set
{
if (value == items)
{ return; }
items = value;
base.RaisePropertyChanged("Items");
}
}
Make sure that new items have valid RowHeight and RowWidth. That can be the problem since while snooping you could see your data.
Make sure that your "Items" you bound as ItemsSource of your listbox is an ObservableCollection<> and not a simple List.
it should work !
I am new to WPF, and trying to create a tree using treeview.
What I want to do is to generate a tree dynamically. Each treeViewItem contains a comboBox and a textBlock. As user expand a node, the app will retrieve children nodes information from a data source. Finally user could select several nodes with the checkBoxes.
Following some online tutorials, I did the following tree: e as shown below:
<Window.Resources>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="HeaderTemplate">
<Setter.Value>
<HierarchicalDataTemplate DataType="{x:Type sotc:TaxNode}" ItemsSource="{Binding Path=Children}">
<StackPanel Orientation="Horizontal">
<CheckBox Name="chk" Margin="2" Tag="{Binding}"/>
<TextBlock Text="{Binding Path=TaxID}" ToolTip="{Binding Path=Lineage}" />
</StackPanel>
</HierarchicalDataTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<Grid>
<TreeView Margin="25,186,22,46">
<TreeViewItem Header="Taxonomy Tree" x:Name="_TaxTree" x:FieldModifier="private">
<TreeViewItem Header="Loading..." TextBlock.FontStyle="Italic"></TreeViewItem>
</TreeViewItem>
</TreeView>
</Grid>
And I have a method to get the selected comboBox
private List<CheckBox> GetSelectedCheckBoxes(ItemCollection items)
{
var list = new List<CheckBox>();
foreach (TreeViewItem item in items)
{
UIElement element = GetChildControl(item, "chk");
if (element != null)
{
var chk = (CheckBox)element;
if (chk.IsChecked.HasValue && chk.IsChecked.Value)
{
list.Add(chk);
}
}
List<CheckBox> l = GetSelectedCheckBoxes(item.Items);
list = list.Concat(l).ToList();
}
return list;
}
private UIElement GetChildControl(DependencyObject parentObject, string childName)
{
UIElement element = null;
if (parentObject != null)
{
int totalChild = VisualTreeHelper.GetChildrenCount(parentObject);
for (int i = 0; i < totalChild; i++)
{
DependencyObject childObject = VisualTreeHelper.GetChild(parentObject, i);
if (childObject is FrameworkElement &&
((FrameworkElement)childObject).Name == childName)
{
element = childObject as UIElement;
break;
}
// get its child
element = GetChildControl(childObject, childName);
if (element != null) break;
}
}
return element;
}
But due to lack of knowledge on WPF, I do not know what is the ItemCollection I should pass to the method.
Any advice or tutorials will be greatly appreciated.
Have a nice holiday
In your XAML, you can add a Name property to your TreeView:
<Grid>
<TreeView Name="MyAwesomeTreeView" Margin="25,186,22,46">
<TreeViewItem Header="Taxonomy Tree" x:Name="_TaxTree" x:FieldModifier="private">
<TreeViewItem Header="Loading..." TextBlock.FontStyle="Italic"></TreeViewItem>
</TreeViewItem>
</TreeView>
</Grid>
Now you can now access the items of this treeview in codebehind like this:
ItemCollection myDataItems = MyAwesomeTreeView.Items
GetSelectedCheckBoxes(myDataItems, MyAwesomeTreeView);
However the method you've shown will not work on TreeViews with data templates. The reason is that a TreeView will only generate the TreeViewItems once they are actually visible on screen. Until then, it only contains the underlying data. To get around this, you need to use the TreeView's ItemContainerGenerator. Modify your checkbox method to:
private List<CheckBox> GetSelectedCheckBoxes(ItemCollection items, ItemsControl source)
{
var list = new List<CheckBox>();
foreach (object dataitem in items)
{
UIElement treeviewitem = source.ItemContainerGenerator.ContainerFromItem(dataitem)
UIElement element = GetChildControl(treeviewitem, "chk");
if (element != null)
{
var chk = (CheckBox)element;
if (chk.IsChecked.HasValue && chk.IsChecked.Value)
{
list.Add(chk);
}
}
List<CheckBox> l = GetSelectedCheckBoxes(item.Items, treeviewitem);
list = list.Concat(l).ToList();
}
return list;
}
But I would like to emphasize that this is not a good way of doing things. I can bet that what you are trying to achieve can be achieved in a much simpler, straightforward and maintainable way, so I suggest that you explain what it is first.
I have a Silverlight application that is using a DataGrid. Inside of that DataGrid I have a DataTemplate that is defined like the following:
<Grid x:Name="myGrid" Tag="{Binding}" Loaded="myGrid_Loaded">
<ItemsControl ItemsSource="{Binding MyItems}" Tag="{Binding}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<StackPanel Orientation="Horizontal" Width="138">
<TextBlock Text="{Binding Type}" />
<TextBox x:Name="myTextBox" TextChanged="myTextBox_TextChanged" />
</StackPanel>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
When a user enters text into the TextBox, I have an event (myTextBox_TextChanged) that must be fired at this point. When that event gets fired, I would like to get the ItemsControl element that is the container for this TextBox. How do I get that ItemsControl from my event handler?
Please note: Because the ItemsControl is in the DataTemplate of DataGrid, I don't believe I can just add an x:Name and reference it from my code-behind. Or is there a way to do that?
Thank you!
Using a combination of ItemsControl.ItemsControlFromItemContainer and VisualTreeHelper.GetParent you should be able to find your ItemsControl
var txt = sender as TextBox;
var panel1 = VisualTreeHelper.GetParent(txt);
var panel2 = VisualTreeHelper.GetParent(panel1);
var contentPresenter = VisualTreeHelper.GetParent(panel2);
var ic = ItemsControl.ItemsControlFromItemContainer(contentPresenter);
You may also want search the web for VisualTreeHelper Recursive functions to make some of this easier.
I like to have this little extension method in a static class somewhere in my app:-
public static IEnumerable<DependencyObject> Ancestors(this DependencyObject root)
{
DependencyObject current = VisualTreeHelper.GetParent(root);
while (current != null)
{
yield return current;
current = VisualTreeHelper.GetParent(current);
}
}
With that you should be able to do something like this:-
ItemsControl control = ((DependencyObject)sender).Ancestors()
.TypeOf<ItemsControl>().FirstOrDefault();
Not sure if this applies but this creates a "toggling button bar" using the same principles.
private void UIClassButton_Click(object sender, RoutedEventArgs e){
Button SenderButton = (Button)sender;
ItemsControl SendersItemControl = ItemsControl.ItemsControlFromItemContainer(VisualTreeHelper.GetParent(SenderButton));
IEnumerable<DependencyObject> DependencyObjectCollection = SendersItemControl.GetContainers();
foreach (ContentPresenter item in DependencyObjectCollection) {
ContentPresenter UIClassPresenter = (ContentPresenter)item;
Button UIClassButton = (Button)UIClassPresenter.GetVisualChildren().First();
if (UIClassButton != SenderButton) {
VisualStateManager.GoToState(UIClassButton, "Normal", true);
}
else {
VisualStateManager.GoToState(UIClassButton, "Pressed", true);
}
}
}
Here's an example of capturing a container that houses your ItemsControl's item:
CheckBox checkbox = sender as CheckBox;
foreach (var item in MembersItemsControl.Items)
{
var container = MembersItemsControl.ItemContainerGenerator.ContainerFromItem(item) as FrameworkElement;
UserInformation user = container.DataContext as UserInformation;
bool isMale = true;
if (user.sex == isMale && checkbox.IsChecked.Value == true)
{
container.Visibility = System.Windows.Visibility.Visible;
}
}
I hope that helps.
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}"/>
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>