0
votes

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>
2
If my approach is somehow stupid or there is any better way to achieve this type of context menu this would also help me a lot.pedrito

2 Answers

0
votes

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;
    }
}
0
votes

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; }
}