有没有办法通过隐藏面板或扩展器使用虚拟化?
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}}">
我正在尝试提高我的 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}}">