Posted
almost 14 years
ago
by
[email protected] (Mauricio Scheffer)
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
... [More]
form we're modeling: 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. [Less]
|
Posted
almost 14 years
ago
by
[email protected] (mausch)
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
... [More]
form we're modeling: 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. [Less]
|
Posted
almost 14 years
ago
by
[email protected] (Mauricio Scheffer)
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
... [More]
form we're modeling: 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. [Less]
|
Posted
almost 14 years
ago
by
[email protected] (Mauricio Scheffer)
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
... [More]
form we're modeling: 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. [Less]
|
Posted
almost 14 years
ago
by
[email protected] (Mauricio Scheffer)
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
... [More]
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:
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. [Less]
|
Posted
almost 14 years
ago
by
[email protected] (Mauricio Scheffer)
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
... [More]
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>>>>, int>>
And no, you can't always var your way out, so to keep this usable I wrapped this in a simpler Formlet type.
Functions that use F# types like FSharpFunc<...> (obviously) and FSharpList> are wrapped so they use System.Func<...> and IEnumerable> 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. 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:
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 (6 characters or longer)"))
.Ap(e.Password(required: true).WithLabelRaw("Enter password again (for confirmation)"))
.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 before writing the error message. If no 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 BrError(string err, List xml) {
return xml.Append(X.E("br"), X.E("span", X.A("class", "error"), err));
}
static Formlet SatisfiesBr(this Formlet f, Funcbool> 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://company.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 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 =
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 (you'll use this to sign in)"))
.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 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");
}
This formlet, unlike previous ones, is a function, because it depends on the current date. It has two 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 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 (postal code if outside the USA)"))
.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 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 IndexFormlet() {
return Formlet.Tuple2()
.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>>((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 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 , 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. [Less]
|
Posted
almost 14 years
ago
by
[email protected] (Mauricio Scheffer)
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
... [More]
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>>>>, int>>
And no, you can't always var your way out, so to keep this usable I wrapped this in a simpler Formlet type.
Functions that use F# types like FSharpFunc<...> (obviously) and FSharpList> are wrapped so they use System.Func<...> and IEnumerable> 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. 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:
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 (6 characters or longer)"))
.Ap(e.Password(required: true).WithLabelRaw("Enter password again (for confirmation)"))
.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 before writing the error message. If no 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 BrError(string err, List xml) {
return xml.Append(X.E("br"), X.E("span", X.A("class", "error"), err));
}
static Formlet SatisfiesBr(this Formlet f, Funcbool> 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://company.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 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 =
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 (you'll use this to sign in)"))
.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 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");
}
This formlet, unlike previous ones, is a function, because it depends on the current date. It has two 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 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 (postal code if outside the USA)"))
.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 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 IndexFormlet() {
return Formlet.Tuple2()
.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>>((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 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 , 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. [Less]
|
Posted
almost 14 years
ago
by
[email protected] (mausch)
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
... [More]
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:
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. [Less]
|
Posted
almost 14 years
ago
by
[email protected] (Mauricio Scheffer)
I've blogged before about formlets, a nice abstraction of HTML forms. I started with a basic implementation, then showed validation. Now I'll present a non-toy implementation of formlets I called FsFormlets (really original name, I know). By
... [More]
"non-toy" I mean this implementation is not a proof of concept or just for didactic purposes, but is meant to be eventually production-quality. I don't like to explain things in a vacuum, so I'll use a typical registration form to show how it works: Even though FsFormlets can be used to generate HTML on its own, there's a much better tool for this in F#: Wing Beats. Wing Beats is an EDSL in F# to generate HTML, much like SharpDOM in C# or Blaze in Haskell, or Eliom's XHTML.M in OCaml (except that XHTML.M actually validates HTML statically). So I've added a module to integrate FsFormlets to Wing Beats. This integration is a separate assembly; FsFormlets is stand-alone (it only requires .NET 3.5 SP1 and the F# runtime). We'll use FsFormlets to express forms, and Wing Beats to express the rest of HTML. Also, we'll handle web requests with Figment, with the help of some more glue code to integrate it with FsFormlets and Wing Beats. Layout Let's start by defining a layout in Wing Beats: let e = XhtmlElement()
let layout title body =
[
e.DocTypeHTML5
e.Html [
e.Head [
e.Title [ &title ]
e.Style [
&".error {color:red;}"
&"body {font-family:Verdana,Geneva,sans-serif; line-height: 160%;}"
]
]
e.Body [
yield e.H1 [ &title ]
yield! body
]
]
]
No formlets so far, this is all pure HTML, expressed as a regular function. Now we'll build the form bottom-up.
ReCaptcha
FsFormlets already includes a reCaptcha formlet (I'll show its innards in a future post). We just have to configure it with a pair of public and private key (get it here) before using it:
let reCaptcha = reCaptcha {PublicKey = "your_public_key"; PrivateKey = "your_private_key"; MockedResult = None}
MockedResult lets you skip the actual validation web call and force a result, for testing purposes.
Date input
Now the date formlet, which is built from three inputs, plus labels and validation:
let f = e.Formlets
let dateFormlet : DateTime Formlet =
let baseFormlet =
yields t3
<*> (f.Text(maxlength = 2, attributes = ["type","number"; "min","1"; "max","12"; "required","required"; "size","3"]) |> f.WithLabel "Month: ")
<*> (f.Text(maxlength = 2, attributes = ["type","number"; "min","1"; "max","31"; "required","required"; "size","3"]) |> f.WithLabel "Day: ")
<*> (f.Text(maxlength = 4, attributes = ["type","number"; "min","1900"; "required","required"; "size","5"]) |> f.WithLabel "Year: ")
let isDate (month,day,year) =
let pad n (v: string) = v.PadLeft(n,'0')
let ymd = sprintf "%s%s%s" (pad 4 year) (pad 2 month) (pad 2 day)
DateTime.TryParseExact(ymd, "yyyyMMdd", CultureInfo.InvariantCulture, DateTimeStyles.None) |> fst
let dateValidator = err isDate (fun _ -> "Invalid date")
baseFormlet
|> satisfies dateValidator
|> map (fun (month,day,year) -> DateTime(int year,int month,int day))
Here, baseFormlet is of type (string * string * string) Formlet, that is, it collects values in their raw form. This baseFormlet is then validated to make sure it's a date, and finally mapped to a DateTime. Note the use of the 'number' input type (a HTML5 input) and also HTML5 validation attributes (required, min, max)
Password input
The password formlet is next:
let doublePassword =
let compressedLength (s: string) =
use buffer = new MemoryStream()
use comp = new DeflateStream(buffer, CompressionMode.Compress)
use w = new StreamWriter(comp)
w.Write(s)
w.Flush()
buffer.Length
let isStrong s = compressedLength s >= 106L
let f =
yields t2
<*> (f.Password(required = true) |> f.WithLabel "Password: ")
< e.Br()
<*> (f.Password(required = true) |> f.WithLabel "Repeat password: ")
let areEqual (a,b) = a = b
f
|> satisfies (err areEqual (fun _ -> "Passwords don't match"))
|> map fst
|> satisfies (err isStrong (fun _ -> "Password too weak"))
There are two things we validate here for passwords: first, the two entered passwords must match; and second, it must be strong enough. To measure password strength I use the compression technique I described in my last post.
The < operator is used to add pure markup to the formlet, using Wing Beats.
Also note the 'required = true' parameter. In the date formlet, we just used required=required as an HTML attribute. Here we used the optional argument required=true, which validates the formlet both at the client (with a required=required HTML attribute) and the server (it has a satisfies clause). In the date formlet we don't really care to validate server-side if the user filled out each input, we just want to know if it's a valid date or not.
The optional parameter 'maxlength' does a similar thing: it outputs a maxlength attribute, and also checks the maximum length of he POSTed value. Sure all browsers implement maxlength properly, but anyone can easily craft a request (e.g. with cUrl, no need to program at all) to circumvent it. I see malicious bots doing things like this every day. This makes sure the data is really valid before you start processing it.
Putting it all together
Moving on, let's write the final formlet and store the posted information in a record:
type PersonalInfo = {
Name: string
Email: string
Password: string
DateOfBirth: DateTime
}
let registrationFormlet ip =
yields (fun n e p d ->
{ Name = n; Email = e; Password = p; DateOfBirth = d })
<*> (f.Text(required = true) |> f.WithLabel "Name: ")
< e.Br()
<*> (f.Email(required = true) |> f.WithLabel "Email: ")
< e.Br()
<*> doublePassword
< e.Br()
< &"Date of birth: " <*> dateFormlet
< e.Br()
< &"Please read very carefully these terms and conditions before registering for this online program, blah blah blah"
< e.Br()
<* (f.Checkbox(false) |> satisfies (err id (fun _ -> "Please accept the terms and conditions")) |> f.WithLabel "I agree to the terms and conditions above")
<* reCaptcha ip
Some notes about this last snippet:
f.Email(required = true) generates an <input type="email" required="required"/> (again, HTML5 !), all server-validated.
Unlike previous formlets, this one is a function, because reCaptcha needs the client's IP address to validate.
If you've read my previous posts about formlets, you may be wondering why I'm using things like f.Text() and f.Checkbox() (which are members of an object) instead of the regular functions input and checkbox. Those functions are also present in FsFormlets and you may use them interchangeably with the object-style formlets, e.g. instead of f.CheckBox(false) you can write checkbox false []. The object-style formlets build on top of functional formlets, adding optional parameters for validation. They also integrate more seamlessly with Wing Beats.
And we're done with the form! Now let's build the actual page that contains it. Using the layout we wrote earlier:
let s = e.Shortcut
let registrationPage form =
layout "Registration" [
s.FormPost "" [
e.Fieldset [
yield e.Legend [ &"Please fill the fields below" ]
yield!! form
yield e.Br()
yield s.Submit "Register!"
]
]
]
This is also pure Wing Beats markup, I think it doesn't need much explanation except for the "yield!! form" which indicates where to render the formlet (yes, the operator doesn't look very friendly, I'll probably change it). Note how easy it is to compose pieces of potentially reusable HTML with Wing Beats as they're just functions.
Handling requests with Figment
Now all we need to do is bind the formlet to a URL to render the page:
get "register" (fun _ -> registrationFormlet "" |> renderToXml |> registrationPage |> Result.wbview)
and handle the form POST:
post "register" (fun ctx ->
let env = EnvDict.fromFormAndFiles ctx.Request
match run (registrationFormlet ctx.IP) env with
| Success v -> Result.redirectf "thankyou?n=%s" v.Name
| Failure(errorForm,_) -> errorForm |> registrationPage |> Result.wbview)
Oh, I almost forgot the little "thank you" after-registration page:
get "thankyou" (fun ctx -> Result.contentf "Thank you for registering, %s" ctx.QueryString.["n"])
Now, this workflow we've just modeled is pretty common:
Show form.
User submits form.
Validate form. If errors, show form again to user.
Process form data.
Redirect.
So it's worthy of abstraction. The only moving parts are the formlet, the page and what to do on successful validation, so instead of mapping get and post individually we can say:
formAction "register" {
Formlet = fun ctx -> registrationFormlet ctx.IP
Page = fun _ -> registrationPage
Success = fun _ v -> Result.redirectf "thankyou?n=%s" v.Name
}
HTML5 ready
Implementation of HTML5 in browsers has exploded in 2010 (see the beautiful html5readiness.com for reference). In particular, HTML5 forms are already implemented in Chrome 10, Opera and Firefox 4 (with caveats). Safari and IE haven't implemented it yet (at least not in versions 5.0.4 and 9 respectively), but WebKit already supports it so I guess Safari and other WebKit-based browsers will implement this in the short-term. So there's little reason not to use the new elements and attributes right now, as long as you also have server-side validation.
For example, here's how Chrome validates required fields when trying to submit:
Whereas in Safari 5.0.4/Windows the server-side validation kicks in:
If you want browser-side validation in Safari, IE, etc, you can easily use an unobtrusive polyfill. A thorough list of cross-browser HTML5 polyfills is available here.
For example, applying jQuery.tools is as easy as:
let jsValidation =
e.Div [
s.JavascriptFile "http://cdn.jquerytools.org/1.2.5/full/jquery.tools.min.js"
e.Script [ &"$('form').validator();" ]
]
and putting it at the bottom of the Wing Beats layout.
All code posted here is part of the sample Figment app.
FsFormlets source code is here. [Less]
|
Posted
almost 14 years
ago
by
[email protected] (mausch)
I've blogged before about formlets, a nice abstraction of HTML forms. I started with a basic implementation, then showed validation. Now I'll present a non-toy implementation of formlets I called FsFormlets (really original name, I know). By
... [More]
"non-toy" I mean this implementation is not a proof of concept or just for didactic purposes, but is meant to be eventually production-quality. I don't like to explain things in a vacuum, so I'll use a typical registration form to show how it works: Even though FsFormlets can be used to generate HTML on its own, there's a much better tool for this in F#: Wing Beats. Wing Beats is an EDSL in F# to generate HTML, much like SharpDOM in C# or Blaze in Haskell, or Eliom's XHTML.M in OCaml (except that XHTML.M actually validates HTML statically). So I've added a module to integrate FsFormlets to Wing Beats. This integration is a separate assembly; FsFormlets is stand-alone (it only requires .NET 3.5 SP1 and the F# runtime). We'll use FsFormlets to express forms, and Wing Beats to express the rest of HTML. Also, we'll handle web requests with Figment, with the help of some more glue code to integrate it with FsFormlets and Wing Beats. Layout Let's start by defining a layout in Wing Beats: let e = XhtmlElement()
let layout title body =
[
e.DocTypeHTML5
e.Html [
e.Head [
e.Title [ &title ]
e.Style [
&".error {color:red;}"
&"body {font-family:Verdana,Geneva,sans-serif; line-height: 160%;}"
]
]
e.Body [
yield e.H1 [ &title ]
yield! body
]
]
]
No formlets so far, this is all pure HTML, expressed as a regular function. Now we'll build the form bottom-up.
ReCaptcha
FsFormlets already includes a reCaptcha formlet (I'll show its innards in a future post). We just have to configure it with a pair of public and private key (get it here) before using it:
let reCaptcha = reCaptcha {PublicKey = "your_public_key"; PrivateKey = "your_private_key"; MockedResult = None}
MockedResult lets you skip the actual validation web call and force a result, for testing purposes.
Date input
Now the date formlet, which is built from three inputs, plus labels and validation:
let f = e.Formlets
let dateFormlet : DateTime Formlet =
let baseFormlet =
yields t3
<*> (f.Text(maxlength = 2, attributes = ["type","number"; "min","1"; "max","12"; "required","required"; "size","3"]) |> f.WithLabel "Month: ")
<*> (f.Text(maxlength = 2, attributes = ["type","number"; "min","1"; "max","31"; "required","required"; "size","3"]) |> f.WithLabel "Day: ")
<*> (f.Text(maxlength = 4, attributes = ["type","number"; "min","1900"; "required","required"; "size","5"]) |> f.WithLabel "Year: ")
let isDate (month,day,year) =
let pad n (v: string) = v.PadLeft(n,'0')
let ymd = sprintf "%s%s%s" (pad 4 year) (pad 2 month) (pad 2 day)
DateTime.TryParseExact(ymd, "yyyyMMdd", CultureInfo.InvariantCulture, DateTimeStyles.None) |> fst
let dateValidator = err isDate (fun _ -> "Invalid date")
baseFormlet
|> satisfies dateValidator
|> map (fun (month,day,year) -> DateTime(int year,int month,int day))
Here, baseFormlet is of type (string * string * string) Formlet, that is, it collects values in their raw form. This baseFormlet is then validated to make sure it's a date, and finally mapped to a DateTime. Note the use of the 'number' input type (a HTML5 input) and also HTML5 validation attributes (required, min, max)
Password input
The password formlet is next:
let doublePassword =
let compressedLength (s: string) =
use buffer = new MemoryStream()
use comp = new DeflateStream(buffer, CompressionMode.Compress)
use w = new StreamWriter(comp)
w.Write(s)
w.Flush()
buffer.Length
let isStrong s = compressedLength s >= 106L
let f =
yields t2
<*> (f.Password(required = true) |> f.WithLabel "Password: ")
< e.Br()
<*> (f.Password(required = true) |> f.WithLabel "Repeat password: ")
let areEqual (a,b) = a = b
f
|> satisfies (err areEqual (fun _ -> "Passwords don't match"))
|> map fst
|> satisfies (err isStrong (fun _ -> "Password too weak"))
There are two things we validate here for passwords: first, the two entered passwords must match; and second, it must be strong enough. To measure password strength I use the compression technique I described in my last post.
The < operator is used to add pure markup to the formlet, using Wing Beats.
Also note the 'required = true' parameter. In the date formlet, we just used required=required as an HTML attribute. Here we used the optional argument required=true, which validates the formlet both at the client (with a required=required HTML attribute) and the server (it has a satisfies clause). In the date formlet we don't really care to validate server-side if the user filled out each input, we just want to know if it's a valid date or not.
The optional parameter 'maxlength' does a similar thing: it outputs a maxlength attribute, and also checks the maximum length of he POSTed value. Sure all browsers implement maxlength properly, but anyone can easily craft a request (e.g. with cUrl, no need to program at all) to circumvent it. I see malicious bots doing things like this every day. This makes sure the data is really valid before you start processing it.
Putting it all together
Moving on, let's write the final formlet and store the posted information in a record:
type PersonalInfo = {
Name: string
Email: string
Password: string
DateOfBirth: DateTime
}
let registrationFormlet ip =
yields (fun n e p d ->
{ Name = n; Email = e; Password = p; DateOfBirth = d })
<*> (f.Text(required = true) |> f.WithLabel "Name: ")
< e.Br()
<*> (f.Email(required = true) |> f.WithLabel "Email: ")
< e.Br()
<*> doublePassword
< e.Br()
< &"Date of birth: " <*> dateFormlet
< e.Br()
< &"Please read very carefully these terms and conditions before registering for this online program, blah blah blah"
< e.Br()
<* (f.Checkbox(false) |> satisfies (err id (fun _ -> "Please accept the terms and conditions")) |> f.WithLabel "I agree to the terms and conditions above")
<* reCaptcha ip
Some notes about this last snippet:
f.Email(required = true) generates an <input type="email" required="required"/> (again, HTML5 !), all server-validated.
Unlike previous formlets, this one is a function, because reCaptcha needs the client's IP address to validate.
If you've read my previous posts about formlets, you may be wondering why I'm using things like f.Text() and f.Checkbox() (which are members of an object) instead of the regular functions input and checkbox. Those functions are also present in FsFormlets and you may use them interchangeably with the object-style formlets, e.g. instead of f.CheckBox(false) you can write checkbox false []. The object-style formlets build on top of functional formlets, adding optional parameters for validation. They also integrate more seamlessly with Wing Beats.
And we're done with the form! Now let's build the actual page that contains it. Using the layout we wrote earlier:
let s = e.Shortcut
let registrationPage form =
layout "Registration" [
s.FormPost "" [
e.Fieldset [
yield e.Legend [ &"Please fill the fields below" ]
yield!! form
yield e.Br()
yield s.Submit "Register!"
]
]
]
This is also pure Wing Beats markup, I think it doesn't need much explanation except for the "yield!! form" which indicates where to render the formlet (yes, the operator doesn't look very friendly, I'll probably change it). Note how easy it is to compose pieces of potentially reusable HTML with Wing Beats as they're just functions.
Handling requests with Figment
Now all we need to do is bind the formlet to a URL to render the page:
get "register" (fun _ -> registrationFormlet "" |> renderToXml |> registrationPage |> Result.wbview)
and handle the form POST:
post "register" (fun ctx ->
let env = EnvDict.fromFormAndFiles ctx.Request
match run (registrationFormlet ctx.IP) env with
| Success v -> Result.redirectf "thankyou?n=%s" v.Name
| Failure(errorForm,_) -> errorForm |> registrationPage |> Result.wbview)
Oh, I almost forgot the little "thank you" after-registration page:
get "thankyou" (fun ctx -> Result.contentf "Thank you for registering, %s" ctx.QueryString.["n"])
Now, this workflow we've just modeled is pretty common:
Show form.
User submits form.
Validate form. If errors, show form again to user.
Process form data.
Redirect.
So it's worthy of abstraction. The only moving parts are the formlet, the page and what to do on successful validation, so instead of mapping get and post individually we can say:
formAction "register" {
Formlet = fun ctx -> registrationFormlet ctx.IP
Page = fun _ -> registrationPage
Success = fun _ v -> Result.redirectf "thankyou?n=%s" v.Name
}
HTML5 ready
Implementation of HTML5 in browsers has exploded in 2010 (see the beautiful html5readiness.com for reference). In particular, HTML5 forms are already implemented in Chrome 10, Opera and Firefox 4 (with caveats). Safari and IE haven't implemented it yet (at least not in versions 5.0.4 and 9 respectively), but WebKit already supports it so I guess Safari and other WebKit-based browsers will implement this in the short-term. So there's little reason not to use the new elements and attributes right now, as long as you also have server-side validation.
For example, here's how Chrome validates required fields when trying to submit:
Whereas in Safari 5.0.4/Windows the server-side validation kicks in:
If you want browser-side validation in Safari, IE, etc, you can easily use an unobtrusive polyfill. A thorough list of cross-browser HTML5 polyfills is available here.
For example, applying jQuery.tools is as easy as:
let jsValidation =
e.Div [
s.JavascriptFile "http://cdn.jquerytools.org/1.2.5/full/jquery.tools.min.js"
e.Script [ &"$('form').validator();" ]
]
and putting it at the bottom of the Wing Beats layout.
All code posted here is part of the sample Figment app.
FsFormlets source code is here. [Less]
|