Reputation: 1342
I am attempting to create table component for Blazor Client Side SPA which implements dynamic display of rows and columns based on items passed. So far I have managed to accomplish the display of rows and columns. Now I wish to implement sorting, I will do this by having a sort button in the header columns.
I have 3 components so far:
DataTable Component (Parent component)
Below are stripped down versions of the code:
Page.razor
@page "/transactions/list"
@using Accounting.Web.Components.Grid;
@using Accounting.Web.Components.DataTable;
<h3>List</h3>
<DataTable Items="users">
<DataTableColumn TRowData="User" Expression="u => u.Id"/>
<DataTableColumn TRowData="User" Expression="u => u.Username" />
<DataTableColumn TRowData="User" Expression="u => u.DateOfBirth" />
<DataTableColumn TRowData="User"><div>Column B</div></DataTableColumn>
</DataTable>
@code {
public class User
{
public int Id { get; set; }
public string Username { get; set; }
public DateTime DateOfBirth { get; set; }
}
public List<User> users { get; set; } = new (){
new User{
Id = 1,
Username = "Me",
DateOfBirth = new DateTime(1981, 12, 23)
},
new User{
Id = 2,
Username = "You",
DateOfBirth = new DateTime(1980, 1, 1)
}
};
}
DataTableRazor.razor
The Data table render each of the column headers and row columns from the data source
@typeparam TRowData <CascadingValue IsFixed="true" Value="this">@ChildContent</CascadingValue> (Items)
@* Render the table *@
<table>
<thead>
<tr>
@foreach (var column in columns)
{
@column.HeaderTemplate;
}
</tr>
</thead>
<tbody>
@{
if (Items != null)
{
var index = 0;
foreach (var item in Items)
{
@* Use @key to help the diff algorithm when updating the collection *@
<tr>
@foreach (var column in columns)
{
@column.CellTemplate(item);
}
</tr>
}
}
}
</tbody>
</table>
@code {
[Parameter]
public ICollection<TRowData> Items { get; set; }
[Parameter]
public RenderFragment ChildContent { get; set; }
private readonly List<DataTableColumn<TRowData>> columns = new();
internal void AddColumn(DataTableColumn<TRowData> column)
{
columns.Add(column);
}
protected override void OnAfterRender(bool firstRender)
{
if (firstRender)
{
StateHasChanged();
}
}
}
DataTableColumn.razor
@typeparam TRowData
@using System.Linq.Expressions
@code {
[CascadingParameter]
public DataTable<TRowData> Owner { get; set; }
[Parameter]
public string Title { get; set; }
[Parameter]
public bool Sortable { get; set; }
[Parameter]
public string Format { get; set; }
[Parameter]
public Expression<Func<TRowData, object>> Expression { get; set; }
[Parameter]
public RenderFragment<TRowData> ChildContent { get; set; }
private RenderFragment<TRowData> cellTemplate;
private RenderFragment headerTemplate;
private Func<TRowData, object> compiledExpression;
private Expression lastCompiledExpression;
public void test()
{
System.Console.WriteLine("test");
}
internal RenderFragment HeaderTemplate
{
get
{
return headerTemplate = (builder =>
{
var title = Title;
if (title == null && Expression != null)
{
title = GetMemberName(Expression);
}
builder.OpenElement(0, "th");
builder.AddContent(1, title);
if (Sortable)
{
builder.OpenComponent(0, typeof(DataTableSort));
builder.CloseComponent();
}
builder.CloseElement();
});
}
}
internal RenderFragment<TRowData> CellTemplate
{
get
{
return cellTemplate ??= (rowData => builder =>
{
builder.OpenElement(0, "td");
if (compiledExpression != null)
{
var value = compiledExpression(rowData);
var formattedValue = string.IsNullOrEmpty(Format) ? value?.ToString() : string.Format("{0:" + Format + "}", value);
builder.AddContent(1, formattedValue);
}
else
{
builder.AddContent(1, ChildContent, rowData);
}
builder.CloseElement();
});
}
}
protected override void OnInitialized()
{
Owner.AddColumn(this);
}
protected override void OnParametersSet()
{
if (lastCompiledExpression != Expression)
{
compiledExpression = Expression?.Compile();
lastCompiledExpression = Expression;
}
}
private static string GetMemberName<T>(Expression<T> expression)
{
return expression.Body switch
{
MemberExpression m => m.Member.Name,
UnaryExpression u when u.Operand is MemberExpression m => m.Member.Name,
_ => throw new NotSupportedException("Expression of type '" + expression.GetType().ToString() + "' is not supported")
};
}
}
The above code is taken from a tutorial online which I am working with.
Now in the HeaderTemplate RenderFragment method you can see I am rendering another component : DataTableSort, see contents below:
<button @onclick="onClicked">sort</button>
@code {
protected async Task onClicked()
{
System.Console.WriteLine("sort");
}
}
This is where I'm getting lost. I need it so that when the button is pressed, an event is passed up to either to the parent Datatable column component or the DataTable grandparent component which where I can sort the items based on the sort button clicked.
What is the best way to go about this?
Upvotes: 0
Views: 447
Reputation: 30001
I use a ListContext
class. The top level component creates an instance and cascades it. Any sub-component can raise an event on the context and any component that needs to react to an event can register an event handler. Mine looks like this:
public sealed class ListContext
{
public event EventHandler<SortRequest>? SortingRequested;
public event EventHandler<PagingRequest?>? PagingRequested;
public event EventHandler<EventArgs>? ListChanged;
public event EventHandler<EventArgs>? PagingReset;
public ListContext() { }
public void NotifyPagingRequested(object? sender, PagingRequest? request)
=> this.PagingRequested?.Invoke(sender, request);
public void NotifySortingRequested(object? sender, SortRequest request)
=> this.SortingRequested?.Invoke(sender, request);
public void NotifyPagingReset(object? sender)
=> this.PagingReset?.Invoke(sender, EventArgs.Empty);
public void NotifyListChanged(object? sender)
=> this.ListChanged?.Invoke(sender, EventArgs.Empty);
}
For reference, the request objects:
public record SortRequest
{
public string? SortField { get; init; }
public bool SortDescending { get; init; }
public bool IsSorting => !string.IsNullOrWhiteSpace(SortField);
}
public record PagingRequest
{
public int PageSize { get; init; } = 1000;
public int StartIndex { get; init; } = 0;
public int Page => StartIndex <= 0
? 0
: StartIndex / PageSize;
public PagingRequest() { }
public PagingRequest(int page)
=> this.StartIndex = PageSize * 0;
}
Upvotes: 2