简体   繁体   English

WPF样式取决于复选框状态

[英]WPF style depending on checkbox state

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: XAML复选框很简单:

<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 . 问题在于,在isAdvanced.IsChecked == false时隐藏IsAdvanced="True"元素具有isAdvanced.IsChecked == false I have the desired behaviour with this style on the window element: 我在window元素上具有此样式的预期行为:

<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. 可以(应该)将IsAdvanced标志添加到任何视觉元素。
  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). 我不担心在背后的代码中工作,但是一个优雅的XAML解决方案是理想的(除了在用户首选项中保存复选框的状态之外,这纯粹是一个UI更改)。


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. 使用资源字典可能会起作用,但是似乎很糟糕,因为UI状态似乎不应该属于字典。 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. 来自HTML世界,我知道这是一件非常不好的事情,除非绝对必要,否则我拒绝提交这些方法。

How about moving this into the ViewModel instead of XAML because this looks like behavior to me. 将其移到ViewModel中而不是XAML中如何操作,因为在我看来这就像行为。

The behavior you want seems to me - each plugin registers a bunch of properties (mapping to UI Controls) as advanced. 在我看来,您想要的行为-每个插件都将一堆属性(映射到UI控件)注册为高级。 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. 让插件编写者实现仅包含一组属性AreAdvancedControlsVisible的接口。 Let them take care of hiding/showing the controls in their UI via property change handler. 让他们负责通过属性更改处理程序在其UI中隐藏/显示控件。 The advanced UI controls can bind to a ShowAdvancedControls flag on the pluginVM, which is toggled on/off from the prop changed handler. 高级UI控件可以绑定到pluginVM上的ShowAdvancedControls标志,该标志可从prop更改的处理程序打开/关闭。 The framework can just loop over the available plugins and set this flag whenever the ShowAdvanced checkbox is set. 只要设置了ShowAdvanced复选框,该框架就可以循环可用的插件并设置此标志。

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. 1.它只为按钮添加了功能,没有别的。 The IsAdvanced flag can (should be able to) be added to any visual element. 可以(应该)将IsAdvanced标志添加到任何视觉元素。

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. 2.它替换/替代原本在按钮上的样式。

Bea Stollnitz has a nice blog article about merging Styles here . Bea Stollnitz 在这里有一篇不错的关于合并样式的博客文章。
It has an extension method for Style called Merge which could be used. 它有一种称为Merge的样式扩展方法,可以使用。

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. 1.继承附加属性时,Visual元素没有样式。 Required Loaded event. 必需的已加载事件。
2. A Style can't be modified when it is in use. 2.样式在使用时无法修改。 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. 如果根容器是StackPanel,则添加它。 The style IsAdvancedStyle will then be inherited by all the children and merged with the active Style. 然后,样式IsAdvancedStyle将被所有子级继承,并与活动样式合并。

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

StyleChildsBehavior.cs 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. 此后,IsAdvancedStyle将合并到StackPanel的所有子级中,这也适用于在运行时添加的子级。

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

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 . 我不使用样式,而是使用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. 但是,我没有将UI放置在VM中(属性将在其中手动传播多个层),而是使用了一个名为ShowAdvanced的附加属性,该属性通过属性继承向下传播。

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. 复选框在整个窗口上方设置了ShowAdvanced属性。 It could set it elsewhere (eg on the grid), but putting it on the window makes more sense IMO: 它可以将其设置在其他位置(例如在网格上),但是将其放在窗口上更符合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: 根据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>

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 . 样式带来了很多问题,而且正如Meleak所展示的那样, 解决方法很杂乱

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. 我在VM中放置“高级”显示逻辑的主要问题是,现在不再可能将多个视图绑定到同一个VM,同时又保持所需的灵活性。 If the 'advanced' logic is in the VM, advanced controls must be shown for all views or no views; 如果VM中存在“高级”逻辑,则必须显示所有视图的高级控件或不显示视图。 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. IMO违反了首先拥有VM的原则。

(Thanks to all who posted here; it's been helpful!) (感谢所有在此发布的人;它非常有帮助!)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM