WPF: context menu in TreeViewItem binding to root - c#

I have TreeView control and I need to bind property from root (window/usercontrol) DataContext in context menu in that treeview.
<TextBox Text="{Binding Header}"></TextBox>
<TreeView ItemsSource="{Binding Items}" Grid.Row="1">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="ContextMenu">
<Setter.Value>
<ContextMenu>
<MenuItem Header="{{ BINDING TO HEADER PROPERTY FROM WINDOW DATACONTEXT}}"/>
</ContextMenu>
</Setter.Value>
</Setter>
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
public ObservableCollection<string> Items { get; set; }
public string Header { get { return _header; } set { _header = value; } }
I've tried multiple things: I've added x:Name="WindowRoot" to Window and {Binding Header, ElementName=WindowRoot} but it didn't work, I've tried multiple FindAncestor and RelativeSource but it didn't work.
Can someone help me?
Edit:
This is simplified case, in my normal application I use Unity + Prism, so ViewModel is AutoDiscovered (prism:ViewModelLocator.AutoWireViewModel="True") and it generally works. By "works" I mean: TreeView shows items from my collection, so it is connected, the problem is with context menu binding only.
In this simplified example I have ugly and simple code-behind, because I only want to test this ContextMenu binding:
public partial class MainWindow : Window
{
public ObservableCollection<string> Items { get; set; }
private string _header = "testtest";
public string Header { get { return _header ; } set { _header = value; } }
public MainWindow()
{
Items = new ObservableCollection<string>();
Items.Add("ItemTest");
InitializeComponent();
this.DataContext = this;
}
}

You could bind the Tag property of the TreeViewItem to the parent window or user control using a {RelativeSource} and then bind the Header property of the MenuItem to the Tag property of the PlacementTarget of the ContextMenu:
<TreeView x:Name="tv" ItemsSource="{Binding Items}" Grid.Row="1">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="Tag" Value="{Binding RelativeSource={RelativeSource AncestorType=Window}}" />
<Setter Property="ContextMenu">
<Setter.Value>
<ContextMenu>
<MenuItem Header="{Binding PlacementTarget.Tag.DataContext.Header, RelativeSource={RelativeSource AncestorType=ContextMenu}}"/>
</ContextMenu>
</Setter.Value>
</Setter>
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
The reason you cannot bind directly to any property of the window from the MenuItem is that a ContextMenu resides in a different element tree than the parent window or user control.

Related

Tooltip set by Style is not working

The tooltip for the below listbox is set using a setter. Nothing appears for a tooltip on mouse over.
I suspect the issue is the itemssource of the listbox itself. The listbox is bound to a list of AttributeItems called CandidateAttributes. An element of that list is an observablecollection called AttributePath, and the property in the Attribute path I am trying to bind the tooltip to is called ConceptualPath. Below is the definition for CandidateAttributes-
public static List<AttributeItem> CoBRRaAttributes { get; set; }
The AttributeItems class-
public class AttributeItem
{
private string _displayName = "";
private ObservableCollection<CoBRRa_WPF.CoBRRaUtilities.ViewModels.QueryTool.AttributeCollection> _AttributePath;
public AttributeItem(int id, string displayName, ObservableCollection<CoBRRa_WPF.CoBRRaUtilities.ViewModels.QueryTool.AttributeCollection> attributePath)
{
DisplayName = displayName;
AttributePath = attributePath;
}
public ObservableCollection<CoBRRa_WPF.CoBRRaUtilities.ViewModels.QueryTool.AttributeCollection> AttributePath
{
get
{
return _AttributePath;
}
set
{
_AttributePath = value;
}
}
}
The xmal-
<ListBox
Name="lstCandidates"
ItemsSource="{Binding Path=UIProperties.CandidateAttributes}"
>
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=DisplayName}">
</TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="Control.ToolTip" Value="{Binding AttributePath.ConceptualPath}"/>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
I can substitute some text in the place of Binding AttributePath.ConceptualPath and the tooltip displays that text. Just can't figure out why it does not work in the binding. How can I get this to work?
You are binding to AttributePath.ConceptualPath but AttributePath returns an ObservableCollection<AttributeCollection> and this one has no ConceptualPath property.
You should either change the type of the AttributePath property to just CoBRRa_WPF.CoBRRaUtilities.ViewModels.QueryTool.AttributeCollection or bind to a specific AttributeCollection, for example the first one:
<Setter Property="Control.ToolTip" Value="{Binding AttributePath[0].ConceptualPath}"/>
Also make sure that ConceptualPath is a public property of the AttributeCollection class.
Edit:
If you want to the display a list of paths in the tooltip, you should use an ItemsControl:
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="Control.ToolTip">
<Setter.Value>
<ItemsControl ItemsSource="{Binding AttributePath}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding ConceptualPath}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Setter.Value>
</Setter>
</Style>

How to visually show multiselect on TreeView in WPF

I'm attempting to enable a TreeView control to support multi select.
The very basic flow works, if you select multiple items in the TreeView while holding down ctrl or shift then it will successfully add those items to a list I have in the view model.
The problem is that when actually clicking on the TreeView items it will only select one visually i.e. only one item is marked as selected. How can I make it highlight/mark multiple items? I don't understand where this is controlled.
The TreeView xaml:
<TreeView x:Name="availableColumnsTreeView"
AutomationProperties.AutomationId="availableColumnsTreeView"
x:Uid="availableColumnsTreeView"
SelectedItemChanged="availableColumnsTreeView_SelectedItemChanged"
ItemsSource="{Binding Path=TreeFieldData, Mode=OneWay, Converter={StaticResource SortingConverter}, ConverterParameter='DisplayName.Text'}"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
ScrollViewer.VerticalScrollBarVisibility="Auto"
Grid.Row="0">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate x:Uid="HierarchicalDataTemplate_1" ItemsSource="{Binding Path=Children, Mode=OneWay, Converter={StaticResource SortingConverter}, ConverterParameter='DisplayName.Text'}">
<TextBlock x:Uid="TextBlock_1" Text="{Binding DisplayName.Text, Mode=OneWay}" />
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
So "availableColumnsTreeView_SelectedItemChanged" is invoked fine, but I need it to actually highlight the selected items.
EDIT: Please read my question before marking it as a duplicate. I tried to be as specific as possible to what my problem is. I'm not looking for a whole solution for multi select hidden away in some one drive document.
I'm not sure if I 100% follow you. Could provide a small example please?
Sure.
Here is xaml:
<TreeView ItemsSource="{Binding Items}">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Path=Children, Mode=OneWay}">
<CheckBox Content="{Binding Text, Mode=OneWay}" IsChecked="{Binding IsSelected}">
<CheckBox.Style>
<Style TargetType="CheckBox">
<Style.Triggers>
<DataTrigger Binding="{Binding IsSelected}" Value="True">
<DataTrigger.Setters>
<Setter Property="Foreground" Value="Red" />
</DataTrigger.Setters>
</DataTrigger>
</Style.Triggers>
</Style>
</CheckBox.Style>
</CheckBox>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
And here is cs:
public class Item : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public string Text { get; set; }
public List<Item> Children { get; set; }
bool _isSelected;
public bool IsSelected
{
get { return _isSelected; }
set
{
_isSelected = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsSelected)));
}
}
public Item(string text)
{
Text = text;
}
}
public partial class MainWindow : Window
{
public List<Item> Items { get; set; } = new List<Item>
{
new Item("1") { Children = new List<Item>
{
new Item("11"),
new Item("12"),
new Item("13"),
}},
new Item("2") { Children = new List<Item>
{
new Item("11"),
new Item("12"),
new Item("13"),
}},
new Item("3"),
};
public MainWindow()
{
InitializeComponent();
DataContext = this;
}
}
I am using CheckBox to select item (no idea how you do it). If item is selected it has its foreground changed to red via data trigger.
As you can see selection (disregards how you implement it, really, I am using single selection TreeView) is stored inside items as IsSelected value. You can traverse hierarchical collection to get a list of selected items (this is called flattering).
Note: IPropertyChanged, it's required if you plan to set IsSelected from code-behind (e.g. select all items on button press).
It should be easy to adapt to your case.
How can I make it highlight/mark multiple items? I don't understand where this is controlled.
You define the appearance of a TreeViewItem container using a TreeViewItem style. If you add an "IsSelected" property to your data object that keeps track of whether the item is currently selected, you could use a DataTrigger that binds to this one and provide the highlighting, e.g.:
<TreeView>
<TreeView.Resources>
<Style TargetType="TreeViewItem">
<Style.Triggers>
<DataTrigger Binding="{Binding IsSelected}" Value="True">
<Setter Property="Background" Value="Yellow" />
</DataTrigger>
</Style.Triggers>
</Style>
</TreeView.Resources>
...
</TreeView>
Make sure that the data class where the "IsSelected" property is defined implements the INotifyPropertyChanged interface and that you set this property in your event handler or command.

How do I add a command to items in dynamically generated ContextMenu

I have a context menu that is being populated from an ObservableCollection. I want the user to be able to click on any of those items, then a method is called passing the clicked item's text as a parameter.
I've started by following the answer to this question. However, I'm getting an error in my console output and my method is not being called.
System.Windows.Data Error: 40 : BindingExpression path error: 'FunctionToCall' property not found on 'object' ''MenuItem' (Name='myMenu')'. BindingExpression:Path=FunctionToCall; DataItem='MenuItem' (Name='myMenu'); target element is 'MenuItem' (Name=''); target property is 'Command' (type 'ICommand')
here is my xaml
<MenuItem Name="myMenu" Header="display text" ItemsSource="{Binding}" >
<MenuItem.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="Command" Value="{Binding FunctionToCall, RelativeSource={RelativeSource AncestorType=MenuItem}}"/>
<Setter Property="CommandParameter" Value="{Binding}"/>
</Style>
</MenuItem.ItemContainerStyle>
</MenuItem>
And my view model code
RelayCommand _command;
public ICommand FunctionToCall
{
get
{
if (_command == null)
{
_command = new RelayCommand(p => this.InnerMethod(p));
}
return _command ;
}
}
public void InnerMethod(object parameter)
{
....
The other answer suggests playing around with adding one or two DataContexts to the Binding, I've tried this and I still get the same error although it says DataContext property cannot be found instead of FunctionToCall.
I found the definition of RelayCommand here.
The real problem is with your binding. Use the DataContext property of MenuItem to actually get to the ViewModel instance
<MenuItem Name="myMenu" Header="display text" ItemsSource="{Binding}" >
<MenuItem.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="Command" Value="{Binding DataContext.FunctionToCall, RelativeSource={RelativeSource AncestorType=MenuItem}}"/>
<Setter Property="CommandParameter" Value="{Binding}"/>
</Style>
</MenuItem.ItemContainerStyle>
</MenuItem>
MenuItem will get ViewModel as DataContext. So actually we want..
MenuItem.DataContext.FunctionToCall
Hopefully you don't need the different menu items to bind to different commands else you have to change your design a little.
As Per Your Comments:
You'll need a List<MenuItem> MenuItems to bind with ContextMenu ItemSource property as
public class MenuItem
{
public string Header { get; set; }
public ICommand Command { get; set; }
}
XAML:
<ContextMenu ItemsSource="{Binding MenuItems}" >
<ContextMenu.ItemContainerStyle>
<Style TargetType="{x:Type MenuItem}" >
<Setter Property="Header" Value="{Binding Header}"/>
<Setter Property="Command" Value="{Binding Command}" />
</Style>
</ContextMenu.ItemContainerStyle>
</ContextMenu>
And add as many contextmenu item you want in your ViewModel AS YOU WANT.
This is how to do it.
public MainWindow()
{
InitializeComponent();
this.DataContext = new ViewModel();
}
xaml
<MenuItem Header="{Binding Item1}" Command="{Binding FunctionToCall}" CommandParameter="{Binding Header, RelativeSource={RelativeSource Self}}"/>
ViewModel
public class ViewModel
{
ICommand _cmd = new CustomCommand();
public ICommand FunctionToCall
{
get { return _cmd; }
set { _cmd = value; }
}
public string Item1 { get; set; }
public ViewModel() { Item1 = "1Header"; }
}
Command
public class CustomCommand : ICommand
{
public bool CanExecute(object parameter)
{
return true;
}
public event EventHandler CanExecuteChanged;
public void Execute(object parameter)
{
MessageBox.Show(parameter.ToString());
}
}
So, in your case assuming you want to pass Header of MenuItem as parameter to your command, do following changes :
<Setter Property="Command" Value="{Binding FunctionToCall}"/>
<Setter Property="CommandParameter" Value="{Binding Header, RelativeSource={RelativeSource Self}}"/>

List box with checkboxes + Command binding not firing in empty space click

I am sure that I am missing something small here, but somehow not able to figure out. I have a list box bound to a collection in my view model. In my datatemplate for listbox item I have a checkbox. The check box has command binding to a ICommand in view model. When displayed some checkbox have text enough to fully cover the width of list box item others do not so a blank space at end is left behind.
The behavior I want is that when the user clicks anywhere in the list box item (even in last empty space) the command should get invoked. The problem is that currently I am only able to invoke the Command in view model if user clicks only in area where checkbox is there (if user clicks on left empty space the command is not invoked)
I have tried various options of using a toggle button with control template as check box. The toggle button covers the whole item space and binds to command as required but when I use check box in the toggle button's control template the Command binding stops working.
Below is my XAML. Any suggestions/pointers should help.
<ListBox ItemsSource="{Binding Values}" SelectionMode="Multiple" ScrollViewer.VerticalScrollBarVisibility="Visible">
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}" BasedOn="{StaticResource MetroListBoxItem}">
<Setter Property="IsSelected" Value="{Binding Selected}"/>
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="HorizontalAlignment" Value="Stretch"/>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<CheckBox Grid.Column="0" IsChecked="{Binding Selected,Mode=OneWay}" Content="{Binding Caption}" Command="{Binding DataContext.ItemSelection, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ListBox}}}" >
<CheckBox.CommandParameter>
<MultiBinding Converter="{StaticResource IListDataConverter}">
<Binding Path="."/>
<Binding RelativeSource="{RelativeSource Self}" Path="IsChecked"/>
</MultiBinding>
</CheckBox.CommandParameter>
</CheckBox>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Girija
Well, simple example without double/triple logic with IsSelected on ListBox and bindings it to CheckBox and also to ViewModel.
In XAML i've create ScrollViewer, ItemsControl.
<ScrollViewer Width="200">
<ItemsControl ItemsSource="{Binding Items}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="local:SelectableObject">
<CheckBox IsChecked="{Binding IsSelected}"
Command="{Binding DataContext.ItemSelection, RelativeSource={RelativeSource FindAncestor, AncestorType=ItemsControl}}">
<CheckBox.Content>
<TextBlock Background="Blue" Text="{Binding Name}"/>
</CheckBox.Content>
</CheckBox>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
In codebehind:
private ICommand _itemSelection;
public ICommand ItemSelection => _itemSelection ?? (_itemSelection = new DelegateCommand(_ => MessageBox.Show("Click")));
public MainWindow()
{
Items.Add(new SelectableObject("Item 1"));
Items.Add(new SelectableObject("Item 2"));
Items.Add(new SelectableObject("Item 3"));
InitializeComponent();
DataContext = this;
}
public ObservableCollection<SelectableObject> Items { get; } = new ObservableCollection<SelectableObject>();
And simple class
public class SelectableObject : DependencyObject
{
public SelectableObject(string name)
{
Name = name;
}
public static readonly DependencyProperty IsSelectedProperty = DependencyProperty.Register("IsSelected",
typeof (bool), typeof (SelectableObject), new FrameworkPropertyMetadata());
public bool IsSelected
{
get { return (bool) GetValue(IsSelectedProperty); }
set { SetValue(IsSelectedProperty, value); }
}
public string Name { get; set; }
}

CommandBinding in ContextMenu

I have a TreeView and have created a basic TreeItem type. Each TreeItem has a header, a TreeItem Collection for children and a collection for a possible context menu. The TreeItem class has those objects:
public delegate void dExecute(TreeItem item);
public dExecute ExecuteTarget { get; set; }
public object Tag { get; set; }
public string Header { get; set; }
public List<TreeItem> Children { get; set; }
public List<TreeItem> ContextMenu { get; set; }
The context menu uses again a HierarchicalDataTemplate to display TreeItem objects (I use the TreeItem class for the items in the treeview AND in the context menu). The context menu looks like this:
<ContextMenu DataContext="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}" Visibility="{Binding ShowContextMenu}" ItemsSource="{Binding ContextMenu}">
<ContextMenu.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<TextBlock Text="{Binding Header}" />
<HierarchicalDataTemplate.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="Command" Value="{Binding Execute}"/>
</Style>
</HierarchicalDataTemplate.ItemContainerStyle>
</HierarchicalDataTemplate>
</ContextMenu.ItemTemplate>
</ContextMenu>
The context menu is rendered as I want it to be. I have created a context menu that I just attach to some of my items in the tree view. This is its content.
public List<TreeItem> ContextMenu
{
get
{
List<TreeItem> list = new List<TreeItem>();
TreeItem ti = new TreeItem("Some Action") { ExecuteTarget = targetMethod};
list.Add(ti);
ti = new TreeItem("test");
ti.Children.Add(new TreeItem("foo") { ExecuteTarget = targetMethod});
ti.Children.Add(new TreeItem("bar") { ExecuteTarget = targetMethod});
ti.Children.Add(new TreeItem("foo") { ExecuteTarget = targetMethod});
TreeItem ti2 = new TreeItem("inner"){ ExecuteTarget = targetMethod};
ti.Children.Add(ti2);
ti2.Children.Add(new TreeItem("foo") { ExecuteTarget = targetMethod});
list.Add(ti);
return list;
}
}
The context menu looks like this.
It looks as it should be. The commands work as they should. EXCEPT for the command on the highest level of the context menu. When I click on "Some Action" nothing happens. I assume that I have to add something to XAML, but I have no idea where.
<ContextMenu DataContext="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}" Visibility="{Binding ShowContextMenu}" ItemsSource="{Binding ContextMenu}">
<ContextMenu.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<TextBlock Text="{Binding Header}" />
<HierarchicalDataTemplate.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="Command" Value="{Binding Execute}"/>
</Style>
</HierarchicalDataTemplate.ItemContainerStyle>
</HierarchicalDataTemplate>
</ContextMenu.ItemTemplate>
<!-- this is what you're missing -->
<ContextMenu.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="Command" Value="{Binding Execute}"/>
</Style>
</ContextMenu.ItemContainerStyle>
</ContextMenu>

Categories

Resources