Blazor and .NET 8 QuickGrid Implementation with CSV Data Export

Introduction

Quickgrid is a razor component that can efficiently display tabular data in Blazor Applications. It is very simple and convenient to implement. It is officially supported in .NET 8 so for sure it is backed by Microsoft.

Features of Quickgrid

A few features of Quickgrid are below,

  1. It supports the following data sources.
    1. In-memory IQueryable
    2. EF-Core IQueryable
    3. Remote data
  2. Two built-in column types are named PropertyColumn and TemplateColumn, and the capability to implement our own column types.
  3. Sorting of data based on columns.
  4. Filtering of data based on columns.
  5. Paging and Virtualization of data.
  6. Styling of Grid.

It is available in .NET 8 and above and supported in all the Hosting Models - Server, WebAssembly, or .NET MAUI. It is available as a separate Nuget Package.

Walkthrough

Download the below Nuget package.

<PackageReference Include="Microsoft.AspNetCore.Components.QuickGrid" Version="8.0.0" />

Basic usage

Create a razor component and add the code below. In my case, it's DemoGrid.razor.

The code is straightforward; we have used the QuickGrid component and referenced the IQueryable list into it. One important thing to use is Render Mode which is an “Interactive Server” in the Home.razor component. This ensures that grid sorting works correctly, along with other features required to make the grid work. Otherwise, it will run into issues. You can try different render modes that suit your needs.

@using Microsoft.AspNetCore.Components.QuickGrid

<QuickGrid Items="@foods">
    <PropertyColumn Property="@(p => p.FoodId)" Sortable="true" />
    <PropertyColumn Property="@(p => p.Name)" Sortable="true" />
    <PropertyColumn Property="@(p => p.ExpiryDate)" Format="dd-MMM-yyyy" Sortable="true" />
    <PropertyColumn Property="@(p => p.Price)" Sortable="true" />
    <PropertyColumn Property="@(p => p.IsVegetarian)" Sortable="true" />
</QuickGrid>

@code {
    record Food(int FoodId, string Name, DateOnly ExpiryDate, decimal Price, bool IsVegetarian);

    IQueryable<Food> foods = new[]
    {
        new Food(1001, "Apple", new DateOnly(2023, 1, 31), 150m, true),
        new Food(1002, "Pasta", new DateOnly(2023, 2, 15), 300m, true),
        new Food(1003, "Chicken", new DateOnly(2023, 1, 20), 850m, false),
        new Food(1004, "Yogurt", new DateOnly(2023, 3, 5), 20m, true),
        new Food(1005, "Salad", new DateOnly(2023, 1, 10), 45m, true),
        new Food(1006, "Chocolate", new DateOnly(2023, 4, 1), 50m, false),
    }.AsQueryable();
}

Now use this DemoGrid.razor on the Home page.

@page "/"
@rendermode InteractiveServer

<PageTitle>Varun Setia Demo App - Home</PageTitle>
<QGDemoBlazorApp.Client.Pages.DemoGrid/>

Our UI looks like this

UI

Our grid looks fine as of now but not good enough to be used in the real world. So let us add all the capabilities available to give a good user experience.

Customizations

Let's add a filter in the IsVegetarian column since not everyone likes Non-Veg food and Veg either. Below is the new code:

@using Microsoft.AspNetCore.Components.QuickGrid

<QuickGrid Items="@GetFoodsQueryable()" @ref="grid">
    <PropertyColumn Property="@(p => p.FoodId)" Sortable="true" />
    <PropertyColumn Property="@(p => p.Name)" Sortable="true" />
    <PropertyColumn Property="@(p => p.ExpiryDate)" Format="dd-MMM-yyyy" Sortable="true" />
    <PropertyColumn Property="@(p => p.Price)" Sortable="true" />
    <PropertyColumn Property="@(p => p.IsVegetarian)" Sortable="true">
        <ColumnOptions>
            <label>
                <input type="checkbox" @bind="ShowVeg" /> Show Veg
                <hr />
                <button onclick="@ResetGridAsync">Reset</button>
            </label>
        </ColumnOptions>
    </PropertyColumn>
</QuickGrid>

@code {
    private bool? ShowVeg = null;
    QuickGrid<Food>? grid;
    record Food(int FoodId, string Name, DateOnly ExpiryDate, decimal Price, bool IsVegetarian);

    async Task ResetGridAsync()
    {
        ShowVeg = null;
        await grid.RefreshDataAsync();
    }

    IQueryable<Food> GetFoodsQueryable()
    {
        if (ShowVeg != null)
        {
            return foods.Where(f => f.IsVegetarian == ShowVeg).AsQueryable();
        }
        else
        {
            return foods.AsQueryable();
        }
    }

    IEnumerable<Food> foods = new Food[]
    {
        new Food(1001, "Apple", new DateOnly(2023, 1, 31), 150m, true),
        new Food(1002, "Pasta", new DateOnly(2023, 2, 15), 300m, true),
        new Food(1003, "Chicken", new DateOnly(2023, 1, 20), 850m, false),
        new Food(1004, "Yogurt", new DateOnly(2023, 3, 5), 20m, true),
        new Food(1005, "Salad", new DateOnly(2023, 1, 10), 45m, true),
        new Food(1006, "Chocolate", new DateOnly(2023, 4, 1), 50m, false),
    };
}

After adding code, our UI looks like this.

Customizations

You will notice the hamburger icon on the left side of the column name. It appears for every column. We introduce a column filter along with the HTML we add. On clicking, it expands, as you can see on the screen. Now we can easily filter data and reset the filter as per our needs. You can add as many filters as you want to.

Now we will add pagination and enable virtualization. This will help us understand how to work with a large number of records. For this demo, I will allow one item per page.

Our Code looks like this.

@using Microsoft.AspNetCore.Components.QuickGrid

<QuickGrid Virtualize="true" Items="@GetFoodsQueryable()" @ref="grid" Pagination="@pagination">
    <PropertyColumn Property="@(p => p.FoodId)" Sortable="true" />
    <PropertyColumn Property="@(p => p.Name)" Sortable="true" />
    <PropertyColumn Property="@(p => p.ExpiryDate)" Format="dd-MMM-yyyy" Sortable="true" />
    <PropertyColumn Property="@(p => p.Price)" Sortable="true" />
    <PropertyColumn Property="@(p => p.IsVegetarian)" Sortable="true">
        <ColumnOptions>
            <label>
                <input type="checkbox" @bind="ShowVeg" /> Show Veg
                <hr />
                <button onclick="@ResetGridAsync">Reset</button>
            </label>
        </ColumnOptions>
    </PropertyColumn>
</QuickGrid>

<Paginator State="@pagination" />

@code {
    private bool? ShowVeg = null;
    QuickGrid<Food>? grid;
    PaginationState pagination = new PaginationState { ItemsPerPage = 1 };
    record Food(int FoodId, string Name, DateOnly ExpiryDate, decimal Price, bool IsVegetarian);

    async Task ResetGridAsync()
    {
        ShowVeg = null;
        await grid.RefreshDataAsync();
    }

    IQueryable<Food> GetFoodsQueryable()
    {
        if (ShowVeg != null)
        {
            return foods.Where(f => f.IsVegetarian == ShowVeg).AsQueryable();
        }
        else
        {
            return foods.AsQueryable();
        }
    }

    IEnumerable<Food> foods = new Food[]
    {
        new Food(1001, "Apple", new DateOnly(2023, 1, 31), 150m, true),
        new Food(1002, "Pasta", new DateOnly(2023, 2, 15), 300m, true),
        new Food(1003, "Chicken", new DateOnly(2023, 1, 20), 850m, false),
        new Food(1004, "Yogurt", new DateOnly(2023, 3, 5), 20m, true),
        new Food(1005, "Salad", new DateOnly(2023, 1, 10), 45m, true),
        new Food(1006, "Chocolate", new DateOnly(2023, 4, 1), 50m, false),
    };
}

After updating the code, our UI looks like this.

Updated UI

Bonus

We will now add export functionality using the CSV helper library. This will help us to export data in CSV format.

For this, we will add the below package.

 <PackageReference Include="CsvHelper" Version="30.0.1" />

Now, we are simply going to update the code to add an export button and use all the supporting code used by Blazor to ensure the file is available to download on the client side. Our final code looks like the one below.

@using CsvHelper
@using Microsoft.AspNetCore.Components.QuickGrid
@using System.Globalization
@using System.Text
@inject IJSRuntime JS

<button onclick="@ExportData">Export</button>

<QuickGrid Virtualize="true" Items="@GetFoodsQueryable()" @ref="grid" Pagination="@pagination">
    <PropertyColumn Property="@(p => p.FoodId)" Sortable="true" />
    <PropertyColumn Property="@(p => p.Name)" Sortable="true" />
    <PropertyColumn Property="@(p => p.ExpiryDate)" Format="dd-MMM-yyyy" Sortable="true" />
    <PropertyColumn Property="@(p => p.Price)" Sortable="true" />
    <PropertyColumn Property="@(p => p.IsVegetarian)" Sortable="true">
        <ColumnOptions>
            <label>
                <input type="checkbox" @bind="ShowVeg" /> Show Veg
                <hr />
                <button onclick="@ResetGridAsync">Reset</button>
            </label>
        </ColumnOptions>
    </PropertyColumn>
</QuickGrid>

<Paginator State="@pagination" />

<script>
    window.downloadFileFromStream = async (fileName, contentStreamReference) => {
        const arrayBuffer = await contentStreamReference.arrayBuffer();
        const blob = new Blob([arrayBuffer]);
        const url = URL.createObjectURL(blob);
        const anchorElement = document.createElement('a');
        anchorElement.href = url;
        anchorElement.download = fileName ?? '';
        anchorElement.click();
        anchorElement.remove();
        URL.revokeObjectURL(url);
    }
</script>

@code {
    private bool? ShowVeg = null;
    QuickGrid<Food>? grid;
    PaginationState pagination = new PaginationState { ItemsPerPage = 1 };
    record Food(int FoodId, string Name, DateOnly ExpiryDate, decimal Price, bool IsVegetarian);

    async Task ResetGridAsync()
    {
        ShowVeg = null;
        await grid.RefreshDataAsync();
    }

    IQueryable<Food> GetFoodsQueryable()
    {
        if (ShowVeg != null)
        {
            return foods.Where(f => f.IsVegetarian == ShowVeg).AsQueryable();
        }
        else
        {
            return foods.AsQueryable();
        }
    }

    string GetCsvString()
    {
        string dataCsv;
        using (var writer = new StringWriter())
        using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture, true))
        {
            var records = GetFoodsQueryable().ToList();
            csv.WriteRecords(records);
            dataCsv = writer.ToString();
        }
        return dataCsv;
    }

    async Task ExportData()
    {
        var csvStr = GetCsvString();
        byte[] byteArray = Encoding.UTF8.GetBytes(csvStr);
        MemoryStream stream = new MemoryStream(byteArray);
        using var streamRef = new DotNetStreamReference(stream: stream);
        await JS.InvokeVoidAsync("downloadFileFromStream", "file.csv", streamRef);
    }

    IEnumerable<Food> foods = new Food[]
    {
        new Food(1001, "Apple", new DateOnly(2023, 1, 31), 150m, true),
        new Food(1002, "Pasta", new DateOnly(2023, 2, 15), 300m, true),
        new Food(1003, "Chicken", new DateOnly(2023, 1, 20), 850m, false),
        new Food(1004, "Yogurt", new DateOnly(2023, 3, 5), 20m, true),
        new Food(1005, "Salad", new DateOnly(2023, 1, 10), 45m, true),
        new Food(1006, "Chocolate", new DateOnly(2023, 4, 1), 50m, false),
    };
}

UI looks like below.

Bonus

We have used many features of QuickGrid in this article.

I hope you enjoyed reading till the end and hopefully will try the above code. I have also uploaded the source so you can refer to it to explore further. Please mention in comments how you feel about this Grid.


Similar Articles