Web developer from Sydney Australia. Currently using asp.net, mvc where possible.

Sunday, June 15, 2008

Mvc.Net preview 3 validation - server and client side

WORKING EXAMPLE: http://www.xsation.net.au/Samples/MvcValidation/Comment.aspx
DOWNLOAD SOURCE: http://www.xsation.net.au/Samples/MvcValidation/MvcValidationSample.zip

If you need some basics on MVC check out:

Over the last few weeks I have been toying with .net .mvc framework.

While enjoying the freedom that mvc offers from the 'old school'(too early?) .net forms , there are a few sore points
when working with mvc:

  1. No composite controllers

  2. No built in support for validation on the server and client site

Scott Gu has hinted that we will see support for validation in the upcoming releases of the mvc framework, however I need something now.

The rest of the post describes the approach I am trialling at the moment using jquery

The controller

First on the controller I wanted my action method to simply look like the following:

  1. Is this a get request? yes -> return the form(view)..

  2. It's a post. Is the form validated? no -> return the form with errors

  3. It's a validate post. Save the entity then redirect somewhere

The code example deals with a simple scenario where we collect a comment and displaying the list of comments collection so far on the page.

In my example, the c# code for the controller looks like this


//Initialize the view data
CommentListData vd = new CommentListData();

//add the current list of comments to the view data
vd.Comments = _comments;

//1. Its is a gets , just return the list of comments and form
if (Request.HttpMethod == "GET")
return Index();

//Check if the form is valid...
if (!vd.Form.Validate(Request.Form))
{
//if its not, the form will have all the errors messages populated in the viewddata (vd).
return View("Index", vd);
}

//Form is valid, so we can add our new comment.
Comment comment = new Comment();

//Set all validated properties
vd.Form.UpdateEntity(comment);
_comments.Add(comment);

///Redirects back to the comment list of comments.
return RedirectToAction( "Index");

Although a little long, it achieves the 3 goals above of how a simple form post should be handled.

The definition of the CommentListData object is show below

   public class CommentListData
{
public CommentListData() { this.Form = new CommentForm(); }

public List Comments { get; set; }
public CommentForm Form { get; set; }
}

This is pretty self explanatory, we have a list of comments and a form. The form object deals with the collection of the 3 inputs from the
posted form, 1) A person's name 2) A person's email and 3) the persons comment.

The form object provides the following basic functions

  1. Holds the raw (string) data posted from the raw http form

  2. Performs validation to determine if the data is valid

  3. Transfers the validated data into the real typed entity

The actual implementation of the CommentForm is posted below.


public class CommentForm : Form
{
public FormField Name = new FormField("Comment_Name");
public FormField EmailAddress = new FormField("Comment_EmailAddress");
public FormField Comments = new FormField("Comment_Comments");

public RequiredFieldValidator NameRequired;
public RequiredFieldValidator EmailAddressRequired;
public RequiredFieldValidator CommentsRequired;
public RegexFieldValidator EmailAddressRegex;

public override void AddValidators(List validators)
{
this.NameRequired = new RequiredFieldValidator("Please enter your name", Name);
this.EmailAddressRequired = new RequiredFieldValidator("Please enter your email address", EmailAddress);
this.CommentsRequired = new RequiredFieldValidator("Please enter your comments", Comments);
this.EmailAddressRegex = new RegexFieldValidator("Please check the format of your email address", EmailAddress, "..*@..*\\...*");

validators.Add(this.NameRequired);
validators.Add(this.EmailAddressRequired);
validators.Add(this.CommentsRequired);
validators.Add(this.EmailAddressRegex);
}
}

Here we define our 3 fields that we wish to collection form the raw http form. Then we add all our validators to the form

The CommentForm inherits from a generic Form object, we pass our Comment object in as the generic. The base form object can handle the following functions now it has a Comment object

  1. It can validate the posted form using the validates we have described

  2. It can set the form up with initial values from a Comment object( used for editing purposes)

  3. It can updated the Comment entity based on a valid form

The Form object uses reflection and some exiting MVC helpers that also use reflection to achieve these functions.

The implementation details for the form object can be browsed in the download available at the top of this article.

The html view layer

The full html for the comment form is posted below. Its should be pretty straight forward for those familiar with .net and html.

For those keeping up to date with mvc you will notice a lack of the Html Helper methods, this is a personal preference. I like to have my html out in open for everyone to see, not hidden in some server method.


<fieldset class="comment">
<legend>Add your comment</legend>
<div class="inner">
<form id="addCommentForm" method="post" action="<%= Url.AddCommentUrl() %>">


<ul id="error_list" class="error" <%= Html.RenderDisplayNone(this.ViewData.Model.IsValid) %> >
<h6>There were errors!</h6>
<% foreach(var e in this.ViewData.Model.Validators){ %>
<li <%= Html.RenderDisplayNone(e.IsValid ?? true) %> class=" <%= e.ClientId %>" ><%= e.Message %></span></li>
<% } %>
</ul>
<div class="row">
<label for="Comment_Name">Name:</label>
<input id="Comment_Name" name="Comment_Name" class="text" value="<%= this.ViewData.Model.Name %>" type="text" />
<span class="error Comment_Name_Required" style="display: none" >« <%= this.ViewData.Model.NameRequired.Message %></span>
</div>

<div class="row">
<label for="Comment_EmailAddress">Email:</label>
<input id="Comment_EmailAddress" name="Comment_EmailAddress" value="<%= this.ViewData.Model.EmailAddress %>" type="text" />
<span class="error Comment_EmailAddress_Required" style="display: none" >« <%= this.ViewData.Model.EmailAddressRequired.Message%></span>
<span class="error Comment_EmailAddress_Regex" style="display: none" >« <%= this.ViewData.Model.EmailAddressRegex.Message%></span>
</div>

<div class="row">
<label for="Comment_Comments">Comments:</label>
<textarea id="Comment_Comments" name="Comment_Comments" rows="10" cols="60"
><%= this.ViewData.Model.Comments %></textarea><span class="error Comment_Comments_Required" style="display: none" >« !</span>
</div>

<div class="row button">
<input value="Add Comment" type="submit" />
</div>

<val:ClientValidators FormId="addCommentForm" Form="<%# this.ViewData.Model %>" runat="server" />
</form>
</div>
</fieldset>

There are a couple of things to note here, firstly the ClientValidators server control a the bottom. This can be viewed in the download
but basically it initializes a bunch of javascript objects to help with client side validation (output of the control shown below). The script attaches the validators to the actual html form DOM object


<script type="text/javascript">var form = document.getElementById('addCommentForm');
form.validators = new Validators();
form.validators.array[0] = new Validator('Please enter your name', 'validate_RequiredField', 'Comment_Name_Required', 'Comment_Name' , '');
form.validators.array[1] = new Validator('Please enter your email address', 'validate_RequiredField', 'Comment_EmailAddress_Required', 'Comment_EmailAddress' , '');
form.validators.array[2] = new Validator('Please enter your comments', 'validate_RequiredField', 'Comment_Comments_Required', 'Comment_Comments' , '');
form.validators.array[3] = new RegexValidator('Please check the format of your email address', 'Comment_EmailAddress_Regex', 'Comment_EmailAddress' , '', '..*@..*\...*');
</script>

Secondly, the html inputs are initialized from the form values, this allows an invalid value to be posted to the server and then redisplayed with an error message. (it allows us to keep invalid values during a round trip)

Thirdly, there is a simple Html helper, to render "style="display: none" based on a boolean value.

JQuery is used to perform the client side validation. It makes use of the array of validators to perform client side validation. A portion of the jquery is show below.


//when ready
$(document).ready(function(){

//get every form in this document (1 in this case)
$("form").each(
function()
{
//if validators exist
if(this.validators)
{
//attach the validates to the inputs
attachValidator(this.validators);

//attach efferts to the inputs
attachValidatorEffects(this.validators);
}

$(this).submit(function()
{
if(this.validators)
{
var isValid = this.validators.validate();
var errorList = $('#error_list');
errorList.hide();

for(var i=0; i < this.validators.array.length; i++)
{
UpdateValidatorDisplay(this.validators.array[i]);
}

if(!isValid)
errorList.fadeIn();
else
$(this).css("visibility","hidden");

return isValid;
}
});
});
});


function attachValidatorEffects(validators)
{
//loop through each validotor
for(var i=0; i < input =" $(validators.array[i].targetDom);" j="0;">

I'll be the first to admit my javascript skills are lacking but I did get the job done. The form in action can be viewed here
http://www.xsation.net.au/Samples/MvcValidation/Comment.aspx.

This is a rough start to validation using mvc and I am sure Scott and the team will come up with something far better. But for now, I hope it either helps you out a bit or provides some discussions points moving forward.

kick it on DotNetKicks.com

7 comments:

Leapfrog said...

I like this as a temporary solution.

How do you propose to handle drop down lists? Not only validating that a selection has been made but populating the lists through your form object.

Mark Kemper said...

I haven't had time to try this out yet but I would probably just create a new FormField object. Maybe called ListFormField, that would have a "List<T> Options" property, which you could used to populate the list on the view.
The FormField already has a initial value, so the required validator should just work (but it would need some testing on the client side)..

Another note, I have recently used this approach to valid forms submited to Generic Handlers, works well. A Generic Handler + UrlRewriting.Net almost = mvc :) (and can be used on existing .net apps)

Anonymous said...

Your blog keeps getting better and better! Your older articles are not as good as newer ones you have a lot more creativity and originality now keep it up!

Anonymous said...

Мне понравился ваш сайтик, так держать.

Anonymous said...

Interesting post as for me. I'd like to read something more concerning this topic. Thanx for giving this information.

Anonymous said...

hey, spring is cooming! good post there, tnx for www.blogger.com

Govind Sharma said...

Amazing and useful article. Thanks for posting this.

Thanks..

Mechanical Seals