The Problem
If you look at the html emitted by your razor view, you'll see what's at issue here.
Without a viewmodel, we should generate the following code (cleaned up a little):
<!-- language: lang-html -->
<pre><code><input type="checkbox" name="IsAdult" id="<b><i>IsAdult</i></b>" />
<input type="text" name="Comments" id="Comments"
data-val-requiredif-dependprop="<b><i>IsAdult</i></b>"
data-val-requiredif-value="Yes"
data-val-requiredif="Please leave a comment" />
</code></pre>
With a viewmodel, we get this:
<!-- language: lang-html -->
<pre><code><input type="checkbox" name="PersonModel.IsAdult" id="<b><i>PersonModel_IsAdult</i></b>" />
<input type="text" name="PersonModel.Comments" id="PersonModel_Comments"
data-val-requiredif-dependprop="<b><i>IsAdult</i></b>"
data-val-requiredif-value="Yes"
data-val-requiredif="Please leave a comment" />
</code></pre>
Whenever a property is nested inside another property, MVC will build a stack of prefixes to generate unique ID's and names. You can see in the first case, IsAdult
is enough to identify the field, however once it has been nested, the id has changed. In unobtrusive validation (rules stored in attributes), everything we need to possibly know about how to validate an element needs to get sent along in the data-attributes
, including how to locate other properties.
The Solution
The data attribute on the class should never know or care about what context it is being called under, so it will continue to blindly point out the relative attribute on which it depends:
<!-- language: lang-cs -->
public bool IsAdult { get; set; }
[RequiredIf("IsAdult", "Yes", Errormessage="Please leave a comment")]
public string Comments { get; set; }
So we'll have to establish the context either on the server or on the client.
On the Server - Nope!
As part of your public class RequiredIfAttribute : ValidationAttribute, IClientValidatable
, you'll have a method that emits client side validation rules that looks something like this:
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
ModelClientValidationRule requiredIfRule = new ModelClientValidationRule();
requiredIfRule.ErrorMessage = ErrorMessageString;
requiredIfRule.ValidationType = "requiredif";
requiredIfRule.ValidationParameters.Add("dependprop", this._propertyName);
requiredIfRule.ValidationParameters.Add("value", Json.Encode(this._value));
yield return requiredIfRule;
}
The temptation here would be to navigate the viewContext.ViewData.TemplateInfo
to return the GetFullHtmlFieldId
, but from what I can tell, this information isn't available yet.
Client - wonky but working:
On the client, we'll wire up the adapter with a method that looks something like this:
<!-- language: lang-js -->
$.validator.unobtrusive.adapters.add('requiredif', ['dependprop', 'value'], function (options) {
options.rules["requiredif"] = {
id: '#' + options.params['dependprop'],
value: JSON.parse(options.params.value)
};
options.messages['requiredif'] = options.message;
});
Notice this still just takes the plain old property name and assumes it can be used as the ID to locate the object.
By making some reasonable assumptions, we can build the full dependency property id. It should always be the case that the requiredif calling element is within the same scope as the property we're identify (that's how we were able to find it on the server via reflection).
So we'll grab that context from the sender, remove that property's name, and append our own like this inside of $.validator.unobtrusive.adapters.add
:
<!-- language: lang-js -->
var curId = options.element.id; // get full id i.e. ViewModel_Comments
var context = curId.replace(/[^_]+$/, ""); // remove last prop i.e. ViewModel_
var targetProp = options.params['dependprop'] // target name i.e. IsAdult
var targetId = '#' + context + targetProp; // build target ID i.e. #ViewModel_IsAdult
options.rules["requiredif"] = {
id: targetId,
value: JSON.parse(options.params.value)
};
That should help find the right client side property - then write whatever other conditions you need to meet.