1

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

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


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

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

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

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

4

3 回答 3

0

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

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

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

于 2010-11-25T08:09:25.617 回答
0

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

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

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

2.它替换/覆盖按钮上的样式。

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

听起来很简单,但以下问题使代码更加复杂。
1. 继承 Attached Property 时,Visual 元素没有样式。必需的加载事件。
2. Style 在使用中是不能修改的。需要样式的复制方法。

因此,我们希望此样式与父容器中所有子容器的活动样式合并。

<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 的所有子项中,这也适用于在运行时添加的子项。

修改了博客链接中的 Merge 扩展方法。

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);
    }
}
于 2010-11-26T19:56:21.087 回答
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>

抛弃样式允许插件编写者在需要时完全更改其控件的布局。他们还可以显示高级控件,但如果需要,可以将其禁用。样式带来了很多问题,正如 Meleak 所展示的,解决方法很混乱

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

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

于 2010-11-28T15:29:11.920 回答