Building a reusable generic template component for server-side Blazor

The goal is to develop a little bit of UI that can be reused in different Blazor projects or with different data. The component knows nothing about the class it’s working with. The Type is supplied by a generic parameter.

This component was developed for my Server-Side Blazor Workshop but I’m making it freely available because it answers so many fundamental questions about Blazor.

Download the code with sample project here.

Imagine you have a master list of some type of item, and you want the user to be able to select one or more of them for whatever reason. Here is what the UI looks like in our demo:

objectpicker.jpg

The component name is ObjectPicker. Here is the complete source:

@typeparam TItem

<table style="width:100%">
    <tr>
        <td style="width:45%;" valign="top">
            All @ItemTypePlural<br />
        </td>
        <td style="width:10%;" valign="top">
            <span>&nbsp;</span>
        </td>
        <td style="width:45%;" valign="top">
            Selected @ItemTypePlural<br />
        </td>
    </tr>
    <tr>
        <td style="width:45%;" valign="top">
            <select @ondblclick="ItemDblClickedFromAllItems"
                    @onchange="ItemSelectedFromAllItems"
                              size="10" style="width:100%;">
                @foreach (var Item in AllItems)
                {
                    if (@ItemValue(Item) == @ItemValue(SelectedItem))
                    {
                        <option selected value="@ItemValue(Item)">
                            @ItemText(Item)
                        </option>
                    }
                    else
                    {
                        <option value="@ItemValue(Item)">
                            @ItemText(Item)
                        </option>
                    }
                }
            </select>
        </td>
        <td style="width:10%;" valign="top">
            <button @onclick="AddSelectedItem"
                              type="button"
                              disabled="@AddSelectedItemButtonDisabled"
                              style="width:100%;">
                &gt;
            </button><br />
            <button @onclick="@AddAllItems"
                              type="button"
                              style="width:100%;">
                &gt;&gt;
            </button><br />
            <button @onclick="@RemoveSelectedItem"
                              type="button"
                              disabled="@RemoveSelectedItemButtonDisabled"
                              style="width:100%;">
                &lt;
            </button><br />
            <button @onclick="@RemoveAllItems"
                              type="button"
                              style="width:100%;">
                &lt;&lt;
            </button><br />
        </td>
        <td style="width:45%;" valign="top">
            <select @ondblclick="ItemDblClickedFromSelectedItems"
                    @onchange="ItemSelectedFromSelectedItems"
                              size="10" style="width:100%;">
                @foreach (var Item in SelectedItems)
                {
                    if (@ItemValue(Item) == @ItemValue(SelectedItem))
                    {
                        <option selected value="@ItemValue(Item)">
                            @ItemText(Item)
                        </option>
                    }
                    else
                    {
                        <option value="@ItemValue(Item)">
                            @ItemText(Item)
                        </option>
                    }
                }
            </select>
        </td>
    </tr>
</table>

@code {

    [Parameter]
    public string ItemType { get; set; }

    [Parameter]
    public string ItemTypePlural { get; set; }

    [Parameter]
    public string TextPropertyName { get; set; }

    [Parameter]
    public string ValuePropertyName { get; set; }

    [Parameter]
    public List<TItem> AllItems { get; set; }

    [Parameter]
    public List<TItem> SelectedItems { get; set; }

    [Parameter]
    public EventCallback<string> ComponentUpdated { get; set; }

    TItem SelectedItem { get; set; }

    bool AddSelectedItemButtonDisabled = true;
    bool RemoveSelectedItemButtonDisabled = true;

    private string ItemValue(TItem Item)
    {
        return Item.GetType()
         .GetProperty(ValuePropertyName)
         .GetValue(Item, null)
         .ToString();
    }

    private string ItemText(TItem Item)
    {
        return Item.GetType()
         .GetProperty(TextPropertyName)
         .GetValue(Item, null)
         .ToString();
    }

    protected override void OnParametersSet()
    {
        if (AllItems.Count > 0)
        {
            // remove the items that exist in SelectedItems
            foreach (var item in SelectedItems)
            {
                var id = item.GetType()
                   .GetProperty(ValuePropertyName)
                   .GetValue(item, null)
                   .ToString();


                var ItemFromAllItems =
                 (from x in AllItems
                  where x.GetType()
                    .GetProperty(ValuePropertyName)
                    .GetValue(x, null)
                    .ToString() == id
                  select x).FirstOrDefault();

                if (ItemFromAllItems != null)
                {
                    AllItems.Remove(ItemFromAllItems);
                }
            }
        }
        if (AllItems.Count > 0)
        {
            SelectedItem = AllItems.First();
        }
        else if (SelectedItems.Count > 0)
        {
            SelectedItem = SelectedItems.First();
        }
        UpdateButtonEnabledStates();
    }

    void ItemDblClickedFromAllItems()
    {
        AddSelectedItem();
    }

    void ItemDblClickedFromSelectedItems()
    {
        RemoveSelectedItem();
    }

    void ItemSelectedFromAllItems(ChangeEventArgs args)
    {
        SelectedItem =
         (from x in AllItems
          where x.GetType()
            .GetProperty(ValuePropertyName)
            .GetValue(x, null)
            .ToString() == args.Value.ToString()
          select x).FirstOrDefault();

        UpdateButtonEnabledStates();
    }

    void UpdateButtonEnabledStates()
    {
        AddSelectedItemButtonDisabled = !AllItems.Contains(SelectedItem);
        RemoveSelectedItemButtonDisabled = !SelectedItems.Contains(SelectedItem);
    }

    void AddAllItems()
    {
        foreach (var Item in AllItems.ToArray())
        {
            SelectedItems.Add(Item);
        }
        if (SelectedItems.Count > 0)
        {
            SelectedItem = SelectedItems.First();
        }
        AllItems.Clear();
        UpdateButtonEnabledStates();
        ComponentUpdated.InvokeAsync("").Wait();
    }

    void RemoveAllItems()
    {
        foreach (var Item in SelectedItems.ToArray())
        {
            AllItems.Add(Item);
        }
        if (AllItems.Count > 0)
        {
            SelectedItem = AllItems.First();
        }
        SelectedItems.Clear();
        UpdateButtonEnabledStates();
        ComponentUpdated.InvokeAsync("").Wait();
    }

    void AddSelectedItem()
    {
        if ((from x in SelectedItems
             where ItemValue(x) == ItemValue(SelectedItem)
             select x).FirstOrDefault() == null)
        {
            SelectedItems.Add(SelectedItem);
            AllItems.Remove(SelectedItem);
            UpdateButtonEnabledStates();
            ComponentUpdated.InvokeAsync("").Wait();
        }
    }

    void RemoveSelectedItem()
    {
        if ((from x in AllItems
             where ItemValue(x) == ItemValue(SelectedItem)
             select x).FirstOrDefault() == null)
        {
            AllItems.Add(SelectedItem);
            SelectedItems.Remove(SelectedItem);
            UpdateButtonEnabledStates();
            ComponentUpdated.InvokeAsync("").Wait();
        }
    }

    void ItemSelectedFromSelectedItems(ChangeEventArgs args)
    {
        SelectedItem =
         (from x in SelectedItems
          where x.GetType()
            .GetProperty(ValuePropertyName)
            .GetValue(x, null)
            .ToString() == args.Value.ToString()
          select x
         ).FirstOrDefault();
        UpdateButtonEnabledStates();
    }
}

Let’s take a look at the very first line:

@typeparam TItem

This is how we can define a data type passed in as parameters. Look at the AllItems parameter:

    [Parameter] public List<TItem> AllItems { get; set; }

When you create an instance of this component, you pass in a list of whatever you want.

The component has to have a way to access the properties we need to use, one for the text that gets displayed, and another for the value (Id) that identifies the object. We expose these property names as parameters:

    [Parameter]
    public string TextPropertyName { get; set; }

    [Parameter]
    public string ValuePropertyName { get; set; }

Take a look at how we instantiate this component:

<ObjectPicker @ref="InstrumentPicker"
              ItemType="Instrument"
              ItemTypePlural="Instruments"
              AllItems="Instruments"
              SelectedItems="SelectedInstruments"
              TextPropertyName="Name"
              ValuePropertyName="InstrumentId"
              ComponentUpdated = "ComponentUpdated"
              />

ItemType and ItemTypePlural are strings that define what the user is looking at.

TextPropertyName is the name of the property in the class that will be displayed

ValuePropertyName is the name of the Id property, in this case InstrumentId

AllItems is a list of all the items that show up on the left.

SelectedItems is a list of all the items on the right

In the ObjectPicker itself, we have this field:

TItem SelectedItem { get; set; }

Whenever an item is selected in either of the <select> boxes, this is the item that was clicked on.

Let’s look at the <select> on the left, which shows AllItems:

            <select @ondblclick="ItemDblClickedFromAllItems"
                    @onchange="ItemSelectedFromAllItems"
                              size="10" style="width:100%;">
                @foreach (var Item in AllItems)
                {
                    if (@ItemValue(Item) == @ItemValue(SelectedItem))
                    {
                        <option selected value="@ItemValue(Item)">
                            @ItemText(Item)
                        </option>
                    }
                    else
                    {
                        <option value="@ItemValue(Item)">
                            @ItemText(Item)
                        </option>
                    }
                }
            </select>

Notice we’re getting the display text and value from two methods: ItemValue and ItemText:

    private string ItemValue(TItem Item)
    {
        return Item.GetType()
            .GetProperty(ValuePropertyName)
            .GetValue(Item, null)
            .ToString();
    }

    private string ItemText(TItem Item)
    {
        return Item.GetType()
            .GetProperty(TextPropertyName)
            .GetValue(Item, null)
            .ToString();
    }

We use a bit of reflection to get the values of these properties. We also use the same technique in a LINQ query to match the selected item based on the value of the value property. This method is called when an item is selected:

    void ItemSelectedFromAllItems(ChangeEventArgs args)
    {
        SelectedItem =
        (from x in AllItems
         where x.GetType()
             .GetProperty(ValuePropertyName)
             .GetValue(x, null)
             .ToString() == args.Value.ToString()
         select x).FirstOrDefault();

        UpdateButtonEnabledStates();
    }

The rest of it is just enabling and disabling buttons, and moving items from one side to the other.

The host app can use the SelectedItems list to update the actual objects, entities, or what have you.

Want more Blazor? How about a one-day workshop online? Details at http://blazor.appvnext.com

Carl



Carl Franklin has been a key leader in the Microsoft developer community since the very early days when he wrote for Visual Basic Programmers Journal. He authored the Q&A column of that magazine as well as many feature articles for VBPJ and other magazines. He has authored two books for John Wiley & Sons on sockets programming in VB, and in 1994 he helped create the very first web site for VB developers, Carl & Gary's VB Home Page.

Carl is a Microsoft MVP for Developer Technologies, and co-host of .NET Rocks!, one of the longest running podcasts ever (2002). Carl is also an accomplished musician and audio/video producer. He started Pwop Studios in 1999 as a record label for his first album, a collaboration with his brother Jay: Strange Communication. Franklin Brothers released Lifeboat To Nowhere in 2011, which has met with rave reviews. In 2013, Carl released his first solo album, Been a While, which features a tune with John Scofield on guitar, as well as an incredible group of musicians local to New London, CT.

Pwop Studios is a full-service audio and video post production studio in New London, CT, where Carl records and produces the podcasts as well as music and video projects - both for himself, Franklin Brothers Band, and the public.