blazor-webassemblymudblazor

How to bind MudAutocomplete value to a context item in a MudTable?


I’m working with a view model that contains collections displayed in MudTables. I need to edit complex items in these tables and implement search functionality using MudAutocomplete.

Here’s the issue: I’ve bound the context item of the table to the autocomplete value. However, when I search for an item, the original value doesn’t change. I also can’t clear the autocomplete, and the validation isn’t triggered.

I expect to be able to modify the value in the autocomplete input and change the context element in the associated items in the table.

we have this try mudblazor as example https://try.mudblazor.com/snippet/cuQyaKkyqKQQUoGG

Here’s a simplified version of my code:

<MudGrid>
    <MudItem xs="12">
        <MudCard Elevation="0">
            <MudCardContent>
                <MudForm @ref="_form" Model="@_selectedTeam" Validation="@(_selectedTeamValidator.ValidateValue)" ValidationDelay="0" IsValid="@(_selectedTeamValidator.Validate(_selectedTeam).IsValid)">
                    <MudStack>
                        <MudText Typo="Typo.h4" Style="text-transform: uppercase">S&P Team</MudText>
                        <MudTable Items="@_selectedTeam.TeamMembers" Dense="true" CanCancelEdit="true" HorizontalScrollbar="true" Breakpoint="Breakpoint.None">
                            <HeaderContent>
                                <MudTh>Role</MudTh>
                                <MudTh>Name</MudTh>
                            </HeaderContent>
                            <RowTemplate>
                                <MudTd DataLabel="Role">@context.Role</MudTd>
                                <MudTd DataLabel="Name">@context.DisplayName</MudTd>
                            </RowTemplate>
                            <RowEditingTemplate>
                                <MudTd DataLabel="Role">@context.Role</MudTd>
                                <MudTd DataLabel="Name">
                                    @if (context.Role == "ResponsibleAdvisor")
                                    {
                                        <MudAutocomplete T="SalesProcessParty"
                                            @bind-Value="@context"
                                            @ref="_responsibleAdvisorAutocomplete"
                                            Validation=@(async () => await _teamMemberValidator.ValidatePropertyValueAsync(context, "PartyId"))
                                            For="@(() => context)"
                                            ToStringFunc="@(x => x?.DisplayName)"
                                            SearchFunc="GetEmployees"
                                            Variant="Variant.Outlined"
                                            Margin="Margin.Dense" />
                                    }

                                    @if (context.Role == "SupportingAdvisor")
                                    {
                                        <MudAutocomplete T="SalesProcessParty"
                                                         @bind-Value="@context"
                                                         @ref="_supportingAdvisorAutocomplete"
                                                         For="@(() => context)"
                                                         Immediate="false"
                                                         Clearable="true"
                                                         ResetValueOnEmptyText="true"
                                                         ToStringFunc="@(x => x.DisplayName)"
                                                         SearchFunc="GetEmployees"
                                                         Variant="Variant.Outlined"
                                                         Margin="Margin.Dense" />
                                    }
                                </MudTd>
                            </RowEditingTemplate>
                        </MudTable>
                    </MudStack>
                </MudForm>
            </MudCardContent>
        </MudCard>
    </MudItem>
</MudGrid>

@code {
    private bool _processing;

    private TeamMemberValidator _teamMemberValidator = new();
    private CustomerMemberValidator _customerMemberValidator = new();

    private CustomerTeamValidator _selectedTeamValidator = new();
    private CustomerTeamViewModel _selectedTeam = new();
    private MudForm _form;

    protected override void OnInitialized()
    {
        _selectedTeam = new()
        {
            TeamMembers = new List<SalesProcessParty>()
            {
                new(){
                    PartyId = Guid.NewGuid(),
                    FirstName = "Lead",
                    LastName = "Leader",
                    Role = "ResponsibleAdvisor"
                },
                new(){
                    PartyId = Guid.NewGuid(),
                    FirstName = "Support",
                    LastName = "Supporter",
                    Role = "SupportingAdvisor"
                }
            },
            CustomerMembers = new List<SalesProcessParty>()
            {
                new(){
                    PartyId = Guid.NewGuid(),
                    FirstName = "Customer",
                    LastName = "Contact",
                    Role = "CustomerContact"
                }
            }
        };

        base.OnInitialized();
    }

    private async Task<IEnumerable<SalesProcessParty>> GetEmployees(string searchText)
    {
        if (string.IsNullOrEmpty(searchText) || string.IsNullOrWhiteSpace(searchText))
            return new List<SalesProcessParty>();

        await Task.Delay(2000);
        var persons = parties.Where(x => !_selectedTeam.TeamMembers.Select(y => y.PartyId).Contains(x.PartyId) && x.DisplayName.Contains(searchText)) ?? new List<SalesProcessParty>();
        return persons;
    }

    private IList<SalesProcessParty> parties = new List<SalesProcessParty>()
    {
        new()
        {
            PartyId = Guid.NewGuid(),
            FirstName = "Person",
            LastName = "One",
        },
        new()
        {
            PartyId = Guid.NewGuid(),
            FirstName = "Person",
            LastName = "Two",
        },
        new()
        {
            PartyId = Guid.NewGuid(),
            FirstName = "Person",
            LastName = "Three",
        },
        new()
        {
            PartyId = Guid.NewGuid(),
            FirstName = "Person",
            LastName = "Four",
        },
        new()
        {
            PartyId = Guid.NewGuid(),
            FirstName = "Person",
            LastName = "Five",
        }
    };

}

Solution

  • Welcome to MudBlazor, there are a number of issues but lets start at the obvious one

    1. You can not two-way or write bind a value of a cell in a row to @context of the row itself.
      Your MudAutocomplete is inside a row, so your binding can only affect properties of that row, you cannot magically change the row itself. Think about this, if the binding worked and your selection results in no object being selected, then the row itself should cease to exist...

      Think of the row as a container, the MudAutocomplete in a cell of that container can only change a value in the container, it cannot change the container itself.

    2. It is also not obvious what is the point of creating multiple auto complete references that all have the same configuration. If your intent was to filter the selections via the context.Role then the better solution is to pass this value through to the search function.

    3. Another observation is that you are binding object references and not lookup values, except that your list of objects in the drop down does not have any corresponding reference to the values in TeamMembers

    Autocomplete in MudBlazor is no different to combo boxes or drop down lists in other frameworks, the standard rules of engagement apply. You can use them to select a primitive or static value from an existing list that can be copied across by value, but to render correctly the list should contain a value that corresponds to the initial bound model value. The bound property or function must be able to receive the type of the item that is selected for the value to stick.

    The structure of your view model just doesn't match the user interface. With this sort of view model, it is more common for the grid to have actions to Add a new record or to Remove existing ones if the SalesProcessParty needed to match an existing record, in this case the MudAutocomplete would be outside of the grid and part of an Add Record template or form.

    Alternatively the view model would have a list of pointer records, so not SalesProcessParty but a linking object that has a PartyId then you can still add and remove records, but now we can bind the MudAutocomplete to the PartyId property and not the whole row @context itself.

    Neither of these options match the UI that you have coded, which suggests to me that TeamMembers should not be a list at all

    A further option is to make ResponsibleAdvisor and SupportingAdvisor properties of the CustomerTeamViewModel and not elements in a list at all. In this case it might not even make sense for SalesProcessParty to inherit from Party at all, instead it could have a property so that we can select and assign a Party to a given SalesProcessParty instance.

    As there are a large number of variables here, I will not try to construct a coded response, however this has still been a valuable thought exercise. In the post you need to clearly state what the final intent is, at the moment I am faced with choosing to either change the UI to match the intent of the ViewModel or changing the ViewModel to match the intent of the UI.

    Respect Occam's Razor, In this case think of it as Occam's Blazor: If it is too complicated then you are probably doing it wrong.