WPF样式取决于复选框状态

时间:2010-11-25 07:54:00

标签: wpf xaml styles wpf-controls multidatatrigger

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

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. 它取代/覆盖了按钮上的样式。
  3. 是否有其他方法可以生成我想要的功能?我并不害怕在代码隐藏中工作,但优雅的XAML解决方案是理想的(因为除了在用户首选项中保存复选框的状态之外,这纯粹是UI更改)。


    其他一些表示高级元素的方法已经浮现在脑海中。这些包括使用动态资源和直接绑定:

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

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

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

3 个答案:

答案 0 :(得分:0)

如何将其移入ViewModel而不是XAML,因为这看起来像是对我的行为。

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

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

答案 1 :(得分:0)

可能有很多更好的方法可以解决这个问题,但我试图解决您对解决方案的两个问题。可以下载here的小样本项目。

  

1.它只为按钮添加功能而没有别的。该   IsAdvanced标志可以(应该可以   to)被添加到任何视觉元素。

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

  

2.它取代/覆盖否则将会出现的样式   按钮。

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

听起来非常直接,但以下问题使代码更加复杂  1.继承附加属性时,Visual元素没有样式。必需的加载事件。
 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);
    }
}

答案 2 :(得分:0)

我决定稍微改变一下这个问题,而且效果很好。

我使用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>

Ditching样式允许插件编写者在需要时完全更改其控件的布局。它们还可以显示高级控件,但如果需要,可以将它们禁用。样式带来了很多问题,正如Meleak所示,workarounds were messy

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

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