I want to achieve a context menu behavior like e.g. visual studio has for toolbars, with a list of checkable items, and a list of commands.
The contextmenu items should come from some observablecollection in view models.
VS ContextMenu for Toolboxes
As these come from different sources. I thought of using a composite collection to achieve this. Binding of one collection should be to Command, other to IsChecked/IsChecked. I also would like to use a separator.
The problem I have is about binding. I cannot use a datatemplate for complete menuitem because this does not include the IsChecked property. Therefore, I'm using ItemContainerStyle for it (see https://stackoverflow.com/a/29130774/5381620).
As long as I only use 1 collection container and have 1 source everything is fine.
However, inserting items from another source (or a Separator) will apply the "style" bindings to all menu items what is not intended and in case of 'Separator' will lead to an exception.
<ContextMenu>
<ContextMenu.Resources>
<CollectionViewSource x:Key="ContextMenuColCollection" Source="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}, Path= DataContext.HeaderContextMenu}"/>
</ContextMenu.Resources>
<ContextMenu.ItemTemplate>
<DataTemplate DataType="{x:Type vm:Collection1VM}" >
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</ContextMenu.ItemTemplate>
<ContextMenu.ItemsSource>
<CompositeCollection>
<MenuItem Header="Settings"/>
<Separator />
<CollectionContainer Collection="{Binding Source={StaticResource ContextMenuColCollection}}"/>
</CompositeCollection>
</ContextMenu.ItemsSource>
<ContextMenu.ItemContainerStyle>
<Style TargetType="{x:Type MenuItem}">
<Setter Property="IsCheckable" Value="True"/>
<Setter Property="IsChecked" Value="{Binding IsSelected}"/>
</Style>
</ContextMenu.ItemContainerStyle>
</ContextMenu>
After trying a lot I finally found a solution which is appropriate for me. Unfortunately, it contains lots of workarounds for different stuff and isn't quite a straight forward solution. I can't believe it was this difficult to create a simple contextmenu.
As described in question, I was not able to use a datatemplate because this would result in an exception caused by the separator, which doesn't implement some properties, e.g. the IsCheckable.
Moving the Style from ContextMenu.ItemContainerStyle to ContextMenu.Resources only applies this to the real MenuItems
(see H.B.'s answer here https://stackoverflow.com/a/18948356/5381620)
<ContextMenu>
<ContextMenu.Resources>
<Style TargetType="MenuItem">
<Setter Property="Header" Value="{Binding Name}"/>
<Setter Property="IsCheckable" Value="{Binding IsCheckable}"/>
<Setter Property="IsChecked" Value="{Binding IsChecked}"/>
<Setter Property="Command" Value="{Binding Cmd}"/>
<!-- this is necessary to avoid binding error, see explanation below-->
<Setter Property="HorizontalContentAlignment" Value="Left"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
</Style>
<!-- collectionViewSource necessary for behavior described here
https://social.msdn.microsoft.com/Forums/vstudio/en-US/b15cbd9d-95aa-47c6-8068-7ae9f7dca88a/collectioncontainer-does-not-support-relativesource?forum=wpf
-->
<CollectionViewSource x:Key="MenuCmds" Source="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}, Path= DataContext.CmdObsColl}"/>
<CollectionViewSource x:Key="MenuCheckable" Source="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}, Path= DataContext.CheckableObsCol}"/>
</ContextMenu.Resources>
<ContextMenu.ItemsSource>
<CompositeCollection>
<CollectionContainer Collection="{Binding Source={StaticResource MenuCmds}}"/>
<Separator />
<CollectionContainer Collection="{Binding Source={StaticResource MenuCheckable}}"/>
</CompositeCollection>
</ContextMenu.ItemsSource>
</ContextMenu>
If using more than one Collection container, there is still some strange binding error, which can be handled by adding the following to the Application.Resources. See the following link from msdn forum for more information.
https://social.msdn.microsoft.com/Forums/vstudio/en-US/42cd1554-de7a-473b-b977-ddbd6298b3d0/binding-error-when-using-compositecollection-for-menuitems?forum=wpf
What I still don't understand is why I still get the binding error, if I only set the ContentAlignment only in Application.Resources or Context.Resources. For some reason it is necessary to set both. If someone could explain this to me I would be quite happy.
<Application.Resources>
<Style TargetType="MenuItem">
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="VerticalContentAlignment" Value="Center" />
</Style>
</Application.Resources>
For binding I use some MenuItemVM class, which is more or less like this and where I can set properties depending on if menuitem should be a checkable one or a command.
class ContextMenuItemVM
{
public string Name { get; }
public bool IsCheckable { get; }
public bool IsChecked { get; set; }
public ICommand Cmd { get; }
}
Move the DataTemplate to <ContextMenu.Resources> and remove the ItemTemplate:
<ContextMenu>
<ContextMenu.Resources>
<CollectionViewSource x:Key="ContextMenuColCollection" Source="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}, Path= DataContext.HeaderContextMenu}"/>
<DataTemplate DataType="{x:Type vm:Collection1VM}" >
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
<local:Converter x:Key="conv" />
</ContextMenu.Resources>
<ContextMenu.ItemsSource>
<CompositeCollection>
<MenuItem Header="Settings"/>
<Separator />
<CollectionContainer Collection="{Binding Source={StaticResource ContextMenuColCollection}}"/>
</CompositeCollection>
</ContextMenu.ItemsSource>
<ContextMenu.ItemContainerStyle>
<Style TargetType="{x:Type MenuItem}">
<Setter Property="IsCheckable" Value="True"/>
<Setter Property="IsChecked" Value="{Binding Path=., Converter={StaticResource conv}}"/>
</Style>
</ContextMenu.ItemContainerStyle>
</ContextMenu>
Then the DataTemplate should be applied to Collection1VM objects only.
When it comes to the IsChecked property, you could either ignore any binding warnings or implement a converter, e.g.:
public class Converter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
Collection1VM vm = value as Collection1VM;
return vm != null && vm.IsChecked;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return value;
}
}
Related
I have a ListBox of custom items ("EditableTextBlock", a normal TextBlock that, on click, becomes a TextBox and then returns a TextBlock after the edits.)
I want to send an event, or to launch a callback to the parent, inform someway that there have been a successful edit;
In the EditableTextBlock I can have a method like
if (e.Key == Key.Enter)
{
this.IsInEditMode = false;
AcceptRenaming(sender, e);
}
and I could use that AcceptRenaming() to send the information somehow, but
I found that, being the item an DataTemplate, I can't use directly an EventHandler (calling from the parent something like
EditTextBlock.RenameEvent += new EventHandler(OnRenameAccepted);)
I tried to create a RoutedEvent following the tips on How do you add an Event Trigger to a data template for a business object?, but I couldn't understand how to register to the EventRegistry to my custom event that I'd create.
What I'm missing here? Or is my approach completely wrong, and there is something else I could/should do?
Thanks in advance
ListBox using the EditableTextBlock:
<ListBox x:Name="lbConfigurationList"
DataContext="{Binding DataContext, RelativeSource={RelativeSource AncestorType={x:Type Window}}}"
ItemsSource="{Binding ConfListVM.ObservableConfList}"
<ListBox.ItemTemplate>
<DataTemplate>
<local:EditableTextBlock Text="{Binding ConfName}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
EditableTextBlock xaml:
<UserControl.Resources>
<DataTemplate x:Key="EditModeTemplate">
<TextBox KeyDown="TextBox_KeyDown" Loaded="TextBox_Loaded" LostFocus="TextBox_LostFocus"
Text="{Binding ElementName=EditableText, Path=Text, UpdateSourceTrigger=PropertyChanged}"/>
</DataTemplate>
<DataTemplate x:Key="DisplayModeTemplate">
<TextBlock Text="{Binding ElementName=EditableText, Path=Text}" >
<TextBlock.ContextMenu>
<ContextMenu>
<MenuItem Header="Rename" Click="RenameContext_Click"/>
</ContextMenu>
</TextBlock.ContextMenu>
</TextBlock>
</DataTemplate>
<Style TargetType="{x:Type local:EditableTextBlock}">
<Style.Triggers>
<Trigger Property="IsInEditMode" Value="True">
<Setter Property="ContentTemplate" Value="{StaticResource EditModeTemplate}" />
</Trigger>
<Trigger Property="IsInEditMode" Value="False">
<Setter Property="ContentTemplate" Value="{StaticResource DisplayModeTemplate}" />
</Trigger>
</Style.Triggers>
</Style>
</UserControl.Resources>
We have use the hierarchical template to populate the menuitem
<UserControl.DataContext>
<local:MenuViewModel/>
</UserControl.DataContext>
<Grid>
<!--Initialize the Menu-->
<Menu Name="Part_Menu" ItemsSource="{Binding MenuCollection}" Background="#E5E5E5" VerticalAlignment="Center">
<Menu.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding MenuItemCollection}">
<TextBlock Text="{Binding Header}" />
<HierarchicalDataTemplate.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="CommandParameter" Value="{Binding Header}"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Command"
Value="{Binding RelativeSource={RelativeSource AncestorType={x:Type local:MenuViewModel}, AncestorLevel=2,Mode=FindAncestor},Path=MenuClick}"></Setter>
</Style>
</HierarchicalDataTemplate.ItemContainerStyle>
</HierarchicalDataTemplate>
</Menu.ItemTemplate>
</Menu>
</Grid>
In this i have tried to bind the MenuClick(ICommand) to MenuItem , but it did not bind correctly
I have check the binding in the following forum link
[http://stackoverflow.com/questions/23941314/wpf-how-can-i-create-menu-and-submenus-using-binding?rq=1][1]
In this command added in the MenuModel , i need to Command in the MenuViewmodel
This way of binding :
{Binding RelativeSource={RelativeSource AncestorType={x:Type local:MenuViewModel},
AncestorLevel=2, Mode=FindAncestor}
..is not working, because the AncestorType does not derive from UIElement.
The Path of the binding should be DataContext.MenuClick, and the AncestorType should be Menu. Putting it all together :
<Setter Property="Command"
Value="{Binding Path=DataContext.MenuClick,
RelativeSource={RelativeSource AncestorType={x:Type Menu},
AncestorLevel=2}}">
</Setter>
Mode=FindAncestor is the default mode, so i left that out.
In the MSDN: RelativeSource.AncestorType Documentation it is only stated that any Type could theoretically be used, however, FindAncestor inspects the visual tree to try and find the given ancestor, so whatever type you are looking for must be present in the visual tree. Hope this helps.
I have a problem with the user control I created. This control consists of a search textbox and a treeview. The treeview shows different data templates for different node types. So I created the usercontrol with a dependency property of type datatemplate which can be bound when using my control. Inside the control, the treeview binds to the dependency property. But sadly the treeviewtemplate selector doesn't get called.
<UserControl x:Class="yyy.yyy.yyy.UI.UserControls.SearchableTreeView.SearchableTreeView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:behaviours="clr-namespace:yyy.yyy.yyy.UI.Behaviours;assembly=yyy"
mc:Ignorable="d"
x:Name="parent"
d:DesignHeight="300" d:DesignWidth="300">
<DockPanel DataContext="{Binding ElementName=parent}">
<TextBox DockPanel.Dock="Top" Margin="5" Text="{Binding SearchText, UpdateSourceTrigger=PropertyChanged}"/>
<TreeView DockPanel.Dock="Top" Margin="5" ItemsSource="{Binding TreeViewItems}" ItemTemplateSelector="{Binding TreeViewTemplateSelector}">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="behaviours:TreeViewItemBehaviour.IsBroughtIntoViewWhenSelected" Value="true"/>
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
<Setter Property="FontWeight" Value="Normal" />
<Setter Property="Visibility" Value="Visible"/>
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="FontWeight" Value="Bold" />
</Trigger>
<DataTrigger Binding="{Binding Path=IsVisible}" Value="false">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
</DockPanel>
The code behind with the dependency property looks like that:
public partial class SearchableTreeView : UserControl
{
public SearchableTreeView()
{
InitializeComponent();
}
public static readonly DependencyProperty TreeViewTemplateSelectorProperty = DependencyProperty.Register(
"TreeViewTemplateSelector", typeof (DataTemplateSelector), typeof (SearchableTreeView), new PropertyMetadata(default(DataTemplateSelector)));
public DataTemplateSelector TreeViewTemplateSelector
{
get { return (DataTemplateSelector) GetValue(TreeViewTemplateSelectorProperty); }
set { SetValue(TreeViewTemplateSelectorProperty, value); }
}
}
And the usercontrol is used in a xaml like that:
<searchableTreeView:SearchableTreeView TreeViewTemplateSelector="{StaticResource TreeViewFieldTemplateSelector}"/>
Where the TreeViewFieldTemplateSelector is a class of type datatemplateselector, which allready worked before i startet to create a usercontrol out of the searchable treeview.
Does anybody know what I'm doing wrong? Or is it not possible to bind a datatemplateselector directly to a treeview?
Thanks
Manuel
You are complicating your system by using a DataTemplateSelector. While it is true that these objects were created for this purpose, there is a much easier way to achieve your requirements. Basically, if you declare a DataTemplate for each data type without specifying the x:Key values, then they will be applied implicitly to all objects of the correct type:
<DataTemplate DataType="{x:Type YourPrefix:YourDataType">
...
</DataTemplate>
<DataTemplate DataType="{x:Type YourPrefix:YourOtherDataType">
...
</DataTemplate>
<DataTemplate DataType="{x:Type YourPrefix:SomeOtherDataType">
...
</DataTemplate>
Now, if you put items of these data types into a collection and data bind that to a collection control, then you'll see your various items rendered as expected, but without the complications of the DataTemplateSelector.
UPDATE >>>
Ok, then try this instead... first remove the DataContext="{Binding ElementName=parent}" setting and then add a RelativeSource Binding for the TreeViewTemplateSelector property:
<DockPanel>
<TextBox DockPanel.Dock="Top" Margin="5" Text="{Binding SearchText,
UpdateSourceTrigger=PropertyChanged}"/>
<TreeView DockPanel.Dock="Top" Margin="5" ItemsSource="{Binding TreeViewItems}"
ItemTemplateSelector="{Binding TreeViewTemplateSelector, RelativeSource={
RelativeSource AncestorType={x:Type YourPrefix:SearchableTreeView}}}">
<TreeView.ItemContainerStyle>
...
</TreeView.ItemContainerStyle>
</TreeView>
</DockPanel>
I've got an observable collection of strings to thats data bound to my XAML contextmenu:
The ViewModel-Property:
public ObservableCollection<string> Indexes
{
get { return _Indexes; }
private set
{
if (value != _Indexes)
{
_Indexes = value;
OnPropertyChanged("Indexes");
}
}
}
The XAML code:
<viewmodel:IndexViewModel x:Key="IndexViewModel" />
<ContextMenu x:Key="ContextMenu_Index" Placement="Mouse" IsOpen="False">
<ContextMenu.ItemsSource>
<CompositeCollection>
<MenuItem Header="No items!" IsEnabled="False" Visibility="Collapsed">
<MenuItem.Style>
<Style TargetType="{x:Type MenuItem}">
<Style.Triggers>
<DataTrigger Binding="{Binding Source={StaticResource IndexViewModel}, Path=Indexes.Count}" Value="0">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
</MenuItem.Style>
</MenuItem>
<CollectionContainer Collection="{Binding Path=Indexes, Source={StaticResource IndexViewModel}}" />
</CompositeCollection>
</ContextMenu.ItemsSource>
<ContextMenu.Style>
<Style TargetType="ContextMenu"></Style>
</ContextMenu.Style>
<ContextMenu.ItemTemplate>
<DataTemplate DataType="string">
<TextBlock Text="{Binding}" MouseDown="TextBlock_Index_MouseDown"></TextBlock>
</DataTemplate>
</ContextMenu.ItemTemplate>
</ContextMenu>
Now I want to show the "No items" menu item if the count of Indexes is 0. But unfortunately it doesn't work this way, the "No items!" menu item is not shown. Do you have some hints?
There is a Dependency Property Setting Precedence List and because of that when you manually set Visibility it has priority over style trigger. Bring default value as setter into your Style instead of setting it against MenuItem and then Style.Trigger will be able to change that value:
<MenuItem Header="No items!" IsEnabled="False">
<MenuItem.Style>
<Style TargetType="{x:Type MenuItem}">
<Setter Property="Visibility" Value="Collapsed" />
<Style.Triggers>
<DataTrigger Binding="{Binding Source={StaticResource IndexViewModel}, Path=Indexes.Count}" Value="0">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
</MenuItem.Style>
</MenuItem>
In my opinion, displaying a MenuItem to say 'No items!' is unhelpful and incorrect... surely your users can tell when there are no MenuItems without being told that. Even if you feel that you absolutely do have to do that, then why don't you simply add an actual item into your data bound collection?:
Indexes.Add("No items!");
In your AddItem method, you'd just need to check for the existence of this item before adding a new item:
if (Indexes.Contains("No items!")) Indexes.Remove("No items!");
Indexes.Add(newItem);
In your comment, you said that you couldn't Style this item differently... I don't know why you'd want to do that anyway, but you could just use the DataTemplateSelector Class to do that for you. It would easier for you to implement your requirements this way.
My problem is with the following code, with binding the IsAvailable property of the MyListBoxItem class. My current solution:
<ListBox ItemTemplate="{StaticResource myTemplate}">
<ListBox.Resources>
<DataTemplate x:Key="myTemplate" DataType="{x:Type local:MyListBoxItem}">
<Label Foreground="Green" Content="{Binding Title}" Tag="{Binding IsAvailable}">
<Label.Style>
<Style TargetType="{x:Type Label}">
<Style.Triggers>
<DataTrigger Binding="{Binding Tag, RelativeSource={RelativeSource Self}}" Value="True">
<Setter Property="FontWeight" Value="Bold"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Label.Style>
</Label>
</DataTemplate>
... (more datatemplates)
</ListBox.Resources>
</ListBox>
My question: In my solution the value of IsAvailable "goes through" two bindings. The first one binds the value to the Tag property of the Label and then in the style triggers, a trigger checks its value and sets a property of the Label. When I used Binding="{Binding IsAvailable, RelativeSource={RelativeSource AncestorType={x:Type local:MyListBoxItem}}}" it didn't work, because the Style can't see any ancestor of the Label (or something similar reason), it resulted binding errors (with code 4 or 40 maybe), for each item added to the ListBox.
So finally: can I make the solution more simple, or there is no another (better) one?
An important thing I've forgot to mention, sorry: I put the DataTemplate in the ListBox's resources because I have more templates (they are basically differ, so I can't style them with triggers), which I have to switch between sometimes...
The ItemTemplate will take the type that the ItemsSource is bound to. Therefore you should be able to simply bind to IsAvailable, as the ListBox's item type is MyListBoxItem. Try this:
<ListBox ItemsSource="...">
<ListBox.ItemTemplate>
<DataTemplate>
<Label Foreground="Green" Content="{Binding Title}" Tag="{Binding IsAvailable}">
<Label.Style>
<Style TargetType="{x:Type Label}">
<Style.Triggers>
<DataTrigger Binding="{Binding Tag, RelativeSource={RelativeSource Self}}" Value="True">
<Setter Property="FontWeight" Value="Bold"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Label.Style>
</Label>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
You'll need to set your ItemsSource property to a Binding to the MyListBoxItem collection.