0

Hello people of StackOverflow,
I am trying the following:

I have a partial for selecting the Country. But I want to use this partial for 2 different models:
_CountrySelectorPartial.cshtml

@model CountrySelectorViewModel

<div class="mb-3">
    <label asp-for="Model.Country" class="control-label"></label>
    <select asp-for="Model.CountryId" asp-items="@(new SelectList(Model.Countries, "Id", "Name", Model.Model?.CountryId))"></select>
    <span asp-validation-for="Model.CountryId" class="text-danger"></span>
</div>

CountrySelectorPartial.cs

public class CountrySelectorViewModel
    {
        public ICountry Model { get; set; }
        
        [Required]
        public ICollection<Country> Countries { get; set; } = new List<Country>();
    }

And these are the classes that implement ICountry:
Branch.cs

public class Branch : IEntity, ICountry
    {
        public int Id { get; set; }
        
        [Required]
        public int CountryId { get; set; }
        public Country Country { get; set; }
        // More properties
    }

Employee.cs

public class Employee : IEntity, ICountry
    {
        public int Id { get; set; }

        [Required]
        public int CountryId { get; set; }
        public Country Country { get; set; }
        // more properties
    }

And I want to use the partial for these classes.
I use the partial like this:

<partial name="Components/_CountrySelector" model="@(new CountrySelectorViewModel { Model = Model.Employee, Countries = Model.Countries })"/>

Or for branch like this:

<partial name="Components/_CountrySelector" model="@(new CountrySelectorViewModel { Model = Model.Branch, Countries = Model.Countries })"/>

But when I submit the form the incoming class is null But when I used Employee instead of ICountry in the CountrySelectorPartial it had the correct data.

Is it possible to use this partial for 2 models using the interface?

EDIT:
For clarity it does show correctly on the page but when submitting the complete form it returns null
Working selectbox
Here is the route where the data is send: Incoming data

BlueDragon709
  • 196
  • 1
  • 12

1 Answers1

0

As far as I knwo, if CountrySelectorViewModel's Model property is ICountry, you still need to check the model's property in the partial view to convert the icountry to Employee or Branch.

Besides, since the edit view which render the partial view is using CreateEmployeeViewModelData not CountrySelectorViewModel, you should set the input with the name instead of directly using asp.net core taghelper. The generated tag name is not right. You should set it as {Employee.Propertyname}.

At last, since you set the I ICountry as the property, if you want to bind it to the subclass, you should create a custom model binder to check the type, you should assign the right binder according its property.

Normally, we will add new property named kind in the ICountry and set it when you want to call the partial view.

More details about how to achieve your requirement, you could refer to below codes:

Since I don't know all your model property, I created them by myself. You should modify it according to your actually model.

Model:

ICountry:

public interface ICountry
{
    public string Kind { get; set; }
}

Branch : public class Branch : ICountry { public int BranchId { get; set; }

    //[Required]
    //public int CountryId { get; set; }
    //public Country Country { get; set; }
    private string _kind;
    public string Kind { get => _kind; set => _kind = "Branch"; }

    // More properties
}

Employee:

public class Employee : ICountry
{
    public int EmployeeId { get; set; }

    private string _kind;
    public string Kind { get => _kind;  set => _kind = "Employee"; }
}

CountrySelectorViewModel:

public class CountrySelectorViewModel
{
    public int Id { get; set; }
    public ICountry Model { get; set; }



    //[Required]
    //public ICollection<Country> Countries { get; set; } = new List<Country>();
}

CreateEmployeeViewModelData:

public class CreateEmployeeViewModelData
{
    public ICountry Employee { get; set; }
}

ICountryModelBinderProvider:

    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context.Metadata.ModelType != typeof(ICountry))
        {
            return null;
        }

        var subclasses = new[] { typeof(Employee), typeof(Branch), };

        var binders = new Dictionary<Type, (ModelMetadata, IModelBinder)>();
        foreach (var type in subclasses)
        {
            var modelMetadata = context.MetadataProvider.GetMetadataForType(type);
            binders[type] = (modelMetadata, context.CreateBinder(modelMetadata));
        }

        return new ICountryModelBinder(binders);
    }

    public class ICountryModelBinder : IModelBinder
    {
        private Dictionary<Type, (ModelMetadata, IModelBinder)> binders;

        public ICountryModelBinder(Dictionary<Type, (ModelMetadata, IModelBinder)> binders)
        {
            this.binders = binders;
        }

        public async Task BindModelAsync(ModelBindingContext bindingContext)
        {
            var modelKindName = ModelNames.CreatePropertyModelName(bindingContext.ModelName, nameof(ICountry.Kind));
            var modelTypeValue = bindingContext.ValueProvider.GetValue(modelKindName).FirstValue;
            //var modelTypeValue = bindingContext.ValueProvider.GetValue("Kind").FirstValue;
          IModelBinder modelBinder;
            ModelMetadata modelMetadata;
            if (modelTypeValue == "Employee")
            {
                (modelMetadata, modelBinder) = binders[typeof(Employee)];
            }
            else if (modelTypeValue == "Branch")
            {
                (modelMetadata, modelBinder) = binders[typeof(Branch)];
            }
            else
            {
                bindingContext.Result = ModelBindingResult.Failed();
                return;
            }

            var newBindingContext = DefaultModelBindingContext.CreateBindingContext(
                bindingContext.ActionContext,
                bindingContext.ValueProvider,
                modelMetadata,
                bindingInfo: null,
                bindingContext.ModelName);

            await modelBinder.BindModelAsync(newBindingContext);
            bindingContext.Result = newBindingContext.Result;

            if (newBindingContext.Result.IsModelSet)
            {
                // Setting the ValidationState ensures properties on derived types are correctly 
                bindingContext.ValidationState[newBindingContext.Result] = new ValidationStateEntry
                {
                    Metadata = modelMetadata,
                };
            }
        }
    }

Startup.cs ConfigureServices method:

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllersWithViews(options => { options.ModelBinderProviders.Insert(0, new ICountryModelBinderProvider()); });
    }

Partial view:

@model CountrySelectorViewModel

<div class="mb-3">
    @*<label asp-for="Model.Country" class="control-label"></label>
        <select asp-for="Model.CountryId" asp-items="@(new SelectList(Model.Countries, "Id", "Name", Model.Model?.CountryId))"></select>
        <span asp-validation-for="Model.CountryId" class="text-danger"></span>*@
    @if (Model.Model.GetType() == typeof(Employee))
    {
        <input name="Employee.EmployeeId" value="@((Model.Model as Employee).EmployeeId)" />
        <input name="Employee.Kind" value="@Model.Model.Kind" />
    }

    @if (Model.Model.GetType() == typeof(Branch))
    {
        <input name="Employee.EmployeeId" value="@((Model.Model as Branch).BranchId)" />
        <input name="Employee.Kind" value="@Model.Model.Kind" />
    }


</div>

Edit view:

<form asp-action="Edit">
    <partial name="_CountrySelectorPartial" model="@(new CountrySelectorViewModel { Model = new Employee{EmployeeId = 1, Kind="Employee"}, Id = 1 })" />
    <input type="submit" value="Click" />
</form>

<hr />

<form asp-action="Edit">
    <partial name="_CountrySelectorPartial" model="@(new CountrySelectorViewModel { Model = new Branch{BranchId = 2,Kind="Branch"}, Id = 2 })" />

    <input type="submit" value="Click" />
</form>

Result:

enter image description here

Brando Zhang
  • 22,586
  • 6
  • 37
  • 65