有没有办法通过隐藏面板或扩展器使用虚拟化?

Is there a way of using virtualization with hidden panels or expanders?

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

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

如您所见,有一些嵌套的 ItemsControl。所以每个顶级项目都有一堆隐藏控件。虚拟化可防止加载屏幕外的项目,但不会阻止项目本身内的隐藏项目。因此,相对简单的初始布局需要花费大量时间。翻阅其中一些view,87%的时间花在解析和Layout上,加载需要几秒

我宁愿在用户决定(如果!)时需要 200 毫秒来展开,而不是 2 秒来加载整个页面。

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

87% 的数字来自诊断:

如果你有

- 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 之外什么都没有加载。一旦它被扩展(通过用户或以编程方式),视图就会创建视觉效果。

我想我应该把解决方案的细节放在一起,这几乎是 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>

最后,与其为子项目使用全新的 class,不如在您的视图模型中创建一个引用 "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%,现在看起来更合理了。

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

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

此处 "ExpandSite" 是 Expander 控件的默认 ControlTemplate 中的 ContentPresenter。 请注意,这已在 .NET 中得到修复 - 请参阅 github.

上 WPF 源的默认样式

如果您有旧版本,您仍然可以使用此固定控件模板以隐式样式更新旧版本。

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

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

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

<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}}">