Blazor QuickGrid using a Remote DataSource

And thou shalt make for it a grating of network of brass; and upon the net shalt thou make four brazen rings in the four corners thereof.
וְעָשִׂיתָ לּוֹ מִכְבָּר מַעֲשֵׂה רֶשֶׁת נְחֹשֶׁת וְעָשִׂיתָ עַל-הָרֶשֶׁת אַרְבַּע טַבְּעֹת נְחֹשֶׁת עַל אַרְבַּע קְצוֹתָיו

I really had to stretch my imagination a bit searching for a reference to a grid in the Torah. The closest word I could find was רשת which usually relates to a network. Exodus 27:4 provided the quote above. I'm guessing that most Hebrew speaking coders use the English word "grid" but part of the reason I spend time on this blog is to expand my knowledge of Biblical Hebrew so there you go.

As of .NET 8, the new QuickGrid Blazor component is officially supported. This grid is intended as a simple, flexible grid for general purpose use. It lacks a lot of features like nested grids and drag to reorder columns that are found in some commercial grid components, but it supplies enough basic functionality to make it the grid of choice in a lot of applications.

While studying the documentation found here, I didn't see code for an end-to-end sample application that leveraged a remote datasource linked to a database. There is an example that leverages a publicly available API from the US FDA Food Enforcement database, but this API seems to lack a few features that I wanted to play with that would demonstrate data sorting and filtering in addition to pagination. As such, I decided to put together a simple demo project on my own that could demonstrate a number of the features you can leverage with this simple grid component.

Source for this example can be found at https://github.com/rlebowitz/Finaltouch.QuickGraph

The Microsoft documentation recommends leveraging the QuickGrid component's ItemProvider attribute. ItemProvider is simply a delegate for a callback method that GridItemsProvider type, where TGridItem is the type of data displayed in the grid. The callback you provide must have a parameter of type GridItemsProviderRequest which specifies the start index, maximum row count, and sort order of data to return. If the grid employs paging or virtualization, your data source also needs to provide a total item count as well.

The Backend

For purposes of this demonstration, I downloaded a public SQLite database containing census information on the most popular male and female baby names grouped by US state. I constructed a pretty simple REST API that retrieves some of the records based on metadata indicating a starting index value, the maximum number of rows to retrieve, the sort order of the data as well as certain kinds of filter data. The metadata class is seen below. The SortProperty enumeration is part of the QuickGrid component library. The Filter class is I created a simple repository class that processes metadata posted to an API controller and returns a result containing a list of records matching the filter criteria.

  
 public class GridMetaData
    {
        public int StartIndex { get; set; }
        public int? Count { get; set; }
        public ICollection? SortProperties { get; set; }
        public Filter? Filter { get; set; }
    }
    

    
    public class Filter
    {
        public string? Field { get; set; }
        public string? Value { get; set; }
        public Operator Operator { get; set; } = Operator.Equals;
        public bool IsValid => Field != null && Value != null;

    }


public enum Operator
    {
        Equals,
        NotEquals,
        GreaterThan,
        LessThan,
        Contains
    }

If the metadata syntax seems a bit odd, it's because I am using the System.Linq.Dynamic.Core library to create LINQ queries dynamically from string fragments. Dynamic LINQ is worth taking a look at if like me, you want to generate filters that can be changed by controls within the grid. I couple Dynamic LINQ with some IQueryable extension methods to make it easier to construct the necessary queries based on the filter criteria.

  
        public class NamesRepository : INamesRepository
    {
        private ILogger Logger { get; set; }
        private BabynamesContext Context { get; set; }

        public NamesRepository(ILogger logger, BabynamesContext context)
        {
            Logger = logger;
            Context = context;
        }

        public NamesResult? GetBabyNames([FromBody] GridMetaData metaData)
        {
            try
            {
                var ordering = Ordering(metaData.SortProperties);
                var ordered = string.IsNullOrEmpty(ordering)
                    ? Context.Babynames.OrderBy(t => t.State)
                    : Context.Babynames.OrderBy(ordering);
                var result = ordered
                    .Filter(metaData.Filter)
                    .Select(t => t)
                    .Skip(metaData.StartIndex)
                    .Take(metaData.Count ?? 10)
                    .AsNoTracking()
                    .ToListAsync();
                var count = Context.Babynames.AsQueryable().Filter(metaData.Filter).CountAsync();

                Task.WaitAll(count, result);

                return new NamesResult
                {
                    Count = count.Result,
                    BabyNames = result.Result
                };
            }
            catch (Exception ex)
            {
                Logger.LogError(ex, "Controller Error");
            }
            return default;
        }

        //https://dynamic-linq.net/basic-simple-query#ordering-results
        private static string Ordering(ICollection? properties)
        {
            List columns = new();
            if (properties == null)
            {
                return string.Empty;
            }
            foreach (var property in properties)
            {
                if (property.Direction == SortDirection.Ascending)
                {
                    columns.Add(property.PropertyName);
                }
                else
                {
                    columns.Add($"{property.PropertyName} desc");
                }
            }
            return string.Join(", ", columns);
        }
    }

The Frontend

As the Razor code sample below shows, I've specified a callback method that matches the GridItemsProvider delegate type called NamesProvider. I've also indicated that I will be using pagination to view data in small chunks one at a time, so I don't need to leverage the Virtualize attribute for QuickGrid. I've chosen to only display a few of the data fields provided by my Sqlite database in this demo application. I specify that all the PropertyColumns are sortable, and, in order to demonstrate how to implement a search function using Dynamic Linq, I've provided an input field that will act as a filter on the State data property.


@page "/"
@using Microsoft.AspNetCore.Components;
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.QuickGrid
@layout Shared.MainLayout

<PageTitle>Baby Names</PageTitle>
<div class="container py-3">
    <div class="row mb-4">
        <div class="col-lg-8 mx-auto text-center">
            <h1 class="display-6">Most Popular Baby Names by State</h1>
        </div>
    </div>

    <div class="row">
        <div class="col-lg-10 mx-auto">
            <div class="grid" tabindex="-1">
                <QuickGrid @ref="Grid" ItemsProvider="@NamesProvider" Class="comic-name" Theme="default" Virtualize="false" Pagination="@Pagination">
                    <PropertyColumn Title="State" Property="@(c => c.State)" Sortable="true" IsDefaultSortColumn="true">
                        <ColumnOptions>
                            <div class="search-box">
                                <input type="search" autofocus @bind="Filter" @bind:event="oninput" placeholder="State Name..." />
                            </div>
                        </ColumnOptions>
                    </PropertyColumn>
                    <PropertyColumn Title="Name" Property="@(c => c.Name)" Sortable="true" />
                    <PropertyColumn Title="Sex" Property="@(c => c.Sex)" Sortable="true" />
                    <PropertyColumn Title="Rank" Property="@(c => c.RankWithinSex)" Sortable="true" />
                </QuickGrid>
            </div>
            <Paginator State="@Pagination" />
        </div>
    </div>
</div>

The code-behind for this Razor page contains the method matching the delegate, and illustrates how data is posted to the REST service that provides sorting, filtering, etc. specifications at the same time. Note that each time that the Filter properties are updated, I call the QuickGrid's RefreshDataAsync method to let the grid know that it needs to pull back data that reflects the changes to the Filter.


public partial class Index
    {
        [Inject]
        private HttpClient Client { get; set; } = default!;
        private GridItemsProvider<Babyname>? NamesProvider { get; set; }

        private PaginationState Pagination = new PaginationState { ItemsPerPage = 10 };
        private QuickGrid<Babyname>? Grid { get; set; }
        private string? FilterText { get; set; }
        private string? Filter
        {
            get { return FilterText; }
            set
            {
                FilterText = value;
                if (Grid != null)
                {
                    Task.Run(Grid.RefreshDataAsync);
                }
            }
        }

        protected override void OnInitialized()
        {
            NamesProvider = Provider;
        }

        private async ValueTask<GridItemsProviderResult<Babyname>> Provider(GridItemsProviderRequest<Babyname> request)
        {
            GridItemsProviderResult<Babyname> result = default;
            var metaData = new GridMetaData
            {
                StartIndex = request.StartIndex,
                Count = request.Count,
                SortProperties = request.GetSortByProperties().ToArray(),
                Filter = new Filter { Field = "state", Operator = Operator.Contains, Value = FilterText },
            };
            var namesResult = await GetNames(metaData);
            if (namesResult != null)
            {
                result = new GridItemsProviderResult<Babyname>()
                {
                    Items = namesResult.BabyNames != null ? namesResult.BabyNames : Array.Empty<Babyname>(),
                    TotalItemCount = namesResult.Count
                };
            }
            return result;
        }

        public async Task<NamesResult?> GetNames(GridMetaData metaData)
        {
            var response = await Client.PostAsJsonAsync("api/BabyNames/GetBabyNames", metaData);
            if (response.IsSuccessStatusCode)
            {
                return await response.Content.ReadFromJsonAsync<NamesResult>();
            }
            return null;
        }
    }

A Blazor Modal Component Based on Bootstrap 5

According to all that I show thee, after the pattern of the tabernacle and the pattern of all the instruments thereof, even so shall ye make it.
כְּכֹל אֲשֶׁר אֲנִי מַרְאֶה אוֹתְךָ אֵת תַּבְנִית הַמִּשְׁכָּן וְאֵת תַּבְנִית כָּל-כֵּלָיו וְכֵן תַּעֲשׂוּ

Exodus 25:9 uses the Hebrew word תבנית (tavniyt) to describe a pattern or template. The word derives from the root בנה which relates to building (construction).

Source can be found at https://github.com/rlebowitz/Finaltouch.Modal

Demo can be found at https://rlebowitz.github.io/Finaltouch.Modal/

I often need to use a modal control for editing small forms in my applications. While it would be possible to write such a control from scratch, it's much simpler to adapt an existing modal for this purpose. I like using Bootstrap 5 these days so I'm presenting the code for a Blazor version of their modal here.

The Bootstrap 5 modal design includes three subsections; a header, body and footer. The header generally includes a title and as the documentation recommends, a dismiss action (means to close the modal). You can of course, incorporate different header content, but I provide an approach that displays a header if you pass a title value, along with a dismiss action. The Razor code below shows a template that incorporates these features.

  
   <div tabindex="-1" class="modal fade @ModalZoom @ModalClass" style="display: @ModalDisplay" @attributes="AriaAttributes">
    <div class="modal-dialog @ModalScroll @ModalCentered">
        <div class="modal-content">
            @if (Title != null)
            {
                <div class="modal-header">
                    <h5 class="modal-title">@Title</h5>
                    <button class="btn-close" data-dismiss="modal" aria-label="Close" @onclick="Close">
                    </button>
                </div>
            }
            <div class="modal-body">
                @Body
            </div>
            @if (Footer != null)
            {
                <div class="modal-footer">
                    @Footer
                </div>
            }
        </div>
    </div>
</div>

<div class="modal-backdrop fade @ModalClass" style="display: @ModalDisplay"></div>
  
  

Looking at the C# code-behind, you'll see a few useful parameters that can be used to alter the behavior of the modal. You can switch between the "standard" animation where the modal appears to drop vertically into view, or use a zoom-like animation. You can center the dialog within the view, and you can add a scrollbar to the modal itself, provided that the content is sufficiently long, instead of using the scrollbar for the entire viewport that will appear automatically. I thought it would be fun to provide some scrollable content using a Hebrew Ipsum Lorem text generator.

There's very little actual code or logic required in this component. I automatically add the various aria attributes that you would see using the regular Javascript version of the Bootstrap component. One important feature to pay attention to are the Task.Delay() statements I utilize in the Open() and Close() methods. These are used to ensure that you can actually see the animation effects; without the delays the cool animations would have no time to complete their execution.

  
    
using Microsoft.AspNetCore.Components;

namespace Finaltouch.Modal.App.Shared
{
    /// <summary>
    /// A Blazor Modal Template Component
    /// </summary>
    public partial class DialogTemplate
    {
        [Parameter]
        public RenderFragment? Body { get; set; }
        [Parameter]
        public RenderFragment? Footer { get; set; }
        [Parameter]
        public string? Title { get; set; }
        [Parameter]
        public bool UseZoom { get; set; }
        [Parameter]
        public bool Scrollable { get; set; }
        public bool Centered { get; set; } = true;
        private string ModalDisplay { get; set; } = "none";
        private string ModalClass { get; set; } = string.Empty;
        private string ModalZoom => UseZoom ? "modal-zoom" : string.Empty;
        private string ModalScroll => Scrollable ? "modal-dialog-scrollable" : string.Empty;
        private string ModalCentered => (Centered && !Scrollable) ? "modal-dialog-centered" : string.Empty;

        private Dictionary<string, object> AriaAttributes
        {
            get
            {
                var attributes = new Dictionary<string, object>();
                if ("block".Equals(ModalDisplay))
                {
                    attributes.Clear();
                    attributes.Add("aria-modal", "true");
                    attributes.Add("role", "dialog");
                }
                else
                {
                    attributes.Clear();
                    attributes.Add("aria-hidden", "true");
                }
                return attributes;
            }
        }

        public async Task Open()
        {
            ModalDisplay = "block";
            await Task.Delay(200);
            ModalClass = "show";
        }

        public async Task Close()
        {
            ModalClass = string.Empty;
            await Task.Delay(200);
            ModalDisplay = "none";
        }

    }
}

Some of the code and inspiration for this component came from this thread on Stack Overflow. It's worth reading to see how other folks have created modal components based on Bootstrap.