Search code examples
c#asp.net-core-mvcrazor-pagesmaster-detailasp.net-core-5.0

ASP.NET Core 5 Razor Pages - editing a master/detail setup - loosing data


I haven't done any web/frontend development in a while, and so I'm a bit rusty. I'm using ASP.NET Core 5 with Razor Pages.

I'm trying to create an edit page that would allow me to edit the children (1:n) of a given entity. For now, I have a country/city sample - list of countries, each with a few cities. What I want to do is show all the countries (works fine), and then pick a country and edit its cities. That's where I'm struggling.

These are my model classes:

public class Country
{
    public string IsoCode { get; set; }
    public string Name { get; set; }
    // more properties in the future
    
    public ICollection<City> Cities { get; set; }
}

public class City
{
    public string Name { get; set; }
    public int Population { get; set; }

    public Country Country { get; set; }
    [ForeignKey("Country")] 
    public string CountryIsoCode { get; set; }

    // more properties to come
}   

The Razor page I have to show and edit the cities for a given country looks something like this:

View:

@page "{isocode}"
@using CountryCityDemo.Models
@model CountryCityDemo.Pages.EditCitiesModel

<h3>Edit cities for a country</h3>

<div class="container">
    <form method="post">
        <div class="row mb-1">
            <div class="col-md-5 font-weight-bold">
                <label class="control-label">@Model.SelectedCountry.Name</label>
            </div>
        </div>

        @foreach (City city in Model.SelectedCountry.Cities)
        {
            <div class="row mb-1">
                <div class="col-md-3 font-weight-bold">
                    <label class="control-label">Name of the city</label>
                </div>
                <div class="col-md-5 font-weight-bold">
                    <input asp-for="@city.Name" class="form-control" />
                </div>
            </div>
            <div class="row mb-1">
                <div class="col-md-3">
                    <label class="control-label">Population</label>
                </div>
                <div class="col-md-2">
                    <input asp-for="@city.Population" class="form-control" />
                </div>
            </div>
        }

        <input type="submit" value="Update" />
    </form>
</div>

Page model class:

public class EditCitiesModel : PageModel
{
    [BindProperty]
    public Country SelectedCountry { get; set; }

    public async Task OnGetAsync(string isocode)
    {
        // get countries from provider - including their cities
        SelectedCountry = CountryProvider.GetCountry(isocode, true);
    }

    public async Task<IActionResult> OnPostAsync(string isocode)
    {
        // fetch the list of cities for this country
        List<City> citiesForCountry = CityProvider.GetCitiesForCountry(isocode).ToList();

        // update those cities
        if (await TryUpdateModelAsync<List<City>>(citiesForCountry, "city"))
        {
            // save the updated cities
        }

        /* I also tried to use the "SelectedCountry" bind property - but had the same results:
        
        SelectedCountry = CountryProvider.GetCountry(isocode);

        if (await TryUpdateModelAsync<Country>(SelectedCountry, "SelectedCountry", c => c.Name, c => c.IsoCode, c => c.PrimaryLanguage))
        { ..... }           
        */
        
        return RedirectToPage("Data");
    }
}

The page comes up just fine, everything is peachy, and I can edit the cities' name and population - no problem. When I click on "Update", the post method on the PageModel class is called - all seems just great. When I inspect the HttpContext.Form, I can see the values for the cities names and populations that I entered - so all appears to be fine.

BUT: after calling the TryUpdateModelAsync method, the citiesForCountry list is empty - no entries anymore, data entered manually is not applied :-(

Same happens when I'm using the SelectedCountry bind property - it's fine after being fetched from the provider, it has its list of cities (with the stored values), but after I call TryUpdateModelAsync, that list of cities is wiped out and the new values aren't stored anywhere....

I also tried to get the "view model" of type Country passed into my OnPostAsync method - like I used to be doing it with ASP.NET MVC back in the days:

    public async Task<IActionResult> OnPostAsync(string isocode, Country edited)
    

I do get the basic information of the country - but again, the master/child Cities are not set, not populated with the values I've entered on the page.

So what am I missing here? I'm thinking there must be a detail I'm overlooking - but I can't seem to make sense of any of the articles, blog posts, tutorials etc. I've seen.... I seem to be doing exactly what they describe - alas, in my case, the data just doesn't quite make it into my view model (or rather: Razor PageModel) on post......


Solution

  • You need to use an indexer to enable binding of data to a collection: https://www.learnrazorpages.com/razor-pages/model-binding#binding-complex-collections. Since you are editing existing data, you would usually use an explicit index based on the key value of each item you are editing. Not sure what your model looks like, so I will use "Key" to represent the value:

    @foreach (City city in Model.SelectedCountry.Cities)
    {
        <input type="hidden" name="SelectedCountry.Cities.Index" value="@city.Key" />
        <input type="hidden" name="SelectedCountry.Cities[@city.Key].Key" value="@city.Key" />
        <div class="row mb-1">
            <div class="col-md-3 font-weight-bold">
                <label class="control-label">Name of the city</label>
            </div>
            <div class="col-md-5 font-weight-bold">
                <input name="SelectedCountry.Cities[@city.Key].Name" class="form-control" value="@city.Name" />
            </div>
        </div>
        <div class="row mb-1">
            <div class="col-md-3">
                <label class="control-label">Population</label>
            </div>
            <div class="col-md-2">
                <input name="SelectedCountry.Cities[@city.Key].Population" class="form-control" value="@city.Population"/>
            </div>
        </div>
    }