Summary
Working with ASP.NET MVC when developing web applications you get a lot of validation support almost for free when you use the Unobtrusive Client Validation feature (which is enabled by default). Once you start introducing client-side dynamic form elements using Knockout.js, or other similar JavaScript frameworks, you need to take some additional steps to get validation for the Knockout-generated elements. This post describes one possible approach using a very simple HTML page.
Demonstration Page
The demonstration page for this is a very simple representation of a hypothetical web application that records games played by players, including the player’s name and the score for a variable number of games. The fields for the player’s name are static in nature (meaning they’re always present on the page) while the list of games and scores are generated using Knockout.
The demonstration page displaying validation errors.
The complete source code for the demonstration page can be found at the end of this post.
Knockout and Data Binding
If you’re not familiar with Knockout, there’s an excellent tutorial available on the Knockout site. Briefly, Knockout provides a framework that makes it easy to perform data binding using HTML and view models in JavaScript.
In our simple case we have a view model that contains the player’s first and last name along with an array containing the played games and scores:
function GameViewModel() {
var self = this;
this.Name = '';
this.Score = '';
}
function ViewModel() {
var self = this;
self.Games = ko.observableArray();
self.AddGame = function () {
self.Games.push(new GameViewModel());
};
self.RemoveGame = function (game) {
self.Games.destroy(game);
}
}
The GameViewModel
class represents a single played game while the ViewModel
class serves as the main view model for the page and contains the player’s name along with the list of played games. We use the Knockout observable array feature here so the page is updated when games are added to or removed from the list of games.
To display the played games we use the data binding features of Knockout:
<table class="table table-condensed">
<thead>
<tr>
<th style="width:50%">Game</th>
<th style="width:30%">Score</th>
<th style="width:20%"><button type="button" class="btn btn-default" data-bind="click: AddGame">Add Game</button></th>
</tr>
</thead>
<tbody data-bind="foreach: Games">
<tr>
<td>
<input type="text" class="form-control validate-name-required" data-bind="value: Name, uniqueName: true" />
</td>
<td>
<input type="text" class="form-control validate-score" data-bind="value: Score, uniqueName: true" />
</td>
<td>
<input type="button" class="btn btn-xs btn-danger" data-bind="click: $root.RemoveGame" value="X" />
</td>
</tr>
</tbody>
</table>
Note the use of the Knockout uniqueName
function in the data-binding. We use this to have Knockout generate a unique name for each field which is needed since jQuery Validate will only validate fields that have the name attribute set.
Validation
The first and last name of the player we validate by simply including the required
attribute, jQuery Validate automatically picks this up when we tell it to validate the form:
<div class="form-group">
<label class="col-md-2 control-label" for="FirstName">First Name:</label>
<div class="col-md-6"><input type="text" class="form-control" name="FirstName" value="" required="required" /></div>
</div>
<div class="form-group">
<label class="col-md-2 control-label" for="LastName">Last Name:</label>
<div class="col-md-6"><input type="text" class="form-control" name="LastName" value="" required="required" /></div>
</div>
To get the Knockout-generated fields to be validate we use a different approach. One of several ways you can identify fields to validate is to define validation rules for fields that have specific CSS classes applied to them. In our case we have two distinct fields to validate in the table generated by Knockout:
- The name of the played game, this field is required, but we want a specific error message if the validation fails
- The score of the played game, if specified this must be an integer between 0 and 25
To start with we define to separate CSS classes that we’ll use to identify the fields in question:
<style>
.validate-name-required {
}
.validate-score {
}
</style>
As you can see we don’t actually define any particular styling in these two classes (although you could if you wanted to), they’re simply used for identification purposes.
The next step is to define our custom validation rules:
function DefineValidationRules() {
$.validator.addMethod("gameName", $.validator.methods.required, "The name of the Game must be specified");
$.validator.addMethod("gameScore", ValidateInteger, "The Score must be an integer");
$.validator.addMethod("gameScoreMin", $.validator.methods.min, $.format("The Score must be greater than or equal to {0}"));
$.validator.addMethod("gameScoreMax", $.validator.methods.max, $.format("The Score value must be less than or equal to {0}"));
$.validator.addClassRules("validate-name-required", { gameName: true });
$.validator.addClassRules("validate-score", { gameScore: true, gameScoreMin: 0, gameScoreMax: 25 });
}
First we define the 4 separate validation rule methods that will be used. Each rule method is defined by a name (which must be a valid JavaScript identifier), the actual function that provides the functionality for the rule and the message to display if the validation fails.
For the gameName
, gameScoreMin
and gameScoreMax
rules we use standard functions from jQuery Validate, but the gameScore
rule uses a custom function since the built-in rule for numbers validates both integers and decimal numbers and we only want integers to be considered valid:
function ValidateInteger(value, element, param) {
if (!value) {
// Null and empty strings are invalid as far as we are concerned.
return false;
}
var parsedNumber = parseInt(value);
if (isNaN(parsedNumber)) {
return false;
}
else {
return (value == parsedNumber);
}
}
Once the rule methods have been defined we next define the actual validation rules to apply by defining the CSS classes to look for and which rule methods to apply to the corresponding elements. The gameScoreMin
and gameScoreMax
rule methods illustrates how the rules can be parameterized so the same rule method can be used in different rules.
The last step required at this point is to tie everything up and initialize the page:
$(document).ready(InitializeForm);
function InitializeForm() {
$.validator.setDefaults({
submitHandler: function () { alert('Submitted'); }
});
DefineValidationRules();
ko.applyBindings(new ViewModel());
$('#gameInfo').validate();
}
When the page is ready we:
- Override the standard form submit handler as we’re doing everything locally without actually submitting any data to a server
- Call the function that defines our custom validation rules
- Initialize the Knockout data binding
- Tell jQuery Validate which form to validate so that when that form is submitted it will first be validated
That’s pretty much all there is to get this working. It should be noted that jQuery Validate is very flexible and there are several other ways to accomplish validation, but this approach is fairly straight-forward and easy to understand.
Source Code
Below is the complete source for the self-contained HTML file that demonstrates the validation. You can also view it on GitHub.
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8" />
<link rel="stylesheet" href="http://ajax.aspnetcdn.com/ajax/jquery.dataTables/1.9.4/css/jquery.dataTables.css" />
<link rel="stylesheet" href="http://ajax.aspnetcdn.com/ajax/jquery.dataTables/1.9.4/css/jquery.dataTables_themeroller.css" />
<link rel="stylesheet" href="http://ajax.aspnetcdn.com/ajax/bootstrap/3.0.0/css/bootstrap.css" />
<link rel="stylesheet" href="http://ajax.aspnetcdn.com/ajax/bootstrap/3.0.0/css/bootstrap-theme.css" />
<link rel="stylesheet" href="http://netdna.bootstrapcdn.com/bootswatch/3.0.0/amelia/bootstrap.min.css" />
<title>Validation Test</title>
<style>
.validate-name-required {
}
.validate-score {
}
</style>
</head>
<body style="margin:24pt;">
<div class="panel panel-default">
<div class="panel-heading">
<h2>Validation with jQuery Validate and Knockout.JS</h2>
</div>
<div class="panel-body">
<form class="form-horizontal" id="gameInfo" method="get" action="">
<div class="form-group">
<label class="col-md-2 control-label" for="FirstName">First Name:</label>
<div class="col-md-6"><input type="text" class="form-control" name="FirstName" value="" required="required" /></div>
</div>
<div class="form-group">
<label class="col-md-2 control-label" for="LastName">Last Name:</label>
<div class="col-md-6"><input type="text" class="form-control" name="LastName" value="" required="required" /></div>
</div>
<div class="form-group">
<div class="col-md-2"></div>
<div class="col-md-10">
<table class="table table-condensed">
<thead>
<tr>
<th style="width:50%">Game</th>
<th style="width:30%">Score</th>
<th style="width:20%"><button type="button" class="btn btn-default" data-bind="click: AddGame">Add Game</button></th>
</tr>
</thead>
<tbody data-bind="foreach: Games">
<tr>
<td>
<input type="text" class="form-control validate-name-required" data-bind="value: Name, uniqueName: true" />
</td>
<td>
<input type="text" class="form-control validate-score" data-bind="value: Score, uniqueName: true" />
</td>
<td>
<input type="button" class="btn btn-xs btn-danger" data-bind="click: $root.RemoveGame" value="X" />
</td>
</tr>
</tbody>
</table>
</div>
</div>
<input class="btn btn-lg btn-primary" type="submit" value="Submit" />
</form>
</div>
</div>
<script src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-2.0.3.js"></script>
<script src="http://ajax.aspnetcdn.com/ajax/jquery.ui/1.10.3/jquery-ui.js"></script>
<script src="http://ajax.aspnetcdn.com/ajax/jquery.validate/1.11.1/jquery.validate.js"></script>
<script src="http://ajax.aspnetcdn.com/ajax/jquery.dataTables/1.9.4/jquery.dataTables.js"></script>
<script src="http://ajax.aspnetcdn.com/ajax/knockout/knockout-2.2.1.js"></script>
<script src="http://ajax.aspnetcdn.com/ajax/bootstrap/3.0.0/bootstrap.js"></script>
<script type="text/javascript">
function GameViewModel() {
var self = this;
this.Name = '';
this.Score = '';
}
function ViewModel() {
var self = this;
self.Games = ko.observableArray();
self.AddGame = function () {
self.Games.push(new GameViewModel());
};
self.RemoveGame = function (game) {
self.Games.destroy(game);
}
}
function ValidateInteger(value, element, param) {
if (!value) {
// Null and empty strings are invalid as far as we are concerned.
return false;
}
var parsedNumber = parseInt(value);
if (isNaN(parsedNumber)) {
return false;
}
else {
return (value == parsedNumber);
}
}
function DefineValidationRules() {
$.validator.addMethod("gameName", $.validator.methods.required, "The name of the Game must be specified");
$.validator.addMethod("gameScore", ValidateInteger, "The Score must be an integer");
$.validator.addMethod("gameScoreMin", $.validator.methods.min, $.format("The Score must be greater than or equal to {0}"));
$.validator.addMethod("gameScoreMax", $.validator.methods.max, $.format("The Score value must be less than or equal to {0}"));
$.validator.addClassRules("validate-name-required", { gameName: true });
$.validator.addClassRules("validate-score", { gameScore: true, gameScoreMin: 0, gameScoreMax: 25 });
}
$(document).ready(InitializeForm);
function InitializeForm() {
$.validator.setDefaults({
submitHandler: function () { alert('Submitted'); }
});
DefineValidationRules();
ko.applyBindings(new ViewModel());
$('#gameInfo').validate();
}
</script>
</body>
</html>