Advice with Dynamic Forms in ASP.NET MVC
Asked Answered
P

4

9

I am working on rendering a dynamic form in an ASP.NET MVC view that will meet these requirements:

  • Fields can be validated
  • State is preserved when the form is invalid

I am looking into creating a custom model binder to achieve this. I am generally planning to do this:

  1. Form fields are defined with these properties
    • Prompt (label next to fields)
    • Type (text, checkboxlist, radiolist, etc.)
    • Choices (for list fields)
    • IsRequired
    • RegularExpression (for text fields)
    • Display Options
    • Collection of field definitions are sent from the controller to the view
    • Fields are rendered into HTML and sent to the browser
    • Form is sent back to the server
    • A custom model binder binds the form to a collection of field definitions that now contains the submitted values
    • Each field is validated
    • If required -> must have a value
    • If RegEx -> must match
    • For each invalid field, an error message is added to modelstate
    • The controller decides what to do
    • If all fields are valid
      • Do whatever with the fields and their values
    • If 1 or more fields are invalid
      • Send the collection of fields back to the view
      • Render the fields again, with their previously attempted values
      • Show the validation summary

I'm not sure if I am doing this in the best or easiest way. Will this approach give me a lot of problems or even work? What can I do to improve upon it?

Poll answered 24/6, 2009 at 19:38 Comment(0)
P
8

I wrote a class library that basically does exactly what my psuedocode in my question describes. It works great.

EDIT:

I finally got around to cleaning up my class library. I have added some new features and created a fairly well documented demo web application.

All of this is hosted here on CodePlex. I hope this helps someone.

Poll answered 16/9, 2009 at 18:48 Comment(6)
I think that I have shared the idea. But, I don't mind sharing the code either. Hang on for a link.Poll
mvcdynamicforms.codeplex.com - No examples, no documentation, just the code. I will try to post documentation and examples sometime.Poll
thanks, even without documentation the code is very straight forward and it did help me to sove a related issue. +1Pyelography
Glad it helped. The source code for the library really is a mess. I have been refactoring it and cleaning things up this week. I have also went ahead and created a basic demo mvc app. I should be able to post my new work on the codeplex site this weekend.Poll
Awesome. I really am going to release this with cleaner code sooner or later.Poll
How can I have custom model with Form property inside? I always get null in controller on post...Deterrent
U
0

I am by no means an expert, but if you are very new to ASP.NET MVC, then I recommend you start with the built-in functionality before rolling your own. It does most of what you have described, except does not encourage UI to be defined/constructed in the controller, since that is the job of the view.

Through the ModelStateDictionary you can add model errors and set model values which will then get bound to your form inputs on validation fail.

Update: Another way to look at it: ask yourself why you are using MVC over classic ASP.NET construction techniques, and then see if your proposed approach aligns with those reasons. For me separation of concerns was a huge reason, as well as granular control over the HTML generated, and I feel like your approach may subvert those things.

To address your edit specifically:

Steps 1 through through are against the MVC paradigm. Step 4, fine. Steps 5 through 7 are pretty much standard MVC practice and fully supported by the framework. E.g., Performing Simple Validation (C#) shows example of validation and presentation of error messages.

Utrecht answered 24/6, 2009 at 19:42 Comment(6)
Yes but this only works field inputs created with HtmlHelper and many of the kinds of fields that I need to render are not supported out of the box (checkboxlist, radiolist, listbox).Poll
It's true there are difficulties with those. I would extend HtmlHelper so it does properly support them, the code is not very complicated to do so. Right now I use my own Utility class to handle these since I have not got around to anything more sophisticated. I don't really understand the lack of proper support for those input types in MVC :[Majewski
In response to your update: I have asked that question. I am using MVC because 99% of my app is very well suited for MVC. But this small part of the application needs to generate forms dynamically. I am not yet comfortable with mixing webforms and mvc, so I want to try and do this in MVC. Also, I don't see how anything I have planned on doing will subvert SOC or control over HTML.Poll
Regarding SOC, I was assuming that your Step 3 was some code you wrote going through your custom model binder and automatically generating all of the HTML for your form fields. Is this not what you meant?Majewski
No. My plans weren't for the model binder to have anything to do with rendering HTML. Instead, the FieldDefinition (step 1) object will be responsible for rendering its HTML. Also, I plan to call the FieldDefiniton's RenderHtml method from the View template.Poll
Also, I am stepping into a bit of a minefield here - there are those who like generated HTML code, and those who prefer it handcrafted with just minimal values inserted by code where necessary. I tend toward the latter. ASP.NET MVC does provide some good scaffolding for the former, which I actually don't mind, whereas I despise Web Forms. But what I like most about ASP.NET MVC is that you can do things many ways, it does not force its agenda.Majewski
M
0

Although not an expert I had to create a solution where my main object had a list of values. Lets call it Object A has a list of ApplicationValues that are mapped in the database. The ApplicationValues have a Key (the form field e.g. PhoneNumber) and the Value.

As the ApplicationValues were an EntitySet I had to create get and set methods to correctly handle setting a specific ApplicationValue. I also had a list of ApplicationRules in my database that defined what these application values could take.

Here is a snippet of code that may help you develop a solution for your needs.

public partial ApplicationValue
{
    public string Key;
    public string Value;
}

public partial ApplicationRule
{
    public string ValidationFormat;
    public string ValidationError;
    public bool Required;
}

public partial class A
{
    public void SetValue(string key, string value)
    {
        //ApplicationValues is the list of values associated to object A
        ApplicationValue v = ApplicationValues.SingleOrDefault
        (k => k.Key == key);

        //if we already have this value
        if (v != null)
        {   //...then we can simply set and return
            v.Value = value;
            return;
        }

        //else we need to create a new ApplicationValue
        v = new ApplicationValue
            {
                AffinityID = this.ID,
                Key = key,
                Value = value
            };

        ApplicationValues.Add(v);
    }

    public string GetValue(ApplicationField key)
    {
        return GetValue(key, String.Empty);
    }

    public string GetValue(ApplicationField key, string defaultValue)
    {
        if (ApplicationValues == null)
            return defaultValue;

        ApplicationValue value = ApplicationValues.SingleOrDefault
        (f => f.Key == key.ToString());

        return (value != null) ? value.Value : defaultValue;
    }

And then to do the form validation I loop through ApplicationRules (which defines if a field is required, contains a regex etc) and match it to the FormCollection.

public ActionResult Details(FormCollection form)
{
    IList<ApplicationRule> applicationRules = //get my rules from the DB

    if (!(ValidateApplication(applicationRules, form, a)))
    {
        ModelState.AddModelError("message", "Please review the errors below.");
        return View(a);
    }
    ...
}

private bool ValidateApplication(IList<ApplicationRule> applicationRules,
                                 FormCollection form, A a)
    {
        //loop through the application rules
        foreach (ApplicationRule ar in applicationRules)
        {
            //try and retrieve the specific form field value using the key
            string value = form[ar.Key];

            if (value == null)
                continue;

            //set the model value just in case there is an error
            //so we can show error messages on our form
            ModelState.SetModelValue(ar.Key, ValueProvider[ar.Key]);

            //if this rule is required
            if (ar.Required)
            {   //...then check if the field has a value
                if (String.IsNullOrEmpty(value))
                {
                    ModelState.AddModelError(ar.Key, "Field is required");
                    continue;
                }
            }

            //if this rule has a validation format
            if (!String.IsNullOrEmpty(ar.ValidationFormat))
            {   //...then check the value is of the correct format
                Regex re = new Regex(ar.ValidationFormat);

                if (!re.IsMatch(value))
                {
                    ModelState.AddModelError(ar.Key, ar.ValidationError);
                    continue;
                }
            }

            a.SetValue(ar.Key, value);
        }

        return ModelState.IsValid;
    }
Mungovan answered 24/6, 2009 at 21:2 Comment(0)
M
0

How dynamic are your field definitions? If they dont change very often you could use code dom to generate the model and controller once the definition is created. I have not tried this in ASP.NET MVC but it might be a good way.

http://msdn.microsoft.com/en-us/library/y2k85ax6.aspx

This article uses code dom for ActionLink generation.

http://blogs.msdn.com/davidebb/archive/2009/06/01/a-buildprovider-to-simplify-your-asp-net-mvc-action-links.aspx#comments

Marguerita answered 24/6, 2009 at 21:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.