Как скрыть разделитель в контекстном меню с помощью MultiBinding?

Я использую контекстное меню в древовидной структуре wpf, и я почти полностью понимаю, что хочу. Прежде чем я объясню проблему, позвольте мне объяснить, что делает определение XAML для контекстного меню.

Для каждого пункта меню в контекстном меню у нас есть команда, которая либо отключает, либо включает пункт меню на основе метода команд CanExecute. Каждая команда устанавливает свойство IsEnabled соответствующего пункта меню в зависимости от результата CanExecute.

IsEnabled для каждого элемента меню привязан к BooleanToVisibilityConverter, который преобразует логическое значение IsEnabled в значение Collapse или Visible для привязки свойства Visibility элемента меню. Это снова работает нормально, и мои пункты меню отображаются и скрываются нормально.

Теперь о проблеме. В приведенном ниже XAML у нас есть два пункта меню (addCategoryMenuItem и removeCategoryMenuItem) над разделителем. Я пытаюсь привязать MultiBinding к свойству IsEnabled этих двух пунктов меню с помощью настраиваемой реализации IMultiValueConverter (MultiBooleanToVisibilityConverter), чтобы, когда два элемента меню отключены, я мог установить свойство Visibility Separator на сворачивание и, следовательно, скрыть разделитель, когда пункты меню отключены.

Для метода Convert в моем конвертере (MultiBooleanToVisibilityConverter) значение параметра (object [] values) я получаю два элемента в массиве, которые содержат значение "{DependencyProperty.UnsetValue}". Они не могут быть преобразованы в логические значения, и, следовательно, мое значение видимости не может быть определено.

Возможно, это имеет какое-то отношение к ElementName, используемому в MultiBinding. Может не найти элемент? Я пробовал использовать RelativeSource, то есть найти предка и т. Д. Но я просто запутался. Я потратил на это несколько часов, поэтому теперь оставляю это сообществу.

С уважением

Мохаммад

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

person dezzy    schedule 19.02.2011    source источник


Ответы (2)


Хорошо, немного отдохнув, мне удалось это решить. Мне пришлось использовать RelativeSource и FindAncestor, чтобы получить объект контекстного меню, а затем получить доступ к коллекции элементов, а затем использовать значение индексатора для получения элемента меню. Я думаю, было бы лучше, если бы я мог использовать имя пункта меню, поскольку мне не нравятся магические числа в моем коде или даже 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>
person dezzy    schedule 19.02.2011
comment
Я бы предложил более общее решение - привязать к себе и заставить конвертер проверять, есть ли какие-либо включенные пункты меню до контекстного меню. Или используйте DataTemplate и привяжите к ObservableCollection в модели представления, которая может определять, какие разделители отображать. Это позволило бы избежать жестко запрограммированных индексов и позволить модульное тестирование логики. - person mancaus; 19.02.2011
comment
Вы также можете посмотреть решение NameScope в этом вопросе SO: Привязка имени элемента из MenuItem в ContextMenu - person Mal Ross; 19.02.2011
comment
Спасибо за предложение, ребята. Это мое первое приложение wpf, и я пытаюсь следовать шаблону mvvm, что означает отсутствие кода в коде позади. Я думаю, что я выберу наблюдаемую коллекцию в моей модели представления, чтобы заполнить контекстное меню и иметь логику для отображения / скрытия разделителя. Это имеет смысл. Mancaus, не уверен, что вы имеете в виду привязать к себе. Не могли бы вы уточнить немного подробнее. Спасибо еще раз. - person dezzy; 19.02.2011
comment
Я согласен с Мэлом Россом по поводу добавленной им ссылки, которая должна решить настоящую проблему, с которой вы столкнулись. Кроме того, в коде программной части нет ничего плохого, если это не бизнес-логика. Или, другими словами, он не должен ссылаться на вашу ViewModel. - person Andre Luus; 11.10.2011

Я расширил обычный разделитель, чтобы создать тот, который автоматически определяет, должен ли он отображаться в зависимости от других элементов в родительском 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;
    }
}
person Michael Olsen    schedule 15.12.2016