Complex custom validation with ASP.NET MVC, jQuery and KnockoutJS
When developing ASP.NET line of business web applications, one very common requirement is to perform validation of user input. In a modern, responsive web application this validation is expected to be performed on the client-side, using JavaScript, as well as on the server-side using standard C# code. ASP.NET MVC ships with jQueryas a standard library and also includes a validation library called jQuery Unobtrusive Validation (latest repository is here), which is an open-source, Microsoft specific add-on to the jQuery Validation plugin.
It's very easy easy to enable jQuery Unobtrusive Validation in an ASP.NET MVC 3+ application. You simply add the following settings into your web.config:
<configuration>
<appSettings>
<add key="ClientValidationEnabled" value="true"/>
<add key="UnobtrusiveJavaScriptEnabled" value="true"/>
</appSettings>
</configuration>
Now, out of the box, jQuery unobtrusive validation allows you to simply decorate your C# model class properties with attributes from the System.ComponentModel.DataAnnotations
namespace, like so:
public class MyViewModel
{
[Required]
[MaxLength(250)]
public string Name { get; set; }
[MaxLength(1000)]
public string Description { get; set; }
}
Here, we're saying that the Name property is both required and has a maximum length of 250 characters. The Description property isn't required, but does have a maximum length of 1000 characters if it's supplied.
jQuery Unobtrusive Validation has out-of-the-box implementations of a number of Data Annotation validation attributes, which all derive from the ValidationAttribute class. Some of these are:
- Required (ensures the input element is provided and not left empty)
- MinLength (ensures the text-based input element has a minimum amount of characters entered)
- MaxLength (ensures the text-based input element has a no more than a maximum amount of characters entered)
- Range (ensures that the numeric-based input element is within a certain range of values)
- RegularExpression (ensures that the input elements value conforms to a provided regular expression)
Note that there are more attributes available within the System.ComponentModel.DataAnnotations
namespace, however, not all of them have a default implementation within jQuery Unobtrusive Validation.
When the View Model is rendered out to the web page, ASP.NET MVC and jQuery Unobtrusive Validation will render HTML mark-up similar to the following:
<input id="myField" data-val="true"
data-val-maxlength="The field Name must not be longer than 250 characters."
data-val-maxlength-max="250"
data-val-required="The Name field is required."
placeholder="Enter Name..."
type="text"
value="">
<span data-valmsg-for="myField" data-valmsg-replace="true"></span>
Here we can see the various data-*
attributes being rendered on the input element. The Unobtrusive Validation JavaScript looks for all elements that are decorated with data-val-*
attributes and uses them to perform client-side validation complete with showing/hiding relevant error messages, for example:
Now, this is all well and good when dealing with the most basic of validation requirements, however, there is frequently a need for validation to encompass far more elaborate business rules and complex logic. Such logic often involves performing certain validation operations not just on a single granular property, but on multiple properties of the model. It is, unfortunately, at this point that the out-of-the-box jQuery Unobtrusive Validation falls short and we must rely on custom developed validation for this.
Moreover, jQuery Unobtrusive Validation is largely geared towards rendering a web page / form that contains a set amount of input elements when first loaded. Again, lots of line of business applications frequently have "dynamic" elements added to the form at run-time. This is most often used when the user of the application is expected to build up a "table" of data, like so:
In the animation above, we can see that new rows are able to be added to the "Rates" table. We can also see that there's validation on each row of the table, whereby the Owner Writer Share and Net Publisher Share text boxes, which each contain a numeric percentage value, have to add up to 100.
Such user interfaces and forms are not uncommon in a line of business application, but the validation required for this goes way beyond that which the jQuery Unobtrusive Validation offers out of the box. Developing custom validation for such a form often requires juggling a number of frameworks or libraries and setting up and configuring things "just right" to ensure everything works as expected. In order to make sense of the various pieces that need to fit together, we'll take a look at each of them one by one.
Creating our custom validator
The first part requires us to create our custom validator. This requires two steps. The first step is to create the C# class which is a custom attribute and implements the server-side validation logic, and the second step is to implement a JavaScript function which "mimics" the C# class and provides the same validation logic to the client browser.
Server-side implementation
The C# class is a custom attribute deriving from ValidationAttribute
class (for server-side implementation) and also implementing the IClientValidatable
interface (in order to "expose" this custom validation logic to the client-side validation framework). Here's our C# class:
[AttributeUsage(AttributeTargets.Property)]
public class MustAddUpToAttribute : ValidationAttribute, IClientValidatable
{
public string OtherPropertyName { get; private set; }
public double Amount { get; private set; }
public MustAddUpToAttribute(double amount, string otherPropertyName, string errorMessage) : base(errorMessage)
{
Amount = amount;
OtherPropertyName = otherPropertyName;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
double thisValue;
double otherValue;
PropertyInfo propertyInfo;
try
{
thisValue = Convert.ToDouble(value);
}
catch(Exception ex)
{
throw new InvalidOperationException("Property Type is not numeric", ex);
}
try
{
propertyInfo = validationContext.ObjectType.GetProperty(OtherPropertyName);
}
catch (ArgumentException ex)
{
throw new InvalidOperationException("Other property not found", ex);
}
try
{
otherValue = Convert.ToDouble(propertyInfo.GetValue(validationContext.ObjectInstance, null));
}
catch (Exception ex)
{
throw new InvalidOperationException("Other property type is not numeric", ex);
}
var sumOfValues = thisValue + otherValue;
return NumericHelpers.NearlyEqual(sumOfValues, Amount, double.Epsilon) ? ValidationResult.Success : new ValidationResult(this.FormatErrorMessage(validationContext.DisplayName));
}
public override string FormatErrorMessage(string name)
{
return string.Format(ErrorMessageString, name, OtherPropertyName);
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
var rule = new ModelClientValidationRule
{
ErrorMessage = FormatErrorMessage(metadata.DisplayName),
ValidationType = "mustaddupto"
};
rule.ValidationParameters.Add("otherpropertyname", OtherPropertyName);
rule.ValidationParameters.Add("amount", Amount);
yield return rule;
}
}
(Note that this class relies on a "NumericHelper" to determine near equality - see here for the implementation).
Also note that the .NET Framework provides an IValidateableObject interface, which isn't used here. The IValidateableObject
interface is used for model-level validation - i.e. ensuring an entire object is valid, rather than one or two properties. Moreover, the IValidateableObject
can only be used server-side with no client-side equivalent (client-side model validation must be performed and managed with bespoke JavaScript functions), therefore, it's often unused.
Our C# class takes three constructor parameters, the amount that the calculation must add up to, the name of the "other property" that we'll get the value from to add to the value of the property decorated with the attribute and an optional error message. The custom attribute is used similar to this:
public class Rate
{
[Required]
public float OwnerWriterShare { get; set; }
[Required]
[MustAddUpTo(100, "OwnerWriterShare", "Owner Writer Share and Net Publisher Share must add up to 100.")]
public float NetPublisherShare { get; set; }
}
Note how we use a string based reference to the "OwnerWriterShare
" property name to tell the MustAddUpTo
attribute that we're getting the value of the NetPublisherShare
property (the property to which the attribute is applied) and then to get the value of the OwnerWriterShare
property, add them together and determine if the total adds up to the total we require - the first parameter to the MustAddUpTo
attribute. Also note that the validation is using both the OwnerWriterShare
and NetPublisherShare
properties, but the attribute itself is only applied to the NetPublisherShare
property. This ensures we're not "doubling up" by effectively having the same validation on both properties which would result in two copies of the same error message on the client.
So, this C# class gives us our server-side implementation, and includes the GetClientValidationRules
method from the IClientValidatable
interface. The implementation of this method requires returning a ModelClientValidationRule
object with the relevant properties set. The ModelClientValidationRule effectively tells the jQuery Unobtrusive Validation framework which client-side implementation it should use along with the relevant parameters required to perform the validation. We'll see the implementation shortly, but the main code line here is:
ValidationType = "mustaddupto"
The value of ValidationType
must always be lower case, and will be the name of our JavaScript function that will implement the client-side validation logic.
Client-side implementation
Having written our C# server-side class, we now must implement the same validation logic on the client-side, which means writing JavaScript. The JavaScript function name will be mustaddupto
. Here's the JavaScript code that implements our MustAddUpTo validator:
// ================================================================================================
// MustAddUpToAttribute
// ================================================================================================
$.validator.addMethod("mustaddupto", function (value, element, params) {
// Get the value of the element that the validation is assigned to and
// the value of the "other" element that we're comparing against.
var thisValue = value;
var otherValue = $('#' + params.otherpropertyname).val();
var amount = params.amount;
// If either this or the other value is null or "empty" simply return true
// as we can't perform validation. Other validation can assert not null/emptiness.
if (thisValue == null || thisValue.length === 0) {
return true;
}
if (otherValue == null || otherValue.length === 0) {
return true;
}
// Check we're dealing with numbers.
if ($.isNumeric(value) && $.isNumeric(value)) {
var thisValueNumber = parseFloat(thisValue);
var otherValueNumber = parseFloat(otherValue);
var sumTotal = thisValueNumber + otherValueNumber;
return nearlyEqual(sumTotal, amount);
}
// If we get here, we're dealing with a data type that we don't recognise
// and can't process, so let the validation pass.
return true;
});
$.validator.unobtrusive.adapters.add("mustaddupto", ["otherpropertyname", "amount"], function (options) {
options.rules["mustaddupto"] = options.params;
options.messages["mustaddupto"] = options.message;
});
// Supporting functions
function nearlyEqual(a, b) {
if (a == b) return true;
var diff = Math.abs(a - b);
if (diff < 4.94065645841247E-320) return true;
a = Math.abs(a);
b = Math.abs(b);
var smallest = (b < a) ? b : a;
return diff < smallest * 1e-12;
}
// ================================================================================================
The bulk of the function is plain old vanilla JavaScript, but let's examine the first few lines of each block of code in more detail as they're the most interesting parts. Firstly, we use the addMethod
method of the validator
object that's attached to the jQuery object in order to create and add our validation method to the jQuery Validation framework. Note that this isn't the Microsoft-specific jQuery Unobtrusive Validation framework, just the jQuery Validation framework, which is the underlying framework for Microsoft's Unobtrusive Validation framework. Once we've added our validation method to the jQuery Validation Framework, we'll hook it up to the Unobtrusive Validation library too by adding an "adapter" to the unobtrusive
object (this is the line that begins $.validator.unobtrusive.adapters.add
). Firstly, we pass in the name first ("mustaddupto
" - this the same name for the ValidationType
property of the ModelClientValidationRule
that we expose from our server-side implementation) and then a function which takes 3 arguments, value, element and params. The value parameter is the value of the "property" (or rather element since this is client-side) to which the validation is attached - it's the same as the
server-side model property that has the attribute on it. The element parameter is the actual element that has the validation applied to it. In our case here, we're only interested in the value itself as the framework will take care of visually highlighting the correct web page element that fails validation, so we don't need to concern ourselves
with it here. What we DO need, though, is the "other" element that we'll need to grab the value of in order to add the two values together. That's passed in the params object, which is the same object as exposed by the ModelClientValidationRule
from the server. So, the JavaScript params object used in these lines:
var otherValue = $('#' + params.otherpropertyname).val();
var amount = params.amount;
has come from this section of the server-side code:
rule.ValidationParameters.Add("otherpropertyname", OtherPropertyName);
rule.ValidationParameters.Add("amount", Amount);
After having set up and registered our validation function with the jQuery Validation library, we then need to also tell the jQuery Unobtrusive Validation framework all about it's existence too. We do this with this section of the JavaScript code:
$.validator.unobtrusive.adapters.add("mustaddupto", ["otherpropertyname", "amount"], function (options) {
options.rules["mustaddupto"] = options.params;
options.messages["mustaddupto"] = options.message;
});
The jQuery Unobtrusive Validation library has a collection of "adapters" which allow us to register (add) our jQuery Validation method to be used by the unobtrusive validation framework. Again, we can see that the key parts of this are the name ("mustaddupto
") - which is the same as the main JavaScript function name and the name exposed from the server, as well as an array of the string param names ("otherpropertyname
" and "amount
").
With the C# class written and the attribute applied to our Model class, and the JavaScript function written and registered with both the jQuery Validation and jQuery Unobtrusive Validation libraries, we can move on to the next step...
Adding dynamic elements to the page
In order to dynamically add new elements to a web page without requiring a full page reload, we will be required to use JavaScript or some JavaScript-based framework in order to add elements into the page DOM after the page is initially rendered. One such framework that actually makes this kind of thing a lot easier to manage is KnockoutJS. Although it's beyond the scope of this post to go into detail on KnockoutJS, it's sufficient to say that Knockout takes a JSON view model representing your form data and deals with the two-way binding, rendering and updating of both existing and new page elements with the underlying data. This makes it very easy to dynamically add new elements onto a form, for example, adding new rows to a table of data.
We can expose our server generated viewmodel to the client-side JavaScript with a single line like this:
var koViewModel = ko.mapping.fromJS(@Html.Raw(JsonConvert.SerializeObject(Model)));
The code above uses the Knockout Mapping plugin to take our serialized Model and convert it into a Knockout view model,
complete with the required observablesto facilitate two-way data binding. Knockout can then work by "binding" values from the underlying view model to page elements that are decorated with appropriate data-bind
attributes, such as this:
<input data-bind="value: MyViewModel.OwnerWriterShare" />
The data-bind
attribute tells Knockout to bind the value of the input element to the value of the OwnerWriterShare
property of the JSON object, MyViewModel
. This binding is two-way, so changes to the model's value will have Knockout updating the input element, and changes to the input element will have knockout updating the underlying model's value.
This is fine for existing page elements, but Knockout also makes it easy to bind to an array within the underlying JSON model. This means that as new elements are added to the array, entirely new sets of page elements are created (or destroyed) as the array grows or shrinks. So, if we have this JSON view model:
{
"MyViewModel" : {
"Rates" : [{
"OwnerWriterShare" : "50",
"NetPublisherShare" : "50"
}, {
"OwnerWriterShare" : "60",
"NetPublisherShare" : "40"
}
]
}
}
And the following definition of a table within our page markup:
<table>
<tbody data-bind="foreach: MyViewModel.Rates">
<tr>
<td><input data-bind="value: $data.OwnerWriterShare" /></td>
<td><input data-bind="value: $data.NetPublisherShare" /></td>
</tr>
</tbody>
</table>
Knockout will repeat the contents of the tbody
for each element in the MyViewModel.Rates
array due to the foreach:
binding. This is great for building up our additional <tr>
table rows dynamically, with each element in each row bound to the correct underlying JSON property, managed by KnockoutJS, thanks to the Knockout data-bind
attribute.
Allowing users to add a new row to a table is as simple as adding a small amount of JavaScript that simply adds a new element to the underlying view model array, and the Knockout foreach
binding will deal with creating the necessary additional page elements. However, we have further work to do in order to add our validation to the dynamically generated elements, and this involves specifying additional data-*
attributes needed for the validation to work, and for that, we'll also require Knockout's help...
Adding validation to dynamically added elements
So, we've got our custom validation functions written and we've wired up Knockout to help generate our page elements and performing the necessary two-way data binding, but now we need to add some additional attributes to our generated markup to ensure that the required validation is added to these elements.
Unobtrusive validation data attributes
Unobtrusive validation works by decorating our elements with data-val-*
attributes. These attributes are ignored by the browser, but JavaScript can read them and the values they contain in order to do all manner of interesting things. In looking at our rendered input element from earlier:
<input data-bind="value: MyViewModel.Name"
data-val="true"
data-val-maxlength="The field Name must not be longer than 250 characters."
data-val-maxlength-max="250"
data-val-required="The Name field is required."
id="MyViewModel_Name"
name="MyViewModel.Name"
placeholder="Enter Name..."
type="text"
value="">
We can see that as well as the Knockout specific data-bind
attribute, we also have a number of attributes the start data-val
. It's these attributes that control the application of validation for the element by the jQuery Unobtrusive Validation library. There's a data-val
attribute that contains a boolean value to indicate whether this element is subject to validation. Then, we have a number of other attributes starting with data-val
that control the actual validation itself. We'll have an attribute named data-val-VALIDATORNAME
for each validation function that is applied to the element, and if that validation required further parameters, these are provided by additional parameters with the required parameter name in the attribute name. So, for example, the data-val-maxlength
attribute contains the error message to be shown when this validation fails and the data-val-maxlength-max
attribute holds the value for the maximum length allowed. We also have a data-val-required
attribute that contains the error message for failure of that particular validation, however, there's no further attributes for that validation as no further parameters are required.
Now, when we're rendering known elements from a server-side view model, we can use ASP.NET MVC's Html Helpers to ensure that all of the these necessary attributes are applied to the rendered elements for us, so we will frequently see code such as this in a Razor view:
<div class="form-group">
@Html.LabelFor(m => m.Username, new { @class = "control-label col-md-3" })
<div class="col-md-9">
@Html.TextBoxFor(m => m.Username, new { @class = "form-control", @placeholder = "Enter Username..." })
@Html.ValidationMessageFor(m => m.Username, string.Empty, new { @class = "text-danger" })
</div>
</div>
The @Html.TextBoxFor
and @Html.ValidationMessageFor
directives are aware of any validation attributes applied to the server-side model and will ensure that the relevant data-val
attributes are applied to the rendered output (the ValidationMessageFor
directive ensuring that the relevant <span>
is rendered to hold an validation error message,
should validation for the input element fail).
However, since we're now very firmly in the realm of generating our page elements client-side rather than from the server, with the use of KnockoutJS to control dynamically adding elements to the page, we can no longer rely on Html Helpers to write out the necessary elements with the required validation attributes. Instead, we need to do all of this manually.
Manually adding data validation attributes
So, here's an example of the markup that we'll have to manually write in order to wire up the unobtrusive validation. We'll examine this in more detail immediately afterwards:
<table>
<thead>
<tr>
<th>Owner Writer Share</th>
<th>Net Publisher Share</th>
</tr>
</thead>
<tbody data-bind="foreach: MyViewModel.Rates">
<tr>
<td>
<input type="number" min="0" max="100" data-input-type="percentage"
data-bind="value: $data.OwnerWriterShare,
attr: { id: 'MyViewModel_Rates_' + $index() + '__OwnerWriterShare',
name: 'MyViewModel.Rates[' + $index() + '].OwnerWriterShare' }"
data-val="true"
data-val-mustaddupto="Owner Writer Share and Net Publisher Share Percentages must add up to 100."
data-val-mustaddupto-amount="100"
data-val-number="The field Owner Writer Share must be a number."
data-val-required="The Owner Writer Share field is required."
value="">
</td>
<td>
<input type="number" min="0" max="100" data-input-type="percentage"
data-bind="value: $data.NetPublisherShare,
attr: { id: 'MyViewModel_Rates_' + $index() + '__NetPublisherShare',
name: 'MyViewModel.Rates[' + $index() + '].NetPublisherShare',
'data-val-mustaddupto-otherpropertyname': 'MyViewModel_Rates_' + $index() + '__OwnerWriterShare' }"
data-val="true"
data-val-mustaddupto="Owner Writer Share and Net Publisher Share Percentages must add up to 100."
data-val-mustaddupto-amount="100"
data-val-number="The field Net Publisher Share must be a number."
data-val-required="The Net Publisher Share field is required."
value="">
</td>
</tr>
</tbody>
</table>
This markup represents the table that will have rows dynamically added to it at run-time by Knockout and which the two text boxes for our percentages that must both add up to 100. We can see that many of the data-val-*
attributes are simply written out in a hard-coded format (i.e. the data-val
, data-val-mustaddupto
, data-val-required
etc. are all "static" values) and they won't vary from one row of dynamically added elements to the next. This is all fairly simple so far, however, the Knockout specific data-bind
attribute is quite special.
As well as containing the binding to the element's value
, we also use the attr
binding, which allows Knockout to render attributes on the element. This is necessary for certain attributes, such as the data-val-mustaddupto-otherpropertyname
attribute as this attribute needs to have a value which contains the exact id
of the element that we need to examine as part of our validation. Of course, since we're dealing with potentially many text boxes over many possible rows, these id
values need to be dynamically generated and then referenced from the respective data-val-mustaddupto-otherpropertyname
attribute. This is achieved by having Knockout render those attributes for us, as well as rendering our id
and name
attributes on the element that requires validation. If we examine the data-bind
attributes for each of the <input>
elements in the two <td>
's we can see that they both use a Knockout attr
binding. Each of the attr
bindings renders both the id
and the name
of the input element:
attr: { id: 'MyViewModel_Rates_' + $index() + '__OwnerWriterShare', name: 'MyViewModel.Rates[' + $index() + '].OwnerWriterShare' }"
We can see that this binding also uses a special Knockout context property, $index
, which provides the attr
binding with the numeric array index of the current data item being processed within the foreach
loop. The resulting markup that's rendered to the page is something similar to this:
<tr>
<td>
<input type="number" min="0" max="100" data-input-type="percentage"
id="MyViewModel_Rates_0__OwnerWriterShare"
name="MyViewModel.Rates[0].OwnerWriterShare"
data-val="true"
data-val-mustaddupto="Owner Writer Share and Net Publisher Share Percentages must add up to 100."
data-val-mustaddupto-amount="100"
data-val-number="The field Owner Writer Share must be a number."
data-val-required="The Owner Writer Share field is required."
value="50">
</td>
<td>
<input type="number" min="0" max="100" data-input-type="percentage"
id="MyViewModel_Rates_0__NetPublisherShare"
name="MyViewModel.Rates[0].NetPublisherShare"
data-val-mustaddupto-otherpropertyname="MyViewModel_Rates_0__OwnerWriterShare"
data-val="true"
data-val-mustaddupto="Owner Writer Share and Net Publisher Share Percentages must add up to 100."
data-val-mustaddupto-amount="100"
data-val-number="The field Net Publisher Share must be a number."
data-val-required="The Net Publisher Share field is required."
value="50">
</td>
</tr>
<tr>
<td>
<input type="number" min="0" max="100" data-input-type="percentage"
id="MyViewModel_Rates_1__OwnerWriterShare"
name="MyViewModel.Rates[1].OwnerWriterShare"
data-val="true"
data-val-mustaddupto="Owner Writer Share and Net Publisher Share Percentages must add up to 100."
data-val-mustaddupto-amount="100"
data-val-number="The field Owner Writer Share must be a number."
data-val-required="The Owner Writer Share field is required."
value="60">
</td>
<td>
<input type="number" min="0" max="100" data-input-type="percentage"
id="MyViewModel_Rates_1__NetPublisherShare"
name="MyViewModel.Rates[1].NetPublisherShare"
data-val-mustaddupto-otherpropertyname="MyViewModel_Rates_1__OwnerWriterShare"
data-val="true"
data-val-mustaddupto="Owner Writer Share and Net Publisher Share Percentages must add up to 100."
data-val-mustaddupto-amount="100"
data-val-number="The field Net Publisher Share must be a number."
data-val-required="The Net Publisher Share field is required."
value="40">
</td>
</tr>
Note how the id
and name
attributes contain the incrementing numeric index, giving each input element a unique id and name. Furthermore, due to this, the second <td>
of each row contains the additional data-val-mustaddupto-otherpropertyname
attribute which correctly references that specific row's input element, allowing validation to be wired up to each dynamically added element in each dynamically added table row.
Ensuring validation is performed for dynamically added elements
Once we have Knockout rendering our id
, name
and validation specific element attributes, we're up and running with our complex validation. Almost. There's one final thing that needs to happen in order for the validation to work. The jQuery Unobtrusive Validation library will initialize it's own internal cache of page elements that require validation when the page has first fully loaded. Due to this, any page elements that are subsequently added to the page DOM by client-side script (such as our additional table rows, added by KnockoutJS) will not be included in the internal list of page elements that the Unobtrusive Validation will validate. Therefore, we need a way of telling jQuery Unobtrusive Validation to re-build it's internal list of page elements each time we dynamically add or remove to or from the page any elements containing validation attributes. This can be done quite simply with a short JavaScript function as follows:
function updateValidation() {
$("#myForm").removeData("validator");
$("#myForm").removeData("unobtrusiveValidation");
$.validator.unobtrusive.parse("#createContract");
}
The #myForm
selector represents an id
of either a form, or some containing element that encompasses all of the dynamic parts of your web page. It's important that this function is called by your own JavaScript code whenever a page element is added or removed from the page DOM. This is usually easy enough to do as adding a table row is usually done in response to the user clicking some kind of "Add" button, which you'll usually have wired up via a Knockout click:
binding to run a JavaScript function, usually defined as part of your Knockout view model. For example, an additional <td>
on our table could contain mark up similar to the following:
<td>
<a href="#" title="Add" data-bind="click: addRateRow">
<span>Add New Rate</span>
</a>
</td>
And the addRateRow
function would be called on the anchor tag's click event, adding a new row to the underlying Knockout view model - causing Knockout to re-render the table with the additional row and associated elements - and finally calling our simple updateValidation()
function to ensure both jQuery Validation and jQuery Unobtrusive Validation are
aware of the newly added elements:
koViewModel.addRateRow = function () {
this.Rates.push(new Rate());
updateValidation();
};
And with that, our custom complex jQuery Validation / Unobtrusive Validation with KnockoutJS dynamically created page elements is complete!