WPF中非递归(无后台代码)动态实现TreeView

wpf,中非,递归,后台,代码,动态,实现,treeview · 浏览次数 : 517

小编点评

**XAML代码:** ```xml ``` **代码解析:** * `HierarchicalDataTemplate` 是一个包含子数据模板的层次数据模板。 * `DataType` 属性指定数据类型。 * `ItemsSource` 属性指定数据源。 * `HierarchicalDataTemplate` 中包含一个 `TextBlock` 元素,用于显示学校名称。 **性能分析:** * 使用 `HierarchicalDataTemplate` 可以节省内存和性能。 * 因为 `HierarchicalDataTemplate` 使用虚拟化,只创建可见的 UI 元素。 * 由于数据量是 100w,实际渲染时间约为 1.5s。 * 虚拟化使 TreeView 只创建 41 个 UI 元素,而不是创建 20 个元素。 **测试结果:** * 模拟 100w 数据量时,渲染时间为 1.5s,内存增加了 160M左右。 * 数据渲染不到 1s,内存增加 20M左右。

正文

在UI界面中,树形视图是比较常用的表示层级结构的方式,WPF中提供了TreeView控件。对于TreeView控件的基本使用已经有很多文章。大都是介绍如何在XAML中使用硬编码的固定信息填充Treeview控件,或者是后台代码递归遍历数据源,动态创建TreeView。这里我想介绍一下如何只通过XAML标记,不用一行后台代码遍历数据实现TreeView。

技术要点与实现

本文的技术关键点是层级式数据模板HierarchicalDataTemplateHierarchicalDataTemplate是一个特殊的DataTemplate,它能够包装第二层模板。通过ItemsSource属性查找下一层级的数据集合,并将它提供给第二层模板。这样描述可能有点晦涩。接下来举例进行描述。

首先假设一个应用场景。用树形结构展现一个地区所有的学校->年级->班级->学生。首先定义几个Model

public class School : ObservableObject
{
    private bool _isOpen;
    /// <summary>
    /// 获取或设置是否展开
    /// </summary>
    [System.Xml.Serialization.XmlIgnore]
    public bool IsOpen { get { return _isOpen; } set { Set(ref _isOpen, value); } }

    private bool _isSelected;
    /// <summary>
    /// 获取或设置是否被选中
    /// </summary>
    [System.Xml.Serialization.XmlIgnore]
    public bool IsSelected { get { return _isSelected; } set { Set(ref _isSelected, value); } }
    
    public string SchoolID { get; set; }
    public string SchoolName { get; set; }
    public ObservableCollection<Grade> listGrade { get; set; }=new ObservableCollection<Grade>() { };
}

public class Grade : ObservableObject
{
    private bool _isOpen;
    [System.Xml.Serialization.XmlIgnore]
    public bool IsOpen { get { return _isOpen; } set { Set(ref _isOpen, value); } }

    private bool _isSelected;
    [System.Xml.Serialization.XmlIgnore]
    public bool IsSelected { get { return _isSelected; } set { Set(ref _isSelected, value); } }
    
    public string GradeID { get; set; }
    public string GradeName { get; set; }
    public ObservableCollection<ClassInfo> ListClass { get; set; }=new ObservableCollection<ClassInfo>() { };
}

public class ClassInfo : ObservableObject
{
    private bool _isOpen;
    [System.Xml.Serialization.XmlIgnore]
    public bool IsOpen { get { return _isOpen; } set { Set(ref _isOpen, value); } }

    private bool _isSelected;
    [System.Xml.Serialization.XmlIgnore]
    public bool IsSelected { get { return _isSelected; } set { Set(ref _isSelected, value); } }

    public string ClassID { get; set; }
    public string ClassName { get; set; }
    public ObservableCollection<Student> Students { get; set; }= new ObservableCollection<Student>() { };

}

public class Student : ObservableObject
{
    private bool _isSelected;
    [System.Xml.Serialization.XmlIgnore]
    public bool IsSelected { get { return _isSelected; } set { Set(ref _isSelected, value); } }

    public string Id { get; set; }
    public string Name { get; set; }
}

接下来根据定义好的Model定义层级式数据模板HierarchicalDataTemplate

<HierarchicalDataTemplate DataType="{x:Type local:School}" ItemsSource="{Binding Path=listGrade}">
    <TextBlock Text="{Binding Path=SchoolName}" />
</HierarchicalDataTemplate>

<HierarchicalDataTemplate DataType="{x:Type local:Grade}" ItemsSource="{Binding Path=ListClass}">
    <TextBlock Text="{Binding Path=GradeName}" />
</HierarchicalDataTemplate>

<HierarchicalDataTemplate DataType="{x:Type local:ClassInfo}" ItemsSource="{Binding Path=Students}">
    <TextBlock Text="{Binding Path=ClassName}" />
</HierarchicalDataTemplate>

<HierarchicalDataTemplate DataType="{x:Type local:Student}">
    <CheckBox Command="{Binding SelectChangeCommand, ElementName=self}" CommandParameter="{Binding}" IsChecked="{Binding IsSelected}">
        <TextBlock Text="{Binding Path=Name}" />
    </CheckBox>
</HierarchicalDataTemplate>

其中最外层数据类型是School,它的下一层数据集合是ObservableCollection<Grade> listGrade,因此HierarchicalDataTemplate中的ItemsSource赋值为listGrade,这里我们再属性控件中只显示学校的名称,因此数据模板只是包含绑定了学校名称SchoolNameTextBlock,如果需要显示其他信息(比如学校年级数量或者学校图标),只需增加相应XAML元素即可。紧接着按照这个方式定义好数据类型Grade,ClassInfo,Student的层级式数据模板即可。
定义好了数据模型和相应的层级式数据模板HierarchicalDataTemplate后,就可以直接把数据元绑定到TreeView上了。假设要绑定的数据源实例是ObservableCollection<School> schools。只需如下调用即可。

<TreeView MaxHeight="480"
            ItemsSource="{Binding schools}"
            VirtualizingPanel.IsVirtualizing="True"
            VirtualizingPanel.VirtualizationMode="Recycling" />

这样使用TreeView是不是特别方便简洁。不用为了展示树形结构,特地定义一个递归类型的数据结构,UI展示全部交给XAML就行。JSON数据反序列化后直接绑定即可(XML或者DateSet也是类似的方法)。避免了递归遍历数据源的操作,也不用考虑递归带来的性能问题。

性能

前边提到不用考虑递归带来的性能问题。那本文介绍的方法对于大量数据的情况下性能到底怎样呢?接下来做一个测试,模拟100W的数据量,具体为240个学校,每个学校3个年级,每个年级20个班,每个班70个学生,总共数据量是240x3x20x70=1008000个。以下是测试结果:
image

从图中可以看到模拟100w数据耗时1.5s,内存增加了160M左右,数据渲染到界面不到1s,内存增加20M左右。结果还是令人满意的。这是因为TreeView支持开启虚拟化(默认是关闭的,设置 VirtualizingPanel.IsVirtualizing="True"开启虚拟化),渲染界面是不会一次把所有UI元素全部创建好,而是根据屏幕上可见区域计算需要渲染的元素个数,创建少量的UI元素,从而减少内存和CPU资源的使用。例如本例中有100w条数据,可见区能显示20条,TreeView只创建了41个UI元素。为什么不是创建20个呢?这是由于为了确保良好的滚动性能,实际会多创建一些UI元素。

TreeView 默认关闭虚拟化,是因为早期的WPF发布版本中的VirtualizingStackPanel不支持层次化数据,虽然现在已支持,但是TreeView默认关闭虚拟化确保兼容性。

与WPF中非递归(无后台代码)动态实现TreeView相似的内容:

WPF中非递归(无后台代码)动态实现TreeView

WPF中提供了TreeView控件,对于TreeView控件的基本使用已经有很多文章。大都是介绍如何在后台代码递归遍历数据源,动态创建TreeView。这里我想介绍一下如何只通过XAML标记,不用一行后台代码遍历数据实现TreeView。

WPF中以MVVM方式,实现RTSP视频播放

前言视频播放在上位机开发中经常会遇到,基本上是两种常见的解决方案 1.采用厂家提供的sdk和前端控件进行展示,常见的海康/大华都提供了相关sdk及文档 2.开启相机onvif协议,捅过rtsp视频流进行播放,前端可以采用web方式,或者wpf中的视频控件进行展示。 项目需求,决定了最终采用开启相机o

在WPF中使用着色器

概念类比 范畴 CPU GPU 二进制文件 .exe .cso / .ps 二进制指令 机器码 CSO(shader指令) 助记符 汇编 SL 高级语言 C# HLSL 高级语言文件 .cs .hlsl / .fx 高级语言编译器 csc.exe fxc.exe API .NET API Direc

在WPF中使用WriteableBitmap对接工业相机及常用操作

写作背景 写这篇文章主要是因为工业相机(海康、大恒等)提供的.NET开发文档和示例程序都是用WinForm项目来说明举例的,而在WPF项目中对图像的使用和处理与在WinForm项目中有很大不同。在WinForm中用System.Drawing.Bitmap来处理图像,而在WPF中是用System.W

在 WPF 中集成 ASP.NET Core 和 WebView2 用于集成 SPA 应用

背景 我们有些工具在 Web 版中已经有了很好的实践,而在 WPF 中重新开发也是一种费时费力的操作,那么直接集成则是最省事省力的方法了。 修改项目文件 我们首先修改项目文件,让 WPF 项目可以包含 ASP.NET Core 的库,以及引用 WebView2 控件。

[WPF]浅析依赖属性(DependencyProperty)

在WPF中,引入了依赖属性这个概念,提到依赖属性时通常都会说依赖属性能节省实例对内存的开销。此外依赖属性还有两大优势。 支持多属性值,依赖属性系统可以储存多个值,配合Expression、Style、Animation等可以给我们带来很强的开发体验。 加入了属性变化通知,限制、验证等功能。方便我们使

Avalonia中的线性渐变画刷LinearGradientBrush

在WPF中使用Shape实现复杂线条动画后,尝试在Avalonia中也实现同样效果。尽管官方提供了从WPF到Avalonia的快速入门文档,但由于第一次使用Avalonia,体验过程中并不是很顺利,主要是卡在线性渐变画刷LinearGradientBrush的使用上。Avalonia中的线性渐变画刷

WPF/C#:实现导航功能

前言 在WPF中使用导航功能可以使用Frame控件,这是比较基础的一种方法。前几天分享了wpfui中NavigationView的基本用法,但是如果真正在项目中使用起来,基础的用法是无法满足的。今天通过wpfui中的mvvm例子来说明在wpfui中如何通过依赖注入与MVVM模式使用导航功能。实践起来

WPF/C#:在WPF中如何实现依赖注入

本文先介绍依赖注入的概念,再解释为什么要进行依赖注入,最后通过 WPF Gallery 这个项目学习如何在WPF中使用依赖注入。

WPF实现Element UI风格的日期时间选择器

### 背景 业务开发过程中遇到一个日期范围选择的需求,和Element UI的DateTimePicker组件比较类似,由两个日历控件组成,联动选择起始时间和结束时间。 ### 问题 WPF中提供了一个`DatePicker`的控件,主要由`DatePickerTextBox`、`Button`和