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:
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> </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%;"> > </button><br /> <button @onclick="@AddAllItems" type="button" style="width:100%;"> >> </button><br /> <button @onclick="@RemoveSelectedItem" type="button" disabled="@RemoveSelectedItemButtonDisabled" style="width:100%;"> < </button><br /> <button @onclick="@RemoveAllItems" type="button" style="width:100%;"> << </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