Updating related entities

2019-08-10 08:32发布

问题:

AppUser identity model:

public virtual ICollection<UserPhones> UserPhones { get; set; }

Using Razor Pages, I call a partial view, like so:

@await Html.PartialAsync("_NameAndID", Model.AppUser)

PageModel:

[BindProperty]
public AppUser AppUser { get; set; }

 public IActionResult OnGet()
    {
        AppUser = _userManager.Users
                //.Include(x => x.UserAddresses) //OMITTED BC USING LAZY LOADING
                .SingleOrDefaultAsync(x => x.UserName == 
                     _httpContext.HttpContext.User.Identity.Name).Result;

        return Page();
    }

Within _NameAndID.cshtml, I explicitly reference a particular telephone from the UserPhones entity. With:

<input type="hidden" asp-for="UserPhones
   .SingleOrDefault(z => z.Type == EnumPhoneType.Mobile).UserPhoneId" />

//other properties removed for brevity           

<div class="rvt-grid__item">
    <label asp-for="UserPhones.SingleOrDefault(z => z.Type == EnumPhoneType.Mobile).PhoneNumber">Mobile Phone</label>
    <input asp-for="UserPhones.SingleOrDefault(z => z.Type == EnumPhoneType.Mobile).PhoneNumber" autocomplete="tel" />
    <span asp-validation-for="UserPhones.SingleOrDefault(z => z.Type == EnumPhoneType.Mobile).PhoneNumber"></span>
</div>

At runtime, the explicit mobile phone number is loaded properly. However when posting to public async Task<IActionResult> OnPostAsync() the related AppUser.UserPhones is null. (The problem)

Can you help?

Thank you in advance!!!!

回答1:

The Reason

The asp-for does not work well for this scenario.

Considering your code in _NameAndID.cshtml :

<input asp-for="UserPhones.SingleOrDefault(z => z.Type == EnumPhoneType.Mobile).PhoneNumber" autocomplete="tel" />

Note the LINQ extension method .SingleOrDefault(...) here. The asp-for here does not know how to get the name for UserPhones.SingleOrDefault(z => z.Type == EnumPhoneType.Mobile).PhoneNumber, so it just render it as PhoneNumber. As a result, the rendered html will be :

<input autocomplete="tel" type="text" id="PhoneNumber" name="PhoneNumber" value="">

Let's say someone inputs an value of 911, when posted to server, the payload will be :

PhoneNumber=911

As your page model on server side is :

    [BindProperty]
    public AppUser AppUser{get;set;}

    public IActionResult OnGet()
    {
        // ...
    }

    public IActionResult OnPostAsync() 
    {
        return Page();
    }

Note the AppUser.UserPhones property is a collection. in other words, AppUser expects a payload like :

UserPhones[0].UserPhoneId=1&UserPhones[0].PhoneNumber=911&UserPhones[1].UserPhoneId=2&UserPhones[1].PhoneNumber=119

However, what you send to the server is :

PhoneNumber=911

So the App.UserPhones will always be null and the AppUser.PhoneNumber property will be 911.

How to Fix

Firstly, in order to bind the UserPhones automatically, I change the type of App.UserPhones to IList<UserPhones> , so that we can use a index syntax

public class AppUser : IdentityUser{

    // public virtual ICollection<UserPhones> UserPhones { get; set; }
    public virtual IList<UserPhones> UserPhones { get; set; }

}

Secondly, don't use complex query in asp-for, use simple index syntax instead. For example, if you would like to post some UserPhones or post all UserPhones, you can add an index for each field :

@for(var i=0;i <Model.UserPhones.Count(); i++) {

    <div class="rvt-grid__item">
        <label asp-for="@Model.UserPhones[i].UserPhoneId"></label>
        <input asp-for="@Model.UserPhones[i].UserPhoneId"/>
        <label asp-for="@Model.UserPhones[i].PhoneNumber"></label>
        <input asp-for="@Model.UserPhones[i].PhoneNumber"/>
        <span asp-validation-for="@Model.UserPhones[i].PhoneNumber"></span>
    </div>
}

In this way, when someone submits the form, AppUser.UserPhones will be the correctly set. Here's a screenshot of demo :