Musaffar Patel
Musaffar Patel

Reputation: 1342

Passing an event from a dynamically rendered component up to parent and / or Grandparent

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

Answers (1)

MrC aka Shaun Curtis
MrC aka Shaun Curtis

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

Related Questions