简体   繁体   中英

Razor Pages - How to Use EditorFor Templates with Abstract Classes

I've tried a million and one different things and poured through and endless number of SO solutions, but I've yet to find a working solution. I have a page model that will contain a list of different classes all deriving from abstract class . I have different editor templates for each class type. So, when the page loads and my items get loaded in, the correct templates show up and everything is as expected. However, things go wrong when I try to post my form back. I'm never getting the proper values and I end up getting an error about trying to submit a form expecting an abstract class/interface.

To take a step back and attempt to get the thought working in its simplest form, we have the following scenario:

public class Device
{
    public string Type => GetType().FullName;
}

public class Laptop : Device
{
    public string CPUIndex { get; set; }
}

public class SmartPhone : Device
{
    public string ScreenSize { get; set; }
}

TestPage.cshtml

<form method="post">
    @Html.EditorFor(m => m.Item, Model.Item.GetType().Name)

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

Laptop Editor Template

@model Laptop

<p>Laptop</p>
<input asp-for="CPUIndex"/>

TestPage.cs (Page model)

public Device Item { get; set; }

public void OnGet()
{
    Item = new Laptop { CPUIndex = "Random Value" };
}

public void OnPost(Device item)
{

}

This will throw an exception when I post the form because I'm trying to access an abstract class. I have found here under the Polymorphic Binding where I need to create a custom binding.

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

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

    public async Task BindModelAsync(ModelBindingContext bindingContext)
    {
        var modelKindName = ModelNames.CreatePropertyModelName(bindingContext.ModelName, nameof(Device.Type));
        var modelTypeValue = bindingContext.ValueProvider.GetValue(modelKindName).FirstValue;

        IModelBinder modelBinder;
        ModelMetadata modelMetadata;
        if (modelTypeValue == "Laptop")
        {
            (modelMetadata, modelBinder) = binders[typeof(Laptop)];
        }
        else if (modelTypeValue == "SmartPhone")
        {
            (modelMetadata, modelBinder) = binders[typeof(SmartPhone)];
        }
        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,
            };
        }
    }
}

public class DeviceModelBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context.Metadata.ModelType != typeof(Device))
        {
            return null;
        }

        var subclasses = new[] { typeof(Laptop), typeof(SmartPhone), };

        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 DeviceModelBinder(binders);
    }
}

Then, I register it in Startup.cs

 services.AddMvc(options =>
            {
                options.ModelBinderProviders.Insert(0, new DeviceModelBinderProvider());
            })

Even with all of this, when I submit my form the value of item in OnPost is null. What am I missing here?

I found it. The missing piece was that I wasn't putting an input field in the HTML to hold the Type property.

I changed my TestPage.cshtml from this

<form method="post">
    @Html.EditorFor(m => m.Item, Model.Item.GetType().Name)

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

To this

<form method="post">
    @Html.HiddenFor(m => m.Item.Type)
    @Html.EditorFor(m => m.Item, Model.Item.GetType().Name)

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM