65.9K
CodeProject 正在变化。 阅读更多。
Home

Avalonia DataGrid - NP.Ava.Visuals 包带来的高级功能

starIconstarIconstarIconstarIconstarIcon

5.00/5 (13投票s)

2022年4月15日

CPOL

7分钟阅读

viewsIcon

32430

解释新增代码,其目的是为 Avalonia DataGrid 添加过滤、布局保存/恢复和列可见性功能

引言

请注意,本文和示例代码均已更新,以兼容最新版本的Avalonia - 11.0.6

Avalonia 是一个优秀的跨平台开源UI框架,用于开发

  • 可在Windows、Mac和Linux上运行的桌面解决方案
  • 在浏览器中运行的Web应用程序(通过WebAssembly)
  • 适用于Android、iOS和Tizen的移动应用程序。

我在 CodeProject 上广泛撰写了关于 Avalonia 的文章,并开发了多个基于 Avalonia 的包/框架,这些内容也在我的 CodeProject 文章中有描述。

Avalonia 近期才变得生产就绪,因此 Telerik、DevExpress 或 Infragistics 等主要的第三方组件提供商尚未为 Avalonia 发布组件。

根据我的经验,大型提供商(如 Telerik)的组件中,只需要两个:窗口对接功能和数据网格。其他所有内容——自定义按钮、框等——都可以(而且应该)由团队轻松地使用 WPF 或 Avalonia 的基本元素构建,以满足 UX 要求。

Avalonia 中缺乏对接框架的功能由我的UniDock 包进行了弥补。

Avalonia 中已经存在 DataGrid。它在很大程度上是内置 WPF DataGrid 的移植。不幸的是,与 WPF 版本一样,Avalonia DataGrid 缺少一些重要功能,包括

  1. 过滤
  2. 更改网格列的可见性
  3. 保存/恢复布局
  4. 分组

除了分组(在我待办列表中,应该很快会添加)之外,我已经为内置的 Avalonia DataGrid 添加了所有上述功能。所有这些功能都已添加到我的NP.Ava.Visuals 库/包中。

本文介绍了一个演示如何使用这些高级功能的示例。

演示代码

演示代码位于 NP.Ava.Demos 仓库中,位于 NP.Demos.VisualSamples/NP.Demos.AdvancedDataGridDemo 项目下。

要获取项目,请克隆仓库并找到 NP.Demos.VisualSamples/NP.Demos.AdvancedDataGridDemo 文件夹,然后打开 NP.Demos.AdvancedDataGridDemo.sln 解决方案(您需要使用 Visual Studio 2022 来完成此操作)。

构建解决方案,确保所有需要的 nuget 包都已被 Visual Studio 下载。

如果您打开解决方案中唯一项目的依赖项的包区域,您将只看到两个项目

其余的 Avalonia 引用都来自 NP.Ava.Visuals 依赖项。

运行解决方案;这将弹出以下窗口

红色矩形框包含文本过滤器。最后一个名为“Cost”的列的过滤器已禁用。

尝试在过滤器的文本框中输入一些 string。您会看到只有包含过滤文本的行才会显示——其余行将变得不可见。

上图显示了其“产品名称”中包含“b”且“产品描述”中包含“nic”的记录。

现在右键单击其中一列(例如制造商),然后选择“移除列”菜单项。

点击后,该列(制造商)将变得不可见。

现在点击顶部的“列可见性设置器”按钮,在打开的下拉列表中,单击制造商列旁边的 checkbutton 即可使其重新可见。

现在通过拖动某些列到其他位置来更改各种列的宽度并更改它们的顺序,例如:

通过按下“保存网格布局”按钮来保存网格布局。

重新启动应用程序,然后按下“恢复网格布局”按钮。保存的布局将被恢复。

演示代码

在演示中,我们定义了一个简单的类 Product

public class Product
{
    public string? Name { get; }

    public string? Description { get; }

    public string? Manufacturer { get; }

    public double? Cost { get; }


    public Product(string? name, string? description, 
                   string? manufacturer, double? cost)
    {
        Name = name;
        Description = description;
        Manufacturer = manufacturer;
        Cost = cost;
    }
}  

以及一个预定义的 the products 集合 - DemoProducts

public class DemoProducts : ObservableCollection<product>
{
    private void AddProduct(string? name, string? description, 
                            string? manufacturer, double? cost)
    {
        this.Add(new Product(name, description, manufacturer, cost));
    }

    public DemoProducts()
    {
        AddProduct("Batmobile", "Nice and comfortable tank that 
                    can jump between rooftops", "Wayne Enterprises", 10000000);
        AddProduct("Instant Tunnel", "Allows a cartoon character to escape", 
                   "ACME Corp", 20000);
        AddProduct("Brains for Scarecrow", "Provides any bright scarecrow 
                    with intellectual confidence", "OZ Production", 50);
        AddProduct("UniDock", "Multiplatform Window Docking Package for Avalonia", 
                   "Nick Polyak Enterprises", 0);
    }
}

其余代码大部分定义在 MainWindow.axaml 文件中,只有样式文件的引用在 App.axaml 中定义。

一个 DemoProducts 类型的对象被定义为 MainWindow.axaml 文件的一个资源。

<Window.Resources>
    <local:DemoProducts x:Key="TheDemoProducts"/>
</Window.Resources>  

MainWindow.axaml 文件中最重要的部分是 DataGrid 本身。

<DataGrid x:Name="TheDataGrid"
          Classes="WithColumnFilters"
          CanUserReorderColumns="True"
          CanUserResizeColumns="True"
          HorizontalAlignment="Left"
          Grid.Row="1"
          np:DataGridFilteringBehavior.DataGridFilterTextBoxClasses=
                                                 "DataGridFilterTextBox"
          np:DataGridFilteringBehavior.RowDataType="{x:Type local:Product}"
          np:DataGridCollectionViewBehavior.ItemsSource=
                                            "{StaticResource TheDemoProducts}">
    <DataGrid.Columns>
        <DataGridTextColumn Header="Product Name"
                            np:DataGridColumnManipulationBehavior.
                                                CanRemoveColumn="False"
                            np:DataGridFilteringBehavior.FilterPropName="Name"
                            Binding="{Binding Path=Name}"/>
        <DataGridTextColumn Header="Product Description"
                            np:DataGridFilteringBehavior.FilterPropName="Description"
                            Binding="{Binding Path=Description}"/>
        <DataGridTextColumn Header="Manufacturer"
                            np:DataGridFilteringBehavior.FilterPropName="Manufacturer"
                            Binding="{Binding Path=Manufacturer}"/>
        <DataGridTextColumn Header="Cost"
                            Binding="{Binding Path=Cost, 
                                      StringFormat='$\{0:#,##0.00\}'}"/>
    </DataGrid.Columns>
</DataGrid>  

为了显示过滤 textbox 和列移除菜单,需要使用 DataGrid 类内的“WithColumnFilters”类,同时 NP.Ava.Visuals 中的“ThemeStyles.axaml”样式文件应在应用程序中可见(我们通过添加以下行来实现):

<StyleInclude Source="avares://NP.Ava.Visuals/Themes/ThemeStyles.axaml"/>  

App.axaml 文件中。

DataGrid 标签中的行 np:DataGridCollectionViewBehavior.ItemsSource="{StaticResource TheDemoProducts}" 将网格的集合源(即 TheDemoProducts 资源)更改为一个 DataGridCollectionView 对象,该对象实际上被分配给 DataGridItems 属性。

DataGridCollectionView 类本质上是一个集合,其中内置了一些有用的功能,允许过滤、分组和排序。

DataGrid 标签中的行 np:DataGridFilteringBehavior.RowDataType="{x:Type local:Product}" 将行类型设置为 Product 类型的对象。这有助于创建基于属性名称的快速预编译过滤器。

np:DataGridFilteringBehavior.DataGridFilterTextBoxClasses="DataGridFilterTextBox" 允许指定用于样式化过滤 TextBox 的类,例如,可以用来更改过滤 TextBox 的背景、大小、Font 等。

现在我们来描述列特定的属性。

Binding 属性简单地允许 DataGrid 将列值绑定到行的一个属性——这是基本 DataGrid(没有任何 NP.Ava.Visuals 的改进)的工作方式。

如果您想显示一个已启用且工作的过滤器 TextBox,您需要将附加属性 np:DataGridFilteringBehavior.FilterPropName 设置为行对象上的某个属性名称,例如:

  np:DataGridFilteringBehavior.FilterPropName="Description"

Cost”列没有指定这样的属性名称,因此其过滤 TextBox 已禁用。

如果您不希望您的列可移除,您必须将附加属性 np:DataGridFilteringBehavior.DataGridFilterTextBoxClasses 设置为 false,正如在“产品名称”列中所做的那样。

np:DataGridColumnManipulationBehavior.CanRemoveColumn="False"  

默认情况下,列是可移除的。

一旦列被移除,我们需要提供一种方法来重新添加它。这就是顶部的“列可见性设置器”按钮的作用。这是相关的代码:

<Button Content="Column Visibility Setter"
        Margin="0,2"
        HorizontalAlignment="Left"
        VerticalAlignment="Center">
    <Button.Flyout>
        <Flyout Placement="Bottom">
            <ContentPresenter Content="{Binding #TheDataGrid.Columns}"
             ContentTemplate="{StaticResource DataGridColumnsVisibilityDataTemplate}"/>
        </Flyout>
    </Button.Flyout>
</Button>  

该按钮只是打开一个 Flyout(一种弹出菜单),其中包含一个内容控件,为 DataGrid 的每一列显示一个条目。DataTemplateNP.Ava.Visuals 项目中的某个文件定义的名为 DataGridColumnsVisibilityDataTemplate 的静态资源提供。

最后,看看底部的布局保存/恢复按钮代码。

<StackPanel HorizontalAlignment="Right"
            Orientation="Horizontal"
            Margin="0,10,0,0"
            Grid.Row="2">
    <Button Content="Save Grid Layout"
            np:CallAction.TheEvent="{x:Static Button.ClickEvent}"
            np:CallAction.TargetObject="{Binding #TheDataGrid}"
            np:CallAction.StaticType="{x:Type np:DataGridColumnManipulationBehavior}"
            np:CallAction.MethodName="SaveDataGridLayoutToFile"
            np:CallAction.HasArg="True"
            np:CallAction.Arg1="MyGridLayoutFile.xml"/>
    <Button Content="Restore Grid Layout"
            Margin="10,0,0,0"
            np:CallAction.TheEvent="{x:Static Button.ClickEvent}"
            np:CallAction.TargetObject="{Binding #TheDataGrid}"
            np:CallAction.StaticType="{x:Type np:DataGridColumnManipulationBehavior}"
            np:CallAction.MethodName="RestoreDataGridLayoutFromFile"
            np:CallAction.HasArg="True"
            np:CallAction.Arg1="MyGridLayoutFile.xml"/>
</StackPanel>  

我们使用 NP.AvaloniaVisuals 包中定义的 CallAction 行为来调用定义在 NP.Avalonia Visuals 包中的 static 方法

DataGridColumnManipulationBehavior.SaveDataGridLayoutToFile
                                   (dataGrid, "MyGridLayoutFile.xml")

来保存 DataGrid 的布局(我们将其作为第一个参数传递给静态方法),保存到文件“MyGridLayoutFile.xml”,我们将其作为第二个参数传递。

恢复布局时,我们使用方法

  DataGridColumnManipulationBehavior.RestoreDataGridLayoutFromFile
                                     (dataGrid, "MyGridLayoutFile.xml")

实例。

CallActionNP.Ava.Visuals 包中定义的一个非常重要且有用的行为,将在其他地方进行详细解释。

实现说明

对于那些好奇过滤、列可见性和布局保存恢复功能是如何创建的人,我将在下面提供简要说明。

过滤功能实现

对于过滤器和列移除功能,我提供了一个带有 WithFilter 类的 DataGrid 列头 Style(参见 NP.Ava.Visuals 项目中的 ThemeStyles.axaml 文件)。它改变了列头,在常规的标题下方插入了过滤 TextBox

<TextBox x:Name="FilterTextBox"
         HorizontalAlignment="Stretch"
         Grid.Row="1"
         Margin="3,1"
         Padding="2,1"
         IsVisible="{Binding $parent[DataGrid].HasFilters}"
         np:ClassesBehavior.TheClasses=
         "{Binding $parent[DataGrid].DataGridFilterTextBoxClasses}"
         IsEnabled="{Binding !!$parent[DataGridColumnHeader].Column.FilterPropName}"
         Text="{Binding $parent[DataGridColumnHeader].ColumnFilterText,
                        Mode=TwoWay}"/>  

然后,我使用各种附加属性和行为来连接过滤行为、定义文本框外观的类以及 TextBox 是否可见或已启用。

然后,我使用 AddClassesToDataGridColumnHeaderBehavior 附加行为来为 DataGrid 中的每个列注入 WithFilter DataGridColumnHeader 类。

更改列可见性

更改列可见性的菜单也是内置在 DataGridColumnHeader.WithFilter 样式中,作为主标题网格的上下文弹出菜单。

<Grid.ContextFlyout>
	<MenuFlyout>
		<MenuItem Header="Remove Column"
					IsEnabled="{Binding $parent[DataGridColumnHeader].
                                Column.CanRemoveColumn}"
					np:CallAction.TheEvent="{x:Static MenuItem.ClickEvent}"
					np:CallAction.StaticType=
                    "{x:Type np:DataGridColumnManipulationBehavior}"
					np:CallAction.TargetObject=
                    "{Binding $parent[DataGridColumnHeader].Column}"
					np:CallAction.MethodName="RemoveColumn">
			<MenuItem.Icon>
				<Path Data="{StaticResource CloseIcon}"
						Stretch="Uniform"
						Fill="Red"
						Width="9"
						VerticalAlignment="Center"
						HorizontalAlignment="Center"/>
			</MenuItem.Icon>
		</MenuItem>
	</MenuFlyout>
</Grid.ContextFlyout>  

CallAction 行为被连接以调用 static 方法

  DataGridColumnManipulationBehavior.RemoveColumn(DataGridColumn column)

当菜单项被点击时。该方法简单地将列的 IsVisible 属性更改为 false

用于恢复网格列的弹出窗口由 DataGridResources.axaml 文件中定义的“DataGridColumnsVisibilityDataTemplateDataTemplate 提供。

<DataTemplate x:Key="DataGridColumnsVisibilityDataTemplate">
    <ItemsControl Items="{Binding}">
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <StackPanel Orientation="Horizontal">
                    <np:NpToggleButton IsChecked="{Binding Path=IsVisible, Mode=TwoWay}"
                                       HorizontalAlignment="Center"
                                       VerticalAlignment="Center"
                                       Margin="3"
                                       IsEnabled="{Binding CanRemoveColumn}"/>
                    <TextBlock Text="{Binding Header}"
                               HorizontalAlignment="Center"
                               VerticalAlignment="Center"
                               Margin="5,0,0,0"/>
                </StackPanel>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>
</DataTemplate>  

对于每一列,它显示一个 CheckBox,后面跟着列名,允许用户切换 checkbox 以使列可见或不可见。

保存/恢复 DataGrid 布局

用于相应保存和恢复数据网格布局的两个方法 SaveDataGridLayoutToFile(...)RestoreDataGridLayoutFromFile(...) 定义在 static DataGridColumnManipulationBehavior static 类中。

这是保存方法的实现:

public static void SaveDataGridLayoutToFile(this DataGrid dataGrid, string fileName)
{
    var colSerializationData =
        dataGrid
            .Columns
                .OrderBy(col => col.DisplayIndex)
                .Select
                (col => new ColumnSerializationData
                        {
                            IsVisible = col.IsVisible,
                            WidthStr = TheDataGridLengthConverter.ConvertToString
                                       (col.Width),
                            HeaderId = col.Header?.ToStr()
                        }).ToArray();

    XmlSerializationUtils.SerializeToFile(colSerializationData, fileName);
}  

我们将列集合转换为 ColumnSerializableData 对象的集合,然后使用 XmlSerializationUtils.SerializeToFile(...) 方法将其保存到文件中。

在恢复方法中,我们执行相反的操作——我们从文件中恢复 ColumnSerializableData 对象的集合,然后将它们的值应用到当前网格。

public static void RestoreDataGridLayoutFromFile(this DataGrid dataGrid, string fileName)
{
    ColumnSerializationData[] colSerializationData =
        XmlSerializationUtils.DeserializeFromFile<ColumnSerializationData[]>(fileName);

    colSerializationData
        .DoForEach
        (
            (col, idx) =>
            {
                DataGridColumn gridCol =
                    dataGrid.Columns.Single(dataGridCol => 
                             dataGridCol.Header?.ToString() == col.HeaderId);

                gridCol.IsVisible = col.IsVisible;
                gridCol.DisplayIndex = idx;
                gridCol.Width = (DataGridLength)TheDataGridLengthConverter.
                                 ConvertFromString(col.WidthStr);
            });
}  

这是 ColumnSerializableData 类的实现:

public class ColumnSerializationData
{
    [XmlAttribute]
    public string? HeaderId { get; set; }

    [XmlAttribute]
    public bool IsVisible { get; set; }

    [XmlAttribute]
    public string? WidthStr { get; set; }
}  

结论

在本文中,我解释了如何为 Avalonia DataGrid 添加重要的缺失功能,包括过滤、布局保存/恢复和控制列可见性。这些额外功能来自我的开源 NP.Ava.Visuals 库,是免费提供的。

我计划进一步撰写关于此库的文章,包括详细描述其最重要的附加行为。

在不久的将来,我还计划为 Avalonia DataGrid 添加分组功能。

历史

  • 2022年4月15日:初始版本
  • 2023 年 12 月 26 日 已升级到 Avalonia 11
© . All rights reserved.