WPF Double Click TreeviewItem Child Node - wpf

I have a treeview Item as such in a treeview that will have a list bound to it:
<TreeViewItem Name="tviOffline" Foreground="Red" FontWeight="Bold"
Header="Offline">
<TreeViewItem.ItemTemplate>
<DataTemplate DataType="{x:Type local:Buddy}">
<StackPanel>
<TextBlock Text="{Binding Nick}" FontSize="10" Foreground="#8CFFD528" />
</StackPanel>
</DataTemplate>
</TreeViewItem.ItemTemplate>
</TreeViewItem>
I cannot figure out how to get each of its childs to have a double click event.
any help is appreciated. thanks much.

<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<EventSetter Event="MouseDoubleClick" Handler="OnItemMouseDoubleClick" />
...

<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<EventSetter Event="MouseDoubleClick" Handler="OnItemMouseDoubleClick" />
...
And THEN, the handler has to be written as follows in order to prevent the double-click from firing on successive parent TreeViewItems:
private void OnItemMouseDoubleClick(object sender, MouseButtonEventArgs args)
{
if (sender is TreeViewItem)
{
if (!((TreeViewItem)sender).IsSelected)
{
return;
}
}
.... do stuff.
}
Thanks to Aurelien Ribon for getting 90% of the way there. The double-click problem seems to be well-known in other postings on Stack Exchange. Just consolidating both solutions into one answer.

This is the only way I managed to get it to work for all the cases:
void MyTreeView_PreviewMouseDoubleClick(object sender, MouseButtonEventArgs e)
{
var clickedItem = TryGetClickedItem(myTreeView, e);
if (clickedItem == null)
return;
e.Handled = true; // to cancel expanded/collapsed toggle
DoStuff(clickedItem);
}
TreeViewItem TryGetClickedItem(TreeView treeView, MouseButtonEventArgs e)
{
var hit = e.OriginalSource as DependencyObject;
while (hit != null && !(hit is TreeViewItem))
hit = VisualTreeHelper.GetParent(hit);
return hit as TreeViewItem;
}

Related

How to disable expanding/collapsing of tree items in WPF TreeView on double-click

How can I disable tree items collapsing/expanding when I double-click on tree item? I still would like to do this by clicking on toggle button, but not when I double-click on item.
This is XAML I have:
<TreeView Grid.Column="0" Grid.Row="0" ItemsSource="{Binding Categories}" helpers:TreeViewHelper.SelectedItem="{Binding SelectedCategory, Mode=TwoWay}" >
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded" Value="True"/>
</Style>
</TreeView.ItemContainerStyle>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate DataType="{x:Type core:Category}" ItemsSource="{Binding SubCategories}">
<Label Content="{Binding Name}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseDoubleClick">
<i:InvokeCommandAction Command="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext.CreateGroupsFromCategoryCommand , Mode=OneWay}" CommandParameter="{Binding}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</Label>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
I would like to do this only in XAML.
Thank you for your help.
You should suppress the double click event on treeviewitem:
<TreeView Grid.Column="0" Grid.Row="0" ItemsSource="{Binding Categories}" helpers:TreeViewHelper.SelectedItem="{Binding SelectedCategory, Mode=TwoWay}" TreeViewItem.PreviewMouseDoubleClick="TreeViewItem_PreviewMouseDoubleClick" >
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded" Value="True"/>
</Style>
</TreeView.ItemContainerStyle>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate DataType="{x:Type core:Category}" ItemsSource="{Binding SubCategories}">
<Label Content="{Binding Name}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseDoubleClick">
<i:InvokeCommandAction Command="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext.CreateGroupsFromCategoryCommand , Mode=OneWay}" CommandParameter="{Binding}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</Label>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
Code Behind:
private void TreeViewItem_PreviewMouseDoubleClick(object sender, MouseButtonEventArgs e)
{
//this will suppress the event that is causing the nodes to expand/contract
e.Handled = true;
}
You could implement a custom EventTrigger in your views code behind, as suggested here: https://stackoverflow.com/a/7688249/1206431
public class HandlingEventTrigger : System.Windows.Interactivity.EventTrigger
{
protected override void OnEvent(System.EventArgs eventArgs)
{
var routedEventArgs = eventArgs as RoutedEventArgs;
if (routedEventArgs != null)
routedEventArgs.Handled = true;
base.OnEvent(eventArgs);
}
}
Add the namespace to your view like this xmlns:views="clr-namespace:YourNamespace.Views"
And then replace <i:EventTrigger EventName="MouseDoubleClick"> with <local:HandlingEventTrigger EventName="PreviewMouseDoubleClick">.
The HandlingEventTrigger will stop the event from being passed further up the visual tree and therefore not expanding/collapsing your tree, and using PreviewMouseDoubleClick instead of MouseDoubleClick will allow you to still fire your own command.
XAML:
<TreeView x:Name="TreeView1" MouseDoubleClick="TreeView1_MouseDoubleClick" />
C# Code:
private void TreeView1_MouseDoubleClick(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
e.Handled = true;
}
Works fine for me.
I know this issue is old, but I found a slightly different way that preserves the wanted double-click behavior but stops the expand/collapse in case it helps someone.
Keep your typical double click method:
private void OnMouseDoubleClick(object sender, MouseButtonEventArgs e)
{
// Do something
}
Create an inherited class from TreeView (such as MyTreeView). Add the following to that inherited class:
protected override void OnMouseDoubleClick(MouseButtonEventArgs e)
{
var tvi = GetTreeViewItemFromObject(ItemContainerGenerator, SelectedItem);
if (tvi != null) tvi.IsExpanded = true;
base.OnMouseDoubleClick(e);
}
/// <summary>
/// Use the object to get its Tree View item
/// </summary>
/// <param name="container"></param>
/// <param name="targetObject"></param>
/// <returns></returns>
private TreeViewItem GetTreeViewItemFromObject(ItemContainerGenerator container, object targetObject)
{
if (container.ContainerFromItem(targetObject) is TreeViewItem target) return target;
for (int i = 0; i < container.Items.Count; i++)
if ((container.ContainerFromIndex(i) as TreeViewItem)?.ItemContainerGenerator is ItemContainerGenerator childContainer)
if (GetTreeViewItemFromObject(childContainer, targetObject) is TreeViewItem childTarget) return childTarget;
return null;
}
The GetTreeViewItemFromObject method was lifted from someplace else. I don't remember where.

How to get the DataGridTextColumn sender's datagrid parent

Hi I have a datagrid and the DataGridTextColumn shown in code below:
<DataGridTextColumn Header="" Width="1*" Binding="{Binding FORECAST_MIN, UpdateSourceTrigger=PropertyChanged,NotifyOnTargetUpdated=True}" >
<DataGridTextColumn.EditingElementStyle>
<Style TargetType="{x:Type TextBox}">
<EventSetter Event="LostFocus" Handler="fMin_LostFocus" />
</Style>
</DataGridTextColumn.EditingElementStyle>
Now in the LostFocus event, I'd like to get the parent datagrid from the sender. Code
private void fMin_LostFocus(object sender, RoutedEventArgs e)
{
//Get the datagrid parent
}
Is there a easy way to do so? Thank you. Something like adding a Tag?
#
Both Jeff and OptimusPrime's answers work. It only allows me to choose one answer.
Jeff's answer should work.
Since you mentioned "Tag". This might be another way to go? Probably not the most elegant way though.
<DataGridTemplateColumn Header="">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBox Text="{Binding FORECAST_MIN, UpdateSourceTrigger=PropertyChanged,NotifyOnTargetUpdated=True}" Tag="{Binding RelativeSource={RelativeSource AncestorType=DataGrid}}" LostFocus="fMin_LostFocus"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
and in your code:
private void fMin_LostFocus(object sender, RoutedEventArgs e)
{
var tb = (TextBox)sender;
DataGrid parentDataGrid = (DataGrid)tb.Tag;
}
You have to traverse the visual tree until you find the proper parent.
DependencyObject depObj = sender as DependencyObject;
while (depObj != null && !(depObj is DataGrid)) {
depObj = VisualTreeHelper.GetParent (depObj);
}
DataGrid dg = (DataGrid) depObj;

AutoExpand treeview in WPF

Is there a way to automatically expand all nodes from a treeview in WPF? I searched and didn't even find an expand function in the treeview property.
Thanks
You can set ItemContainerStyle and use IsExpanded property.
<Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid>
<TreeView>
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded" Value="True"/>
</Style>
</TreeView.ItemContainerStyle>
<TreeViewItem Header="Header 1">
<TreeViewItem Header="Sub Item 1"/>
</TreeViewItem>
<TreeViewItem Header="Header 2">
<TreeViewItem Header="Sub Item 2"/>
</TreeViewItem>
</TreeView>
</Grid>
</Page>
If you need to do this from code, you can write viewmodel for your tree view items, and bind IsExpanded property to corresponding one from model. For more examples refer to great article from Josh Smith on CodeProject: Simplifying the WPF TreeView by Using the ViewModel Pattern
This is what I use:
private void ExpandAllNodes(TreeViewItem rootItem)
{
foreach (object item in rootItem.Items)
{
TreeViewItem treeItem = (TreeViewItem)item;
if (treeItem != null)
{
ExpandAllNodes(treeItem);
treeItem.IsExpanded = true;
}
}
}
In order for it to work you must call this method in a foreach loop for the root node:
// this loop expands all nodes
foreach (object item in myTreeView.Items)
{
TreeViewItem treeItem = (TreeViewItem)item;
if (treeItem != null)
{
ExpandAllNodes(treeItem);
treeItem.IsExpanded = true;
}
}
if you want expand manually you can try
Xaml:
<TreeView x:Name="TreePeople">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded" Value="True" />
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
c#:
bool Expanded = false;
// The event subscription method (for a button click)
private void ButtonExpand__Click(object sender, RoutedEventArgs e)
{
Expanded = !Expanded;
Style Style = new Style
{
TargetType = typeof(TreeViewItem)
};
Style.Setters.Add(new Setter(TreeViewItem.IsExpandedProperty, Expanded));
TreePeople.ItemContainerStyle = Style;
}
Carlo's answer was better because it opens all levels
This improves upon that example with a little more concise code example.
private void ExpandAllNodes(TreeViewItem treeItem)
{
treeItem.IsExpanded = true;
foreach (var childItem in treeItem.Items.OfType<TreeViewItem>())
{
ExpandAllNodes(childItem);
}
}
Call it by using this line of code
TreeViewInstance.Items.OfType<TreeViewItem>().ToList().ForEach(ExpandAllNodes);
Another programmatical way to manipulate full expansion of tree items, maybe via c# code, is using the TreeViewItem.ExpandSubTree() command on a root node.
private void ExpandFirstRootNode()
{
TreeViewControl.Items[0].ExpandSubtree();
}

Selecting a ListBoxItem when its inner ComboBox is focused

I have a DataTemplate that will be a templated ListBoxItem, this DataTemplate has a
ComboBox in it which when it has focus I want the ListBoxItem that this template
represents to become selected, this looks right to me. but sadly enough it doesn't work =(
So the real question here is within a DataTemplate is it possible to get or set the value
of the ListBoxItem.IsSelected property via a DataTemplate.Trigger?
<DataTemplate x:Key="myDataTemplate"
DataType="{x:Type local:myTemplateItem}">
<Grid x:Name="_LayoutRoot">
<ComboBox x:Name="testComboBox" />
</Grid>
<DataTemplate.Triggers>
<Trigger Property="IsFocused" value="true" SourceName="testComboBox">
<Setter Property="ListBoxItem.IsSelected" Value="true" />
</Trigger>
</DataTemplate.Triggers>
</DataTemplate>
<ListBox ItemTemplate="{StaticResource myDataTemplate}" />
I found a solution for your problem.
The problem is that when you have a control on your listboxitem, and the control is clicked (like for inputting text or changing the value of a combobox), the ListBoxItem does not get selected.
this should do the job:
public class FocusableListBox : ListBox
{
protected override bool IsItemItsOwnContainerOverride(object item)
{
return (item is FocusableListBoxItem);
}
protected override System.Windows.DependencyObject GetContainerForItemOverride()
{
return new FocusableListBoxItem();
}
}
--> Use this FocusableListBox in stead of the default ListBox of WPF.
And use this ListBoxItem:
public class FocusableListBoxItem : ListBoxItem
{
public FocusableListBoxItem()
{
GotFocus += new RoutedEventHandler(FocusableListBoxItem_GotFocus);
}
void FocusableListBoxItem_GotFocus(object sender, RoutedEventArgs e)
{
object obj = ParentListBox.ItemContainerGenerator.ItemFromContainer(this);
ParentListBox.SelectedItem = obj;
}
private ListBox ParentListBox
{
get
{
return (ItemsControl.ItemsControlFromItemContainer(this) as ListBox);
}
}
}
A Treeview does also have this problem, but this solution does not work for a Treeview, 'cause SelectedItem of Treeview is readonly.
So if you can help me out with the Treeview please ;-)
I found that I preferred to use this:
<Style TargetType="ListBoxItem">
<Style.Triggers>
<Trigger Property="IsKeyboardFocusWithin" Value="True">
<Setter Property="IsSelected" Value="True"></Setter>
</Trigger>
</Style.Triggers>
</Style>
Simple and works for all the listboxitems, regardless of what's inside.
No idea why your trigger don't work. To catch the get focus event of the combo box (or any control inside a listbox item) you can use attached routed events. You could put the code also in a derived listbox if you need this behavior in other parts of your application.
XAML:
<Window x:Class="RoutedEventDemo.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Specialized="clr-namespace:System.Collections.Specialized;assembly=System"
xmlns:System="clr-namespace:System;assembly=mscorlib"
Height="300" Width="300">
<Window.Resources>
<DataTemplate x:Key="myDataTemplate">
<Grid>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding}" Margin="5,0"/>
<ComboBox Width="50">
<ComboBoxItem>AAA</ComboBoxItem>
<ComboBoxItem>BBB</ComboBoxItem>
</ComboBox>
</StackPanel>
</Grid>
</DataTemplate>
</Window.Resources>
<Grid>
<ListBox ItemTemplate="{StaticResource myDataTemplate}">
<ListBox.ItemsSource>
<Specialized:StringCollection>
<System:String>Item 1</System:String>
<System:String>Item 2</System:String>
<System:String>Item 3</System:String>
</Specialized:StringCollection>
</ListBox.ItemsSource>
</ListBox>
</Grid>
</Window>
Code behind hooking up to all got focus events.
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace RoutedEventDemo
{
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
EventManager.RegisterClassHandler(typeof(UIElement),
GotFocusEvent,
new RoutedEventHandler(OnGotFocus));
}
private static void OnGotFocus(object sender, RoutedEventArgs e)
{
// Check if element that got focus is contained by a listboxitem and
// in that case selected the listboxitem.
DependencyObject parent = e.OriginalSource as DependencyObject;
while (parent != null)
{
ListBoxItem clickedOnItem = parent as ListBoxItem;
if (clickedOnItem != null)
{
clickedOnItem.IsSelected = true;
return;
}
parent = VisualTreeHelper.GetParent(parent);
}
}
}
}

WPF DataGridTemplateColumn. Am I missing something?

<data:DataGridTemplateColumn Header="Name">
<data:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}">
</DataTemplate>
</data:DataGridTemplateColumn.CellTemplate>
<data:DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<TextBox Text="{Binding Name}">
</DataTemplate>
</data:DataGridTemplateColumn.CellEditingTemplate>
</data:DataGridTemplateColumn>
It's clear example of Template column, right? What could be wrong with that?
So, here is the thing - when a user navigates through DataGrid with hitting TAB-key, it needs to hit the TAB twice(!) to be able to edit text in TextBox. How could I make it editable as soon as the user gets the column focus, I mean even if he just starts typing?
Ok. I found a way - into Grid.KeyUp() I put the code below:
if (Grid.CurrentColumn.Header.ToString() == "UserName")
{
if (e.Key != Key.Escape)
{
Grid.BeginEdit();
// Simply send another TAB press
if (Keyboard.FocusedElement is Microsoft.Windows.Controls.DataGridCell)
{
var keyEvt = new KeyEventArgs(Keyboard.PrimaryDevice, Keyboard.PrimaryDevice.ActiveSource, 0, Key.Tab) { RoutedEvent = Keyboard.KeyDownEvent };
InputManager.Current.ProcessInput(keyEvt);
}
}
}
your issue stems from the fact that each cell puts its editor in a content control which first receives focus, then you have to tab once again to the editor. If you have a look at the code for DataGridTemplateColumn in the GenerateEditingElement method it calls a method LoadTemplateContent which does this:
private FrameworkElement LoadTemplateContent(bool isEditing, object dataItem, DataGridCell cell)
{
DataTemplate template = ChooseCellTemplate(isEditing);
DataTemplateSelector templateSelector = ChooseCellTemplateSelector(isEditing);
if (template != null || templateSelector != null)
{
ContentPresenter contentPresenter = new ContentPresenter();
BindingOperations.SetBinding(contentPresenter, ContentPresenter.ContentProperty, new Binding());
contentPresenter.ContentTemplate = template;
contentPresenter.ContentTemplateSelector = templateSelector;
return contentPresenter;
}
return null;
}
see how it creates a new content presenter to put the template in. Other people have dealt with this problem in a variety of ways, I derive my own column type to deal with this stuff. (so i dont create an extra element or set the content presenter to not receive focus) In this example they are using focus manager to deal with the same issue (i havent tested this code)
<tk:DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<Grid FocusManager.FocusedElement="{Binding ElementName=txt1}">
<TextBox Name="txt1" Text="{Binding XPath=#ISBN}"
BorderThickness="0" GotFocus="TextBox_GotFocus"/>
</Grid>
</DataTemplate>
</tk:DataGridTemplateColumn.CellEditingTemplate>
If you have a user control as your editor then you can use the pattern with the focus manager or use an event handler for the OnLoaded event.
The issue that you faced is that the control (e.g. TextBox) within the DataGridTemplateColumn is contained within a DataGridCell. By default the DataGridCell has tab-stop functionality. Thus the reason for having to hit TAB twice to get focus to your TextBox control. The solution is to disable the tab-stop functionality for the DataGridCell. This can be done via a style for the DataGridCell.
Here's the solution:
<Style TargetType="{x:Type DataGridCell}">
<Setter Property="KeyboardNavigation.IsTabStop" Value="False" />
</Style>
Here is my approach. Its very close to #Nalin Jayasuriya answer, but I didn't want to create a style. Also this solution selects the text in the TextBox. Anyway - the XAML for the hole DataGrid looks like this.
<DataGrid Name="TextBlockDataGrid" ItemsSource="{Binding Path=Rows}" Style="{StaticResource DefaultSettingsDataGrid}">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Text}" IsReadOnly="True"/>
<DataGridTemplateColumn Width="*">
<DataGridTemplateColumn.CellStyle>
<Style TargetType="{x:Type DataGridCell}">
<Setter Property="KeyboardNavigation.IsTabStop" Value="False"/>
</Style>
</DataGridTemplateColumn.CellStyle>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Border BorderThickness="{Binding ErrorBorderThickness}" BorderBrush="{Binding ErrorBorderBrush}">
<TextBox Text="{Binding UserText, Mode=TwoWay, UpdateSourceTrigger=LostFocus}"
HorizontalAlignment="Right"
GotKeyboardFocus="TextBox_GotKeyboardFocus"
PreviewMouseDown="TextBox_PreviewMouseDown"
Style="{StaticResource DefaultTextBox}"/>
</Border>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
And the code-behind.
private void TextBox_GotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
{
try
{
((TextBox)sender).SelectAll();
}
catch (Exception ex) { GlobalDebug.debugForm.WriteText(ex); }
}
private void TextBox_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
try
{
// If its a triple click, select all text for the user.
if (e.ClickCount == 3)
{
((TextBox)sender).SelectAll();
return;
}
// Find the TextBox
DependencyObject parent = e.OriginalSource as UIElement;
while (parent != null && !(parent is TextBox))
{
parent = System.Windows.Media.VisualTreeHelper.GetParent(parent);
}
if (parent != null)
{
if (parent is TextBox)
{
var textBox = (TextBox)parent;
if (!textBox.IsKeyboardFocusWithin)
{
// If the text box is not yet focussed, give it the focus and
// stop further processing of this click event.
textBox.Focus();
e.Handled = true;
}
}
}
}
catch (Exception ex) { GlobalDebug.debugForm.WriteText(ex); }
}
For a more info, have a look at my blog:
http://blog.baltz.dk/post/2014/11/28/WPF-DataGrid-set-focus-and-mark-text
My approach is to use a TriggerAction which sets the focus to the template element you want when it loads.
The trigger is very simple:
public class TakeFocusAndSelectTextOnVisibleBehavior : TriggerAction<TextBox>
{
protected override void Invoke(object parameter)
{
Dispatcher.BeginInvoke(
DispatcherPriority.Loaded,
new Action(() =>
{
AssociatedObject.Focus();
AssociatedObject.SelectAll();
}));
}
}
The DataTemplate looks like this:
<DataTemplate>
<TextBox Text="{Binding Path=Price, Mode=TwoWay}"
MinHeight="0"
Padding="1,0"
Height="20">
<Interactivity:Interaction.Triggers>
<Interactivity:EventTrigger EventName="Loaded">
<Behaviors:TakeFocusAndSelectTextOnVisibleBehavior />
</Interactivity:EventTrigger>
</Interactivity:Interaction.Triggers>
</TextBox>
</DataTemplate>
You can write other triggers for other element types.

Resources