Click here to Skip to main content
15,867,949 members
Articles / Programming Languages / C#

Avalonia DataGrid - Advanced Features coming from NP.Ava.Visuals Package

Rate me:
Please Sign up or sign in to vote.
5.00/5 (13 votes)
26 Dec 2023CPOL7 min read 23.6K   15   35
Explain the new code whose purpose is to add Filtering, Layout Saving/Restoring and Column Visibility functionality to Avalonia DataGrid
Avalonia is a great and powerful tool for desktop applications development, but, at this point does not have some complex controls. For example, its DataGrid misses filtering, grouping, layout saving/restoring and other column manipulation functionality. This article describes the code shipped as NP.Ava.Visuals open source library/package that provides all the missing functionality except for the grouping.

Introduction

Note that both the article and the samples code have been updated to work with latest version of Avalonia - 11.0.6

Avalonia is a great multiplatform open source UI framework for developing

  • Desktop solutions that will run across Windows, Mac and Linux
  • Web applications to run in Browser (via WebAssembly)
  • Mobile applications for Android, iOS and Tizen.

I wrote extensively about Avalonia on CodeProject and produced several Avalonia based packages/frameworks also described in my CodeProject articles.

Avalonia has only recently become production ready and because of that, the major 3rd party component providers like Telerik, DevExpress or Infragistics still have not released the components for Avalonia.

In my experience, only two components from the large providers like Telerik were necessary - a window docking functionality and a data grid. Everything else - custom buttons, boxes, etc. can (and should) be easily built by the team out of the WPF or Avalonia primitives to meet the UX requirements.

The lack of the docking framework in Avalonia is compensated by my UniDock package.

DataGrid already exists in Avalonia. Largely, it is a port of the built-in WPF DataGrid. Unfortunately, just like its WPF counterpart, the Avalonia DataGrid is missing some important features, including:

  1. Filtering
  2. Changing the Visibility of the Grid Columns
  3. Saving/Restoring the Layout
  4. Grouping

I have added all the above capabilities to the built-in Avalonia DataGrid aside from Grouping (which is on my list and should be added soon). All of them are added to my NP.Ava.Visuals library/package.

This article presents a sample demonstrating how to use those advanced features.

Demo Code

Demo code is located within NP.Ava.Demos repository, under NP.Demos.VisualSamples/NP.Demos.AdvancedDataGridDemo project.

To get the project, clone the repository NP.Demos.VisualSamples/NP.Demos.AdvancedDataGridDemo folder and open NP.Demos.AdvancedDataGridDemo.sln solution (you will need to employ Visual Studio 2022 for that).

Build the solution, make sure that all the needed nuget packages have been downloaded by your Visual Studio.

If you open the packages regions of the dependency of the only project within the solution, you will see only two projects:

Image 1

The rest of the Avalonia references are all coming from NP.Ava.Visuals dependency.

Run the solution; here is the window that is going to pop up:

Image 2

The red rectangular curve contains the text filters. Filter for the last column "Cost" is disabled.

Try typing some string within the filters' text boxes. You will see that only the rows that contain the filter text will be shown - the rest will become invisible:

Image 3

The picture above shows the record that contain 'b' in its Product Name and 'nic' in its Product Description.

Now right click on one of the columns (say Manufacturer) and choose "Remove Column" menu item:

Image 4

Once it is clicked, the column (Manufacturer) becomes invisible:

Image 5

Now click "Column Visibility Setter" button at the top, in the open dropdown click on the checkbutton next to Manufacturer column to make it visible again:

Image 6

Now change the widths of various columns and change their order by dragging some columns to other locations, e.g.:

Image 7

Save the grid layout by pressing button "Save Grid Layout".

Restart the application and press button "Restore Grid Layout". The saved layout will be restored.

Demo Code

Within the demo, we define a simple class Product:

C#
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;
    }
}  

and a predefined collection of products - DemoProducts:

C#
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);
    }
}

Most of the rest of the code is defined within MainWindow.axaml file, only references to style files are defined within App.axaml.

An object of type DemoProducts is defined as a resource with MainWindow.axaml file:

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

The most important part of MainWindow.axaml file is the DataGrid itself:

XAML
<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>  

In order to display the filtering textboxes and column removing menu, one needs to use "WithColumnFilters" class within the DataGrid classe, while the "ThemeStyles.axaml" styles file from NP.Ava.Visuals should be visible within the application (which is achieved by us adding line):

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

to the App.axaml file).

The line np:DataGridCollectionViewBehavior.ItemsSource="{StaticResource TheDemoProducts}" of the DataGrid tag, changes the collection source of the grid (which is simply the TheDemoProducts resource) into a DataGridCollectionView object which is actually assigned to the Items property of the DataGrid.

DataGridCollectionView class is essentially a collection, with some useful functionality built into it allowing Filtering, Grouping and Sorting.

Line np:DataGridFilteringBehavior.RowDataType="{x:Type local:Product}" of the DataGrid tag, sets the row type to the object of type Product. This helps creating fast precompiled filter based on the property name.

Line np:DataGridFilteringBehavior.DataGridFilterTextBoxClasses="DataGridFilterTextBox" allows one to specify classes to style the filtering TextBox, e.g., one can use it to change the background of the filtering TextBox, its size, its Font, etc.

Now let us describe the column specific properties.

Binding property simply allows the DataGrid to bind the column value to a property of the row - this is how the basic DataGrid (without any improvements from NP.Ava.Visuals) works.

If you want to show an enabled and working filter TextBox you need to set the attached property np:DataGridFilteringBehavior.FilterPropName to some property name on the row object, e.g.:

XAML
np:DataGridFilteringBehavior.FilterPropName="Description"

Column "Cost" does not specify such property name and because of that, its Filtering TextBox is disabled.

If you do not want your column to be removable, you have to set the attached property np:DataGridFilteringBehavior.DataGridFilterTextBoxClasses to false, the way it is done on "Product Name" column:

XAML
np:DataGridColumnManipulationBehavior.CanRemoveColumn="False"  

By default, the columns are removable.

Once a column is removed, we need to provide a way to re-add it back. This is the purpose of "Column Visibility Setter" button at the top. Here is the related code:

XAML
<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>  

The button simply opens a Flyout (a kind of a menu popup) that contains a content control displaying an entry for every Column of the DataGrid. The DataTemplate is provides by Static Resource named DataGridColumnsVisibilityDataTemplate defined in one of the files within NP.Ava.Visuals project.

Finally, take a look at the Layout saving/restoring buttons code at the bottom:

XAML
<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>  

We are using CallAction behavior defined within NP.AvaloniaVisuals package to call static method:

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

defined within NP.Avalonia Visuals package, in order to save the layout of the DataGrid (which we pass as the first argument to the static method) into file "MyGridLayoutFile.xml" which we pass as the second argument.

When restoring a layout, we employ method:

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

instead.

CallAction is a very important and useful behavior defined within NP.Ava.Visuals package that will be explained in detail elsewhere.

Implementation Notes

For those who are curious about how the filtering, column visibility and layout saving restoring functionality were created, I provide a brief description below.

Implementation of Filtering Functionality

For the filters and column removal functionality, I provide a DataGrid column header Style with WithFilter class (see ThemeStyles.axaml file within NP.Ava.Visuals project). It changes the column header inserting the filtering TextBox under the usual header:

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

Then, I use various attached properties and behaviors to wire the filtering behavior, classes that define the look and feel of the text box and whether the TextBox is visible or enabled.

Then, I use AddClassesToDataGridColumnHeaderBehavior attached behavior to inject the WithFilter DataGridColumnHeader class for every column within the DataGrid.

Changing Column Visibility

The menu to change the column Visibility is also built into DataGridColumnHeader.WithFilter styles as a context flyout menu to the main grid of the header:

XAML
<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>  

The CallAction behavior is wired to call static method:

C#
DataGridColumnManipulationBehavior.RemoveColumn(DataGridColumn column)

when the menu item is clicked. The method simple changes the column's IsVisible property to false.

The popup for restoring the grid columns is provided by "DataGridColumnsVisibilityDataTemplate" DataTemplate defined within DataGridResources.axaml file:

XAML
<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>  

For each column, it displays a CheckBox followed by the name of the column allowing the user to toggle the checkbox making the column visible or not.

Saving/Restoring DataGrid Layout

The two methods SaveDataGridLayoutToFile(...) and RestoreDataGridLayoutFromFile(...) for saving and restoring the data grid layout correspondingly are defined within static DataGridColumnManipulationBehavior static class.

Here is the saving method's implementation:

C#
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);
}  

We convert the column collection into a collection of ColumnSerializableData objects and then save it to a file using XmlSerializationUtils.SerializeToFile(...) method.

In the restoring method - we do the opposite - we restore a collection of ColumnSerializableData object from a file and then apply their values to the current grid:

C#
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);
            });
}  

And here is the implementation of ColumnSerializableData class:

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

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

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

Conclusion

In this article, I explained how to add the important missing features to the Avalonia DataGrid including Filtering, Layout saving/restoring and controlling the column visibility. These extra functionality comes for free from my open source NP.Ava.Visuals library.

I plan to write more about this library, including giving detail descriptions of its most important attached behaviors.

In the near future, I also plan to add grouping to the Avalonia DataGrid.

History

  • 15th April, 2022: Initial version
  • 26th December, 2023 upgraded to Avalonia 11

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect AWebPros
United States United States
I am a software architect and a developer with great passion for new engineering solutions and finding and applying design patterns.

I am passionate about learning new ways of building software and sharing my knowledge with others.

I worked with many various languages including C#, Java and C++.

I fell in love with WPF (and later Silverlight) at first sight. After Microsoft killed Silverlight, I was distraught until I found Avalonia - a great multiplatform package for building UI on Windows, Linux, Mac as well as within browsers (using WASM) and for mobile platforms.

I have my Ph.D. from RPI.

here is my linkedin profile

Comments and Discussions

 
QuestionNice work but Problematical Pin
Kent Swan26-Dec-23 18:23
Kent Swan26-Dec-23 18:23 
AnswerRe: Nice work but Problematical Pin
Nick Polyak27-Dec-23 5:58
mvaNick Polyak27-Dec-23 5:58 
GeneralRe: Nice work but Problematical Pin
Kent Swan27-Dec-23 9:08
Kent Swan27-Dec-23 9:08 
GeneralRe: Nice work but Problematical Pin
Nick Polyak27-Dec-23 11:13
mvaNick Polyak27-Dec-23 11:13 
GeneralRe: Nice work but Problematical Pin
Kent Swan27-Dec-23 11:40
Kent Swan27-Dec-23 11:40 
GeneralRe: Nice work but Problematical Pin
Nick Polyak27-Dec-23 12:14
mvaNick Polyak27-Dec-23 12:14 
GeneralRe: Nice work but Problematical Pin
Kent Swan27-Dec-23 13:38
Kent Swan27-Dec-23 13:38 
GeneralRe: Nice work but Problematical Pin
Nick Polyak27-Dec-23 14:17
mvaNick Polyak27-Dec-23 14:17 
QuestionДвигать строки вниз вверх Pin
Konstantin Reim23-Oct-22 3:13
Konstantin Reim23-Oct-22 3:13 
Вопрос: есть ли возможность сделать так.
Есть таблица

столбик 1 | столбик 2 | столбик 3 | столбик 4

       1. |        х1 |        х1 |        х1
       2. |        х2 |        х2 |        х2
       3. |        х3 |        х3 |        х3

При нажатии на столбик № 1 на строку № 3 - строка № 3 меняется местами со 2-й строкой и т.д.
В итоге получим:

столбик 1 | столбик 2 | столбик 3 | столбик 4

       1. |        х3 |        х3 |        х3
       2. |        х1 |        х1 |        х1
       3. |        х2 |        х2 |        х2

AnswerRe: Двигать строки вниз вверх Pin
Nick Polyak24-Oct-22 15:14
mvaNick Polyak24-Oct-22 15:14 
GeneralRe: Двигать строки вниз вверх Pin
Konstantin Reim24-Oct-22 15:19
Konstantin Reim24-Oct-22 15:19 
GeneralMy vote of 5 Pin
Konstantin Reim23-Oct-22 3:13
Konstantin Reim23-Oct-22 3:13 
GeneralRe: My vote of 5 Pin
Nick Polyak24-Oct-22 15:08
mvaNick Polyak24-Oct-22 15:08 
QuestionNice Work. Pin
Jammer3-Jun-22 6:20
Jammer3-Jun-22 6:20 
AnswerRe: Nice Work. Pin
Nick Polyak7-Jun-22 12:10
mvaNick Polyak7-Jun-22 12:10 
GeneralRe: Nice Work. Pin
Jammer10-Jun-22 3:24
Jammer10-Jun-22 3:24 
GeneralRe: Nice Work. Pin
Nick Polyak10-Jun-22 4:23
mvaNick Polyak10-Jun-22 4:23 
GeneralRe: Nice Work. Pin
Jammer12-Jun-22 2:19
Jammer12-Jun-22 2:19 
GeneralRe: Nice Work. Pin
Nick Polyak12-Jun-22 6:15
mvaNick Polyak12-Jun-22 6:15 
GeneralRe: Nice Work. Pin
Jammer13-Jun-22 1:46
Jammer13-Jun-22 1:46 
QuestionMust have user control! Variable number of columns Pin
Sergiu Jurca30-May-22 9:20
Sergiu Jurca30-May-22 9:20 
AnswerRe: Must have user control! Variable number of columns Pin
Nick Polyak7-Jun-22 12:09
mvaNick Polyak7-Jun-22 12:09 
GeneralRe: Must have user control! Variable number of columns Pin
Sergiu Jurca16-Dec-22 2:43
Sergiu Jurca16-Dec-22 2:43 
GeneralRe: Must have user control! Variable number of columns Pin
Nick Polyak16-Dec-22 6:27
mvaNick Polyak16-Dec-22 6:27 
AnswerRe: Must have user control! Variable number of columns Pin
Nick Polyak27-Dec-22 10:18
mvaNick Polyak27-Dec-22 10:18 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.