FubuMVC.Validation – Convention based validation

FubuMVC.Validation FubuMVC relies very heavily on the principle of Convention over Configuration; let’s look at how FubuMVC uses this to automatically determine the following:

- What a controller is
- What view belongs to what controller action
- What Url is handled by what controller action

But before we look into these small examples I would like to also explain that these Conventions can easily be overridden with your own conventions, so you are not stuck with how we want you to work. Jeremy Miller has writen a very good article about this on MSDN All examples below are using the default conventions.

Controllers


The Controllers The framework will scan a given assembly and namespace for classes where the name of the class ends with “Controller” these will then automatically be added as Controllers. In the next step all methods matching a certain signature will automatically be added as Controller Actions where the Index method is the default action.


Views


The Views When a Controller is found a convention determines that the name of the Controller without “Controller” is the location where the views reside. Then each Controller Action name is also the actual name of the View for that Controller Action.

Urls


A Url is determined by a Convention that states that each Controller will have a default Url that is equal of the name of the controller class without “Controller” and a secondary Url that includes the name of the Controller Action. Also any other Controller Actions get their own Url assigned prefixed with the Controller name.

    1 namespace Fohjin.Core.Web.Controllers
    2 {
    3     public class HomeController
    4     {
    5         private readonly IRepository _repository;
    6 
    7         public HomeController(IRepository repository)
    8         {
    9             _repository = repository;
   10         }
   11 
   12         public IndexViewModel Index(IndexSetupViewModel inModel)
   13         {
   14             var posts = _repository.Query<Post>().OrderByDescending(p => p.Published);
   15             return new IndexViewModel {Posts = posts.ToList().Select(p => new PostDisplay(p))};
   16         }
   17     }
   18 
   19     public class IndexSetupViewModel : ViewModel
   20     {
   21     }
   22 
   23     [Serializable]
   24     public class IndexViewModel : ViewModel
   25     {
   26         public IEnumerable<PostDisplay> Posts { get; set; }
   27     }
   28 }

So following these conventions the above Controller will assume there is a View called Index.aspx in folder /Views/Home/. And it will automatically register the following Urls: /Home and /Home/Index all without having to configure these things explicitly.

Think about it, most things in live are based on the Convention over Configuration principle, this comes very naturally to us. Once you know how to drive a car you can drive any car, once you have figured out how to open a door you can open other doors as well. The same goes for software, once you have told it how to handle one Controller it can handle others as well without explicitly having to tell about it.

Separation of Concern is also a principle that is very important for FubuMVC, meaning that one controller will always only have one model going in and only one model going out, also called IMIOMO. Further a Controller should be doing composition instead of implementing actual logic, so services are used to do specific things and they are called from within the Controller. But it doesn’t end there FubuMVC also has the notion of Behaviors to separate concerns into smaller (individually testable) classes, these Behaviors can then be attached to the execution path of a Controller. This enables us to create a NHibernate session in a Behavior and do forms authentication in another Behavior without having to affect the Controllers.

On a side note: everything that contains logic is injected using an Inversion of Control container!

So after this small introduction about what the principles of “Convention over Configuration” and “Separation of Concern” mean in the context of FubuMVC I would like to introduce you with my ideas about Convention based Validation of user submitted data. (Later I would like to explore ideas on Entity validation as well, but there might be some issues with who is responsible for that logic).

Convention based Validation


Lets day we have several different forms on our site and we determine that about 90% of the form fields are required fields. We have certain fields for E-mail addresses and others for website addresses, but also phone numbers, personal identification numbers and credit card numbers. All of these have specific logic that is pretty much the same for all of them, right, let’s call them validation rules?

What I wanted to accomplish is that you create Conventions for these validation rules once and that they automatically apply to all view models that needs to be validated. One common approach is to use custom attributes to mark specific fields that need to be validated by specific validation rules, and this I don’t like. I have an unnatural dislike towards attributes, so I wanted something else, and came up with this (you can find the code in the FubuMVC Contrib):

    1 ValidationConfig.Configure = x =>
    2 {
    3     x.ByDefault.PropertiesMatching(property => !property.Name.StartsWith("Optional"), rule => 
    4         rule.WillBeValidatedBy<IsRequired<CanBeAnyViewModel>>());
    5 
    6     x.ByDefault.PropertiesMatching(property => property.Name.Contains("Email"), rule => {
    7         rule.WillBeValidatedBy<IsRequired<CanBeAnyViewModel>>();
    8         rule.WillBeValidatedBy<IsEmail<CanBeAnyViewModel>>();
    9     });
   10 
   11     x.ByDefault.PropertiesMatching(property => property.Name.Contains("Url"), rule =>
   12         rule.WillBeValidatedBy<IsUrl<CanBeAnyViewModel>>());
   13 
   14     x.AddViewModelsFromAssembly
   15         .ContainingType<ViewModel>()
   16         .Where(t => t.Namespace.EndsWith("Web.Controllers"));
   17 
   18     x.OverrideConfigFor<BlogPostCommentViewModel>()
   19         .PropertiesMatching(property => property.Name.StartsWith("Post"), rule =>
   20             rule.WillBeValidatedBy<IsRequired<BlogPostCommentViewModel>>());
   21 };

These Conventions are able to properly validate the following View Model:

    1 public class BlogPostCommentViewModel : ICanBeValidated
    2 {
    3     public int PostYear { get; set; }
    4     public int PostMonth { get; set; }
    5     public int PostDay { get; set; }
    6     public string Slug { get; set; }
    7     public string DisplayName { get; set; }
    8     public string Email { get; set; }
    9     public string Body { get; set; }
   10     public string OptionalUrl { get; set; }
   11     public bool Remember { get; set; }
   12     public bool Subscribed { get; set; }
   13 
   14     private readonly IValidationResults _validationResults = new ValidationResults();
   15     public IValidationResults ValidationResults
   16     {
   17         get { return _validationResults; }
   18     }
   19 }

As you can see there is a ICanBeValidated interface, this interface is needed because by implementing it the Validator can save the validation resuls and pass them along to the Controller if needed, and also the View extension methods make use of this to be able to display a message. I think this is a very clean way of validating user input, the view model doesn’t know about how it will be validated. And another good thing about it is that the actual validation happens in a Behavior separated from the Controllers. Currently it is up to the Controllers to decided what they want to do with this information, but my next step is to create a Behavior that can redirect the request back to the user so it doesn’t even hit the Controller (will have to see if this is possible).

I have also added two Validation extension methods to be used in the actual Views to report to the user that a validation error has occurred. Take a look at the code below which is the partial view of this sites comment form:

    1 <%@ Import Namespace="Fohjin.Core.Web.DisplayModels"%>
    2 <%@ Import Namespace="FubuMVC.Core"%>
    3 <%@ Control Language="C#" AutoEventWireup="true" Inherits="LoggedOutCommentForm" %>
    4 <%@ Import Namespace="FubuMVC.Validation.Extensions"%>
    5 <div class="comment-submit curvy_corner_all">
    6     <%= this.FormFor("{0}".ToFormat(this.UrlTo().CommentToPublishedPost(Model.Post))).Class("user")%>
    7         <fieldset>
    8             <h2>Leave a Comment</h2><a name="leave_a_comment"></a>    
    9             <%= this.Validate().NavigateHereWhenInvalid() %>
   10             <%= this.Validate(m => m.DisplayName).DisplayWhenInvalid("<span style="color:red;">You should provide a name if you want to leave a comment</span><br />") %>
   11             <%= this.TextBoxFor(m => m.DisplayName).ElementId("comment_name").Attr("tabindex", "1").Attr("title", "Your name...")%><br />
   12             <%= this.Validate(m => m.Email).DisplayWhenInvalid("<span style="color:red;">I would like your e-mail address so I can show your Gravatar with your comment</span><br />")%>
   13             <%= this.TextBoxFor(m => m.Email).ElementId("comment_email").Attr("tabindex", "2").Attr("title", "Your email (shows your gravatar)...")%><br />
   14             <%= this.Validate(m => m.OptionalUrl).DisplayWhenInvalid("<span style="color:red;">If you want to leave a website url, then make sure it is correct :)</span><br />")%>
   15             <%= this.TextBoxFor(m => m.OptionalUrl).ElementId("comment_url").Attr("tabindex", "3").Attr("title", "Your website address (optional)...")%><br />
   16             <%= this.Validate(m => m.Body).DisplayWhenInvalid("<span style="color:red;">Hmmm leavinga comment without a comment, perhaps something went wrong?</span><br />")%>
   17             <%= this.TextBoxFor(m => m.Body).ElementId("comment_body").MultilineMode().Attr("cols", "75").Attr("rows", "10").Attr("tabindex", "4").Attr("title", "Leave a comment...")%>
   18             <label><%= this.CheckBoxFor(m => m.Remember).ElementId("comment_remember").Attr("tabindex", "5")%> Remember</label>
   19             <label><%= this.CheckBoxFor(m => m.Subscribed).ElementId("comment_usersubscribed").Attr("tabindex", "6")%> Subscribe for updates to this post</label>
   20             <%= this.SubmitButton("Submit", "comment_submit").ElementId("comment_submit").Attr("tabindex", "7")%>
   21         </fieldset>
   22         <script type="text/javascript">window.stringResources = { "comment_name.Your name...": "Your name...", "comment_email.Your email (shows your gravatar)...": "Your email (shows your gravatar)...", "comment_url.Your website address (optional)...": "Your website address (optional)...", "comment_body.Leave a comment...": "Leave a comment..." };</script>
   23     </form>
   24 </div>

Specifically look at these three lines:

    1 <%= this.Validate().NavigateHereWhenInvalid() %>
    2 <%= this.Validate(m => m.DisplayName).DisplayWhenInvalid("<span style="color:red;">You should provide a name if you want to leave a comment</span><br />") %>
    3 <%= this.TextBoxFor(m => m.DisplayName).ElementId("comment_name").Attr("tabindex", "1").Attr("title", "Your name...")%><br />

The first line is just a way to have the browser automatically move to the form again if it was not valid, the second line reviews the ValidaionResults to determine if the provided fiels was valid or no, and if no it will display the text, as you can see it is using the same syntax as the actual input field.

On my tot list is that I want to add that you can define jQuery methods that need to be triggered on the client to enable client side validation, and providing the same behavior as the server side validation rules. This will be somewhat of a challenge, but that is what it makes fun. Also working on a lessons learned post about this aswell. In the meantime give me some feedback!

No comments yet!

Mark is reading

 
Creative Commons License
This work is licensed under a Creative Commons Attribution 3.0 Unported License.