mcintyre321 / FormFactory

MVC5, Core or standalone - Generate rich HTML5 forms from your ViewModels, or build them programatically
http://formfactoryaspmvc.azurewebsites.net/
MIT License
303 stars 103 forks source link

How to get the form values to the model? #59

Closed SignalRT closed 6 years ago

SignalRT commented 6 years ago

If I have a model and I send this model to the View, Can I get the form values to the form automatically? or I should try JObject also the get the values one by one and update the model manually?

mcintyre321 commented 6 years ago

Are you talking about A. getting the values into the form so the form displays correctly, or B. posting the values from the form to the server side ?

Code example would help me understand what you are after.

SignalRT commented 6 years ago

I'm talking about B.

In the example I see:

@using (var form = Html.FormForAction((AccountController ctlr, AccountModel model) => ctlr.Update(model))) { form.AdditionalClasses = "form-horizontal"; form.Render(); }

But in .NET Core the HTML.FormForAction are commented. And this example seems that are only on the FormFactory.AspMvc.Example, in the FormFactory.AspNetCore.Example It seems that there is no View to this example.

mcintyre321 commented 6 years ago

Hmm, I think FormForAction never got ported to Core, sorry. You can create the form tag using normal html/TagHelpers, then render just the fields using FF (using FF.PropertiesFor(model).Render())

SignalRT commented 6 years ago

That's not my problema, my problem is how (once that I make submit) I get the values in the model. It's any way to do it automatically or should I do it manually?

mcintyre321 commented 6 years ago

The posted values should model bind to the model type. What does your controller code look like?

SignalRT commented 6 years ago

Ok,

It's my fault. I was generating dynamically and compiling the classes of the forms, because I need that the final user are allowed to change the forms, and that works ok in for the render part, but it doesn´t work automatically for the View to Controller class when I don´t know the type in compile time. I tested it with a class in the project and it works well. I'm only need to figure how to make that the view can pass the data to the controller when both View and Controller knows the type at runtime.

mcintyre321 commented 6 years ago

Sounds neat! A generic controller and .MakeGenericType may be what you are after

On Mon, 26 Mar 2018, 16:59 SignalRT, notifications@github.com wrote:

Ok,

It's my fault. I was generating dynamically and compiling the classes of the forms, because I need that the final user are allowed to change the forms, and that works ok in for the render part, but it doesn´t work automatically for the View to Controller class when I don´t know the type in compile time. I tested it with a class in the project and it works well. I'm only need to figure how to make that the view can pass the data to the controller when both View and Controller knows the type at runtime.

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/mcintyre321/FormFactory/issues/59#issuecomment-376218175, or mute the thread https://github.com/notifications/unsubscribe-auth/AAQ0-mTVZwpm0oL48B5FsXU9DdOwZmMDks5tiRB1gaJpZM4S3a3M .

reiphil commented 6 years ago

@mcintyre321 - Trying to figure this out on my end as well...

In an example test controller, I fed it just the nestedFormExample2:

public class TestController : Controller
    {
        public ActionResult Index()
        {
            return View("Index", new IndexModel() { ex = new NestedFormsExample2() });
        }

        public class IndexModel
        {
            public NestedFormsExample2 ex { get; set; }
        }

        public ActionResult Save(NestedFormsExample2 form)
        {
            return Json(form);
        }
    }

When I submit back the output is {"ContactMethod":{"SocialMediaType":{"FacebookName":null}}}

Even if I select "No contact method" or "Phone contact method". Unsure why this model binding is not hitting correctly. Any tips?

I tried this on both the mvc and aspnetcore examples, both before and after pullrequest 62 (nested icollections).

mcintyre321 commented 6 years ago

Do you have the json that gets posted?

To get postbacks to work, it may be necessary to use polymorphic deserialization, which requires json to be posted with type information e.g.:

{
  "ContactMethod": {
    "ObjectType ": "PhoneContactMethod",
    "Number": "123",
    "Type": {
      "ObjectType ": "Landline",
      "Provider": "BT"
    }
  }
}

to

public class TestController : Controller
{
    ....

    public ActionResult Save(JToken form)
    {
        NestedFormsExample2 deserialized = form.ToObject<NestedFormsExample2>();
        ...
    }
}

You can do this by a. using the https://github.com/manuc66/JsonSubTypes libray to mark up the base objects, so Json.Net knows what Type something should be deserialised as, and b. adding a property to add the discriminator as a hidden field in the form

[JsonConverter(typeof(JsonSubtypes), nameof(ContactMethod.ObjectType))]
[JsonSubtypes.KnownSubType(typeof(PhoneContactMethod), nameof(PhoneContactMethod))]
[JsonSubtypes.KnownSubType(typeof(NoContactMethod), nameof(NoContactMethod))]
[JsonSubtypes.KnownSubType(typeof(SocialMedia), nameof(SocialMedia))]
public abstract class ContactMethod
{
    public ContactMethod(){
        ObjectType = this.GetType().Name;
    }
    [Hidden]
    public string ObjectType { get; set;} 
}

BTW I haven't actually tried JsonSubTypes , but I think it should work! Let me know how you get on

adamsd308 commented 6 years ago

Hi, I work along with @reiphil Here was my solution that I came up with last night. Beware lengthy post

1st - Created c# interface to standardize polymorphic nested forms

public interface IClassFormSelector<T>
{
    T SelectedClass { get; set; }
    IEnumerable<T> SelectedClass_choices();
}

which is then used like this:

public class ProjectBuilderOptions : Core.Models.IClassFormSelector<Builder>
{
    public ProjectBuilderOptions()
    {
        SelectedClass = new SimpleBuilder() { DescriptionSimple = "Simple" };
    }
    public Builder SelectedClass { get; set; }
    public IEnumerable<Builder> SelectedClass_choices()
    {
        yield return SelectedClass is SimpleBuilder ? SelectedClass.Selected() : new SimpleBuilder();
        yield return SelectedClass is ConcurrentBuilder ? SelectedClass.Selected() : new ConcurrentBuilder();
        yield return SelectedClass is GraphicBuilder ? SelectedClass.Selected() : new GraphicBuilder();
    }
}

and then I have a projectmodel which is the root of the form:

public class ProjectModel {
    [Required]
    public string Name { get; set; }
    [Required]
    public string Description { get; set; }
    public List<Builder> Builders { get; set; } = new List<Builder>();
    private bool Builders_show => false;
    public ICollection<ProjectBuilderOptions> _Form_Builders { get; set; }  = new List<ProjectBuilderOptions>();

    public ProjectModel() {

    }
}

Notice that I use _Form_Builders and this just means I want to bind the list of selectedclasses back to Builders later on. From here the form rendering works as expected.

2nd - fixed a bug in FormFactory.js in order to set the modelname correctly on nested collection elements I added:

var idPrefix = AssembleId($(this).closest(".ff-collection").find("> ul").children().last());
if (idPrefix != "") {
      modelName = idPrefix + "." + modelName;
}
......
function AssembleId(elementRef) {
    var id = "";
    var parent = $(elementRef).closest(".ff-collection-new-item").find("input[type='hidden']");
    if (parent.length > 0) {
        var input = parent.eq(0);
        id += $(input).prop("name").replace(".Index", "[" + $(input).val() + "]");
    }
    return id;
}

3rd updated FF.cs to include full object type info

var typeVm = new PropertyVm(typeof(string), "__type")
{
    DisplayName = "",
    IsHidden = true,
    Value = model.GetType().FullName + ", " +  model.GetType().Assembly.FullName
};

4th added some javascript to my index.cshtml to handle creating a nice json object out of html form inputs that are rendered to the screen. To do this I utilize jquery-serialize-object. The below script will first create a nice json format to represent our form and then second go through and recursively process the json form and handle linking _Form_Builder "SelectedClass" values with Builder array.

$("#frm").submit(function (e) {
    e.preventDefault();
    var frmData = $(this).serializeObject();
    traverse(frmData, process);

    $.ajax({
        type: 'POST',
        url: "/home/save",
        data: JSON.stringify(frmData),
        dataType: "text",
        contentType: "text/plain",
        processData: false
    });

});

function process(key, value, parent) {

    if (key.indexOf("_Form_") > -1) { 

        var arr = [];
        for (var i in value) { 
            if (typeof value[i].SelectedClass !== 'undefined') {
                arr.push(value[i].SelectedClass);
            }
        }
        parent[key.replace("_Form_", "")] = arr;
        delete parent[key];
    }
    if (value !== null && typeof (value) == "object") {
        traverse(value, process);
    }
    console.log(parent);
    console.log(key + " : " + value);
}

function traverse(o, func) {
    for (var i in o) {
        func.apply(this, [i, o[i], o]);
        if (o[i] !== null && typeof (o[i]) == "object") {
            traverse(o[i], func);
        }
    }
}
here is the form after serializing it
{  
   "__type":"Project.Web.Controllers.HomeController+ProjectModel, Project.Web, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
   "Name":"test",
   "Description":"test 2",
   "_Form_Builders":{  
      "Index":"fdc3dd1b-f7bb-4f7d-81e0-080cd585dc29",
      "fdc3dd1b-f7bb-4f7d-81e0-080cd585dc29":{  
         "__type":"Project.Web.Controllers.HomeController+ProjectBuilderOptions, Project.Web, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
         "d29430ad-b6cb-43e6-bdb3-adb3462cf505":"on",
         "SelectedClass":{  
            "__type":"Project.Web.Controllers.HomeController+ConcurrentBuilder, Project.Web, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
            "DescriptionConcurrent":"Concurrent!",
            "_Form_Tasks":{  
               "Index":"dbe505eb-0560-425a-9f6d-bf5c2ca6e0e2",
               "aa54face-cfb3-4801-8743-0661a1e41c68":{  
                  "__type":"Project.Web.Controllers.HomeController+BuildActionOptions, Project.Web, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
                  "a04800a4-5a68-41c1-b2b0-6a9a140ca4af":"on",
                  "SelectedClass":{  
                     "__type":"Project.Web.Controllers.HomeController+WinBatchAction, Project.Web, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
                     "Description":"Windows Batch Action",
                     "BatchCommand":""
                  }
               },
               "dbe505eb-0560-425a-9f6d-bf5c2ca6e0e2":{  
                  "__type":"Project.Web.Controllers.HomeController+BuildActionOptions, Project.Web, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
                  "a04800a4-5a68-41c1-b2b0-6a9a140ca4af":"on",
                  "SelectedClass":{  
                     "__type":"Project.Web.Controllers.HomeController+GraphicsBuildAction, Project.Web, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
                     "Name":"",
                     "Disabled":"false",
                     "Category":"",
                     "TestSettingsPath":"",
                     "_testUniqueIdentifier":"",
                     "TestContainers":"",
                     "TrxOutputFilePath":"",
                     "Description":"",
                     "ContinueOnError":"false",
                     "CommandText":""
                  }
               }
            }
         }
      }
   }
}
--------------------------------
and here it is after doing the recursive processing on it (its a little easier to read now)
--------------------------------
{
  "__type": "Project.Web.Controllers.HomeController+ProjectModel, Project.Web, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
  "Name": "test",
  "Description": "test 2",
  "Builders": [
    {
      "__type": "Project.Web.Controllers.HomeController+ConcurrentBuilder, Project.Web, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
      "DescriptionConcurrent": "Concurrent!",
      "Tasks": [
        {
          "__type": "Project.Web.Controllers.HomeController+WinBatchAction, Project.Web, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
          "Description": "Windows Batch Action",
          "BatchCommand": ""
        },
        {
          "__type": "Project.Web.Controllers.HomeController+GraphicsBuildAction, Project.Web, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
          "Name": "",
          "Disabled": "false",
          "Category": "",
          "TestSettingsPath": "",
          "_testUniqueIdentifier": "",
          "TestContainers": "",
          "TrxOutputFilePath": "",
          "Description": "",
          "ContinueOnError": "false",
          "CommandText": ""
        }
      ]
    }
  ]
}

Finally 5th added our save method which will deserialize the json form body that was posted

[HttpPost]
public JsonResult Save() {
    Stream req = Request.Body;
    string body = "";
    using (StreamReader reader = new StreamReader(Request.Body, System.Text.Encoding.UTF8))
    {
        body = reader.ReadToEnd();
    }
    body = body.Replace("__type","$type");
    var obj = Newtonsoft.Json.JsonConvert.DeserializeObject<ProjectModel>(body, new Newtonsoft.Json.JsonSerializerSettings() {
        TypeNameHandling = Newtonsoft.Json.TypeNameHandling.Objects,
        TypeNameAssemblyFormatHandling = Newtonsoft.Json.TypeNameAssemblyFormatHandling.Full
    });

    return Json(body);
}

So far it has no issues deserializing these polymorphic complex types. Hopefully this can help someone else out :)

mcintyre321 commented 6 years ago

Nice, glad to see you are really using FormFactory to it's full potential!

I've used form2js to serialize the forms to the ASP.NET bindable format with collection support - if you find jquery-serialize-object isn't working (e.g. for collection ordering), you could try that.

There is a security risk from using real Type names in your __type variable - an attacker can put any Type name in there and get your system to create and fill an instance. I would recommend using the project I linked above, or using the KnownTypesBinder from https://www.newtonsoft.com/json/help/html/SerializeSerializationBinder.htm