WPF 样式取决于复选框状态

发布于 2024-10-04 07:28:54 字数 1936 浏览 0 评论 0原文

我正在创建一个设置编辑器,插件编写者可以在其中定义自己的用户界面来配置其插件。我正在实现一项功能,如果未选中复选框,则隐藏某些“高级”元素。

复选框 XAML 很简单:

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

理想情况下(稍后详细介绍),实现者只需向高级控件添加一个标志(当未选中“高级”复选框时应隐藏该标志),如下所示:

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

问题在于隐藏当 isAdvanced.IsChecked == false 时,IsAdvanced="True" 元素。我在窗口元素上使用此样式实现了所需的行为:

<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 解决方案是理想的选择(因为除了将复选框的状态保存在用户首选项中之外,这纯粹是 UI 更改)。


我想到了一些其他表示高级元素的方法。其中包括使用动态资源和直接绑定:

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

使用资源字典可能会起作用,但这似乎是一个非常糟糕的解决方案,因为 UI 状态似乎不应该属于字典。手动绑定非常混乱,因为复选框的状态必须以某种方式发送到元素,除了硬编码值之外,我认为它不会变得混乱。

这两种替代解决方案都将语义(“这是一个高级选项”)与外观(“高级选项应该折叠”)联系起来。来自 HTML 世界,我知道这是一件非常糟糕的事情,除非绝对必要,否则我拒绝接受这些方法。

I am creating a settings editor where plugin writers can define their own user interface for configuring their plugins. I am implementing a feature to hide certain "advanced" elements if a checkbox is unchecked.

The checkbox XAML is trivial:

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

Ideally (more on this later), implementors would just add a flag to advanced controls (which should be hidden when the "advanced" checkbox is unchecked) like so:

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

The problem lies in making the magic of hiding the IsAdvanced="True" elements when isAdvanced.IsChecked == false. I have the desired behaviour with this style on the window element:

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

However, this method presents two problems:

  1. It only adds functionality to buttons and nothing else. The IsAdvanced flag can (should be able to) be added to any visual element.
  2. It replaces/overrides the styles which would otherwise be on the button.

Is there some other way to produce the functionality I want? I'm not afraid of working in the code-behind, but an elegant XAML solution is ideal (as this is purely a UI change, aside from saving the state of the checkbox in the user's preferences).


Some other methods of signifying advanced elements have come to mind. These include using a dynamic resource and directly binding:

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

Using a resource dictionary would probably work, but it seems like a really bad solution as UI state doesn't seem like it should belong in a dictionary. Binding manually is quite the mess because the state of the checkbox has to be sent somehow to the element, and aside from hardcoding values I don't see it not becoming a mess.

Both of these alternate solutions tie semantics ("this is an advanced option") to appearance ("advanced options should be collapsed"). Coming from the HTML world, I know this is a very bad thing, and I refuse to submit to these methods unless absolutely necessary.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

痴梦一场 2024-10-11 07:28:54

将其移动到 ViewModel 而不是 XAML 中怎么样,因为这对我来说看起来像是行为。

在我看来,你想要的行为 - 每个插件都会将一堆属性(映射到 UI 控件)注册为高级。有一个全局设置可以打开/关闭高级属性。发生这种情况时,请更新所有插件以显示/隐藏其高级属性

让插件编写者实现一个包含仅设置属性 AreAdvancedControlsVisible 的接口。让他们通过属性更改处理程序在 UI 中隐藏/显示控件。高级 UI 控件可以绑定到pluginVM 上的 ShowAdvancedControls 标志,该标志通过 prop 更改处理程序打开/关闭。
框架可以循环遍历可用的插件,并在设置 ShowAdvanced 复选框时设置此标志。

How about moving this into the ViewModel instead of XAML because this looks like behavior to me.

The behavior you want seems to me - each plugin registers a bunch of properties (mapping to UI Controls) as advanced. There is a global setting to turn on/off advanced properties. When this happens, update all plugins to show/hide their advanced properties

Have plugin writers implement an interface containing a set only property AreAdvancedControlsVisible. Let them take care of hiding/showing the controls in their UI via property change handler. The advanced UI controls can bind to a ShowAdvancedControls flag on the pluginVM, which is toggled on/off from the prop changed handler.
The framework can just loop over the available plugins and set this flag whenever the ShowAdvanced checkbox is set.

马蹄踏│碎落叶 2024-10-11 07:28:54

可能有很多更好的方法来解决这个问题,但我尝试解决您的解决方案中遇到的两个问题。可以在此处下载此小型示例项目。

1.它仅向按钮添加功能,仅此而已。这
IsAdvanced 标志可以(应该可以
to) 添加到任何视觉元素。

添加一个附加属性,使所有子项都继承该值,到最顶层的容器可以解决这个问题。

2.它替换/覆盖原本会出现在
按钮。

Bea Stollnitz 有一篇关于合并样式的精彩博客文章此处
它有一个名为 Merge 的 Style 扩展方法,可以使用。

听起来很简单,但以下问题使代码变得更加复杂。
1. 继承附加属性时,视觉元素没有样式。必需的已加载事件。
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);
    }
}

There are probably alot of better ways to solve this problem but I tried to work past the two issues you had with your solution. Small sample project with this can be downloaded here.

1.It only adds functionality to buttons and nothing else. The
IsAdvanced flag can (should be able
to) be added to any visual element.

Adding an Attached Property, that make all children inherit the value, to the top-most container could fix this.

2.It replaces/overrides the styles which would otherwise be on the
button.

Bea Stollnitz has a nice blog article about merging Styles here.
It has an extension method for Style called Merge which could be used.

Sounded pretty straight forward but the following problems made the code more complex.
1. The Visual elements doesn't have a style when the Attached Property is inherited. Required Loaded event.
2. A Style can't be modified when it is in use. Required a copy method for the Style.

So, we want this Style to be merged with the active Style for all children in the parent container.

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

If the root container is a StackPanel we then add this. The style IsAdvancedStyle will then be inherited by all the children and merged with the active Style.

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

After this the IsAdvancedStyle will be merged in all children of the StackPanel and this goes for children that are added in run-time as well.

Modified Merge extension method from the blog link.

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);
    }
}
注定孤独终老 2024-10-11 07:28:54

我决定把问题稍微反转一下,效果很好。

我没有处理样式,而是按照 Gishu。但是,我没有将 UI 放置在 VM 中(其中属性将手动传播多个层),而是使用名为 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>

放弃样式允许插件编写者根据需要完全更改控件的布局。它们还可以显示高级控件,但如果需要,可以将其禁用。样式带来了很多问题,正如 Melak 所示,解决方法是凌乱

我将“高级”显示逻辑放入虚拟机中的主要问题是,现在不太可能将多个视图绑定到同一虚拟机,同时保持所需的灵活性。如果“高级”逻辑位于虚拟机中,则必须为所有视图或视图显示高级控件;你不能为一个人显示它们而为另一个人隐藏它们。在我看来,这首先打破了拥有虚拟机的原则。

(感谢所有在这里发帖的人;这很有帮助!)

I decided to invert the problem a little bit, and it worked well.

Instead of dealing with styles, I used property binding as suggested by Gishu. However, instead of placing the UI in the VM (where properties would propagate several layers manually), I used an attached property named ShowAdvanced which propagates down via property inheritance.

Creating this property is trivial:

public static readonly DependencyProperty ShowAdvancedProperty;

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

The checkbox sets the ShowAdvanced property above on the entire window. It could set it elsewhere (e.g. on the grid), but putting it on the window makes more sense IMO:

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

Changing the visibility (or whatever other properties desired) depending on the ShowAdvanced property becomes easy:

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

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

Ditching styles allows plugin writers to completely change the layout of their controls if they need to. They can also show advanced controls but keep them disabled if desired. Styles brought up a lot of problems and, as Meleak showed, the workarounds were messy.

My main problem with putting the 'advanced' display logic in the VM is that it is now less likely you can get away with binding multiple views to the same VM while maintaining the flexibility desired. If the 'advanced' logic is in the VM, advanced controls must be shown for all views or no views; you can't show them for one and hide them for another. This, IMO, breaks the principles of having a VM in the first place.

(Thanks to all who posted here; it's been helpful!)

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文