Стиль WPF в зависимости от состояния флажка - PullRequest
1 голос
/ 25 ноября 2010

Я создаю редактор настроек, в котором авторы плагинов могут определять свой собственный пользовательский интерфейс для настройки своих плагинов. Я реализую функцию, позволяющую скрыть определенные «дополнительные» элементы, если флажок снят.

Флажок XAML тривиален:

<CheckBox Name="isAdvanced">_Advanced</CheckBox>

В идеале (подробнее об этом позже), разработчики просто добавят флаг к расширенным элементам управления (который должен быть скрыт, когда флажок «расширенный» снят), например:

<Button library:MyLibraryControl.IsAdvanced="True">My Button</Button>

Проблема заключается в том, чтобы сделать магию сокрытия элементов IsAdvanced="True", когда isAdvanced.IsChecked == false. У меня есть желаемое поведение с этим стилем на элементе окна:

<Window.Resources>
    <Style TargetType="Button">
        <Style.Triggers>
            <MultiDataTrigger>
                <MultiDataTrigger.Conditions>
                    <Condition Binding="{Binding (library:MyLibraryControl.IsAdvanced), RelativeSource={RelativeSource Mode=Self}}" Value="True" />
                    <Condition Binding="{Binding IsChecked, ElementName=isAdvanced}" Value="False" />
                </MultiDataTrigger.Conditions>

                <Setter Property="UIElement.Visibility" Value="Collapsed" />
            </MultiDataTrigger>
        </Style.Triggers>
    </Style>
</Window.Resources>

Однако этот метод представляет две проблемы:

  1. Это только добавляет функциональность кнопкам и ничего больше. Флаг IsAdvanced может (должен быть в состоянии) быть добавлен к любому визуальному элементу.
  2. Он заменяет / отменяет стили, которые в противном случае были бы на кнопке.

Есть ли какой-то другой способ создания нужной мне функциональности? Я не боюсь работать с выделенным кодом, но элегантное XAML-решение идеально (так как это чисто изменение пользовательского интерфейса, за исключением сохранения состояния флажка в настройках пользователя).


Некоторые другие методы обозначения продвинутых элементов пришли на ум. К ним относится использование динамического ресурса и прямая привязка:

<Button Visibility="{DynamicResource IsAdvancedVisibility}">My Button</Button>
<Button Visibility="{Binding IsChecked, RelativeSource={...}, ValueConverter={...}}">My Button</Button>

Использование словаря ресурсов, вероятно, сработало бы, но кажется, что это действительно плохое решение, так как состояние пользовательского интерфейса не должно относиться к словарю. Привязка вручную - довольно сложная задача, потому что состояние флажка должно быть отправлено как-то элементу, и, кроме значений жесткого кодирования, я не вижу, чтобы он не стал беспорядком.

Оба эти альтернативных решения связывают семантику («это расширенный параметр») с внешним видом («расширенные параметры должны быть свернуты»). Исходя из мира HTML, я знаю, что это очень плохо, и я отказываюсь подчиняться этим методам, если абсолютно не требуется.

Ответы [ 3 ]

0 голосов
/ 26 ноября 2010

Вероятно, есть много лучших способов решения этой проблемы, но я попытался обойти две проблемы, которые у вас были с вашим решением. Небольшой пример проекта с этим можно скачать здесь .

1.Это только добавляет функциональность кнопкам и ничего больше. Флаг IsAdvanced может (должен быть в состоянии к) быть добавленным к любому визуальному элементу.

Добавление присоединенного свойства, которое заставляет все дочерние элементы наследовать значение, к самому верхнему контейнеру может исправить это.

2. Он заменяет / переопределяет стили, которые в противном случае были бы на кнопка.

У Bea Stollnitz есть хорошая статья в блоге о слиянии стилей здесь .
У него есть метод расширения для стиля под названием Merge, который можно использовать.

Звучит довольно прямо, но следующие проблемы сделали код более сложным.
1. Элементы Visual не имеют стиля, когда свойство Attached наследуется. Обязательное загруженное событие.
2. Стиль не может быть изменен, когда он используется. Требуется метод копирования для стиля.

Итак, мы хотим, чтобы этот стиль был объединен с активным стилем для всех дочерних элементов в родительском контейнере.

<Style x:Key="IsAdvancedStyle">
    <Style.Triggers>
        <MultiDataTrigger>
            <MultiDataTrigger.Conditions>
                <Condition Binding="{Binding (library:MyLibraryControl.IsAdvanced), RelativeSource={RelativeSource Mode=Self}}" Value="True" />
                <Condition Binding="{Binding IsChecked, ElementName=isAdvanced}" Value="False" />
            </MultiDataTrigger.Conditions>
            <Setter Property="Control.Visibility" Value="Collapsed" />
        </MultiDataTrigger>
    </Style.Triggers>
</Style>

Если корневым контейнером является StackPanel, мы добавляем это. Стиль IsAdvancedStyle будет затем унаследован всеми дочерними элементами и объединен с активным стилем.

<StackPanel local:StyleChildsBehavior.StyleChilds="{StaticResource IsAdvancedStyle}">

StyleChildsBehavior.cs

public class StyleChildsBehavior
{
    public static readonly DependencyProperty StyleChildsProperty =
        DependencyProperty.RegisterAttached("StyleChilds",
                                            typeof(Style),
                                            typeof(StyleChildsBehavior),
                                            new FrameworkPropertyMetadata(null,
                                                    FrameworkPropertyMetadataOptions.Inherits,
                                                    StyleChildsCallback));

    public static void SetStyleChilds(DependencyObject element, Style value)
    {
        element.SetValue(StyleChildsProperty, value);
    }
    public static Style GetStyleChilds(DependencyObject element)
    {
        return (Style)element.GetValue(StyleChildsProperty);
    }

    private static void StyleChildsCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (DesignerProperties.GetIsInDesignMode(d) == true)
        {
            return;
        }
        Style isAdvancedStyle = e.NewValue as Style;
        if (isAdvancedStyle != null)
        {
            FrameworkElement element = d as FrameworkElement;
            if (element != null)
            {
                if (element.IsLoaded == false)
                {
                    RoutedEventHandler loadedEventHandler = null;
                    loadedEventHandler = new RoutedEventHandler(delegate
                    {
                        element.Loaded -= loadedEventHandler;
                        MergeStyles(element, isAdvancedStyle);
                    });
                    element.Loaded += loadedEventHandler;
                }
                else
                {
                    MergeStyles(element, isAdvancedStyle);
                }
            }
        }
    }
    private static void MergeStyles(FrameworkElement element, Style isAdvancedStyle)
    {
        if (element != null)
        {
            Style advancedStyle = GetStyleCopy(isAdvancedStyle);
            advancedStyle.Merge(element.Style);
            element.Style = advancedStyle;
        }
    }
    private static Style GetStyleCopy(Style style)
    {
        string savedStyle = XamlWriter.Save(style);
        using (MemoryStream memoryStream = new MemoryStream(Encoding.ASCII.GetBytes(savedStyle)))
        {
            ParserContext parserContext = new ParserContext();
            parserContext.XmlnsDictionary.Add("library", "clr-namespace:HideAll;assembly=HideAll");
            return XamlReader.Load(memoryStream, parserContext) as Style;
        }
    }
}

После этого IsAdvancedStyle будет объединен во всех дочерних элементах StackPanel, и это также касается дочерних элементов, которые также добавляются во время выполнения.

Изменен метод расширения слияния из ссылки в блоге.

public static void Merge(this Style style1, Style style2)
{
    if (style1 == null || style2 == null)
    {
        return;
    }
    if (style1.TargetType.IsAssignableFrom(style2.TargetType))
    {
        style1.TargetType = style2.TargetType;
    }
    if (style2.BasedOn != null)
    {
        Merge(style1, style2.BasedOn);
    }
    foreach (SetterBase currentSetter in style2.Setters)
    {
        style1.Setters.Add(currentSetter);
    }
    foreach (TriggerBase currentTrigger in style2.Triggers)
    {
        style1.Triggers.Add(currentTrigger);
    }
}
0 голосов
/ 28 ноября 2010

Я решил немного перевернуть проблему, и она сработала хорошо.

Вместо того, чтобы иметь дело со стилями, я использовал привязку свойств, как предложено Gishu .Однако вместо размещения пользовательского интерфейса в виртуальной машине (где свойства будут распространяться на несколько слоев вручную), я использовал прикрепленное свойство с именем ShowAdvanced, которое распространяется через наследование свойств.

Создание этого свойства тривиально:

public static readonly DependencyProperty ShowAdvancedProperty;

ShowAdvancedProperty = DependencyProperty.RegisterAttached(
    "ShowAdvanced",
    typeof(bool),
    typeof(MyLibraryControl),
    new FrameworkPropertyMetadata(
        false,
        FrameworkPropertyMetadataOptions.Inherits | FrameworkPropertyMetadataOptions.OverridesInheritanceBehavior
    )
);

Флажок устанавливает свойство ShowAdvanced выше для всего окна.Он может установить его в другом месте (например, в сетке), но его размещение в окне имеет больше смысла. IMO:

<CheckBox Grid.Column="0"
    IsChecked="{Binding (library:MyLibraryControl.ShowAdvanced), ElementName=settingsWindow}"
    Content="_Advanced" />

Изменение видимости (или любых других требуемых свойств) в зависимости от свойства ShowAdvanced становитсяпросто:

<Foo.Resources>
    <BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
</Foo.Resources>

<Button Visibility="{Binding (library:MyLibraryControl.ShowAdvanced), RelativeSource={RelativeSource Self}, Converter={StaticResource BooleanToVisibilityConverter}}">I'm Advanced</Button>

Стили канавок позволяют авторам плагинов полностью изменять макет своих элементов управления, если это необходимо.Они также могут показывать расширенные элементы управления, но при желании держать их отключенными.Стили вызвали много проблем, и, как показал Мелеак, обходные пути были беспорядочными .

Моя главная проблема с использованием «продвинутой» логики отображения в ВМ заключается в том, что теперь она меньшескорее всего, вам удастся связать несколько представлений с одной и той же виртуальной машиной, сохраняя при этом необходимую гибкость.Если в VM используется «расширенная» логика, расширенные элементы управления должны отображаться для всех представлений или без представлений;Вы не можете показать их для одного и скрыть их для другого.Это, IMO, в первую очередь нарушает принципы наличия виртуальной машины.

(Спасибо всем, кто писал здесь; это было полезно!)

0 голосов
/ 25 ноября 2010

Как насчет перемещения этого в ViewModel вместо XAML, потому что для меня это выглядит как поведение.

Мне кажется, что вам нужно поведение - каждый плагин регистрирует набор свойств (сопоставление элементам управления UI) как продвинутый,Существует глобальная настройка для включения / выключения дополнительных свойств.Когда это происходит, обновите все плагины, чтобы показать / скрыть их расширенные свойства

Пусть разработчики плагинов реализуют интерфейс, содержащий свойство только для набора AreAdvancedControlsVisible.Позвольте им позаботиться о сокрытии / отображении элементов управления в их пользовательском интерфейсе через обработчик изменения свойств.Расширенные элементы управления пользовательского интерфейса могут привязываться к флагу ShowAdvancedControls на pluginVM, который включается / выключается из обработчика измененных проп.Каркас может просто зацикливаться на доступных плагинах и устанавливать этот флаг всякий раз, когда установлен флажок ShowAdvanced.

...