Is it wrong to dynamically add “data-val” and “data-val-required” in the View?

I have a ViewModel that I can decorate with the [Required] attribute (see below). I've come to the point where I need to let the client control which fields are required or not. They can configure this trough XML and all this info is stored in the Model when it's first created. Now I have fields that are not decorated with [Required] but still need to get validated (as per "user settings") before submitting (for example the Phone field).

public class MyBusinessObjectViewModel
{
    [Required]
    public string Email { get; set; } //compulsory

    public string Phone { get; set; } //not (yet) compulsory, but might become
}

If the user will not enter the Phone number, the data will still get posted. Wanting not to mess with custom validators, I just add the "data-val" and "data-val-required" attributes to the Html, like this:

Dictionary<string, object> dict = new Dictionary<string, object>();
dict.Add("data-val", "true");
dict.Add("data-val-required", "This field is required.");
@Html.TextBoxFor(x => x, dict); 

This forces the client side validation for all the properties that are dynamically set as required. Is this good practice? What kind of side effects can I expect?

Answers


You should look into extending the meta model framework with your own metadata provider to do the actual binding between your site's configuration and the model metadata. You can actually set the required property flag to true on the property model metadata during the metadata creation process. I can't remember for sure whether this causes the built in editor templates to generate the attribute, but I think it does. Worst case scenario you can actually create and attach a new RequiredAttribute to the property, which is a tad bit kluggy, but works pretty well in certain scenarios.

You could also do this with IMetadataAware attributes, especially if Required is the only metadata aspect your users can customize, but the implementation really depends on what you're trying to do.

One major advantage of using a custom ModelMetadataProvider in your specific case is that you can use dependency injection (via ModelMetadataProviders) to get your customer settings persistence mechanism into scope, whereas with the data attribute you only get to write an isolated method that runs immediately after the metadata model is created.

Here is a sample implementation of a custom model metadata provider, you'd just have to change the client settings to whatever you wanted to use.

UPDATED but not tested at all

public class ClientSettingsProvider
{
    public ClientSettingsProvider(/* db info */) { /* init */ }

    public bool IsPropertyRequired(string propertyIdentifier)
    {
       // check the property identifier here and return status
    }

}

public ClientRequiredAttribute : Attribute
{
    string _identifier;
    public string Identifier { get { return _identifer; } }
    public ClientRequiredAttribute(string identifier)
    { _identifier = identifier; }
}

public class RequiredModelMetadataProvider : DataAnnotationsModelMetadataProvider
{
    ClientSettings _clientSettings;

    public RequiredModelMetadataProvider(ClientSettings clientSettings)
    {
        _clientSettings = clientSettings;
    }

    protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
    {
        // alternatively here is where you could 'inject' a RequiredAttribute into the attributes list

        var clientRequiredAttribute = attributes.OfType<ClientRequiredAttribute>().SingleOrDefault();
        if(clientRequiredAttribute != null && _clientSettings.IsPropertyRequired(clientRequiredAttribute.Identifier))
        {
            // By injecting the Required attribute here it will seem to 
            // the base provider we are extending as if the property was
            // marked with [Required]. Your data validation attributes should
            // be added, provide you are using the default editor templates in
            // you view.
            attributes = attributes.Union(new [] { new RequiredAttribute() });
        }

        var metadata = base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName);

        // REMOVED, this is another way but I'm not 100% sure it will add your attributes
        // Use whatever attributes you need here as parameters...
        //if (_clientSettings.IsPropertyRequired(containerType, propertyName))
        //{
        //    metadata.IsRequired = true;
        //}

        return metadata;
    }
}

USAGE

public class MyModel
{
     [ClientRequired("CompanyName")]
     public string Company { get; set; }
}

public class MyOtherModel
{
     [ClientRequired("CompanyName")]
     public string Name { get; set; }

     public string Address { get; set; }
}

Both of these models would validate the string "CompanyName" against your client settings provider.


Not wanting to mess with custom validators, so you messed in the View obfuscating the logic of your validation by removing it from the place where it is expected to be found.

Really, don't be afraid of creating a custom attribute validator. What you are doing right now is getting a technical debt.


Need Your Help

How can I reject my app from the App Store?

iphone ios apple app-store itunesconnect

I am in iTunes Connect &gt; View Details &gt; Binary Details

call javascript from cocoa

javascript objective-c cocoa webview

I have a cocoa application which contains a webview. Inside the webview is essentially a table of selectable information. I would like to access a user's selection inside this webview (html/js) fro...

About UNIX Resources Network

Original, collect and organize Developers related documents, information and materials, contains jQuery, Html, CSS, MySQL, .NET, ASP.NET, SQL, objective-c, iPhone, Ruby on Rails, C, SQL Server, Ruby, Arrays, Regex, ASP.NET MVC, WPF, XML, Ajax, DataBase, and so on.