How to hide the Separator in a context menu using MultiBinding?

5.2k views Asked by At

I am using a context menu on a wpf treeview and I am pretty much there as to what I want. Before I explain the problem let me explain what the XAML definition for the context menu is doing.

For each menu item in the context menu we have a command that either disables or enables the menu item based on the commands CanExecute method. Each command will set the IsEnabled property of the corresponding menu item depending on the result of CanExecute.

IsEnabled for each the menu item is bound to a BooleanToVisibilityConverter which converts the the IsEnabled bool value to a Collapse or Visible value to be bound the Visibility propery of the menu item. This again works fine and my menu items are displaying and hiding fine.

Now for the problem. In the XAML below we have two menu items(addCategoryMenuItem and removeCategoryMenuItem) above a separator. I am trying to MultiBinding to the IsEnabled property of these two menu items via a custom implementation of IMultiValueConverter (MultiBooleanToVisibilityConverter) so that when the two menu items are disabled I can set the Visibility property of the Separator to collapsed and hence hide the separator when the menu items are disabled.

For the Convert method in my Converter(MultiBooleanToVisibilityConverter) the parameter value (object [] values) I get two items in the array that hold the value "{DependencyProperty.UnsetValue}". These cannot be cast to boolean values and hence my Visibility value cannot be worked out.

Maybe is has something do with ElementName used in the MultiBinding. Can it not find the element? I have tried using RelativeSource i.e find ancestor etc. But I just got confused. I have spent hours on this so now I leave it to the community.

Kind regards

Mohammad

<ContextMenu x:Key="CategoryMenu">
    <ContextMenu.ItemContainerStyle>
        <Style TargetType="{x:Type Control}">
            <Setter Property="Visibility" Value="{Binding Path=IsEnabled, RelativeSource={RelativeSource Self}, Mode=OneWay, Converter={StaticResource booleanToVisibilityConverter}}" />
        </Style>
    </ContextMenu.ItemContainerStyle>
    <ContextMenu.Items>
        <MenuItem x:Name="addCategoryMenuItem" Header="add category" Command="{Binding AddCategory}">
            <MenuItem.Icon>
                <Image Source="/Images/add.png" Width="16" Height="16" />
            </MenuItem.Icon>
        </MenuItem>
        <MenuItem x:Name="removeCategoryMenuItem" Header="remove category" Command="{Binding RemoveCategory}">
            <MenuItem.Icon>
                <Image Source="/Images/remove.png" Width="16" Height="16" />
            </MenuItem.Icon>
        </MenuItem>
        <Separator>
            <Separator.Visibility>
                <MultiBinding Converter="{StaticResource multiBooleanToVisibilityConverter}">
                    <Binding Mode="OneWay" ElementName="addCategoryMenuItem" Path="IsEnabled" />
                    <Binding Mode="OneWay" ElementName="removeCategoryMenuItem" Path="IsEnabled" />
                </MultiBinding>
            </Separator.Visibility>
        </Separator>
        <MenuItem x:Name="refreshCategoryMenuItem" Header="refresh" Command="{Binding RefreshCategory}">
            <MenuItem.Icon>
                <Image Source="/Images/refresh.png" Width="16" Height="16" />
            </MenuItem.Icon>
        </MenuItem>
    </ContextMenu.Items>
</ContextMenu>
3

There are 3 answers

4
dezzy On BEST ANSWER

Ok, after some rest I have managed to solve it. I had to use RelativeSource and FindAncestor to get the context menu object and then access the items collection and then use an indexer value to get the menu item. I think it would be better if I could use the menu item name as I don't like magic numbers in my code or indeed xaml.

<Separator>
    <Separator.Visibility>
        <MultiBinding Converter="{StaticResource multiBooleanToVisibilityConverter}">
            <Binding Mode="OneWay" RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type ContextMenu}}" Path="Items[0].IsEnabled" />
            <Binding Mode="OneWay" RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type ContextMenu}}" Path="Items[1].IsEnabled" />
        </MultiBinding>
    </Separator.Visibility>
</Separator>
0
Spook On

Michael Olsen's idea is excellent - I modified it, so that the separator checks both if there is a valid item before and after the separator.

public class AutoSeparator : Separator
{
    public AutoSeparator()
    {
        if (DesignerProperties.GetIsInDesignMode(this))
            return;

        Visibility = Visibility.Collapsed; // Starting collapsed so we don't see them disappearing

        Loaded += OnLoaded;
    }

    private void OnLoaded(object sender, RoutedEventArgs e)
    {
        Dispatcher.BeginInvoke(new Action(UpdateVisibility), DispatcherPriority.Render);
    }

    private void UpdateVisibility()
    {
        ItemCollection items = ((ItemsControl)Parent).Items;
        int index = items.IndexOf(this);

        if (index == -1)
            return;

        int i;
        for (i = index - 1; i >= 0; i--)
        {
            if (items[i] is UIElement uiElement)
            {
                // Invisible item cannot be valid predecessor
                if (uiElement.Visibility != Visibility.Visible)
                    continue;
                // Separator is invalid predecessor
                else if (uiElement is Separator or AutoSeparator)
                    return;
                // Anything else is valid predecessor
                else
                    break;
            }
        }

        // No valid item found before separator
        if (i < 0)
            return;

        for (i = index + 1; i < items.Count; i++)
        {
            if (items[i] is UIElement uiElement)
            {
                // Invisible item cannot be valid successor
                if (uiElement.Visibility != Visibility.Visible)
                    continue;
                // Separator is invalid successor
                else if (uiElement is Separator or AutoSeparator)
                    return;
                // Anything else is valid successor
                else
                    break;
            }
        }

        if (i >= items.Count)
            return;

        Visibility = Visibility.Visible;
    }
}
0
Michael Olsen On

I extended the normal separator to create one that automatically figures out whether it should show depending on the other items in the parent ItemsControl.

public class AutoVisibilitySeparator : Separator
{
    public AutoVisibilitySeparator()
    {
        if (DesignerProperties.GetIsInDesignMode(this))
            return;

        Visibility = Visibility.Collapsed; // Starting collapsed so we don't see them disappearing

        Loaded += OnLoaded;
    }

    private void OnLoaded(object sender, RoutedEventArgs e)
    {
        // We have to wait for all siblings to update their visibility before we update ours.
        // This is the best way I've found yet. I tried waiting for the context menu opening or visibility changed, on render and lots of other events
        Dispatcher.BeginInvoke(new Action(UpdateVisibility), DispatcherPriority.Render);
    }

    private void UpdateVisibility()
    {
        var showSeparator = false;

        // Go through each sibling of the parent context menu looking for a visible item before and after this separator
        var foundThis = false;
        var foundItemBeforeThis = false;
        foreach (var visibleItem in ((ItemsControl)Parent).Items.OfType<UIElement>().Where(i => i.Visibility == Visibility.Visible || i == this))
        {
            if (visibleItem == this)
            {
                // If there were no visible items prior to this separator then we hide it
                if (!foundItemBeforeThis)
                    break;

                foundThis = true;
            }
            else if (visibleItem is AutoVisibilitySeparator || visibleItem is Separator)
            {
                // If we already found this separator and this next item is not a visible item we hide this separator
                if (foundThis)
                    break;

                foundItemBeforeThis = false; // The current item is a separator so we reset the search for an item
            }
            else
            {
                if (foundThis)
                {
                    // We found a visible item after finding this separator so we're done and should show this
                    showSeparator = true;
                    break;
                }

                foundItemBeforeThis = true;
            }
        }

        Visibility = showSeparator ? Visibility.Visible : Visibility.Collapsed;
    }
}