xVal 0.8 (Beta) Now Released
This morning I’ve released xVal 0.8 (Beta). It’s a fairly substantial update – it adds the following features:
- Support for NHibernate.Validator (in addition to the existing support for System.ComponentModel.DataAnnotations and Castle Validator)
- Internationalization of default client-side messages
- Much easier to implement custom validation logic (server-side and client-side)
- Fluent syntax for defining ad-hoc validation rules directly in a view
- Support for comparison validators (e.g., “PasswordConfirm” must equal “Password”)
- Works with ASP.NET MVC Release Candidate
- Better performance (rules are now cached per model type)
- A few bugfixes
To recap, xVal is a validation framework bridge for ASP.NET MVC. It lets you use whatever server-side validation technology you prefer (and there are plenty that work well with ASP.NET MVC), and then xVal makes client-side validation happen automatically. On the client, you can enable either jQuery.Validation or ASP.NET (WebForms-style) native validation, or you can write your own plugin.
This is a “beta” quality release, in that the feature set is more or less right for 1.0. The short term goal now is not to add any more major features, but instead to focus on providing proper documentation (instead of just blog posts) and make sure it works robustly in all circumstances.
Now I’ll quickly explain the new features in 0.8.
Support for NHibernate.Validator
If you’re using NHibernate, you might also be using NHibernate.Validator. It’s a server-side validation framework that plugs directly into NHibernate’s pipeline, so you can be sure objects never reach the database unless they satisfy your rules.
xVal can now detect NHibernate.Validator rules and translate many of them into client-side rules. To make this work, assuming you already have an application that uses NHibernate.Validator, add a reference to xVal.dll and xVal.RulesProviders.NHibernateValidator.dll and then enable it in your Global.asax.cs file:
protected void Application_Start() { RegisterRoutes(RouteTable.Routes); xVal.ActiveRuleProviders.Providers.Add(new NHibernateValidatorRulesProvider(ValidatorMode.OverrideXmlWithAttribute)); }
If you want to use jQuery.Validator on the client, then add the following references to your master page:
<head runat="server"> <asp:PlaceHolder runat="server"> <script type="text/javascript" src="<%= Url.Content("~/Scripts/jquery-1.3.2.min.js") %>"></script> <script type="text/javascript" src="<%= Url.Content("~/Scripts/jquery.validate.js") %>"></script> <script type="text/javascript" src="<%= Url.Content("~/Scripts/xVal.jquery.validate.js") %>"></script> </asp:PlaceHolder> </head>
Or, if you want to use ASP.NET Native validation on the client, then add the following references to your master page instead:
<head runat="server"> <asp:PlaceHolder runat="server"> <script type="text/javascript" src="<%= Page.ClientScript.GetWebResourceUrl(typeof(System.Web.UI.Page), "WebForms.js") %>"></script> <script type="text/javascript" src="<%= Page.ClientScript.GetWebResourceUrl(typeof(System.Web.UI.Page), "WebUIValidation.js") %>"></script> <script type="text/javascript" src="<%= ResolveUrl("~/Scripts/xVal.AspNetNative.js")%>"></script> </asp:PlaceHolder> </head>
Finally, apply client-side validation in any view by using Html.ClientSideValidation():
<%= Html.ClientSideValidation<member>() %>
Note that before Visual Studio will recognize Html.ClientSideValidation(), you must either add <%@ Import Namespace=”xVal.Html” %> to the top of your view, or reference the namespace globally in your web.config file:
<system.web> <pages> <namespaces> <!-- leave rest as-is --> <add namespace="xVal.Html"/> </namespaces> </pages> </system.web>
So now, if your model classes define NHibernate.Validator rules, such as:
public class Member { public virtual int MemberID { get; internal set; } [NotNullNotEmpty] [Length(3, 50)] public virtual string Name { get; set; } [NotNullNotEmpty] [Email] [Length(200)] public virtual string EmailAddress { get; set; } }
… then those rules will be applied on the client:
Note that NHibernate.Validator support is pretty early at the moment. I’m interested to get any feedback about how the client-side interpretation of the rules could be improved (for example, should Date properties automatically be required?).
Internationalization of default client-side messages
It’s always been possible to internationalize the server-generated validation messages – just nominate the resource type and resource string name when defining a rule:
[Required(ErrorMessageResourceType = typeof(MyResourceType), ErrorMessageResourceName = "ValueRequired")]
But it’s inconvenient to have to type that out every time. Wouldn’t it be better if you could also internationalize the default client-generated messages too? Now you can.
To try this out, download the demo project and also the latest xVal release. Copy xVal.Messages.en-US.js from the xVal release into the demo project’s ~/Scripts/ folder, and then reference it from the master page:
<head runat="server"> <asp:PlaceHolder runat="server"> <!-- Leave the other references as they are --> <script type="text/javascript" src="<%= Url.Content("~/Scripts/xVal.Messages.en-US.js") %>"></script> </asp:PlaceHolder> </head>
If you run the project now, it will appear that nothing’s changed. However, simply by referencing xVal.Messages.en-US.js, you’ve told xVal to use messages defined in that file instead of using its built-in defaults. If you want to change the English messages, just edit xVal.Messages.en-US.js.
To support another language, make a copy of xVal.Messages.en-US.js, rename it to refer to the other language (e.g., xVal.Messages.es-ES.js), put that copy in your ~/Scripts/ folder, and then edit the messages so they’re in the other language.
Now, change the reference in your master page:
<script type="text/javascript" src="<%= Url.Content("~/Scripts/xVal.Messages.es-ES.js") %>"></script>
And that’s it! You’ve now got Spanish messages:
If you want to change the language dynamically at runtime, you should add some code that varies which xVal.Messages.*.js file gets referenced, perhaps according to the current thread culture.
Update: Thanks to the community for contributing a comprehensive set of default translations. Language files for Brazilian Portuguese, Danish, Spanish, Dutch, Polish, French, German, and Swedish will be included in the next release.
Implementing Custom Validation Logic
You could previously create custom server-side validation logic by subclassing whatever base class your server-side validation framework uses for validation rules. For System.ComponentModel.DataAnnotations, that means subclassing ValidationAttribute. For Castle Validator, it means making an attribute that implements IValidator.
Now, whatever server-side validation framework you use, you can also make custom rules run custom client-side logic by implementing xVal’s ICustomRule interface.
To try this out, download the demo project and add a new class to the DomainModel project called DivisibleByAttribute:
public class DivisibleByAttribute : ValidationAttribute, ICustomRule { private int Divisor { get; set; } public DivisibleByAttribute(int divisor) { Divisor = divisor; ErrorMessage = string.Format("Must be a multiple of {0}.", divisor); } public override bool IsValid(object value) { if(!(value is int)) return false; return ((int)value) % Divisor == ; } public CustomRule ToCustomRule() { return new CustomRule( "DivisibleByN", // JavaScript function name new { divisor = Divisor }, // Params for JavaScript function ErrorMessage // Message if invalid ); } }
Since this derives from ValidationAttribute, your DataAnnotations runner will enforce it on the server when you attach it to some model property:
[Required] [Range(1, 20)] [DivisibleBy(4)] public int NumGuests { get; set; }
To run similar logic on the client, implement a JavaScript function called DivisibleByN:
<script type="text/javascript"> function DivisibleByN(value, element, params) { return value % params.divisor == ; } </script>
Note that you should put this before any call to Html.ClientSideValidation(). Now, you’ll get client-side and server-side validation of your custom rule:
If you want to internationalize your custom rule’s message, then alter DivisibleByAttribute’s ToCustomRule() function so that it uses the CustomRule constructor that lets you specify a resource type and a resource string.
Defining Ad-Hoc Validation Rules in a View
xVal focuses on model based validation. That means you normally define rules in your model layer. However, sometimes it’s useful to throw in a few extra rules that apply only to a specific MVC view. There’s now a quick and easy way of doing this.
To try it out, download the demo project, go to the PlaceBooking.aspx view, and then edit the Html.ClientSideValidation() call to the following:
<%= Html.ClientSideValidation<booking>("booking") .AddRule("Name", new RegularExpressionRule("^[a-z]{3}\\d{3}$", RegexOptions.IgnoreCase) { ErrorMessage = "Must be three letters followed by three numbers." }) .AddRule("BookingDate", new RangeRule(DateTime.Now, null) { ErrorMessage = "Must be in the future." }) %>
(Note: you’ll also need to add a <%@ Import Namespace=”xVal.Rules”%> directive to the top of the view.)
Now, your view’s ad-hoc rules will be combined with the model rules:
If you’re the sort of person who doesn’t like model based validation at all, you could use this feature to build your complete rules configuration inline in a view. Just start with a RuleSet.Empty and add ad-hoc rules to it:
<%= Html.ClientSideValidation("booking", RuleSet.Empty) .AddRule("Name", new RequiredRule()) .AddRule("BookingDate", new RequiredRule()) .AddRule("BookingDate", new DataTypeRule(DataTypeRule.DataType.Date)) /* Etc */ %>
The advantages of model-based validation are clear (DRY), but this gives you extra flexibility that might come in handy.
What next?
The next step is to let people start using this and ask for feedback about how it could be improved. I’ll shy away from adding major new features right now because I don’t personally have time to manage an enormous project. I’m more interested in any bugs you might find or ways in which the current feature set could be applied better. There are already a few issues registered on Codeplex that I’ll be attending to, or better still, you could submit a patch!
Update: Thanks to everyone for all the translations. We’ve now covered all the main ones, so no more are needed.
With thanks to MarkJPerry, DavidHayden, sjmueller, trevor, goneale, imm102, lobex, DerekMcDermott, richardu, Peter Mounce, Billy McCafferty, Mike Saunders, Andrew Scott, Vadim, and various others who have provided useful feedback on the previous release.