Pass sub object to partial view from views with different models, have it bind when form is posted

1.1k Views Asked by At

I have an ASP.net MVC Core 2 (can upgrade to 3 if required) web app.

There are several different classes, landlord, tenant, contractor - they each have an address object, and other different properties.

I have a partial view that is passed the address object from each view.

<partial name="_AddressPartial" , model="@Model" />

This works fine. But when the form is posted, each address field is posted flat, as the names of the address are things like "Street", not "Address.Street" - because the address model is passed to the partial. So the model binder is looking for "Street" etc, not as part of an Address object.

I could pass the object from each page the partial is on, and prefix "Address." to each field name, but Landlord, Contractor, Tenant etc are all different classes so I'd need a different partial per page which defeats the point.

I might be able to use a base class with Address, then derive all the other classes from that, and bind the base class to the partial? But that seems like overkill just to get a partial working.

I could flatten the Address object into each ViewModel - this would create significantly more work when writing to the database though (you can't just pass Dapper an Address model, you'll have to manually write some SQL for the update), and create lots of repetition.

How can I re-use a partial between all the pages, keep it DRY, and avoid the listed problems.

I've googled it and the above is all I could find. Am I missing something?

2

There are 2 best solutions below

2
Fei Han On

when the form is posted, each address field is posted flat, as the names of the address are things like "Street", not "Address.Street" - because the address model is passed to the partial. So the model binder is looking for "Street" etc, not as part of an Address object.

To fix it, you can try following approaches.

Approach 1: set HtmlFieldPrefix with "Address" in _AddressPartial.cshtml as below.

@model Address

@{ 
    Html.ViewData.TemplateInfo.HtmlFieldPrefix = "Address"; 
}
<div class="form-group">
    <label asp-for="City" class="control-label"></label>
    <input asp-for="City" class="form-control" />
    <span asp-validation-for="City" class="text-danger"></span>
</div>
<div class="form-group">
    <label asp-for="Street" class="control-label"></label>
    <input asp-for="Street" class="form-control" />
    <span asp-validation-for="Street" class="text-danger"></span>
</div>

@*other input fields*@

Approach 2: implement a custom model binder to bind data to Street etc properties.

Address class

public class Address
{
    public string City { get; set; }

    public string Street { get; set; }
}

Custom model binder AddressBinder

// code logic here
// ...

var model = new Address();

if (bindingContext.ValueProvider.GetValue("City").FirstOrDefault() != null&& bindingContext.ValueProvider.GetValue("Street").FirstOrDefault() != null)
{
    var city = bindingContext.ValueProvider.GetValue("City").FirstOrDefault();
    var street = bindingContext.ValueProvider.GetValue("Street").FirstOrDefault();

    model.City = city;
    model.Street = street;
}


bindingContext.Result = ModelBindingResult.Success(model);
return Task.CompletedTask;

Apply it to Address property

[ModelBinder(BinderType = typeof(AddressBinder))]
public Address Address { get; set; }

Test and Result

For approach 1:

<div class="row">
    <div class="col-md-4">
        <form asp-action="NewContractor">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="Cname" class="control-label"></label>
                <input asp-for="Cname" class="form-control" />
                <span asp-validation-for="Cname" class="text-danger"></span>
            </div>
            <partial name="_AddressPartial" model="@Model" />
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div> 

enter image description here

For approach 2:

public class Landlord
{
    public string Name { get; set; }
    [ModelBinder(BinderType = typeof(AddressBinder))]
    public Address Address { get; set; }
}

enter image description here

0
Jämes On

I'm a bit late to the party, but I think you should use the attribute for instead of model in the partial element, similarly to:

<partial name="_AddressPartial" for="@Model.Address" />

Assuming Model.Address is a property that stores an instance of your shared Address class. Thus, ASP.NET Core will properly map the fields' name with the prefix Address.