Blazor - Simple Data Grid

I have a reoccurring use case to for a simple data grid in many of my applications.  There are several good data grids for Blazor available that have a lot of features and can display millions of records.  My needs are not that advance, simple functionality and less than 1000 records.  Just enough that a regular table is not enough, and the full data grids are a little overkill. I could get one of the available grids and only use the parts I need, but I thought I would be better to build own for a couple of reasons.
  1. Get more experience with Blazor 
  2. Fully understand what it takes to build a reusable UI component 
  3. I did not want to have to learn new syntax for a data grid 

My requirements for the data grid are: 
  • CSS styled 
  • Sort-able columns 
  • With indicator 
  • Show record selected 
  • Pagination of data 

I am going to take the Fetch data sample Blazor application and update it to use my new data grid. 

CSS Styled 

This was the easy requirement.  I kept the bootstrap style used for table formatting in the sample.  Since I am just using a normal html table, any CSS framework that has table formatting built in would work the same.  You would just have to update the  table classes.  

Sort-able Columns 

I broke this requirement into 3 parts: 
  1. Make the header clickable 
  2. Handle the click 
  3. Show the sort direction indicator 

Part 1 – Make the header clickable 

To do this you add an on-click event to each header: 
    <th @onclick='(() => Sort("Column Name", Column Index))'>

We pass the column name to use for the actual sorting.  We need the column index, so we know what column was clicked on to set the direction indicator.  

Part 2 – Handle the click event 

To handle the actual sort, I use a standard generic sort method to sort a collection based on the property we are sorting on.  You pass in the collection as an IQueryable, the property to sort on and the sort direction, ascending or descending.  You can see the source code for the details. 

This sort method does use reflection, so if you have a large collection, there might be better ways to sort.  

Part 3 – Show the sort direction indicator 

This requirement ended up being the most challenging for a couple of reasons.  First, I had to add the span to hold the indicator: 
    <span class="fa @(GetSortStyle(column Index, sortIndex, sortDirection))"></span> 

Column Index is the column number we are on.  The sort index is a page level variable of the column we are sorting.  Sort direction is up or down value. 

We need to have the sort index so only display the indicator on the column that was sorted, otherwise the indicator shows on every column. 

The tricky part of this was finding and using a good indicator for the sort direction.  I found that Font Awesome has a good indicator, but to my surprise, the Font Awesome package is no included in the standard bootstrap framework.  I had to add the link to _Host.cshtml:
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/all.css" integrity="sha384-50oBUHEmvpQ+1lW4y57PTFmhCaXp0ML5d60M1M7uH2+nqUivzIebhndOJK28anvf" crossorigin="anonymous"> 

I do know that there is a sort indicator in bootstrap, class='icon-arrow-up', but did not show consistently, so I went with the Font Awesome one. 

Show Row Selected 

This requirement was to highlight the row when clicked on.  This is useful when we add action buttons so the user knows which record, they will be working with. 

This was handled in 3 parts: 
  1. Set the row index 
  2. Set the On Click event 
  3. Handle the Click Event

Part 1 Set the row index 
Since we can not assume that all objects we will be using to display in our grid will have a index value we can set to keep track of the row they are on, we have to generate that when we build the rows. 

Just like most table population process, we will loop through a collection and build out the row.  In most of these cases a foreach is used.  We will need to use a for loop, so we have access to the loop counter. 
                @for (int i = 0; i < data.Count; i++) 
                { 
                    int index = i + 1; 
                    <tr @onclick='(() => RowClickEvent(index))'> 
                        <td>@data[i].Date.ToShortDateString()</td> 
                        <td>@data[i].Summary</td> 
                        <td>@data[i].TemperatureC</td> 
                        <td>@data[i].TemperatureF</td> 
                    </tr> 
                } 

Note that we are setting the var index to the value of i.  In C#, you cannot access the increment  value inside of the loop, it always returns the max value. 

We need to set our own index because with the current event arguments available in Blazor, we can not get to the actual HTML element that was clicked on. 

Part 2 Set the On Click event 
You can see for each table row, <tr> we are adding an on click event, RowClickEvent and passing in the index.  That will be the row that is clicked.  We need to know what row we are setting the highlight CSS on. 

Part 3 – Handle the On Click 
All we are doing to show which row was selected is to change the background color of the row to a highlight color, for us it is a yellow.  Setting a css class on an element in Blazor is fairly straight forward. 

The tricky part if that we will need to remove any other highlight css on any other row that might be previously selected.  Currently the only way to do this is via JavaScript using JSInterop.

JavaScript:

        function hightlight_row(rowId) { 
            var table = document.getElementById('datatable'); 
            var cells = table.getElementsByTagName('td');  
            var rowsNotSelected = table.getElementsByTagName('tr'); 
            for (var row = 0; row < rowsNotSelected.length; row++) { 
                rowsNotSelected[row].style.backgroundColor = ""; 
                rowsNotSelected[row].classList.remove('selected'); 
            } 
            if (rowId > 0) { 
                var rowSelected = table.getElementsByTagName('tr')[rowId]; 
                rowSelected.style.backgroundColor = "yellow"; 
                rowSelected.className += " selected"; 
            } 
      }

C#
    await JSRuntime.InvokeVoidAsync("hightlight_row", rowIndex);

As you can see, we get the table, then all the cells.  We then go through and remove the “selected” css class from all the rows.  This will actually come in handy when we sort and click on the pagination.  Next we check to make sure we have a no-zero index.  Then for that row, we add our “selected” css and add the yellow background. 

When we sort or do a pagination, we call this script with a 0 as the index and it will remove any highlights.  We want to do this because we don’t to change the records in the table but have a stall row selected.  

Pagination of data 

For this requirement I wanted the following functionality: 
  • Be able to set the page size 
  • Provide a UI to allow the user to skip to different pages 
  • Limit the number of page button that are displayed to a certain amount 
  • Provide a Previous button 
  • Provide a next button 
  • Current page number is highlighted 
  • Have previous and next button not enabled if on first or last page respectfully 

Since we are in Blazor, we are creating a component.  We will need to use parameters to pass in the page variables we need to control the pagination UI.

The main part of this was defining the UI.  Bootstrap has a built-in pagination control.  That is what I start with: 
            <nav aria-label="Page navigation example"> 
                <ul class="pagination"> 
                    <li class="page-item"><a class="page-link" href="#">Previous</a></li> 
                    <li class="page-item"><a class="page-link" href="#">1</a></li> 
                    <li class="page-item"><a class="page-link" href="#">2</a></li> 
                    <li class="page-item"><a class="page-link" href="#">3</a></li> 
                    <li class="page-item"><a class="page-link" href="#">Next</a></li> 
               </ul> 
            </nav> 

We need to track additional information: 
  • Page Count 
  • Total number of records in the collection / page size 
  • Page Size 
  • Set by user 
  • Current Page 
  • The correct record set that is displayed 

As we change pages, we will need to highlight and enable and disable controls.  
    @if (PageCount > 1) 
    { 
        <ul class="pagination justify-content-end"> 
        @if (ShowFirstLast) 
        { 
            if (CurrentPage == 1) 
            { 
                <li class="page-item disabled noselect"><a class="page-link" tabindex="-1"><span aria-  hidden="true">&laquo;</span><span class="sr-only">@FirstText</span></a></li> 
            } 
            else 
            { 
                <li class="page-item noselect"><a class="page-link sort-link" @onclick="@(() => PagerButtonClicked(1))"><span aria-hidden="true">&laquo;</span><span class="sr-only">@FirstText</span></a></li> 
            } 
        } 
        @if (HasPrevious) 
        { 
            <li class="page-item noselect"><a class="page-link sort-link" @onclick="@(() => PagerButtonClicked(CurrentPage - 1))"><span aria-hidden="true">@PreviousText</span><span class="sr-only">Go to previous page</span></a></li> 
        } 
        else 
        { 
            <li class="page-item disabled noselect"><a class="page-link" tabindex="-1"><span aria-hidden="true">@PreviousText</span><span class="sr-only">Go to previous page</span></a></li> 
        } 
        @if (ShowPageNumbers) 
        { 
            for (var i = Start; i <= Finish; i++) 
            { 
                var currentIndex = i; 
                if (i == CurrentPage) 
                { 
                    <li class="page-item active noselect"><a class="page-link">@i</a></li> 
                } 
                else 
                { 
                    <li class="page-item noselect"><a class="page-link sort-link" @onclick="@(() => PagerButtonClicked(currentIndex))">@currentIndex</a></li> 
                } 
            } 
        } 
        @if (HasNext) 
        { 
            <li class="page-item"><a class="page-link sort-link" @onclick="@(() => PagerButtonClicked(CurrentPage + 1))"><span aria-hidden="true">@NextText</span><span class="sr-only">Go to next page</span></a></li> 
        } 
        else 
        { 
            <li class="page-item disabled"><a class="page-link" href="#" tabindex="-1"><span aria-hidden="true">@NextText</span><span class="sr-only">Go to next page</span></a></li> 
        } 
        @if (ShowFirstLast) 
        { 
            if (CurrentPage == PageCount) 
            { 
                <li class="page-item disabled"><a class="page-link" href="#" tabindex="-1"><span aria-hidden="true">@LastText</span><span class="sr-only">>Go to last page</span></a></li> 
            } 
            else 
            { 
                <li class="page-item"><a class="page-link sort-link" @onclick="@(() => PagerButtonClicked(PageCount))"><span aria-hidden="true">@LastText</span><span class="sr-only">Go to last page</span></a></li> 
            } 
        } 
    </ul>  

We need to wire in what happens when the user clicks on a page button.  Pagination is a component that is not of aware of the collection if is paging through.  We pass in a function to be called from the parent to actual page the collection.    
    [Parameter] public Func<int, Task> OnPageChanged { get; set; } = null; 

    private void PagerButtonClicked(int page) 
    { 
        OnPageChanged?.Invoke(page); 
    } 

In the actual parent event handler, we can user standard linq to do the paging. 

        public List<T> GetPage(IQueryable<T> data, int page )
        { 
            if (page > 0 && page <= PageCount) 
            { 
                return (from a in data 
                         select a).Skip(page - 1).Take(PageSize).ToList(); 
            } 
            return data.ToList(); 
        }  

In addition, by placing this in a service, we can generalize it and handle any collection type. 

        public List<T> Sort(IQueryable<T> data, string sortProperty, string sortDir)
        {
            if (data != null && sortProperty.IsNotEmpty())
            {
                if(sortDir.IsEmpty())
                {
                    sortDir = "OrderBy";
                }
                return RecordSorterHelper.ApplyOrder<T>(data.AsQueryable(), sortProperty, sortDir).ToList();
            }
            return data.ToList();
        }


This implementation will give us a simple but very useful data grid we can re-use with minimum code duplication. 

Next Steps 

So what else can we add to add even more features: 
  • Search 
  • Filtering 
  • Movable columns 
  • Saved layout 

These all are excellent ideas that maybe we will try and blog about in the future or if we really need these additional features, we can use one the Blazor data grids that are ready available. 



Comments

Popular posts from this blog

Yes, Blazor Server can scale!

Blazor new and improved Search Box

Blazor Wizard Step Component