5

I'm building a dynamic form creator in .net core. A "form" will consist of many different form elements. So the form model will look something like this:

public class FormModel {
  public string FormName {get;set;}
  public List<IElements> Elements{get;set;}
}

I have classes for TextBoxElement, TextAreaElement, CheckBoxElement that all implement the IElemets interface. And I have EditorTemplates for each element. The code to render the form works great. Though posting the form does not work because of the List of Interfaces.

I've been looking on how to implement a custom model binder, and seen some few examples on the web but I did not get anyone to work.

I would appreciate if someone could show me how to implement a custom model binder for this example.

Plan B: Post form as json to a web api and let JSON.Net covert it. I have tried it and it worked. In startup.cs i added:

services.AddMvc().AddJsonOptions(opts => opts.SerializerSettings.TypeNameHandling = TypeNameHandling.Auto);

It returns type when it has to, eg. the objects in the Elements-list but not on the FormModel. But i really would like to know how to solve it with a custom model binder instead.

jessew
  • 63
  • 6
  • How would the model binder know which type to instantiate for the list elements when posting? – Lucero Aug 21 '16 at 08:39
  • Not sure why someone voted this down. I suppose you would have to post some additional information which would let your custom model binder know what type of Class to instantiate. – Lee Gunn Aug 21 '16 at 09:50
  • I think JSON.NET can handle types by serialising/deserialising the type name. If you're using JSON, then perhaps you can leverage that some how. – Lee Gunn Aug 21 '16 at 10:10
  • My thought was to have hidden fields with the type. – jessew Aug 21 '16 at 11:24
  • JSON.Net can deserialize if you include a $type parameter with the type if you do something lite this. JsonConvert.DeserializeObject(json, new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.Auto }); Im not sure how you add this setting to it works when posting to a web api. Also in that case i would only want this setting when posting json, but not on return, any one now if that's possible ? – jessew Aug 21 '16 at 11:28
  • So, are you currently posting KeyValues pairs or JSON? And if it's KeyValue pairs, can you change it to JSON? – Lee Gunn Aug 21 '16 at 12:07
  • Currently I am posting KeyValue pairs. Regular form post, and i think best solution would be with a custom model binder, then it will work both if i post as json or as a regular form, right? But as plan b i've been considering to post form as json instead and configure JSON.Net to handle it, Im not sure how i do that either thou, but I haven't really looked into plan B yet. All ideas are appreciated. – jessew Aug 21 '16 at 12:58

1 Answers1

6

Ok, this works for me. I'm still getting to grips with the new model binding so I may be doing something silly but it's a start!

TEST FORM

<form method="post">
    <input type="hidden" name="Elements[0].Value" value="Hello" />
    <input type="hidden" name="Elements[0].Type" value="InterfacePost.Model.Textbox" />

    <input type="hidden" name="Elements[1].Value" value="World" />
    <input type="hidden" name="Elements[1].Type" value="InterfacePost.Model.Textbox" />

    <input type="hidden" name="Elements[2].Value" value="True" />
    <input type="hidden" name="Elements[2].Type" value="InterfacePost.Model.Checkbox" />

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

INTERFACE

public interface IElement
{
    string Value { get; set; }
}

TEXTBOX IMPLEMENTATION

public class Textbox : IElement
{
    public string Value { get; set; }
}

CHECKBOX IMPLEMENTATION

public class Checkbox : IElement
{
    public string Value { get; set; }
}

MODEL BINDER PROVIDER

public class ModelBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        if (context.Metadata.ModelType == typeof(IElement))
        {
            return new ElementBinder();
        }

        // else...
        return null;
    }
}

MODEL BINDER

public class ElementBinder : IModelBinder
{
    public async Task BindModelAsync(ModelBindingContext bindingContext)
    {
        if (bindingContext.ModelType == typeof(IElement))
        {
            var type = bindingContext.ValueProvider.GetValue($"{bindingContext.ModelName}.Type").FirstValue;

            if (!String.IsNullOrWhiteSpace(type))
            {

                var element = Activator.CreateInstance(Type.GetType(type)) as IElement;

                element.Value = bindingContext.ValueProvider.GetValue($"{bindingContext.ModelName}.Value").FirstValue;

                bindingContext.Result = ModelBindingResult.Success(element);
            }
        }
    }
}

HOOK UP MODEL BINDER PROVIDER

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc(options =>
        {
            options.ModelBinderProviders.Insert(0, new ModelBinderProvider());
        });
    }
}

FORM MODEL

public class FormModel
{
    public string FormName { get; set; } // Not using this

    public List<IElement> Elements { get; set; }
}

ACTION

Notice the three types, Textbox, Textbox and Checkbox.

enter image description here

Lee Gunn
  • 8,417
  • 4
  • 38
  • 33
  • I tried this example. bindingContext.ValueProvider.GetValue returns null bindingContext.ModelName is an empty string – John Soer Sep 03 '16 at 22:28
  • Never mind I missed the name="Elements[0].Type" in the html – John Soer Sep 04 '16 at 01:12
  • This works well, however if the List happens to have no items, then the whole Form model is returned as null. What could be done to solve that problem? – devtoka Apr 19 '19 at 12:55
  • 1
    @Tak90 If it's null - I guess it's because no data has been POSTed. Could you simply just check for a null in code? – Lee Gunn Apr 19 '19 at 13:05
  • @LeeGunn Thanks getting back to me. The issue is that receiving an empty list in our case is still valid, so we would expect a model with a name but an empty or null list. However, upon looking at my code I have realised that it is not your code that misbehaving, but rather an error in mine. See my question [Here](https://stackoverflow.com/questions/55759979/how-to-handle-model-binding-of-complex-type-with-list-of-interfaces-and-nested-l), I have answered it myself. Many thanks. – devtoka Apr 19 '19 at 13:32