3

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>
dezzy
  • 479
  • 6
  • 18

2 Answers2

3

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;
    }
}
David Makogon
  • 69,407
  • 21
  • 141
  • 189
Michael Olsen
  • 404
  • 3
  • 14
2

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>
dezzy
  • 479
  • 6
  • 18
  • I'd suggest the more general solution of binding to self and having the converter check whether there are any enabled menu items prior to the context menu. Or, use a DataTemplate and bind to an ObservableCollection in a view model that can determine which separators to display. This would avoid the hard-coded indices and allow the logic to be unit tested. – mancaus Feb 19 '11 at 09:49
  • 1
    You may also want to look at the NameScope solution in this SO question: [ElementName Binding from MenuItem in ContextMenu](http://stackoverflow.com/questions/1013558/) – Mal Ross Feb 19 '11 at 13:43
  • Thanks for the suggestion guys. This is my first wpf application and I am trying to follow the mvvm pattern which means no code in the code behind. I think I will go with an observable collection on my view model to populate the context menu and have the logic to display/hide the separator. This makes sense. To mancaus, not sure what you mean "binding to self". Could you kindly clarify a bit more. Thanks again. – dezzy Feb 19 '11 at 13:59
  • 1
    I agree with Mal Ross about the link he added, that should solve the actual problem you had. Also, there is nothing wrong with code-behind, as long as it isn't business logic. Or, putting it another way, it shouldn't reference your ViewModel. – Andre Luus Oct 11 '11 at 07:17