How can I bind an ObservableCollection of ViewModels to a MenuItem? - wpf

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).

Related

Bind to property of parent from an explicit enumerable in XAML

Not sure if this is possible. I have an observable collection "OptionsList" with simple objects that have a "Name" and "IsEnabled" property.
Theres a menu that looks like
Configuration
|--Option1
|--Option2
|--Option3
|--Enabled
The first sub menu "Option1,Option2,Option3" bind correctly but then from within the I try to access those items from the first sub menu and bind to their data context but i cant seem to access them via RelativeSource for some reason.
<MenuItem Header="Configuration">
<MenuItem Header="Service" ItemsSource="{Binding OptionsList}">
<MenuItem.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="Header" Value="{Binding Name}"/>
<Setter Property="ItemsSource">
<Setter.Value>
<x:Array Type="MenuItem">
<MenuItem Header="Enabled" IsCheckable="True"
DataContext="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type MenuItem}}, Path=DataContext}"
IsChecked="{Binding Path=IsEnabled}"/>
</x:Array>
</Setter.Value>
</Setter>
</Style>
</MenuItem.ItemContainerStyle>
</MenuItem>
</MenuItem>
The IsChecked is bound with wrong Path. The implicit source of the Binding here is already a DataContext which is bound to the DataContext of the parent MenuItem. So with the Path DataContext.IsEnabled - it will actually look for DataContext.DataContext.IsEnabled - of course that cannot be resolved.
You can simply remove the DataContext.:
IsChecked="{Binding IsEnabled}"
Another problem is the DataContext will auto flow down the child MenuItem, so you don't need to set the DataContext for the inner MenuItems, which actually does not work (I've tried it and somehow the RelativeSource binding does not work - it's not some kind of disconnected visual tree - because the DataContext is flown down OK - so it is very strange in this case):
<MenuItem Header="Enabled" IsCheckable="True" IsChecked="{Binding IsEnabled}"/>
Here is a safer approach in which we use a HierarchicalDataTemplate. Note that the ItemsSource is set to a dummy array with 1 element. Inside the ItemTemplate we will use a Binding walking up to the parent element for the IsChecked property
<MenuItem Header="Service" ItemsSource="{Binding OptionsList}">
<MenuItem.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="ItemTemplate">
<Setter.Value>
<HierarchicalDataTemplate>
<HierarchicalDataTemplate.ItemsSource>
<x:Array Type="{x:Type sys:Int32}">
<sys:Int32>0</sys:Int32>
</x:Array>
</HierarchicalDataTemplate.ItemsSource>
<HierarchicalDataTemplate.ItemTemplate>
<DataTemplate>
<TextBlock Text="Enabled"/>
</DataTemplate>
</HierarchicalDataTemplate.ItemTemplate>
<HierarchicalDataTemplate.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="IsCheckable" Value="True"/>
<Setter Property="IsChecked"
Value="{Binding DataContext.IsEnabled, RelativeSource={RelativeSource AncestorType=MenuItem}}"/>
</Style>
</HierarchicalDataTemplate.ItemContainerStyle>
<TextBlock Text="{Binding Name}"/>
</HierarchicalDataTemplate>
</Setter.Value>
</Setter>
</Style>
</MenuItem.ItemContainerStyle>
I think the item display doesn't work because of some kind of reuse of the MenuItems in WPF (Not sure whether this is a bug in MenuItem). Playing around with x:Shared="False" didn't fixed it.
There is a different approach to achieve your goal:
Create a helper class that is a child of your option class and provide one instance of this helper as child of the option.
Bind in XAML to this helper class
Here is some code that shows in detail how to do:
XAML:
<MenuItem Header="Service" ItemsSource="{Binding OptionsList}">
<MenuItem.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="Header" Value="{Binding Name}" />
<Setter Property="ItemsSource" Value="{Binding ToggleItem}" />
<Setter Property="ItemContainerStyle">
<Setter.Value>
<Style TargetType="MenuItem">
<Setter Property="Header" Value="{Binding Name}" />
<Setter Property="IsCheckable" Value="True" />
<Setter Property="IsChecked" Value="{Binding IsEnabled}" />
</Style>
</Setter.Value>
</Setter>
</Style>
</MenuItem.ItemContainerStyle>
</MenuItem>
C#
public class OptionHelper
{
private readonly Option owner;
public OptionHelper(Option owner)
{
this.owner = owner;
}
public bool IsEnabled
{
get { return this.owner.IsEnabled; }
set { this.owner.IsEnabled = value; }
}
}
public class Option : INotifyPropertyChanged
{
public ObservableCollection<OptionHelper> ToggleItem { get; private set; }
public Option(string name, bool isEnabled)
{
this.ToggleItem = new ObservableCollection<OptionHelper>() { new OptionHelper(this) };
this.name = name;
this.isEnabled = isEnabled;
}
// your code here...
}
I know this is not the perfect solution but it works... Wonder if someone find a solution without the helper.

DisplayMemberPath when using ItemsPanelTemplate

I have a list of objects of type SomeObject that is defined like:
public struct SomeObject
{
public SomeObject(int id, string imgPath)
{
Id = id;
ImagePath = imgPath;
}
public int Id;
public string ImagePath;
}
I want to display these objects in a listbox, that shows one image for each object and arranges them in a tile layout.
The ItemsTemplate is just an Image.
The ItemsPanelTemplate is a TilePanel.
When I use just strings (image paths) as list items instead of the SombeObject items, everything works and the images are displayed correctly.
<ListBox ScrollViewer.HorizontalScrollBarVisibility="Disabled"
SelectionMode="Extended"
SelectionChanged="OnItemSelectionChanged"
ItemsSource="{Binding RelativeSource={RelativeSource AncestorType={x:Type controls:VerticalTileBox}},
Path=SomeObjectsCollectionView, UpdateSourceTrigger=PropertyChanged}"
>
<ListBox.ItemTemplate>
<DataTemplate>
<Image Source ="{Binding}"
VerticalAlignment="Center" HorizontalAlignment="Center" Stretch="Uniform" >
</Image>
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<controls:VirtualizingTilePanel ChildSize="{Binding
RelativeSource={RelativeSource AncestorType={x:Type controls:VerticalTileBox}},
Path=ItemSize, UpdateSourceTrigger=PropertyChanged, Mode=OneWay}" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="VerticalContentAlignment" Value="Stretch"/>
<Setter Property="Padding" Value="0"/>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
When using SomeObject items, this code gives me a list of tiles but the images are not displayed.
System.Windows.Data Error: 40 : BindingExpression path error: 'ImagePath' property not found on 'object' ''SomeObject'
(HashCode=1739718653)'. BindingExpression:Path=ImagePath; DataItem='SomeObject' (HashCode=1739718653);
target element is 'Image' (Name=''); target property is 'Source' (type 'ImageSource')
This is expected, since I have not told the control, what exactly to display (which property of the SomeObject).
Using the DisplayMemberPath in addition to the ItemsPanelTemplate does not work, so how do I set the property to be used for display in the image?
Thanks for your help!
edit:
# nkoniishvt
That really sounds like the right way to do it, but I cannot get it to work properly. What am I doing wrong?
I added this DP to the UserControl code behind:
public static readonly DependencyProperty ItemsTemplateProperty =
DependencyProperty.Register("ItemsTemplate",
typeof(DataTemplate),
typeof(VerticalTileBox),
new PropertyMetadata(null));
public DataTemplate ItemsTemplate
{
get {return (DataTemplate)GetValue(ItemsTemplateProperty);}
set {SetValue(ItemsTemplateProperty, value);}
}
and changed the xaml like this:
<ListBox
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
SelectionMode="Extended"
SelectionChanged="OnItemSelectionChanged"
ItemsSource="{Binding RelativeSource={RelativeSource AncestorType={x:Type controls:VerticalTileBox}},
Path=ItemCollection, UpdateSourceTrigger=PropertyChanged}"
ItemTemplate="{Binding RelativeSource={RelativeSource AncestorType={x:Type controls:VerticalTileBox}},
Path=ItemsTemplate, UpdateSourceTrigger=PropertyChanged}"
>
<ItemsPanelTemplate>
<controls:VirtualizingTilePanel ChildSize="{Binding
RelativeSource={RelativeSource AncestorType={x:Type controls:VerticalTileBox}},
Path=ItemSize, UpdateSourceTrigger=PropertyChanged, Mode=OneWay}" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="VerticalContentAlignment" Value="Stretch"/>
<Setter Property="Padding" Value="0"/>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
The data template is defined like this:
<DataTemplate x:Name="SomeImageItemTemplate" DataType="utilities:SomeObject">
<Image Source ="{Binding Path=ImagePath}"
VerticalAlignment="Center" HorizontalAlignment="Center" Stretch="Uniform" />
</DataTemplate>
And the overall thing is used in my view like this:
<ctrls:VerticalTileBox Source="{Binding ImageListView, UpdateSourceTrigger=PropertyChanged}" ItemsTemplate="{DynamicResource SomeImageItemTemplate}"/>
The result is that the tiles are displayed but they are all empty except for the objects' class names (SomeObject).
You can't have a generic DataTemplate if the data you put as ItemsSource of your ListBox is unknown and your UserControl must remain generic.
You should let the consumer of your UserControl provide the DataTemplate instead, either in the UserControl.Resources or in a DataTemplate DP you need to create and bind to the ListBox's ItemTemplate.
Panels shouldn't say how a children is displayed, only where and how big it should be.

Windows Phone - Bind Button of UserControl to ViewModel

i am struggling with binding a Button inside of a UserControl (which is placed inside of a ListBox.ItemTemplate) to my ViewModel.
Page A contains the following Listbox:
<ListBox x:Name="lbUpcomingBetgames" ItemsSource="{Binding UpcomingBetgames}" Background="{x:Null}">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="Margin" Value="0,4,0,4"/>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<customControl:BetgameInfoControl x:Name="upcomingBetgameControl" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
My ViewModel contains a RelayCommand that i want to bind to in my CustomControl BetgameInfoControl:
public RelayCommand BetgameRegisterClicked
{
get;
private set;
}
My CustomControl contains a Button:
<Button Grid.Row="0" Content="Accept" Height="50" Command="{Binding Path=Parent.DataContext.BetgameRegisterClicked, ElementName=upcomingBetgameControl}" CommandParameter="{Binding Description}" />
The RelayCommand is never called. I tried different ways to bind the Command-Property in my Button but nothing worked.
Can someone help me please?
You need to change the datacontext of your button like this =>
DataContext={Binding ElementName=lbUpcomingBetgames,path=DataContext.BetgameRegisterClicked}

Pass listbox selected item information through to usercontrol (WPF)

I have a listbox, and each listbox item is a custom usercontrol I made. I have used styles to remove all of the default highlighting for a listbox item (ie. removing the blue background highlight for a selected item).
What I want is to be able to do something special to my user control to denote that the listbox item is highlighted. Such as make the border on the user control more bold, something like that.
If I could get a boolean into the user control, I think from there I'd be able to figure out how to make the necessary changes to the user control... through a converter or something most likely.
What I'm not sure of, is how do I pass into the usercontrol the information that shows whether the listbox item which the user control is in is highlighted.
The code in question is like this:
<ListBox.ItemTemplate>
<DataTemplate>
<hei:OrangeUserCtrl DataContext="{Binding}" Height="40" Width="40" />
</DataTemplate>
</ListBox.ItemTemplate>
How can I pass in to the user control (preferably as a true/false) if the listbox item it is in is highlighted?
Thanks
You can use Tag property and RelativeSource binding.
In my example when item is highlighted I changed Border properties (BorderBrush=Red and BorderThickness=3).
Source code:
Simple class to hold data:
class Person
{
public string Name { get; set; }
public string Surname { get; set; }
}
ListBox:
<ListBox ItemsSource="{Binding}">
<ListBox.ItemTemplate>
<DataTemplate>
<local:MyCustomPresenter DataContext="{Binding}"
Tag="{Binding Path=IsSelected, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ListBoxItem}, UpdateSourceTrigger=PropertyChanged}"
Height="60" Width="120" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
UserControl to display custom data:
<UserControl x:Class="WpfTextWrapping.MyCustomPresenter"
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"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Border Margin="10">
<Border.Style>
<Style TargetType="Border">
<Setter Property="BorderBrush" Value="Green" />
<Setter Property="BorderThickness" Value="1" />
<Style.Triggers>
<DataTrigger Binding="{Binding Path=Tag, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}, UpdateSourceTrigger=PropertyChanged}" Value="True">
<Setter Property="BorderBrush" Value="Red" />
<Setter Property="BorderThickness" Value="3" />
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<StackPanel Orientation="Vertical">
<TextBlock Text="{Binding Name}" />
<TextBlock Text="{Binding Surname}" />
</StackPanel>
</Border>
</UserControl>
If I understand you well, you need to add a property to your custom UserControl bound to the nested ComboBox something like :
public object MySelectedItem
{
get { return myNestedCombox.SelectedItem; }
set { myNestedCombox.SelectedItem = value; }
}
You need to NotifyPropertyChanged as well.

MenuItem and IcollectionView

I want to display a list of available languages in a menu.
The languages are available as an ICollectionView.
This is the code:
<Menu DockPanel.Dock="Top">
<Menu.Resources>
<Style x:Key="LanguageMenuStyle" TargetType="MenuItem">
<Setter Property="Header" Value="{Binding Name}"></Setter>
<Setter Property="IsCheckable" Value="True"/>
</Style>
</Menu.Resources>
<MenuItem Header="Language" ItemsSource="{Binding Languages}"
ItemContainerStyle="{StaticResource LanguageMenuStyle}">
</MenuItem>
</Menu>
Languages is an ICollectionView created as default view from a list of cultures.
The menu is displayed correctly.
Now I would like to bind to the CurrentChanged event when the selection in the menu changes, but since there is no IsSynchronizedWithCurrentItem property, how could I do that?
And is there a way to allow only one item to be checked at a time?

Resources