WPF Treeview Expand Action - wpf

im trying to add a button inside every item of the treeview.When the button is clicked:
- expand the tree if there are some item of the selected parent
- if not a parent: do an action.
The double action will allow me to remove completely the togglebutton.
So this is what i figured out so far:
<TreeView>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Items}" >
<Button Content ="{Binding Title}" Click="OnItemClick"/>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
My next step is to change the style of the treeview in order to have only a button that does all the work. So depending if its a parent of something or not, the button will expand for new item or will do an action inside the code.
public void OnItemClick(object sender, RoutedEventArgs args)
{
FrameworkElement parent = (FrameworkElement)((Button)sender).Parent;
if (parent is TreeViewItem)
{
//ACTION
}
else
//EXPAND
Console.WriteLine(parent);
}
So when I get the parent element of the button I want to check if the parent button (treeitemview) is expandible or not. But for now the parent I get is always null.
So the blocking step is:
is there a way do get the type of the parent of the button (treeitemview) and check if it is expandible?
Thank you

You could use the following helper method to find the parent TreeViewItem in the visual tree:
private static T FindParent<T>(DependencyObject dependencyObject) where T : DependencyObject
{
var parent = VisualTreeHelper.GetParent(dependencyObject);
if (parent == null)
return null;
var parentT = parent as T;
return parentT ?? FindParent<T>(parent);
}
Usage:
TreeViewItem parent = FindParent<TreeViewItem>((Button)sender);
if (parent != null)
//ACTION

Related

TreeViewItem.ParentTreeViewItem is internal but is there a way to get it?

I'm trying to write code that can accept Key.Down and Key.Up and change the selection of a TreeView that is using several HierarchicalDataTemplates. In the children of a TreeViewItem, I need to get its parent so that I can determine what the next node should be selected. I noticed that TreeViewItem has a ParentTreeViewItem property, but its set to internal and therefore not exposed to access. Is there another way to emulate how to get the parent of a TreeViewItem as a TreeViewItem? Note: Parent is always null when using HierarchicalDataTemplate. Thanks in advance.
You can always use the VisualTreeHelper.GetParent to find any parent element:
private bool TryGetVisualParent<TParent>(DependencyObject element, out TParent parent) where TParent : DependencyObject
{
parent = null;
if (element is null)
{
return false;
}
element = VisualTreeHelper.GetParent(element);
if (element is TParent parentElement)
{
parent = parentElement;
return true;
}
return TryGetVisualParent(element, out parent);
}
Usage Example
private void OnTreeViewItem_Selcted(object sender, RoutedEventArgs e)
{
var selectedItem = e.OriginalSource as TreeViewItem;
if (TryGetVisualParent(selectedItem, out TreeViewItem parentItem))
{
// Handle 'parentItem'
}
}
Use TreeViewItem.Parent Property,If the parent property is null, it means there is no parent node
<TreeView>
<TreeViewItem Header="root">
<TreeViewItem Header="child" Name="child_item"></TreeViewItem>
</TreeViewItem>
</TreeView>

WPF Get the referenced item from a ContextMenu on a ListViewItem

I have a ListView with ContextMenu on each ListViewItem that has Click event,
how can I detect in the event handler which Item was clicked in this ContextMenu?
I need the item ID.
<Style TargetType="{x:Type ListViewItem}">
.
.
.
<Setter Property="Control.Template">
<Setter.Value>
<ControlTemplate TargetType="tv:TreeListViewItem">
<Grid>
<Grid.ContextMenu>
<ContextMenu>
<MenuItem Header="Open in current tab" Click="MenuItemCurrentTab_Click"/>
<MenuItem Header="Open in new tab" Click="MenuItemNewTab_Click"/>
</ContextMenu>
</Grid.ContextMenu>
See this thread..
Following the same way as the answer from the link you would
<Grid.ContextMenu>
<ContextMenu>
<MenuItem Header="Open in current tab"
Click="MenuItemCurrentTab_Click"
CommandParameter="{Binding RelativeSource={RelativeSource Self}, Path=Parent}"/>
...
private void MenuItemCurrentTab_Click(object sender, RoutedEventArgs e)
{
MenuItem menuItem = sender as MenuItem;
if (menuItem != null)
{
ContextMenu parentContextMenu = menuItem.CommandParameter as ContextMenu;
if (parentContextMenu != null)
{
ListViewItem listViewItem = parentContextMenu.PlacementTarget as ListViewItem;
}
}
}
UPDATE
Add this to get the parent ListViewItem from the Grid
public T GetVisualParent<T>(object childObject) where T : Visual
{
DependencyObject child = childObject as DependencyObject;
while ((child != null) && !(child is T))
{
child = VisualTreeHelper.GetParent(child);
}
return child as T;
}
private void MenuItemCurrentTab_Click(object sender, RoutedEventArgs e)
{
MenuItem menuItem = sender as MenuItem;
if (menuItem != null)
{
ContextMenu parentContextMenu = menuItem.CommandParameter as ContextMenu;
if (parentContextMenu != null)
{
Grid grid = parentContextMenu.PlacementTarget as Grid;
ListViewItem listViewItem = GetVisualParent<ListViewItem>(grid);
}
}
}
private void MenuItemCurrentTab_Click(object sender, RoutedEventArgs e)
{
MenuItem menuItem = (MenuItem)e.Source;
ContextMenu menu = (ContextMenu)menuItem.Parent;
ListViewItem item = (ListViewItem)menu.PlacementTarget;
// do something with item
}
But it's probably better idea to create single ContextMenu, give it proper name, and use it for all list view items.
A recurring problem, with many attempts to solve but all have their drawbacks. The accepted answer here, for instance, supposes that each ListViewItem has its own ContextMenu. This works but, especially with a larger number of list items, has a considerable cost in XAML complexity and can be slow. And really isn't necessary at all. If we only use a single ContextMenu on the ListView itself, some other solutions suggest to use
<MenuItem CommandParameter="{Binding PlacementTarget.SelectedItem, RelativeSource={RelativeSource AncestorType=ContextMenu}}" />
which seems to solve the problem at first sight (PlacementTarget points to the ListView, its SelectedItem points to the list item, so the menu item handler can use the CommandParameter to get the originating list item), but, unfortunately, fails if the ListView has multiple selection enabled (SelectedItem will point to one of the items selected but not necessarily the one currently clicked) or if we use ListView.PreviewMouseRightButtonDown to disable the selection on right-click (which is, arguably, the only logical thing to do with multiple selections).
There is, however, an approach that has all the benefits:
single ContextMenu on the ListView itself;
works with all selection schemes, single, multiple, disabled;
even with multiple selection, it will pass the currently hovered item to the handler.
Consider this ListView:
<ListView ContextMenuOpening="ListView_ContextMenuOpening">
<ListView.ContextMenu>
<ContextMenu>
<MenuItem Header="Menu1" Click="Menu1_Click" CommandParameter="{Binding Parent, RelativeSource={RelativeSource Self}}" />
</ContextMenu>
</ListView.ContextMenu>
</ListView>
The CommandParameter is used to pass the parent of the MenuItem, ie. the ContextMenu itself. But the main trick comes in the menu opening handler:
private void ListView_ContextMenuOpening(object sender, ContextMenuEventArgs e) {
var menu = (e.Source as FrameworkElement).ContextMenu;
menu.Tag = (FrameworkElement)e.OriginalSource;
}
Inside this handler, we still know the original source of the event, the root FrameworkElement of the list item DataTemplate. Let's store it in the Tag of the menu for later retrieval.
private void Menu1_Click(object sender, RoutedEventArgs e) {
if (sender is MenuItem menu)
if (menu.CommandParameter is ContextMenu context)
if (context.Tag is FrameworkElement item)
if (item.DataContext is DataType data) {
//process data
}
}
In the menu click handler, we can look up the original ContextMenu we stored in the command parameter, from that we can look up the root FrameworkElement of the list item that we stored just before, and finally get the object stored in the list item (of type DataType).
ListViewItem item = myListView.SelectedItem as ListViewItem;
Seems to work just fine as the item is selected when you right-click it.

Silverlight: retrieving controls nested within a Listbox

i made a listbox that generates dynamic controls such as dropdowns & datepicker. i wanted to retrieve the data within the rows. Normally, in windows forms we commonly index the ...Items[i].FindControl("ControlID") method. How do you do about in XAML?
I need to retrieve the changes upon clicking a button.
btw, here's a simple view of my xaml:
<ListBox>
<stackpanel>
<TextBlock />
<stackpanel>
<grid>
<combobox />
<combobox/>
<datepicker />
</grid>
</stackpanel>
</stackpanel>
</ListBox>
Thank you so much!
private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
FrameworkElement selectedItem = (sender as ListBox).SelectedItem as FrameworkElement;
List<FrameworkElement> children = new List<FrameworkElement>();
children = GetChildren(selectedItem, ref children);
}
private List<FrameworkElement> GetChildren(FrameworkElement element, ref List<FrameworkElement> list)
{
int count = VisualTreeHelper.GetChildrenCount(element);
for (int i = 0; i < count; i++)
{
FrameworkElement child = VisualTreeHelper.GetChild(element, i) as FrameworkElement;
if(child != null)
{
list.Add(child);
GetChildren(child, ref list);
}
}
return list;
}
This returns all the FrameworkElements (including paths, borders etc). You can easily extend it and call the GetChildren method recursively only if the child is of certain type (ComboBox, StackPanel etc)
I have a helper class with the following two methods to assist with this sort of task.
XAML:
<ListBox Height="236" HorizontalAlignment="Left" Margin="31,23,0,0"
Name="listBox1" VerticalAlignment="Top" Width="245">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Name="sp">
<TextBlock Name="id">id</TextBlock>
<TextBox Name="test" Text="{Binding Key}"></TextBox>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Using with a List box, you could pass in the selected item:
var v1 =(ListBoxItem) listBox1.ItemContainerGenerator.ContainerFromIndex(
listBox1.SelectedIndex);
TextBox tb = GetChildByName<TextBox>(v1, "test");
tb.Text = "changed";
and you would get the correct textbox for that selected list box item. You can then use that reference to change properties on it.
public T GetChildByName<T>(DependencyObject parent, string name)
where T : class
{
T obj = RecGetChildByName<T>(parent, name) as T;
if (obj == null) throw new Exception("could find control "
+ "of name as child");
return obj;
}
private DependencyObject RecGetChildByName<T>(DependencyObject parent,
string name)
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
{
var child = VisualTreeHelper.GetChild(parent, i);
Control childControl = child as Control;
if (childControl != null)
{
if (childControl.Name == name) return child;
}
if (VisualTreeHelper.GetChildrenCount(child) > 0)
return RecGetChildByName<T>(child, name);
}
return null;
}
The most straightforward way would be to set a two-way binding on of your controls to objects and then the objects will tell you what the values were set to.
Also you can go through your tree by going through the Content properties of the objects until you get to the leaf objects.
Alternatively, you can use the Selected item and call the VisualTreeHelper's GetChild Method until you're at the leaf objects.

Passing origin of ContextMenu into WPF Command

Interesting problem related to firing commands from context menu items...
I want to fire a command to insert a row in my control, InsertRowCmd. This command needs to know where to insert the row.
I could use Mouse.GetPosition(), but that would get me the position of the mouse currently, which would be over the menu item. I want to get the origin of the context menu instead.
Does any one have any suggestions on how to pass the origin of the context menu as a parameter to the command?
Sample code:
<UserControl x:Name="MyControl">
<!--...-->
<ContextMenu x:Name="menu">
<MenuItem Header="Insert Row" Command="{x:Static customCommands:MyCommands.InsertRowCmd}" CommandParameter="?"/>
</ContextMenu>
</UserControl>
My current ideas are as follows:
-Use click handler instead so that I can find the origin in code. The problem is that I would then have to handle enabling/disabling.
-Handle click event and save the origin of the context menu. Pass this saved information into the command. I have verified that click events fire before the command is executed.
Any ideas?
EDIT:
I'm using Josh Smith's CommandSinkBinding to route the command handling into my ViewModel class. So the code that handles the command execution knows nothing about the view.
You'll need to use TranslatePoint to translate the top-left (0, 0) of the ContextMenu to a coordinate in the containing grid. You could do so by binding the CommandParameter to the ContextMenu and use a converter:
CommandParameter="{Binding IsOpen, ElementName=_menu, Converter={StaticResource PointConverter}}"
Another approach would be an attached behavior that automatically updates an attached readonly property of type Point whenever the ContextMenu is opened. Usage would look something like this:
<ContextMenu x:Name="_menu" local:TrackBehavior.TrackOpenLocation="True">
<MenuItem Command="..." CommandParameter="{Binding Path=(local:TrackBehavior.OpenLocation), ElementName=_menu}"/>
</ContextMenu>
So the TrackOpenLocation attached property does the work of attaching to the ContextMenu and updating a second attached property (OpenLocation) whenever the ContextMenu is opened. Then the MenuItem can just bind to OpenLocation to get the location at which the ContextMenu was last opened.
Following on from Kent's answer, I used his attached property suggestion and ended up with this (using Josh Smith's example for attached behaviors):
public static class TrackBehavior
{
public static readonly DependencyProperty TrackOpenLocationProperty = DependencyProperty.RegisterAttached("TrackOpenLocation", typeof(bool), typeof(TrackBehavior), new UIPropertyMetadata(false, OnTrackOpenLocationChanged));
public static bool GetTrackOpenLocation(ContextMenu item)
{
return (bool)item.GetValue(TrackOpenLocationProperty);
}
public static void SetTrackOpenLocation(ContextMenu item, bool value)
{
item.SetValue(TrackOpenLocationProperty, value);
}
public static readonly DependencyProperty OpenLocationProperty = DependencyProperty.RegisterAttached("OpenLocation", typeof(Point), typeof(TrackBehavior), new UIPropertyMetadata(new Point()));
public static Point GetOpenLocation(ContextMenu item)
{
return (Point)item.GetValue(OpenLocationProperty);
}
public static void SetOpenLocation(ContextMenu item, Point value)
{
item.SetValue(OpenLocationProperty, value);
}
static void OnTrackOpenLocationChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
{
var menu = dependencyObject as ContextMenu;
if (menu == null)
{
return;
}
if (!(e.NewValue is bool))
{
return;
}
if ((bool)e.NewValue)
{
menu.Opened += menu_Opened;
}
else
{
menu.Opened -= menu_Opened;
}
}
static void menu_Opened(object sender, RoutedEventArgs e)
{
if (!ReferenceEquals(sender, e.OriginalSource))
{
return;
}
var menu = e.OriginalSource as ContextMenu;
if (menu != null)
{
SetOpenLocation(menu, Mouse.GetPosition(menu.PlacementTarget));
}
}
}
and then to use in the Xaml, you just need:
<ContextMenu x:Name="menu" Common:TrackBehavior.TrackOpenLocation="True">
<MenuItem Command="{Binding SomeCommand}" CommandParameter="{Binding Path=(Common:TrackBehavior.OpenLocation), ElementName=menu}" Header="Menu Text"/>
</ContextMenu>
However, I also needed to add:
NameScope.SetNameScope(menu, NameScope.GetNameScope(this));
to the constructor of my view, otherwise the binding for the CommandParameter couldn't lookup ElementName=menu.
In addition to Kent's answer, think about a "standard way". F.e. when a ListBox has a ContextMenu, you do not need menu's position, because the selected item is set before the menu popped up. So, if your control would have something that gets "selected" on the right click...

ComboBox with ItemTemplate that includes a button

So, lets say I have a ComboBox with a custom data template. One of the items in the data template is a button:
<ComboBox Width="150" ItemsSource="{Binding MyItems}">
<ComboBox.ItemTemplate>
<DataTemplate>
<Button Content="ClickMe" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
The problem with this is that the button eats the click, and the item does not get selected if the button is selected. This means that the pull-down does not go away, and no item is selected.
I get WHY this is happening.
Is there a way to work around it? Possibly a way to process the button click (I am binding to a command) and tell it to continue up the chain so the combo box can also process the click?
Note: I am seeing my problem in Silverlight, but I am guessing that the exact same behavior can be seen with WPF.
OK, I got it figured out. It is a total hack, but it still lets me bind my command to the button and continue to have Combo-box behavior for selecting the item:
<ComboBox x:Name="MyCombo" Width="150" ItemsSource="{Binding MyItems}">
<ComboBox.ItemTemplate>
<DataTemplate>
<Button Content="ClickMe" Click="Button_Click" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
And in the code behind:
private void Button_Click(object sender, RoutedEventArgs e)
{
MyCombo.SelectedItem = (sender as Button).DataContext;
MyCombo.IsDropDownOpen = false;
}
If I really wanted to, I could bind the SelectedItem and IsDropDownOpen to properties in my ViewModel but I decided against it to keep this behavior as a hack extension of the XAML, in an effort to keep my ViewModel clean.
Your best bet would probably be to set the SelectedItem in the button's command.
I found another possibility for the MVVM context. I used an derived class for ComboBox and if an item is adden which derives from ButtonBase I attach to the Click event to close the ComboBox.
This works for my project - but just, because the items itself are buttons, it would not work if they just contain buttons as a child element.
public class MyComboBox : ComboBox
{
public MyComboBox()
{
// use Loaded event to modify inital items.
Loaded += OnLoaded;
}
private void OnLoaded(object sender, RoutedEventArgs routedEventArgs)
{
if (Items != null)
{
foreach (var item in Items)
{
var button = item as ButtonBase;
if (button != null)
{
ModifyButtonItem(button);
}
}
}
}
protected override void OnItemsChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
base.OnItemsChanged(e);
// Check added items. If an item is a button, modify the button.
if (e.NewItems != null)
{
foreach (var item in e.NewItems)
{
var button = item as ButtonBase;
if (button != null)
{
ModifyButtonItem(button);
}
}
}
}
private void ModifyButtonItem(ButtonBase button)
{
button.Click += (sender, args) => { IsDropDownOpen = false; };
}
}
I don't know if there is a way to do what you want. If you were to put a Button in a ListBox, for example, the same behavior occurs - clicking the Button does not cause its item in the ListBox to be selected. In fact, this is the case for any control in an ItemsControl that supports selection.
You might be able to do something with the Click event and mark it as not handled so that it continues up the visual tree, but even then I'm not sure if that would work or not.

Resources