I'm on a bit of a Blazor kick lately. Blazor sounds amazing, and to be honest when I hear the words "you can write C# and run it in the browser", all I can think is, "where do I sign up?"
In my previous post I walked through creating a new Blazor application in .NET Core and using it to manage a list of To-Do items.
It occurred to me that something very basic was missing from that previous post: the ability to sort the list of To-Do items by the columns in the display table. Given that Blazor is compiled to WebAssembly, and that I suck at Javascript, my go-to solutions like jQuery Datatables probably wouldn't work in a Blazor app (though to be fair, I haven't tried them).
So, why not do it myself, and learn a bit more about Blazor in the process? Come along with me as I take our sample Blazor project and refactor it to make the To-Do items table sortable.
The Sample Project
There's a sample project with the code in this post over on GitHub. Check it out!
The Starting Page
We'll start with that To-Do list page markup from the earlier post. For posterity, here's that page:
@page "/todo"
@inject HttpClient Http
<h1>To-Do List</h1>
@if (items == null)
{
<p><em>Loading...</em></p>
}
else if (!items.Any())
{
<p><em>No ToDo items exist. Please add some.</em></p>
}
else
{
<table class="table">
<thead>
<tr>
<th>Remove</th>
<th>Date</th>
<th>Description</th>
<th>Is Complete</th>
</tr>
</thead>
<tbody>
@foreach (var item in items)
{
<tr>
<td><button onclick="@(() => RemoveTodo(item.ID))"><i class="oi oi-trash"></i></button></td>
<td>@item.DateCreated</td>
<td>@item.Description</td>
<td><input type=checkbox onchange="@(() => ToggleToDo(item.ID))" /> @item.IsComplete</td>
</tr>
}
</tbody>
</table>
}
@if (items != null)
{
<input placeholder="A new ToDo item" bind="@newItem" />
<button onclick="@AddTodo">Create</button>
}
@functions{
IList<ToDoItem> items = new List<ToDoItem>();
private string newItem;
protected override async Task OnInitAsync()
{
items = await Http.GetJsonAsync<List<ToDoItem>>("sample-data/todoitems.json");
}
private void AddTodo()
{
if (!string.IsNullOrWhiteSpace(newItem))
{
items.Add(new ToDoItem()
{
DateCreated = DateTime.Now,
Description = newItem,
ID = Guid.NewGuid()
});
newItem = string.Empty; //We need to reset this string,
//otherwise the text box will
//still contain the value typed in.
}
}
private void ToggleToDo(Guid id)
{
//First find the item
var todo = items.First(x => x.ID == id);
todo.IsComplete = !todo.IsComplete;
}
private void RemoveTodo(Guid id)
{
items.Remove(items.First(x => x.ID == id));
}
class ToDoItem
{
public Guid ID { get; set; }
public string Description { get; set; }
public bool IsComplete { get; set; }
public DateTime DateCreated { get; set; }
}
}
You can also see it in the sample project on GitHub. (Please note that the sample project was coded against Preview 3 of .NET Core 3, and may not work with other previews. When Blazor goes full RTM, I will update the sample project to work against that version of .NET Core.)
From this baseline, we can start creating the HTML and C# that will allow us to sort this table.
Requirements
When we're done with this improvement, we want the following things:
- The list of tasks is able to be sorted by Date and Description columns, in both ascending and descending order.
- We want to show an icon indicating the current sort direction.
Let's get going!
The Look
Here's what our ToDo list item table looks like at the moment:
<table class="table">
<thead>
<tr>
<th>Remove</th>
<th>Date</th>
<th>Description</th>
<th>Is Complete</th>
</tr>
</thead>
<tbody>
@foreach (var item in items)
{
<tr>
<td><button onclick="@(() => RemoveTodo(item.ID))"><i class="oi oi-trash"></i></button></td>
<td>@item.DateCreated</td>
<td>@item.Description</td>
<td><input type=checkbox onchange="@(() => ToggleToDo(item.ID))" /> @item.IsComplete</td>
</tr>
}
</tbody>
</table>
The first thing we need to do is make the Date and Description header text look like they are clickable, and the simplest way to do that is to make them look like links.
Here's a CSS class which does just that:
.sort-link{
cursor:pointer;
color:blue;
text-decoration:underline;
}
<table class="table">
<thead>
<tr>
<th>Remove</th>
<th>
<span class="sort-link">Date</span>
</th>
<th>
<span class="sort-link">Description</span>
</th>
<th>Is Complete</th>
</tr>
</thead>
<tbody>
...
</tbody>
</table>
Now, I know there is probably a better way to do this, but the fact is that I suck at CSS and this was the best I could come up with. If you've got a better way, let me know in the comments!
Now our issue is this: when the header sort links are clicked, what should happen? To solve that, we need a new method added to the @functions
block:
@functions{
IList<ToDoItem> items = new List<ToDoItem>();
//We need a field to tell us which direction
//the table is currently sorted by
private bool IsSortedAscending;
//We also need a field to tell us which column the table is sorted by.
private string CurrentSortColumn;
...
private void SortTable(string columnName)
{
//Sorting against a column that is not currently sorted against.
if(columnName != CurrentSortColumn)
{
//We need to force order by ascending on the new column
//This line uses reflection and will probably
//perform inefficiently in a production environment.
items = items.OrderBy(x =>
x.GetType()
.GetProperty(columnName)
.GetValue(x, null))
.ToList();
CurrentSortColumn = columnName;
IsSortedAscending = true;
}
else //Sorting against same column but in different direction
{
if(IsSortedAscending)
{
items = items.OrderByDescending(x =>
x.GetType()
.GetProperty(columnName)
.GetValue(x, null))
.ToList();
}
else
{
items = items.OrderBy(x =>
x.GetType()
.GetProperty(columnName)
.GetValue(x, null))
.ToList();
}
//Toggle this boolean
IsSortedAscending = !IsSortedAscending;
}
}
}
The method SortTable()
should be run every time one of the header sort links is clicked. Therefore we need a delegate which runs that method, to which we must pass the name of the property we are sorting by (which is not necessarily the display name of the table column).
That looks like this:
<table class="table">
<thead>
<tr>
<th>Remove</th>
<th>
<span class="sort-link" onclick="@(() => SortTable("DateCreated"))">Date</span>
</th>
<th>
<span class="sort-link" onclick="@(() => SortTable("Description"))">Description</span>
</th>
<th>Is Complete</th>
</tr>
</thead>
<tbody>
...
</tbody>
</table>
When you run the app, the table now looks something like this...
...and the columns get sorted! We've done it!
Except, which column is currently being sorted by? There's no obvious indicator of such a thing. Let's build one.
The Indicator
What I want now is a little arrow icon to be displayed next to the column header which the table is currently being sorted by. This example will use FontAwesome's icons for the arrows, so if your project doesn't already have FontAwesome you will need to include it.
The first issue we have is: which column needs the icon? We've already solved that by using a CurrentSortColumn
property on our page. Because we now have a IsSortedAscending
boolean property, we know which direction the sort is currently going. All we need do now is combine those properties with some onclick
delegates and a little CSS, which gets us this:
<table class="table">
<thead>
<tr>
<th>Remove</th>
<th>
<span class="sort-link" onclick="@(() => SortTable("DateCreated"))">Date</span>
<span class="fa @(GetSortStyle("DateCreated"))"></span>
</th>
<th>
<span class="sort-link" onclick="@(() => SortTable("Description"))">Description</span>
<span class="fa @(GetSortStyle("Description"))"></span>
</th>
<th>Is Complete</th>
</tr>
</thead>
<tbody>
<!--...-->
</tbody>
</table>
@functions{
IList<ToDoItem> items = new List<ToDoItem>();
private bool IsSortedAscending;
private string CurrentSortColumn;
...
private string GetSortStyle(string columnName)
{
if(CurrentSortColumn != columnName)
{
return string.Empty;
}
if(IsSortedAscending)
{
return "fa-sort-up";
}
else
{
return "fa-sort-down";
}
}
private void SortTable(string columnName)
{
if(columnName != CurrentSortColumn)
{
//We need to force order by descending on the new column
items = items.OrderBy(x =>
x.GetType()
.GetProperty(columnName)
.GetValue(x, null))
.ToList();
CurrentSortColumn = columnName;
IsSortedAscending = true;
}
else //Sorting against same column but in different direction
{
if(IsSortedAscending)
{
items = items.OrderByDescending(x =>
x.GetType()
.GetProperty(columnName)
.GetValue(x, null))
.ToList();
}
else
{
items = items.OrderBy(x =>
x.GetType()
.GetProperty(columnName)
.GetValue(x, null))
.ToList();
}
IsSortedAscending = !IsSortedAscending;
}
}
}
Tada! We now have icons that appear in our columns, like so:
Now it's obvious which column is being sorted by!
Drawbacks
At time of writing, Blazor does not support partial views. The moment it does, I will be able to make this solution much more friendly to maintain, but for now this will have to do (and it's pretty good for something that was experimental a few weeks ago).
Also, I will freely admit that I am a terrible front-end programmer and there's probably a much better way to do this with Blazor. That said, I fully believe solving little problems like this one is a FANTASTIC way to learn about a framework or system in a more comprehensive fashion. We can always come back later to make it better.
Summary
Our system is now able to sort the To-Do items by Date and Description. Each column can be sorted independently, and either in ascending or descending order. Finally, we have little arrow icons to indicate which direction the sorting is using.
The most important thing, though, is we got a little more experience using Blazor. I, for one, cannot friggin wait until Blazor is a full-fledged member of .NET Core. C# in the browser? Sign me up!
Don't forget to check out the sample project over on GitHub!
Happy Coding!