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.
Related
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;
}
}
So I have a list view that displays post items (Delivery date, type, tracking number etc) and I have a context menu set up that either opens up the tracking website or copies the tracking number to the clipboard.
What I want is for the contextmenu only to appear for listitems that have a tracking number. I've got the idea of changing the visibility of the contextmenu but it's the binding to the tracking number I'm having the trouble with.
<ContextMenu x:Key="MyElementMenu">
<MenuItem Header="Track Item" Click="MenuItem_Click"></MenuItem>
<MenuItem Header="Copy to Clipboard" Click="MenuItem_CopyToClipboard"></MenuItem>
</ContextMenu>
<!--Sets a context menu for each ListBoxItem in the current ListBox-->
<Style TargetType="{x:Type ListViewItem}">
<Setter Property="ContextMenu" Value="{StaticResource MyElementMenu}"/>
</Style>
This is what I have currently.
<MyControl.Resources>
<BooleanToVisibilityConverter x:Key="BoolToVis"/>
</MyControl.Resources>
<!--Sets a context menu for each ListBoxItem in the current ListBox-->
<Style TargetType="{x:Type ListViewItem}">
<Setter Property="ContextMenu">
<Setter.Value>
<ContextMenu IsEnabled="{Binding HasTrackingNumber}" Visibility="{Binding HasTrackingNumber, Converter={StaticResource BoolToVis}">
<MenuItem Header="Track Item" Click="MenuItem_Click"></MenuItem>
<MenuItem Header="Copy to Clipboard" Click="MenuItem_CopyToClipboard"></MenuItem>
</ContextMenu>
</Setter.Value>
</Setter>
</Style>
This should give you what you need. Not sure if you use the ContextMenu elsewhere, but if you dont you can always just set it in the style of the ListViewItem Style. Then you dont need to have it referenced from elsewhere. Either way its more about adding the Binding from the item. In your ListItem Viewmodel you could add something like:
public bool HasTrackingNumber => TrackingNumber == 0 || TrackingNumber == null;
(I dont know what type your tracking number is so you can do your own logic checks to know if it "has" a valid tracking number)
That seems like a case for a trigger:
<Style TargetType="{x:Type ListViewItem}">
<Setter Property="ContextMenu" Value="{StaticResource MyElementMenu}"/>
<Style.Triggers>
<!--
Maybe the tracking number property is called something else, maybe it's 0
instead of null when absent. You didn't say.
-->
<DataTrigger Binding="{Binding TrackingNumber}" Value="{x:Null}">
<Setter Property="ContextMenu" Value="{x:Null}"/>
</DataTrigger>
</Style.Triggers>
</Style>
Can you please give me a hint, to make HasItems Property better.
I have a TreeView like this:
<TreeView ItemsSource="{Binding Customers}">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Style.Triggers>
<DataTrigger Binding="{Binding HasItems, RelativeSource={RelativeSource Self}}" Value="True">
<Setter Property="Foreground" Value="Red" />
</DataTrigger>
<DataTrigger Binding="{Binding HasItems, RelativeSource={RelativeSource Self}}" Value="False">
<Setter Property="Foreground" Value="Blue" />
</DataTrigger>
</Style.Triggers>
<Setter Property="AutomationProperties.AutomationId" Value="{Binding AutomationId}" />
<Setter Property="IsExpanded" Value="True" />
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
</Style>
</TreeView.ItemContainerStyle>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Customers}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}" />
<TextBlock Text="{Binding FamilyName}" Margin="5,0,0,0" />
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
Code behind:
public ObservableCollection<Customer> Customers { get; set; }
public MainWindow()
{
InitializeComponent();
DataContext = this;
Customers = new ObservableCollection<Customer>();
var homer = new Customer("Homer", "Simpson");
homer.Customers.Add(new Customer("Bart", "Simpson"));
homer.Customers.Add(new Customer("Lisa", "Simpson"));
homer.Customers.Add(new Customer("Maggie", "Simpson"));
var chief = new Customer("Chief", "Wiggum");
chief.Customers.Add(new Customer("Ralf", "Wiggum"));
Customers.Add(homer);
Customers.Add(chief);
}
The Class Customer implements INotifyPropertyChanged and everything is fine.
As you see here, I have a DataTrigger to change color depending on "HasItems" Property of the TreeViewItem.
The problem is: HasItems is true, even if all children are Hidden or Collapsed.
See here: I made the VISIBILITY of son of "Chief Wiggum" Collapsed. And "Chief Wiggum"-TreeViewItem is still red.
Well, as you could figure out, the fact the items are hidden doesn't mean the tree view has no items.
One possible approach is changing your DataTrigger in the following way:
<DataTrigger Binding="{Binding Items, Converter={StaticResource HasVisibleItemsConverter}, RelativeSource={RelativeSource Self}}" Value="True">
Create a HasVisibleItemsConverter converter class that implements IValueConverter, there you should check if there are any items that are visible - I'll leave that for your own exercise.
Then you create an instance of HasVisibleItemsConverter in the Resources area (either Window.Resources or UserControl.Resources):
<Window.Resources>
<conv:HasVisibleItemsConverter x:Key="HasVisibleItemsConverter" />
</Window.Resources>
And don't forget to add conv="..." in the namespace definition for your Window/UserControl pointing to the assembly and namespace where your converter is.
I have a DataTemplate which contains a CheckBox and ListBox. When the CheckBox is checked, I want to change the ItemTemplate property on the ListBox to change the appearance of each item.
Right now, it looks like this:
<DataTemplate DataType={x:Type MyViewModel}>
<DockPanel>
<CheckBox DockPanel.Dock="Bottom"
Content="Show Details"
HorizontalAlignment="Right"
IsChecked="{Binding ShowDetails}"
Margin="0 5 10 5" />
<ListBox ItemsSource="{Binding Items}"
ItemTemplate="{StaticResource SimpleItemTemplate}"
Margin="10 0 10 5">
<ListBox.Triggers>
<DataTrigger Binding="{Binding ShowDetails}" Value="True">
<Setter Property="ItemTemplate"
Value="{StaticResource DetailedItemTemplate}" />
</DataTrigger>
</ListBox.Triggers>
</ListBox>
</DockPanel>
</DataTemplate>
However, when I try to compile, I get the following error messages:
Value 'ItemTemplate' cannot be assigned to property 'Property'. Invalid PropertyDescriptor value.
and
Cannot find the static member 'ItemTemplateProperty' on the type 'ContentPresenter'.
I'm still fairly new to WPF, so perhaps there is something I'm not quite understanding?
You need to do this through the ListBox Style rather than directly through its Triggers collection. A FrameworkElement's Triggers collection can only contain EventTriggers (so I'm surprised your sample got as far as complaining about the properties!). Here's what you need to do:
<ListBox ItemsSource="{Binding Items}">
<ListBox.Style>
<Style TargetType="ListBox">
<Setter Property="ItemTemplate" Value="{StaticResource SimpleItemTemplate}" />
<Style.Triggers>
<DataTrigger Binding="{Binding ShowDetails}" Value="True">
<Setter Property="ItemTemplate"
Value="{StaticResource DetailedItemTemplate}" />
</DataTrigger>
</Style.Triggers>
</Style>
</ListBox.Style>
</ListBox>
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.