Friday, April 8, 2011

Formlets in C# and VB.NET

All this talk about formlets in F#, by now you might think this is something that only works in functional languages. Nothing further from the truth. Tomáš Petříček has already blogged about formlets in C#, and I've been working on a wrapper around FsFormlets to be used in C# and VB.NET I called CsFormlets (I'm really good with names if you haven't noticed).

In this post I'll assume you already know about formlets. If you don't, I recommend reading Tomas' article. If you want to know more about my particular implementation of formlets, see my previous posts on the subject. If you're just too lazy to read all those lengthy articles, that's ok, read on, you'll still get a good feeling of formlets.

So if F# is a first-class .NET language, why is it necessary to wrap FsFormlets for C# consumption? Well, for one the formlet type is utterly unmanageable in C#. The formlet type in FsFormlets is (expressed in F#):

type 'a Formlet = 'a Error ErrorList XmlWriter Environ XmlWriter NameGen

where Error, ErrorList, etc, each are individual applicative functors. Type aliases in F# make it easy to hide the 'real' type underneath that, but unfortunately, C# doesn't support type aliases with type parameters, so the formlet type becomes this monster:

FSharpFunc<int, Tuple<Tuple<FSharpList<XNode>, FSharpFunc<FSharpList<Tuple<string, InputValue>>, Tuple<FSharpList<XNode>, Tuple<FSharpList<string>, FSharpOption<T>>>>>, int>>

And no, you can't always var your way out, so to keep this usable I wrapped this in a simpler Formlet<T> type.

Functions that use F# types like FSharpFunc<...> (obviously) and FSharpList<Tuple<T,U>> are wrapped so they use System.Func<...> and IEnumerable<KeyValuePair<T,U>> respectively. F# options are converted to/from nullables whenever possible. Extension methods are provided to work more easily with F# lists and option types. Active patterns (used in F# to match success or failure of formlet) are just not available. Also, applicative operators like <*>, <*, etc are just not accessible in C#, so I wrapped them in methods of Formlet<T>. This yields a fluent interface, as we'll see in a moment.

As usual, I'll demo the code with a concrete form, which looks like this:

form_cs

As I wanted to make this example more real-world than previous ones, I modeled it after the signup form of a real website, don't remember which one but it doesn't really matter. This time it even has decent formatting and styling!

As usual we'll build the form bottom-up.

Password

First the code to collect the password:

static readonly FormElements e = new FormElements();

static readonly Formlet<string> password = 
    Formlet.Tuple2<string, string>() 
        .Ap(e.Password(required: true).WithLabelRaw("Password <em>(6 characters or longer)</em>")) 
        .Ap(e.Password(required: true).WithLabelRaw("Enter password again <em>(for confirmation)</em>")) 
        .SatisfiesBr(t => t.Item1 == t.Item2, "Passwords don't match") 
        .Select(t => t.Item1) 
        .SatisfiesBr(t => t.Length >= 6, "Password must be 6 characters or longer");

Formlet.Tuple2 is just like "yields t2" in FsFormlets, i.e. it sets up a formlet to collect two values in a tuple. Unfortunately, type inference is not so good in C# so we have to define the types here. We'll see later some alternatives to this.

Ap() is just like <*> in FsFormlets.

SatisfiesBr() applies validation. Why "Br"? Because it outputs a <br/> before writing the error message. If no <br/> was present, the error "Password must be 6 characters or longer" would overflow and show in two lines, which looks bad.
This is defined as a simple extension method, implemented using the built-in Satisfies():

static IEnumerable<XNode> BrError(string err, List<XNode> xml) { 
    return xml.Append(X.E("br"), X.E("span", X.A("class", "error"), err)); 
}

static Formlet<T> SatisfiesBr<T>(this Formlet<T> f, Func<T, bool> pred, string error) { 
    return f.Satisfies(pred, 
        (_, x) => BrError(error, x), 
        _ => new[] { error }); 
}

Now you may be wondering about X.E() and X.A(). They're just simple functions to build System.Xml.Linq.XElements and XAttributes respectively.

Back to the password formlet: ever since C# 3.0, Select() is the standard name in C# for what is generally known as map, so I honor that convention in CsFormlets. In this case, it's used to discard the second collected value, since password equality has already been tested in the line above.

Account URL

Moving on, the formlet that collects the account URL:

static readonly Formlet<string> account = 
    Formlet.Single<string>() 
        .Ap("http://") 
        .Ap(e.Text(attributes: new AttrDict {{"required","required"}})) 
        .Ap(".example.com") 
        .Ap(X.E("div", X.Raw("Example: http://<b>company</b>.example.com"))) 
        .Satisfies(a => !string.IsNullOrWhiteSpace(a), "Required field") 
        .Satisfies(a => a.Length >= 2, "Two characters minimum") 
        .Satisfies(a => string.Format("http://{0}.example.com", a).IsUrl(), "Invalid account") 
        .WrapWith(X.E("fieldset"));

You should notice at least two weird things here. If you don't, you're not paying attention! :-)

First weird thing: I said Ap() is <*> , but you couldn't apply <*> to pure text (.Ap("http://")) or XML as shown here, only to a formlet! This is one of the advantages of C#: Ap() is overloaded to accept text and XML, in which case it lifts them to Formlet<Unit> and then applies <*
Because of these overloads Ap() could almost be thought of as append instead of apply.

Second weird thing: instead of writing e.Text(required: true) as in the password formlet, I explicitly used required just as HTML attribute. However, requiredness is checked server-side after all markup. This is for the same reason I defined SatisfiesBr() above: we wouldn't like the error message to show up directly after the input like this:

http:// Required field.example.com

Alternatively, I could have used a polyfill for browsers that don't support the required attribute, but I'm going for a zero-javascript solution here, and also wanted to show this flexibility.

It's also possible to define default conventions for all error messages in formlets (i.e. always show errors above the input, or below, or as balloons) but I won't show it here.

Oh, in case it's not evident, X.Raw() parses XML into System.Xml.Linq.XNodes.

User

Let's put things together in a User class

static readonly Formlet<User> user = 
    Formlet.Tuple5<string, string, string, string, string>() 
        .Ap(e.Text(required: true).WithLabel("First name")) 
        .Ap(e.Text(required: true).WithLabel("Last name")) 
        .Ap(e.Email(required: true).WithLabelRaw("Email address <em>(you'll use this to sign in)</em>")) 
        .Ap(password) 
        .WrapWith(X.E("fieldset")) 
        .Ap(X.E("h3", "Profile URL")) 
        .Ap(account) 
        .Select(t => new User(t.Item1, t.Item2, t.Item3, t.Item4, t.Item5));

Nothing new here, just composing the password and account URL formlets along with a couple other inputs, yielding a User.

Card expiration

Let's tackle the last part of the form, starting with the credit card expiration:

static Formlet<DateTime> CardExpiration() { 
    var now = DateTime.Now; 
    var year = now.Year; 
    return Formlet.Tuple2<int, int>() 
        .Ap(e.Select(now.Month, Enumerable.Range(1, 12))) 
        .Ap(e.Select(year, Enumerable.Range(year, 10))) 
        .Select(t => new DateTime(t.Item2, t.Item1, 1).AddMonths(1)) 
        .Satisfies(t => t > now, t => string.Format("Card expired {0:#} days ago!", (now-t).TotalDays)) 
        .WrapWithLabel("Expiration date<br/>"); 
}

This formlet, unlike previous ones, is a function, because it depends on the current date. It has two <select/> elements: one for the month, one for the year, by default set to the current date.

Billing info

Now we use the card expiration formlet in the formlet that collects other billing data:

static readonly IValidationFunctions brValidationFunctions = 
    new Validate(new ValidatorBuilder(BrError));

static Formlet<BillingInfo> Billing() { 
    return Formlet.Tuple4<string, DateTime, string, string>() 
        .Ap(e.Text(required: true).Transform(brValidationFunctions.CreditCard).WithLabel("Credit card number")) 
        .Ap(CardExpiration()) 
        .Ap(e.Text(required: true).WithLabel("Security code")) 
        .Ap(e.Text(required: true).WithLabelRaw("Billing ZIP <em>(postal code if outside the USA)</em>")) 
        .Select(t => new BillingInfo(t.Item1, t.Item2, t.Item3, t.Item4)) 
        .WrapWith(X.E("fieldset")); 
}

Transform() is just a simple function application. brValidationFunctions.CreditCard is a function that applies credit card number validation (the Luhn algorithm). The validation function is initialized with the same BrError() convention I defined above, i.e. it writes a <br/> and then the error message.

Top formlet

Here's the top-level formlet, the one we'll use in the controller to show the entire form and collect all values:

static Formlet<RegistrationInfo> IndexFormlet() { 
    return Formlet.Tuple2<User, BillingInfo>() 
        .Ap(X.E("h3", "Enter your details")) 
        .Ap(user) 
        .Ap(X.E("h3", "Billing information")) 
        .Ap(Billing()) 
        .Select(t => new RegistrationInfo(t.Item1, t.Item2)); 
}

LINQ & stuff

I've been using Formlet.Tuple in these examples, but you could also use Formlet.Yield, which behaves just like "yields" in FsFormlets. In F# this is no problem because functions are curried, but this is not the case in C#. Even worse, type inference is really lacking in C# compared to F#. This makes Formlet.Yield quite unpleasant to use:

Formlet.Yield<Func<User,Func<BillingInfo,RegistrationInfo>>>((User a) => (BillingInfo b) => new RegistrationInfo(a,b))

With a little function to help with inference such as this one, it becomes

Formlet.Yield(L.F((User a) => L.F((BillingInfo b) => new RegistrationInfo(a, b))))

Still not very pretty, so I prefer to use Formlet.Tuple and then project the tuple to the desired type.

Another way to define formlets in CsFormlets is using LINQ syntax. Tomas explained in detail how this works in a recent blog post. For example, the last formlet defined with LINQ:

static Formlet<RegistrationInfo> IndexFormletLINQ() { 
    return from x in Formlet.Raw(X.E("h3", "Enter your details")) 
           join u in user on 1 equals 1 
           join y in Formlet.Raw(X.E("h3", "Billing information")) on 1 equals 1 
           join billing in Billing() on 1 equals 1 
           select new RegistrationInfo(u, billing); 
}

Also, where can be used to apply validation, although you can't define the error message in each case or where it will be displayed.

The LINQ syntax has some pros and cons.

Pros

  • Less type annotations required.
  • No need to define at the start of the formlet what values and types we will collect.

Cons

  • join and on 1 equals 1 look somewhat odd.
  • Pure text and XML need to be explicitly lifted.
  • Less flexible than chaining methods. If you use where to apply validation, you can't define the message. If you want to use Satisfies(), WrapWith() or any other extension method, you have break up the formlet expression.

Personally, I prefer chaining methods over LINQ, but having a choice might come in handy sometimes.

VB.NET

The title of this post is "Formlets in C# and VB.NET", so what is really different in VB.NET? We could, of course, translate everything in this post directly to VB.NET. But VB.NET has a distinctive feature that is very useful for formlets: XML literals. Instead of:

(C#) xml.Append(X.E("br"), X.E("span", X.A("class", "error"), err));

In VB.NET we can write:

(VB.NET) xml.Append(<br/>, <span class="error"><%= err %></span>)

which is not only clearer, but also more type-safe: you can't write something like <span 112="error">, it's a compile-time error.

To be continued...

This post is too long already so I'll leave ASP.NET MVC integration and testing for another post. If you want to play with the bits, the CsFormlets code is here. All code shown here is part of the CsFormlets sample app. Keep in mind that you also need FsFormlets, which is included as a git submodule, so after cloning CsFormlets you have to init the submodules.

No comments:

Post a Comment