I'm trying to give a context menu items with different look and functionality, but I cannot find a way to bind commands to these items. Each menu item's view model is derived from one class AbstractEntryViewModel. Here is a shortened example of my current project's structure. Using ContextMenu.Resources is the only way I found to bind a template to a certain type.
<ContextMenu ItemsSource="{Binding Entries}">
<ContextMenu.Resources>
<DataTemplate DataType="{x:Type local:NopEntryViewModel}">
<!-- Content -->
</DataTemplate>
<HierarchicalDataTemplate
DataType="{x:Type local:GroupEntryViewModel}"
ItemsSource="{Binding Entries}">
<!-- Content -->
</HierarchicalDataTemplate>
<!-- More templates -->
</ContextMenu.Resources>
</ContextMenu>
internal abstract AbstractEntryViewModel : INotifyPropertyChanged {
public abstract void Invoke ();
// ...
}
internal NopEntryViewModel : AbstractEntryViewModel {
public override void Invoke () {}
}
internal GroupEntryViewModel : AbstractEntryViewModel {
public override void Invoke () { /* ... */ }
// ...
}
// More view models
Normally I can bind a command to a MenuItem like so
<MenuItem Command="{Binding StaticResourceOrViewModelProperty}" />
How can I do the same thing with data templates? Is there an invisible container, a wrapper for the data template's content, that I can use to bind a command to?
For simplicity, suppose 2 derived ViewModels, VM1 and VM2, with respectively a Command Command1 and a Command2.
Two steps:
1) Define in the base ViewModel this property:
public Type Type
{
get { return GetType(); }
}
We cannot use GetType() directly, we need a wrapping property since WPF Bindings work only with properties.
2) Use this property to set a DataTrigger on the Style for the ContextMenu MenuItem:
<ContextMenu.Resources>
<Style TargetType="{x:Type MenuItem}">
<Style.Triggers>
<DataTrigger Binding="{Binding Type}" Value="{x:Type local:VM1}">
<Setter Property="Command" Value="{Binding Command1}"/>
</DataTrigger>
<DataTrigger Binding="{Binding Type}" Value="{x:Type local:VM2}">
<Setter Property="Command" Value="{Binding Command2}"/>
</DataTrigger>
<!-- add other DataTriggers, one for every ViewModel -->
</Style.Triggers>
</Style>
</ContextMenu.Resources>
The DataTemplates are set as you do, with:
<DataTemplate DataType="{x:Type local:VM1}">
<!-- Content -->
</DataTemplate>
<DataTemplate DataType="{x:Type local:VM2}">
<!-- Content -->
</DataTemplate>
Related
I've seen a lot of examples how to use Data Template Selector to show different controls according to the x:TargetType. I want to create an items control that show a RadioButton, TextBox or TextBlock according to the class tyepe.
My class could be like this:
public class MyExample<T>
{
public string Name {get;set;}
public Type Type => TypeOf(T)
public T Value {get;set;}
}
I know that the Xaml can't recognize generics and I don't want to create a markup extension for supporting generics, I want to keep it simple. I don't want to create a concrete class for each type.
I know that I can use a Data Trigger to set the content template according to a property (for example type name, or Type Type) but I think that should be an easier way using a Data Template Selector. Can I use the TargetType on the Type Property instead of class type?
Can I use the TargetType on the Type Property instead of class type?
No.
The obvious solution would be to create a sub-type and a corresponding DataTemplate for each type of T. The implementation for each sub-type would be a one-liner:
public class MyExampleInt : MyExample<int> { }
public class MyExampleString : MyExample<string> { }
If you don't want to create concrete sub-types for whatever reason, you could use a DataTemplateSelector to select a template based on the type of each MyExample<T>:
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
switch (item)
{
case MyExample<int> i:
return IntTemplate;
case MyExample<string> s:
return StringTemplate;
}
return null;
}
This worked for me:
Window x:Class="WpfApp1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp1" xmlns:syncfusion="http://schemas.syncfusion.com/wpf"
mc:Ignorable="d"
Background="Red"
d:DataContext="{d:DesignInstance local:ViewModel}"
WindowStartupLocation="CenterScreen"
Title="MainWindow" d:Height="100" d:Width="150" Width="300" Height="300">
<Window.Resources>
<Style x:Key="TextBlockStyle" TargetType="TextBlock">
<Setter Property="TextAlignment" Value="Center"/>
</Style>
<DataTemplate x:Key="DefaultTemplate">
<TextBlock Text="{Binding Value}" Style="{StaticResource TextBlockStyle}"/>
</DataTemplate>
<DataTemplate x:Key="NumberTemplate">
<syncfusion:UpDown Name="NumericUpdDown" Value="{Binding Value}"/>
</DataTemplate>
<DataTemplate x:Key="TextTemplate">
<TextBlock Text="{Binding Value}" Style="{StaticResource TextBlockStyle}" />
</DataTemplate>
<DataTemplate x:Key="CheckBoxTemplate">
<CheckBox IsChecked="{Binding Value}" HorizontalAlignment="Center"/>
</DataTemplate>
<DataTemplate x:Key="MyDataTemplate">
<ContentControl Content="{Binding}">
<ContentControl.Style>
<Style TargetType="{x:Type ContentControl}">
<!-- Default Template -->
<Setter Property="ContentTemplate" Value="{StaticResource DefaultTemplate}" />
<!-- Triggers to change Template -->
<Style.Triggers>
<DataTrigger Binding="{Binding Type.Name}" Value="Int32">
<Setter Property="ContentTemplate" Value="{StaticResource NumberTemplate}" />
</DataTrigger>
<DataTrigger Binding="{Binding Type.Name}" Value="String">
<Setter Property="ContentTemplate" Value="{StaticResource TextTemplate}" />
</DataTrigger>
<DataTrigger Binding="{Binding Type.Name}" Value="Boolean">
<Setter Property="ContentTemplate" Value="{StaticResource CheckBoxTemplate}" />
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
</DataTemplate>
</Window.Resources>
<ItemsControl
ItemsSource="{Binding Definitions}"
ItemTemplate="{StaticResource MyDataTemplate}">
</ItemsControl>
</Window>
The reason that I don't want concrete type class is avoid adding a lot of code, everytime that I add a new type. What do you think about this solution ?
In my scenario, I have an ItemsControl where the DataTemplate needs to change in a few ways depending on the properties of the item. One of the things that needs to change is the context menu for the item.
I've put together this example to show my situation. Take the following classes:
Class MainWindow
Public Property Things As New List(Of Thing) From {New Thing With {.Name = "Thing 1", .Type = ThingType.A},
New Thing With {.Name = "Thing 2", .Type = ThingType.B},
New Thing With {.Name = "Thing 3", .Type = ThingType.C}}
End Class
Public Class Thing
Public Property Name As String
Public Property Type As ThingType
End Class
Public Enum ThingType
A
B
C
End Enum
Thing has some data to display (Name), and something that should influence the template (Type).
Now take the following XAML:
<Window x:Class="MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:VBTest"
DataContext="{Binding RelativeSource={RelativeSource Self}}">
<Window.Resources>
<ContextMenu x:Key="DefaultMenu">
<MenuItem Header="A or C"/>
</ContextMenu>
<ContextMenu x:Key="BMenu">
<MenuItem Header="B Only"/>
</ContextMenu>
</Window.Resources>
<ItemsControl ItemsSource="{Binding Things}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="local:Thing">
<TextBlock Name="Title" Text="{Binding Name}"/>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Type}" Value="B">
<Setter TargetName="Title" Property="FontWeight" Value="Bold"/>
<Setter Property="ContextMenu" Value="{StaticResource BMenu}"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Window>
In DataTemplate.Triggers, I define a trigger for items of type B. For these items, I want to change some aspects of the template, so I use TargetName on the Setter to change the FontWeight of a TextBlock (in the real code I change a number of properties for a number of elements). But I also want to change the ContextMenu for the entire item. To do this, I use a Setter without TargetName and the property ContextMenu.
The above works somehow. If you run this and right-click "Thing 2" you see BMenu. But how does this work? What object is the Setter being applied to? DataTemplate has no ContextMenu property.
And I ask this for more than just curiosity. I want all other types of items (A and C) to use DefaultMenu. That should be the default, with the DataTrigger overriding it for B items. But I don't even know what object I'm Setter-ing at this point, so I don't know where or how to set this default.
What object is the Setter being applied to?
It's applied to the ContentPresenter item container that wraps each element in the Things source collection.
You can set the default ContextMenu using an ItemContainerStyle:
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="ContextMenu" Value="{StaticResource DefaultMenu}"/>
</Style>
</ItemsControl.ItemContainerStyle>
I am trying to bind a property of a RibbonTabHeader to a property of its corresponding RibbonTab. However, it seems that the RibbonTab is not an ancestor of the RibbonTabHeader. I'm trying to bind on custom dependency properties, but for simplicity's sake we'll suppose this is what I want to do:
<Style x:Key="DynamicHeader" TargetType="r:RibbonTabHeader">
<Setter Property="Tooltip"
Value="{Binding Name,
RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type r:RibbonTab}}/>
Knowing that this equivalent produces the expected result, where the tooltip is "rbnTab1":
<Style x:Key="DynamicHeader" TargetType="r:RibbonTabHeader">
<Setter Property="Tooltip"
Value="{Binding Name,
ElementName=rbnTab1}/>
How could I recreate this behaviour directly in the style so that I can apply it to any header of any desired tab? Like so:
<r:RibbonTab Name="rbnTab2" Header="Tab 2" HeaderStyle="{StaticResource DynamicHeader}">
<r:RibbonTab Name="rbnTab3" Header="Tab 3" HeaderStyle="{StaticResource DynamicHeader}">
In order to provide closure to this topic, here is what I ended up doing:
In the end, I couldn't find a binding path to a property from the tab itself. Instead, rather than binding to a property from the tab, I define a custom style for the tab header which is BasedOn the original style, and I set the property for the header itself within that style. For good measure, my example also includes the custom dependency property I was using (although I don't think the issue is there because I use other custom dependency properties just fine):
MainWindow.xaml:
<Style x:Key="DynamicHeader" TargetType="r:RibbonTabHeader">
<Setter Property="BorderBrush" Value="{Binding Path=(ext:Tab.TabColor),
RelativeSource={RelativeSource Self},
Converter={core:StringToBrushConverter}}"/>
[...]
</Style>
[...]
<r:RibbonTab Name="rbnTab2" Header="Tab 2">
<r:RibbonTab.HeaderStyle>
<Style TargetType="RibbonTabHeader" BasedOn="{StaticResource DynamicHeader}">
<Setter Property="ext:Tab.TabColor" Value="CornflowerBlue"/>
</Style>
</r:RibbonTab.HeaderStyle>
</r:RibbonTab>
ControlExtensions.cs: (Custom Dependency Property)
public class Tab {
public static readonly DependencyProperty TabColorProperty =
DependencyProperty.RegisterAttached("TabColor", typeof(string), typeof(Tab), new
PropertyMetadata(default(string)));
public static void SetTabColor(UIElement element, string value) {
element.SetValue(TabColorProperty, value);
}
public static string GetTabColor(UIElement element)
{
return (string)element.GetValue(TabColorProperty);
}
}
I currently have a TreeView which has the following structure:
<TreeView ItemsSource="{Binding RootViewModels}"
FontSize="12">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="local:TreeViewItemBehaviour.IsBroughtIntoViewWhenSelected"
Value="True" />
<Setter Property="IsExpanded"
Value="{Binding IsExpanded, Mode=TwoWay}" />
<Setter Property="IsSelected"
Value="{Binding IsSelected, Mode=TwoWay}" />
<Setter Property="Visibility"
Value="{Binding IsVisible, Mode=TwoWay, Converter={StaticResource boolVisibilityConverter}}" />
</Style>
</TreeView.ItemContainerStyle>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<TextBlock Text="{Binding SomeDisplayText}" />
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
where RootViewModels and Children are of type ObservableCollection<SomeItemViewModel>
In the same View I have a ComboBox and I want the selected item in this ComboBox to serve as the criteria to filter the TreeView by. I have a ViewModelRepository which is also of type ObservableCollection<SomeItemViewModel> and is provided by my IoC container. I use this to create my other ObservableCollections that are in the XAML above.
At the moment, I'm trying to set the Visibility of each SomeItemViewModel in my ViewModelRepository when an item in the ComboBox is selected. This seems to work for anything below the first two levels in the tree, but not for the 2nd level itself (the first level being the root which is always visible).
However, it doesn't work consistently. When I apply the "filter" the correct nodes are set invisible, but if I then expand a node which contains "filtered" nodes then any subsequent "filters" fail.
I've read about binding ItemsControls to a CollectionViewSource in order to do filtering, but I can't find an example of it's usage with the TreeView. Does anyone have any pointers on how I can use it here?
you could use ICollectionView to filter your collection.
something like this:
ICollectionView view = CollectionViewSource.GetDefaultView(this.YourObservableCollection);
view.Filter = o => {
var itemtofilter = o as yourviewmodeltype;
//check your conditions here
...
return (bool)itemtofilercondition;
};
edit: you have to recreate the view if you call new YourObservableCollection();
When I bind Menu Items with an ObservableCollection, only the "inner" area of the MenuItem is clickable:
alt text http://tanguay.info/web/external/mvvmMenuItems.png
In my View I have this menu:
<Menu>
<MenuItem
Header="Options" ItemsSource="{Binding ManageMenuPageItemViewModels}"
ItemTemplate="{StaticResource MainMenuTemplate}"/>
</Menu>
Then I bind it with this DataTemplate:
<DataTemplate x:Key="MainMenuTemplate">
<MenuItem
Header="{Binding Title}"
Command="{Binding DataContext.SwitchPageCommand,
RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Menu}}}"
Background="Red"
CommandParameter="{Binding IdCode}"/>
</DataTemplate>
Since each ViewModel in the ObservableCollection ManageMenuPageItemViewModels has a property Title and IdCode, the above code works fine at first sight.
HOWEVER, the problem is that the MenuItem in the DataTemplate is actually inside another MenuItem (as if it is being bound twice) so that in the above DataTemplate with Background="Red" there is a Red box inside each menu item and only this area can be clicked, not the whole menu item area itself (e.g. if the user clicks on the area where the checkmark is or to the right or left of the inner clickable area, then nothing happens, which, if you don't have a separate color is very confusing.)
What is the correct way to bind MenuItems to an ObservableCollection of ViewModels so that the whole area inside each MenuItem is clickable?
UPDATE:
So I made the following changes based on advice below and now have this:
alt text http://tanguay.info/web/external/mvvmMenuItemsYellow.png
I have only a TextBlock inside my DataTemplate, but I still can't "color the whole MenuItem" but just the TextBlock:
<DataTemplate x:Key="MainMenuTemplate">
<TextBlock Text="{Binding Title}"/>
</DataTemplate>
And I put the Command binding into Menu.ItemContainerStyle but they don't fire now:
<Menu DockPanel.Dock="Top">
<Menu.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="Background" Value="Yellow"/>
<Setter Property="Command" Value="{Binding DataContext.SwitchPageCommand,
RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Menu}}}"/>
<Setter Property="CommandParameter" Value="{Binding IdCode}"/>
</Style>
</Menu.ItemContainerStyle>
<MenuItem
Header="MVVM" ItemsSource="{Binding MvvmMenuPageItemViewModels}"
ItemTemplate="{StaticResource MainMenuTemplate}"/>
<MenuItem
Header="Application" ItemsSource="{Binding ApplicationMenuPageItemViewModels}"
ItemTemplate="{StaticResource MainMenuTemplate}"/>
<MenuItem
Header="Manage" ItemsSource="{Binding ManageMenuPageItemViewModels}"
ItemTemplate="{StaticResource MainMenuTemplate}"/>
</Menu>
I found using MVVM with MenuItems to be very challenging. The rest of my application uses DataTemplates to pair the View with the ViewModel, but that just doesn't seem to work with Menus because of exactly the reasons you've described. Here's how I eventually solved it. My View looks like this:
<DockPanel>
<Menu DockPanel.Dock="Top" ItemsSource="{Binding Path=(local:MainViewModel.MainMenu)}">
<Menu.ItemContainerStyle>
<Style>
<Setter Property="MenuItem.Header" Value="{Binding Path=(contracts:IMenuItem.Header)}"/>
<Setter Property="MenuItem.ItemsSource" Value="{Binding Path=(contracts:IMenuItem.Items)}"/>
<Setter Property="MenuItem.Icon" Value="{Binding Path=(contracts:IMenuItem.Icon)}"/>
<Setter Property="MenuItem.IsCheckable" Value="{Binding Path=(contracts:IMenuItem.IsCheckable)}"/>
<Setter Property="MenuItem.IsChecked" Value="{Binding Path=(contracts:IMenuItem.IsChecked)}"/>
<Setter Property="MenuItem.Command" Value="{Binding}"/>
<Setter Property="MenuItem.Visibility" Value="{Binding Path=(contracts:IMenuItem.Visible),
Converter={StaticResource BooleanToVisibilityConverter}}"/>
<Setter Property="MenuItem.ToolTip" Value="{Binding Path=(contracts:IMenuItem.ToolTip)}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Path=(contracts:IMenuItem.IsSeparator)}" Value="true">
<Setter Property="MenuItem.Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type MenuItem}">
<Separator Style="{DynamicResource {x:Static MenuItem.SeparatorStyleKey}}"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</Menu.ItemContainerStyle>
</Menu>
</DockPanel>
If you notice, I defined an interface called IMenuItem, which is the ViewModel for a MenuItem. Here's the code for that:
public interface IMenuItem : ICommand
{
string Header { get; }
IEnumerable<IMenuItem> Items { get; }
object Icon { get; }
bool IsCheckable { get; }
bool IsChecked { get; set; }
bool Visible { get; }
bool IsSeparator { get; }
string ToolTip { get; }
}
Notice that the IMenuItem defines IEnumerable Items, which is how you get sub-menus. Also, the IsSeparator is a way to define separators in the menu (another tough little trick). You can see in the xaml how it uses a DataTrigger to change the style to the existing separator style if IsSeparator is true. Here's how MainViewModel defines the MainMenu property (that the view binds to):
public IEnumerable<IMenuItem> MainMenu { get; set; }
This seems to work well. I assume you could use an ObservableCollection for the MainMenu. I'm actually using MEF to compose the menu out of parts, but after that the items themselves are static (even though the properties of each menu item are not). I also use an AbstractMenuItem class that implements IMenuItem and is a helper class to instantiate menu items in the various parts.
UPDATE:
Regarding your color problem, does this thread help?
Don't put the MenuItem in the DataTemplate. The DataTemplate defines the content of the MenuItem. Instead, specify extraneous properties for the MenuItem via the ItemContainerStyle:
<Menu>
<Menu.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="Header" Value="{Binding Title}"/>
...
</Style>
</Menu.ItemContainerStyle>
<MenuItem
Header="Options" ItemsSource="{Binding ManageMenuPageItemViewModels}"
ItemTemplate="{StaticResource MainMenuTemplate}"/>
</Menu>
Also, take a look at HierarchicalDataTemplates.
Here is how I have done my menus. It may not be precisely what you need, but I think it is pretty close.
<Style x:Key="SubmenuItemStyle" TargetType="MenuItem">
<Setter Property="Header" Value="{Binding MenuName}"></Setter>
<Setter Property="Command" Value="{Binding Path=MenuCommand}"/>
<Setter Property="ItemsSource" Value="{Binding SubmenuItems}"></Setter>
</Style>
<DataTemplate DataType="{x:Type systemVM:TopMenuViewModel}" >
<Menu>
<MenuItem Header="{Binding MenuName}"
ItemsSource="{Binding SubmenuItems}"
ItemContainerStyle="{DynamicResource SubmenuItemStyle}" />
</Menu>
</DataTemplate>
<Menu DockPanel.Dock="Top" ItemsSource="{Binding Menus}" />
TopMenuViewModel is a collection of the menus that will appear on the menu bar. They each contain the MenuName that will be displayed and a collection called SubMenuItems that I set to be the ItemsSource.
I control the way the SubMenuItems are displayed by way of the style SumMenuItemStyle. Each SubMenuItem has its own MenuName property, Command property of type ICommand, and possibly another collection of SubMenuItems.
The result is that I am able to store all my menu information in a database and dynamically switch what menus are displayed at runtime. The entire menuitem area is clickable and displays correctly.
Hope this helps.
Just make your DataTemplate to be a TextBlock (or maybe a stack panel with an icon and a TextBlock).