Tuesday, April 12, 2011

Formlets in C# and VB.NET (part 2)

In my last post I introduced CsFormlets, a wrapper around FsFormlets that brings formlets to C# and VB.NET. I showed how to model a typical registration form with CsFormlets. Now let's see how we can use formlets in ASP.NET MVC.

To recap, here's the form we're modeling:

form_cs

and here's the top-level formlet's signature, the one we'll reference in our ASP.NET MVC controller:

static Formlet<RegistrationInfo> IndexFormlet() { ... }

RegistrationInfo contains all information about this form. You can place this anywhere you want. I chose to put all formlets related to this form in a static class SignupFormlet, in a Formlets directory and namespace. In MVC terms, formlets fulfill the responsibilities of view (for the form), validation and binding.

We'll start with a regular controller with an action to show the formlet:

public class SignupController : Controller {
    [HttpGet]
    public ActionResult Index() {
        return View(model: SignupFormlet.IndexFormlet().ToString());
    }
}

Our view is trivial (modulo styles and layout), as it's only a placeholder for the formlet:

Views/Signup/Index.cshtml

@using (Html.BeginForm()) {
    @Html.Raw(Model)
    <input type="submit" value="Register" />
}

Now we need an action to handle the form submission. The simplest thing to do is handling the formlet manually (let this be case 1):

[HttpPost]
public ActionResult Index(FormCollection form) {
    var result = SignupFormlet.IndexFormlet().Run(form);
    if (!result.Value.HasValue())
        return View(model: result.ErrorForm.Render());
    var value = result.Value.Value;
    return RedirectToAction("ThankYou", new { name = value.User.FirstName + " " + value.User.LastName });
}

The problem with this approach is, if you want to test the action you have to build a test form, so you're actually testing both binding and the processing of the bound object. No problem, just extract methods to the desired level, e.g. (case 2)

[HttpPost]
public ActionResult Index(FormCollection form) {
    var result = SignupFormlet.IndexFormlet().Run(form);
    return Signup(result);
}

[NonAction]
public ActionResult Signup(FormletResult<RegistrationInfo> registration) {
    if (!registration.Value.HasValue())
        return View(model: registration.ErrorForm.Render());
    return Signup(registration.Value.Value);
}

[NonAction]
public ActionResult Signup(RegistrationInfo registration) {
    return RedirectToAction("ThankYou", new { name = registration.User.FirstName + " " + registration.User.LastName });
}

So we can easily test either Signup method.

Alternatively we can use some ASP.NET MVC mechanisms. For example, a model binder (case 3):

[HttpPost]
public ActionResult Index([FormletBind(typeof(SignupFormlet))] FormletResult<RegistrationInfo> registration) {
    if (!registration.Value.HasValue())
        return View(model: registration.ErrorForm.Render());
    var value = registration.Value.Value;
    return RedirectToAction("ThankYou", new { name = value.User.FirstName + " " + value.User.LastName });
}

By convention the method used to get the formlet is [action]Formlet (you can of course override this).

We can take this even further with an action filter (case 4):

[HttpPost]
[FormletFilter(typeof(SignupFormlet))]
public ActionResult Index(RegistrationInfo registration) {
    return RedirectToAction("ThankYou", new {name = registration.User.FirstName + " " + registration.User.LastName});
}

In this case the filter encapsulates checking the formlet for errors and automatically renders the default action view ("Index" in this case, but this is an overridable parameter) with the error form provided by the formlet. The formlet and filter ensure that the registration argument is never null or invalid when it hits the action.

However convenient they may be, the filter and model binder come at the cost of static type safety. Also, in a real-world application, case 4 is likely not flexible enough, so I'd probably go for the integration case 2 or 3.

Testing formlets

Testing formlets themselves is simple: you just give the formlet a form (usually a NameValueCollection, unless a file is involved), then assert over its result. Since formlets are composable you can easily test some part of it independently of other parts. For example, here's a test for the credit card expiration formlet checking that it correctly validates past dates:

[Fact]
public void CardExpiration_validates_past_dates() {
    var twoMonthsAgo = DateTime.Now.AddMonths(-2);
    var result = SignupFormlet.CardExpiration().Run(new NameValueCollection { 
        {"f0", twoMonthsAgo.Month.ToString()},
        {"f1", twoMonthsAgo.Year.ToString()},
    });
    Assert.False(result.Value.HasValue());
    Assert.True(result.Errors.Any(e => e.Contains("Card expired")));
}

Conclusion

CsFormlets provides composable, testable, statically type-safe validation and binding to C# / VB.NET web apps. Here I showed a possible ASP.NET MVC integration which took less than 200 LoC; integrating with other web frameworks should be similarly easy.

One thing I think I haven't mentioned is that CsFormlets' FormElements generates HTML5 form elements, just like FsFormlets. In fact, pretty much everything I have written about formlets applies to CsFormlets as well, since it's just a thin wrapper.

All code shown here is part of the CsFormlets sample app. Code repository is on github. CsFormlets depends on FsFormlets (therefore also the F# runtime) and runs on .NET 3.5 SP1.

2 comments:

Anonymous said...

How do you edit with formlet?

mausch said...

@Anonymous: if you mean preloading a formlet with values, all formlet elements take an optional parameter value