jwaliszko / ExpressiveAnnotations

Annotation-based conditional validation library.
MIT License
351 stars 123 forks source link

Unable to validate dependant properties of type "enum" #160

Closed kerbou closed 7 years ago

kerbou commented 7 years ago

Hi

Long story short: I have the following setup in my MVC view because I want to validate that Value is a valid URL if Type is set to "Website". Here goes:

[Required]
public ProductMetadataType Type { get; set; }

[RequiredIf("(Type == ProductMetadataType.Website && !IsUrl(Value))", ErrorMessage = "A valid url is required")]
public string Value { get; set; }

ProductMetadataType is an enum type rendered in a dropdown-select on my webpage (output is rendered as a standard 'select' element with option elements as children). For some reason I get an error: "DOM field Type value conversion to enumeration failed. Given value was not recognized as a valid number" in the browser console. Stepping through the code it seems that the clientside code is unable to parse the selected value to an item in my ProductMetadataType enum

I cannot see why this is the case - I tried a few things like explicitly providing a value to each enum element but nothing works for me. Gut feeling tells me that I might not obey some sort of naming conventions when operating with enums but it is quite hard for me to figure out what I am expected to do in order to get things working.

I'll be happy to elaborate and provide further details if required. Thanks in advance.

jwaliszko commented 7 years ago

First issue I've noticed, unrelated to the type conversion exception, is a misuse of the attribute. It's because RequiredIf is executed only when annotated field value is not provided. As soon as you provide the value, such an attribute becomes inactive. In other words - when you start typing the value, validation logic provided to this RequiredIf expression is never executed again. What should be used instead is this:

[RequiredIf("Type == ProductMetadataType.Website", ErrorMessage = "Url missing")] // if value *not* provided, fail (mark as required) when expression evaluates to true
[AssertThat("IsUrl(Value)", ErrorMessage = "Url invalid")] // if value provided, fail (mark as incorrect) when expression evaluates to false
public string Value { get; set; }

When it comes to the type conversion error itself, it can be related to the mismatch between enumerations representation in the HTML and the ea script understanding of such a data. If the HTML contains string identifiers, while at the same time ea script is instructed to expect integral numerics (see enumsAsNumbers settings flag), error is raised. Compatibility is required to be preserved, i.e.

either do this:

<select id="Type" name="Type">
    <option value="0">Website</option>
    ...
</select>

ea.settings.apply({
    ...
    enumsAsNumbers: true // true by default
});

or this:

<select id="Type" name="Type">
    <option value="Website">Website</option>
    ...
</select>

ea.settings.apply({
    ...
    enumsAsNumbers: false
});

I encourage you to take a look at the sample project. I've provided EnumDropDownListFor html extension there, which renders the drop down list for the enumeration types for you, i.e.

@Html.EnumDropDownListFor(model => model.Type, true) // second argument is optional - true by default to be compatible with default ea script settings (i.e. enumsAsNumbers flag)
jwaliszko commented 7 years ago

Just an update - assuming that you'd like to differentiate the content of the Value field based on the actual drop down selection, you need to use conditional operator:

[RequiredIf("Type == ProductMetadataType.Website", ErrorMessage = "Url missing")]
[RequiredIf("Type == ProductMetadataType.Email", ErrorMessage = "Email missing")]
[AssertThat("Type == ProductMetadataType.Website ? IsUrl(Value) : true", ErrorMessage = "A valid url is required")]
[AssertThat("Type == ProductMetadataType.Email ? IsEmail(Value) : true", ErrorMessage = "A valid email is required")]
public string Value { get; set; }
kerbou commented 7 years ago

Hi there. Thanks for the elaborate answer. I got it working - it turns out that the HTML generated by ASP.NET MVC's @Html.DropdownListFor(...) Razor engine does not provide the HTML expected by your code, but instead outputs <option value=blabla>blabla</option> elements.

So - again, long story short: I coded the SELECT-box by hand to output <option value=0>blabla</option> elements and now the conditional attributes work like a charm :o)

Best regards Kristian

jwaliszko commented 7 years ago

To handle the HTML sample you've shown, switch of the enumsAsNumbers flag value (from true to false - as described above) could also work. Nevertheless, cool you've sorted it out the other way around.