blazorblazor-server-sidecascadingdropdownblazor-editform

How to maintain individual state for multiple cascading dropdowns in a Blazor form?


Link to GitHub for quick access to minimum reproducible example (code is provided below, as well): https://github.com/thecodeiackiller/cascadingSelectListProblem

Note: For those unfamiliar with the fitness arena, in a given workout, you typically have a workout type (Leg day, upper body, etc.) and within that workout, you have exercise types (your main/heavier exercises, your secondary exercises, and your accessory exercises (working the small muscles).

Expected Chronological User Flow:

Problem: List of exercise Names DOES NOT update for the second row. The list of exercise names remains the same as the first exercise, even though the ExerciseTypes are different, which should change the exercises that show up.

AddWorkout.razor component for my current implementation:

@using stateManagementMinimumReproducibleExample.Repositories
@using stateManagementMinimumReproducibleExample.Models
@page "/addworkout"
@rendermode InteractiveServer
@inject UserExerciseRepository userExerciseRepository

<EditForm Model="userExercise">
    <label for="workoutType"> Workout Type: </label>
    <InputSelect id="workoutType" @bind-Value="userExercise.WorkoutType" @bind-Value:after="GetExercisesByWorkoutType">
        @foreach (var type in Enum.GetValues(typeof(WorkoutType)))
        {
            <option value="@type">@type</option>
        }
    </InputSelect>

    @for (int i = 0; i < numberOfExercisesInWorkout; i++)
    {
        var j = i;
        if (j >= exerciseListForSelectedDay.Count)
        {
            exerciseListForSelectedDay.Add(new UserExercise());
        }

        <div>
            <label for="exerciseType"> Exercise Type: </label>
            <InputSelect id="exerciseType" @bind-Value="exerciseListForSelectedDay[j].ExerciseType" @bind-Value:after="GetExercisesByExerciseType">
                @foreach (var type in Enum.GetValues(typeof(ExerciseType)))
                {
                    <option value="@type">@type</option>
                }
            </InputSelect>

            <label for="exerciseName"> Exercise: </label>
            <InputSelect id="exerciseName" @bind-Value="exerciseListForSelectedDay[j].ExerciseId">
                @foreach (var exercise in listOfExerciseByExerciseType)
                {
                    <option value="@exercise.Id">@exercise.Name</option>
                }
            </InputSelect>
        </div>
    }
</EditForm>


@code {
    private int numberOfExercisesInWorkout = 2;

    private UserExercise userExercise = new();

    private List<UserExercise> exerciseListForSelectedDay = new();

    public List<Exercise> listOfExercisesByWorkoutType = new();

    public List<Exercise> listOfExerciseByExerciseType = new();

    public async Task GetExercisesByWorkoutType()
    {
            listOfExercisesByWorkoutType = await   userExerciseRepository.GetExercisesBasedOnWorkoutType(userExercise.WorkoutType);
            StateHasChanged();
    }

    public void GetExercisesByExerciseType()
    {
            listOfExerciseByExerciseType = userExerciseRepository.GetExercisesFromWorkoutListBasedOnExerciseType(listOfExercisesByWorkoutType, userExercise.ExerciseType);;
    }
}

I have thought about the possibility of creating a dictionary the store the the list of exercises for each exercise within the workout. In addition, I've thought storing a List as a property in my UserExercise class, and then adding the filtered list of exercises to the property. I'm not sure exactly what path I should go down or if there were any potential Blazor-specific state management alternatives that were out there.

The code above should give you enough information to diagnose the problem. If you need a visual, here are the steps to quickly reproduce problem after forking and cloning the GitHub repo:

At this point you'll see that the filtered list has not updated for the second row based on the selection of a different ExerciseType than that of the first row. How can I get the appropriate list(s) of exercises to appear for the second (and beyond)? Thank you.


Solution

  • You could try this below code to resolve the issue:

    AddWorkout.razor:

    @using stateManagementMinimumReproducibleExample.Repositories
    @using stateManagementMinimumReproducibleExample.Models
    @page "/addworkout"
    @rendermode InteractiveServer
    @inject UserExerciseRepository userExerciseRepository
    
    <EditForm Model="userExercise">
        <label for="workoutType"> Workout Type: </label>
        <InputSelect id="workoutType" @bind-Value="userExercise.WorkoutType" @bind-Value:after="GetExercisesByWorkoutType">
            @foreach (var type in Enum.GetValues(typeof(WorkoutType)))
            {
                <option value="@type">@type</option>
            }
        </InputSelect>
    
        @for (int i = 0; i < numberOfExercisesInWorkout; i++)
        {
            var j = i;
            if (j >= exerciseListForSelectedDay.Count)
            {
                exerciseListForSelectedDay.Add(new UserExercise());
            }
    
            <div>
                <label for="exerciseType"> Exercise Type: </label>
                <InputSelect id="exerciseType" @bind-Value="exerciseListForSelectedDay[j].ExerciseType" @bind-Value:after="() => GetExercisesByExerciseType(j)">
                    @foreach (var type in Enum.GetValues(typeof(ExerciseType)))
                    {
                        <option value="@type">@type</option>
                    }
                </InputSelect>
    
                <label for="exerciseName"> Exercise: </label>
                <InputSelect id="exerciseName" @bind-Value="exerciseListForSelectedDay[j].ExerciseId">
                    @foreach (var exercise in exerciseListForSelectedDay[j].FilteredExercises)
                    {
                        <option value="@exercise.Id">@exercise.Name</option>
                    }
                </InputSelect>
            </div>
        }
    </EditForm>
    
    @code {
        private int numberOfExercisesInWorkout = 2;
        private UserExercise userExercise = new();
        private List<UserExercise> exerciseListForSelectedDay = new();
    
        public List<Exercise> listOfExercisesByWorkoutType = new();
    
        public async Task GetExercisesByWorkoutType()
        {
            listOfExercisesByWorkoutType = await userExerciseRepository.GetExercisesBasedOnWorkoutType(userExercise.WorkoutType);
            // Update each exercise list based on the new workout type
            foreach (var exercise in exerciseListForSelectedDay)
            {
                exercise.FilteredExercises = userExerciseRepository.GetExercisesFromWorkoutListBasedOnExerciseType(listOfExercisesByWorkoutType, exercise.ExerciseType);
            }
            StateHasChanged();
        }
    
        public async Task GetExercisesByExerciseType(int index)
        {
            // Filter the exercises based on the ExerciseType for the specific row (index)
            var selectedExerciseType = exerciseListForSelectedDay[index].ExerciseType;
            exerciseListForSelectedDay[index].FilteredExercises = userExerciseRepository.GetExercisesFromWorkoutListBasedOnExerciseType(listOfExercisesByWorkoutType, selectedExerciseType);
            await InvokeAsync(StateHasChanged);
        }
    }
    

    UserExercise.cs:

    namespace stateManagementMinimumReproducibleExample.Models
    {
        public class UserExercise
        {
            public int Id { get; set; }
            public int ExerciseId { get; set; }
            public ExerciseType ExerciseType { get; set; }
            public WorkoutType WorkoutType { get; set; }
    
            // Add this property to store the filtered exercises for each row
            public List<Exercise> FilteredExercises { get; set; } = new List<Exercise>();
        }
    
    }
    

    So basically, in your code you are trying to update the list globally and that it was using single list and because of that you were seeing same exercise. to avoid this issue, you could create separate filter list for row.

    Result:

    enter image description here