ASP.NET - Blazor - returning clicked generic type from templated component

Viewed 1218

I'm learning Blazor and ASP.NET and have been learning C# for the last 6 months.

I have made a simple templated component:

@typeparam GenericType

<ul>
    @foreach (GenericType item in Items)
    { 
        <li @onclick="(x)=> ItemClicked(item)">FragmentToRender(item)</li>
    }    
</ul>

@code {
    [Parameter] public RenderFragment<GenericType> FragmentToRender { get; set; }

    [Parameter] public IReadOnlyList<GenericType> Items { get; set; }

    public void ItemClicked(GenericType item)
    {
         //To figure out...
    }
}

And I'm using it in a page component:

<TestComponent GenericType="Thing" Items="ListOfThings">
    <FragmentToRender>
        <p>@context.Field</p>
    </FragmentToRender>
</TestComponent>

@code
{
    private List<Thing> ListOfThings =
        new List<Thing> {
            new Thing("Test"),
            new Thing("Test2")
        };

    public class Thing
    {
        public readonly string Field;

        public Thing(string field) => Field = field;
    }    
}

When the OnClick event of the li element in the component is triggered, how can I pass the specific instance of the item back to the page component (i.e so a different component can do something with the clicked item like upload it's data somewhere)?

Many Thanks

2 Answers

You should use an EventCallback to pass the data.

@typeparam GenericType

<ul>
    @foreach (GenericType item in Items)
    { 
        <li @onclick="(x)=> ItemClicked(item)">FragmentToRender(item)</li>
    }    
</ul>

@code {
    [Parameter] public RenderFragment<GenericType> FragmentToRender { get; set; }

    [Parameter] public IReadOnlyList<GenericType> Items { get; set; }

    // Added EventCallback parameter
    [Parameter] public EventCallback<GenericType> OnClick { get; set; }

    public void ItemClicked(GenericType item)
    {
         // Checking if EventCallback is set
         if(OnClick.HasDelegate)
         {
             // Calling EventCallback
             OnClick.InvokeAsync(item);
         }
    }
}

And then just pass the parameter OnClick to that component to get the item

@* Passing the OnClick parameter *@
<TestComponent GenericType="Thing" Items="ListOfThings" OnClick="@HandleClick">
    <FragmentToRender>
        <p>@context.Field</p>
    </FragmentToRender>
</TestComponent>

@code
{

    private void HandleClick(Thing item)
    {
        // Do what you want with the item
    }

    private List<Thing> ListOfThings =
        new List<Thing> {
            new Thing("Test"),
            new Thing("Test2")
        };

    public class Thing
    {
        public readonly string Field;

        public Thing(string field) => Field = field;
    }    
}

Note: I've made some alteration in your code sample... Point to note:

  • Add a @ symbol before FragmentToRender(item). It instructs the compiler to treat FragmentToRender(item) as executable code. Otherwise, it is used as the content of the li element.

  • In the second version of the li element, we place the the event call back in the body of the lambda expression. If you use this version, comment out the
    ItemClicked method.

TemplatedComponent.razor

 @typeparam GenericType

<ul>
    @foreach (GenericType item in Items)
    {
        <li @onclick="() => ItemClicked(item)">@FragmentToRender(item)</li>
        @*<li @onclick="@(() => SelectedItem.InvokeAsync(item))">@FragmentToRender(item)</li>*@
    }
</ul>

@code {
[Parameter] public RenderFragment<GenericType> FragmentToRender { get; set; }

[Parameter] public IReadOnlyList<GenericType> Items { get; set; }
// Define an event call back property.
[Parameter] public EventCallback<GenericType>  SelectedItem { get; set; }

public async Task ItemClicked(GenericType item)
{
    // Check if the event call back property contains a delegate. It's 
    // important to understand that the EventCallback type is not a true 
    // delegate. It is actually a struct that may contain a delegate 
    if(SelectedItem.HasDelegate)
    {
      await  SelectedItem.InvokeAsync(item);
    }

}
}

TestComponent.razor

<TemplatedComponent GenericType="Thing" Items="ListOfThings" 
                                     SelectedItem="SelectedItem">
    <FragmentToRender>
        <p>@context.Field</p>
    </FragmentToRender>
</TemplatedComponent>


@code
{
 // Define a method that will be called by the event call back 'delegate'. It
 // receives a single parameter from the calling delegate.
private async Task SelectedItem(Thing item)
{
    Console.WriteLine(item.Field);
    await Task.CompletedTask;
}
private List<Thing> ListOfThings =
    new List<Thing> {
        new Thing("Test"),
        new Thing("Test2")
        };

public class Thing
{
    public readonly string Field;

    public Thing(string field) => Field = field;
}
}

Index.razor

 @page "/"

<TestComponent/>

Hope this helps...

Related