Как скрыть разделитель в контекстном меню с помощью MultiBinding? - PullRequest
3 голосов
/ 19 февраля 2011

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

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

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

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

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

Возможно, это как-то связано с 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>

Ответы [ 2 ]

2 голосов
/ 19 февраля 2011

Хорошо, после некоторого отдыха мне удалось решить это. Мне пришлось использовать 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>
1 голос
/ 15 декабря 2016

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