【问题标题】:Is there a way of using virtualization with hidden panels or expanders?有没有办法通过隐藏面板或扩展器使用虚拟化?
【发布时间】:2016-06-29 11:04:19
【问题描述】:

我正在尝试提高我的 WPF 应用程序的性能,但我在使用复杂的 ItemsControl 时遇到了问题。虽然我添加了虚拟化,但仍然存在性能问题,我想我已经找到了原因。

每个项目都包含一系列可扩展区域。因此,用户在开始时会看到摘要,但可以通过展开向下钻取以查看更多信息。下面是它的外观:

如您所见,有一些嵌套的 ItemsControl。所以每个顶级项目都有一堆隐藏控件。虚拟化可防止加载屏幕外项目,但不会阻止项目本身内的隐藏项目。结果,相对简单的初始布局需要大量时间。浏览其中一些视图,87% 的时间用于解析和布局,加载需要几秒钟。

当(如果!)用户决定时,我更希望它需要 200 毫秒来展开,而不是 2 秒来加载整个页面。

真心求教。但是,我想不出使用 MVVM 添加控件的好方法。 WPF 中是否支持任何扩展器或基于可见性的虚拟化,或者我会创建自己的实现?

87% 的数字来自诊断:

【问题讨论】:

  • 什么是"隐藏在项目本身中的项目"?虚拟化不会创建ContentPresenter,但在某个地方你有ObservableCollection<>,它必须完全加载。那么,2s 包括什么?您是否进行了分析以了解真正的瓶颈是什么?
  • 您想虚拟化位于Expander 内的部分项目(对于创建的项目)?如果是,那么您可以在 ViewModel 中处理折叠状态,以便在展开时添加更多数据(例如,使用 object 保存该数据,使用数据模板在不是 null 时创建一些视觉效果) .
  • 这就是布局的方式。您可以延迟获取数据,直到它打开。我很惊讶这需要 2 秒。
  • @ContentPresenter, hidden items = Visibility.Collapsed,显示时设置为可见。这是一个非常好的主意,我可以在可见性状态发生变化时设置可观察的集合,这将非常容易。我会看看它是如何管理的。
  • 嗯,我完全移除了扩展器,我面临着相似的加载时间。我添加了一个编辑

标签: c# wpf performance virtualization


【解决方案1】:

如果你有

- Expander
      Container
          some bindings
    - Expander
          Container
              some bindings
+ Expander
+ Expander
... invisible items

那么是的,Container 和所有绑定都在视图显示时初始化(ItemsControl 为可见项创建 ContentPresenter)。

如果您想在折叠时虚拟化Expander 的内容,那么您可以使用数据模板

public ObservableCollection<Item> Items = ... // bind ItemsControl.ItemsSource to this

class Item : INotifyPropertyChanged
{
    bool _isExpanded;
    public bool IsExpanded // bind Expander.IsExpanded to this
    {
        get { return _isExpanded; }
        set
        {
            Data = value ? new SubItem(this) : null;
            OnPropertyChanged(nameof(Data));
        }
    }

    public object Data {get; private set;} // bind item Content to this
}

public SubItem: INotifyPropertyChanged { ... }

希望不用解释如何在xaml中对SubItem进行数据模板化。

如果你这样做,那么最初Data == null 并且除了Expander 之外什么都没有加载。一旦它被扩展(由用户或以编程方式)视图将创建视觉效果。

【讨论】:

  • 使用模板是个好主意,我已经按照您的描述实现了它,并且性能得到了很大改善。谢谢!
【解决方案2】:

我想我会给出解决方案的细节,这几乎是 Sinatr 答案的直接实现。

我使用了一个内容控件,带有一个非常简单的数据模板选择器。模板选择器只是检查内容项是否为空,并在两个数据模板之间进行选择:

public class VirtualizationNullTemplateSelector : DataTemplateSelector
{
    public DataTemplate NullTemplate { get; set; }
    public DataTemplate Template { get; set; }

    public override DataTemplate SelectTemplate(object item, DependencyObject container)
    {
        if (item == null)
        {
            return NullTemplate;
        }
        else
        {
            return Template;

        }
    }
}

原因是我使用的 ContentControl 即使内容为空,仍然会布局数据模板。所以我在xaml中设置了这两个模板:

            <ContentControl Content="{Binding VirtualizedViewModel}"  Grid.Row="1" Grid.ColumnSpan="2" ><!--Visibility="{Binding Expanded}"-->
                <ContentControl.Resources>
                    <DataTemplate x:Key="Template">
                        <StackPanel>
                            ...complex layout that isn't often seen...
                        </StackPanel>
                    </DataTemplate>
                    <DataTemplate x:Key="NullTemplate"/>
                </ContentControl.Resources>
                <ContentControl.ContentTemplateSelector>
                    <Helpers:VirtualizationNullTemplateSelector Template="{StaticResource Template}" NullTemplate="{StaticResource NullTemplate}"/>
                </ContentControl.ContentTemplateSelector>
            </ContentControl>

最后,与其对子项使用全新的类,不如在您的视图模型中创建一个引用“this”的“VirtualizedViewModel”对象:

    private bool expanded;
    public bool Expanded
    {
        get { return expanded; }
        set
        {
            if (expanded != value)
            {
                expanded = value;
                NotifyOfPropertyChange(() => VirtualizedViewModel);
                NotifyOfPropertyChange(() => Expanded);
            }
        }
    }


    public MyViewModel VirtualizedViewModel
    {
        get
        {
            if (Expanded)
            {
                return this;
            }
            else
            {
                return null;
            }
        }
    }

我将 2-3 秒的加载时间减少了大约 75%,现在看起来更合理了。

【讨论】:

    【解决方案3】:

    这个简单的解决方案帮助了我:

    <Expander x:Name="exp1">
       <Expander.Header>
         ...
       </Expander.Header>
       <StackPanel
          Margin="10,0,0,0"
          Visibility="{Binding ElementName=exp1, Path=IsExpanded, Converter={StaticResource BooleanToVisibilityConverter}}">
          <Expander x:Name="exp2">
            <Expander.Header>
              ...
            </Expander.Header>
            <StackPanel
              Margin="10,0,0,0"
              Visibility="{Binding ElementName=exp2, Path=IsExpanded, Converter={StaticResource BooleanToVisibilityConverter}}">
    

    【讨论】:

    • 这个解决方案非常简单快捷,似乎将扩展器的内容可见性绑定到它的扩展属性会极大地影响渲染。唯一的问题是折叠项目不再是动画而是即时的
    【解决方案4】:

    实现此目的的更简单方法是将内容的默认可见性更改为已折叠。在这种情况下,WPF 最初不会创建它,但只有在 Trigger 将其设置为 Visible 时才会创建它:

    <Trigger Property="IsExpanded" Value="true">
      <Setter Property="Visibility"Value="Visible" TargetName="ExpandSite"/>
    </Trigger>
    

    这里的“ExpandSite”是 Expander 控件的默认 ControlTemplate 中的 ContentPresenter。 请注意,这已在 .NET 中得到修复 - 请参阅 github 上 WPF 源中的默认样式。

    如果你有旧版本,你仍然可以使用这个固定的控件模板来更新旧的隐式样式。

    您可以将相同的技术应用于任何其他面板或控件。

    很容易检查控件是否已使用Snoop 创建。将其附加到应用程序后,您可以使用左上角的文本框过滤可视化树。如果在树中没有找到一个控件,则表示它尚未创建。

    【讨论】:

    • 我已经尝试过遵循这个但无法使其工作,我认为更完整的示例会有所帮助。 Style 触发器上不允许使用“TargetName”,Triggers 集合的成员必须是没有“Property”的 EventTriggers。我得到的两个错误。
    • 查看来自 github 的链接源。您可以看到在 ControlTemplate.Triggers 中使用了 TargetName="ExpandSite"。实现延迟加载的最简单方法是更新到较新的 .NET Framework 版本。如果这不是一个选项,您可以使用 WPF Style Snooper 之类的工具从您的 .NET 版本中获取默认的 Expander 样式,使用此触发器对其进行扩展,并使用您在应用程序中修改的样式覆盖默认样式。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2022-11-25
    • 2014-01-18
    • 2019-08-20
    • 1970-01-01
    • 1970-01-01
    • 2013-06-22
    • 1970-01-01
    相关资源
    最近更新 更多