【问题标题】:Dynamic content in xamlxaml 中的动态内容
【发布时间】:2018-01-29 23:49:51
【问题描述】:

如果我想根据条件显示一些东西,那么简单的方法是使用可见性绑定:

<Something Visibility="{Binding ShowSomething, Converter=..." ... />

如果Something 具有复杂的结构(许多子项、绑定、事件、触发器等),仍会创建可视化树,并可能导致性能问题。


更好的方法是通过触发器添加内容:

<ContentControl>
    <ContentControl.Style>
        <Style TargetType="ContentControl">
            <Style.Triggers>
                <DataTrigger Binding="{Binding ShowSomething}" Value="SomeValue">
                    <Setter Property="Content">
                        <Setter.Value>
                            <Something ... />
                        </Setter.Value>
                    </Setter>
                </DataTrigger>
            </Style.Triggers>
        </Style>
    </ContentControl.Style>
</ContentControl>

但那是一场噩梦,同意吗?拥有多个这样的动态部分会污染 xaml 并使其难以导航。

还有其他方法吗?


我尽可能使用数据模板,但是当动态部分仅依赖于属性值时,创建一个专用的Type 并实际定义数据模板太多了。当然,该属性可以重构为一种类型,然后可以使用它自己的数据模板,但是嗯。我真的不想每次都这样做,太多的小类型和在 xaml 中定义的实际数据模板听起来对我来说同样糟糕。

我实际上喜欢第二种方法,但我想改进它,例如通过制作 xaml 扩展或自定义控件。我决定问这个问题是因为:1)我很懒;) 2)我不确定什么是最好的方法 3)我确信其他人(xaml 大师)已经解决了这个问题。

【问题讨论】:

  • 你考虑过使用DataTemplateSelector吗?
  • @AlexSeleznyov,我经常使用它们。它不是可以直接在 xaml 中使用的东西(您需要支持它的容器元素,例如 ItemsControl.ItemTemplateSelector),而且它通常非常具体,无论是 UI 元素类型还是底层数据类型。如果您有答案,在 xaml 中看起来 prettier 并允许将 any 属性绑定到 any 值以显示 any 内容,然后请发布。这就是我的问题所在。理想情况下,我希望在 xaml 中使用 Visibility 的单行方法而没有缺点。
  • "你需要一个支持它的容器元素" - ContentControl 已经做到了。只需设置它的 ContentTemplateSelector。
  • 或者,让你的转换器返回一些东西:&lt;ContentControl Content="{Binding ShowSomething, Converter=...}"/&gt;
  • 顺便说一句,this post 有示例代码,并且比基于DataTrigger 的行数要少得多。

标签: c# wpf xaml dynamic


【解决方案1】:

我能想到的最可重用的解决方案是创建自定义控件并将其内容包装在 ControlTemplate 中,以便在需要时延迟加载。

这是一个示例实现:

[ContentProperty(nameof(Template))]
public class ConditionalContentControl : FrameworkElement
{
    protected override int VisualChildrenCount => Content != null ? 1 : 0;

    protected override Size ArrangeOverride(Size finalSize)
    {
        if (Content != null)
        {
            if (ShowContent)
                Content.Arrange(new Rect(finalSize));
            else
                Content.Arrange(new Rect());
        }
        return finalSize;
    }

    protected override Visual GetVisualChild(int index)
    {
        if (index < 0 || index > VisualChildrenCount - 1)
            throw new ArgumentOutOfRangeException(nameof(index));
        return Content;
    }

    private void LoadContent()
    {
        if (Content == null)
        {
            if (Template != null)
                Content = (UIElement)Template.LoadContent();
            if (Content != null)
            {
                AddLogicalChild(Content);
                AddVisualChild(Content);
            }
        }
    }

    protected override Size MeasureOverride(Size constraint)
    {
        var desiredSize = new Size();
        if (Content != null)
        {
            if (ShowContent)
            {
                Content.Measure(constraint);
                desiredSize = Content.DesiredSize;
            }
            else
                Content.Measure(new Size());
        }
        return desiredSize;
    }

    protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
    {
        base.OnPropertyChanged(e);
        if (e.Property == ShowContentProperty)
        {
            if (ShowContent)
                LoadContent();
        }
        else if (e.Property == TemplateProperty)
        {
            UnloadContent();
            Content = null;
            if (ShowContent)
                LoadContent();
        }
    }

    private void UnloadContent()
    {
        if (Content != null)
        {
            RemoveVisualChild(Content);
            RemoveLogicalChild(Content);
        }
    }

    #region Dependency properties

    private static readonly DependencyPropertyKey ContentPropertyKey = DependencyProperty.RegisterReadOnly(
        nameof(Content),
        typeof(UIElement),
        typeof(ConditionalContentControl),
        new FrameworkPropertyMetadata
        {
            AffectsArrange = true,
            AffectsMeasure = true,
        });
    public static readonly DependencyProperty ContentProperty = ContentPropertyKey.DependencyProperty;
    public static readonly DependencyProperty ShowContentProperty = DependencyProperty.Register(
        nameof(ShowContent),
        typeof(bool),
        typeof(ConditionalContentControl),
        new FrameworkPropertyMetadata
        {
            AffectsArrange = true,
            AffectsMeasure = true,
            DefaultValue = false,
        });
    public static readonly DependencyProperty TemplateProperty = DependencyProperty.Register(
        nameof(Template),
        typeof(ControlTemplate),
        typeof(ConditionalContentControl),
        new PropertyMetadata(null));

    public UIElement Content
    {
        get => (UIElement)GetValue(ContentProperty);
        private set => SetValue(ContentPropertyKey, value);
    }

    public ControlTemplate Template
    {
        get => (ControlTemplate)GetValue(TemplateProperty);
        set => SetValue(TemplateProperty, value);
    }

    public bool ShowContent
    {
        get => (bool)GetValue(ShowContentProperty);
        set => SetValue(ShowContentProperty, value);
    }

    #endregion
}

请注意,此实现在加载内容后不会卸载内容,而只是将其排列为(0,0) 大小。为了在不应该显示的情况下从可视化树中卸载内容,我们需要进行一些修改(此代码示例仅限于修改后的代码):

(...)

    protected override int VisualChildrenCount => ShowContent && Content != null ? 1 : 0;

    protected override Size ArrangeOverride(Size finalSize)
    {
        if (Content != null && ShowContent)
            Content.Arrange(new Rect(finalSize));
        return finalSize;
    }

    protected override Visual GetVisualChild(int index)
    {
        if (index < 0 || index > VisualChildrenCount - 1)
            throw new ArgumentOutOfRangeException(nameof(index));
        return Content;
    }

    private void LoadContent()
    {
        if (Content == null && Template != null)
            Content = (UIElement)Template.LoadContent();
        if (Content != null)
        {
            AddLogicalChild(Content);
            AddVisualChild(Content);
        }
    }

    protected override Size MeasureOverride(Size constraint)
    {
        var desiredSize = new Size();
        if (Content != null && ShowContent)
        {
            Content.Measure(constraint);
            desiredSize = Content.DesiredSize;
        }
        return desiredSize;
    }

    protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
    {
        base.OnPropertyChanged(e);
        if (e.Property == ShowContentProperty)
        {
            if (ShowContent)
                LoadContent();
            else
                UnloadContent();
        }
        else if (e.Property == TemplateProperty)
        {
            UnloadContent();
            Content = null;
            if (ShowContent)
                LoadContent();
        }
    }

(...)

使用示例:

<StackPanel>
    <CheckBox x:Name="CB" Content="Show content" />
    <local:ConditionalContentControl ShowContent="{Binding ElementName=CB, Path=IsChecked}">
        <ControlTemplate>
            <Border Background="Red" Height="200" />
        </ControlTemplate>
    </local:ConditionalContentControl>
</StackPanel>

【讨论】:

  • 相当令人印象深刻。我试图达到同样的效果,但我的尝试在很多方面都更糟(使用ContentControl 作为基础)。我将尝试它,同时有几个问题:你为什么要包含部分而不卸载到答案中(删除它?)?是否可以在 xaml 中以某种方式省略使用 ControlTemplate 标记(减去 2 行)?我不确定,但例如Popup 不会(是吗?)加载它的内容,你不必在那里写ControlTemplate(虽然我可能错了,必须测试它)。
  • 1.我把它扔进去是因为卸载和重新加载内容到可视树中也可能是昂贵的操作,所以你有一个替代方案。 2. 如果您不介意在解析 XAML 时实例化内容并且只是不想推迟将其加载到可视化树中,那么请确保它是可能的(而且非常简单 - 我会发布它作为另一个答案)。否则会很棘手(需要创建自定义延迟加载器以与XamlDeferLoadAttribute 一起使用),但我认为它仍然是可能的。
  • 我测试了Popup,我错了它会加载所有内容。如果可能的话,没有ControlTemplate 会好得多。
【解决方案2】:

如果您不介意在解析 XAML 时实例化的内容,并且只想将其保留在可视化树之外,那么这里有一个实现此目标的控件:

[ContentProperty(nameof(Content))]
public class ConditionalContentControl : FrameworkElement
{
    private UIElement _Content;
    public UIElement Content
    {
        get => _Content;
        set
        {
            if (ReferenceEquals(value, _Content)) return;
            UnloadContent();
            _Content = value;
            if (ShowContent)
                LoadContent();
        }
    }

    protected override int VisualChildrenCount => ShowContent && Content != null ? 1 : 0;

    protected override Size ArrangeOverride(Size finalSize)
    {
        if (Content != null && ShowContent)
            Content.Arrange(new Rect(finalSize));
        return finalSize;
    }

    protected override Visual GetVisualChild(int index)
    {
        if (index < 0 || index > VisualChildrenCount - 1)
            throw new ArgumentOutOfRangeException(nameof(index));
        return Content;
    }

    private void LoadContent()
    {
        if (Content != null)
        {
            AddLogicalChild(Content);
            AddVisualChild(Content);
        }
    }

    protected override Size MeasureOverride(Size constraint)
    {
        var desiredSize = new Size();
        if (Content != null && ShowContent)
        {
            Content.Measure(constraint);
            desiredSize = Content.DesiredSize;
        }
        return desiredSize;
    }

    protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
    {
        base.OnPropertyChanged(e);
        if (e.Property == ShowContentProperty)
        {
            if (ShowContent)
                LoadContent();
            else
                UnloadContent();
        }
    }

    private void UnloadContent()
    {
        if (Content != null)
        {
            RemoveVisualChild(Content);
            RemoveLogicalChild(Content);
        }
    }

    #region Dependency properties

    public static readonly DependencyProperty ShowContentProperty = DependencyProperty.Register(
        nameof(ShowContent),
        typeof(bool),
        typeof(ConditionalContentControl),
        new FrameworkPropertyMetadata
        {
            AffectsArrange = true,
            AffectsMeasure = true,
            DefaultValue = false,
        });

    public bool ShowContent
    {
        get => (bool)GetValue(ShowContentProperty);
        set => SetValue(ShowContentProperty, value);
    }

    #endregion
}

用法:

<StackPanel>
    <CheckBox x:Name="CB" Content="Show content" />
    <local:ConditionalContentControl ShowContent="{Binding ElementName=CB, Path=IsChecked}">
        <Border Background="Red" Height="200" />
    </local:ConditionalContentControl>
</StackPanel>

请注意,尽管这种方法有其缺点,例如如果内容没有立即加载,与相对来源的绑定将报告错误。

【讨论】:

  • 我个人不想要。那么绑定到Visibility 有什么区别呢?但也许有人会觉得它有用,谢谢。
  • 不同之处在于,如果将Visibility 设置为Collapsed,则该元素尽管不可见,但仍然是可视化树的一部分。
  • 如果我对项目列表使用动态内容,那么“加载”是非常不需要的。我更喜欢延迟(延迟)加载,这是使用数据触发器的主要原因。视觉树中的存在并不是解释问题的最佳标准。
【解决方案3】:

我决定发布我的尝试作为答案:

public class DynamicContent : ContentControl
{
    public bool ShowContent
    {
        get { return (bool)GetValue(ShowContentProperty); }
        set { SetValue(ShowContentProperty, value); }
    }
    public static readonly DependencyProperty ShowContentProperty =
        DependencyProperty.Register("ShowContent", typeof(bool), typeof(DynamicContent),
        new PropertyMetadata(false,
            (sender, e) => ((DynamicContent)sender).ChangeContent((bool)e.NewValue)));

    protected override void OnContentChanged(object oldContent, object newContent)
    {
        base.OnContentChanged(oldContent, newContent);
        ChangeContent(ShowContent);
    }

    void ChangeContent(bool show) => Template = show ? (ControlTemplate)Content : null;
}

它简短、清晰(是吗?)并且有效。

想法是使用ContentControl.Content 来指定控件模板并在ShowContentContent(以支持设计时间)值发生更改时将控件Template 更改为显示/隐藏 .

测试示例(包括相对绑定和按名称绑定):

<StackPanel Tag="Test">
    <CheckBox x:Name="comboBox"
              Content="Show something"
              IsChecked="{Binding ShowSomething}" />
    <local:DynamicContent ShowContent="{Binding IsChecked, ElementName=comboBox}">
        <ControlTemplate>
            <local:MyCheckBox IsChecked="{Binding IsChecked, ElementName=comboBox}"
                      Content="{Binding Tag, RelativeSource={RelativeSource AncestorType=StackPanel}}" />
        </ControlTemplate>
    </local:DynamicContent>
</StackPanel>

查看延迟的内容:

public class MyCheckBox : CheckBox
{
    public MyCheckBox()
    {
        Debug.WriteLine("MyCheckBox is constructed");
    }
}

【讨论】:

    猜你喜欢
    • 2016-06-11
    • 2012-12-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-11-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多