Search code examples
asp.net-mvcdata-annotationsmodelstate

asp.net mvc model state errors keys


So I discovered an interesting problem. I have a model like this:

public class ApplicantModel
{
    [Display(Name = "Firstname", ResourceType = typeof(Resources))]
    [MaxLength(50, ErrorMessageResourceName = "FirstName", ErrorMessageResourceType = typeof(Validations), ErrorMessage = null)]
    [Required(ErrorMessageResourceName = "FirstName", ErrorMessageResourceType = typeof(Validations), ErrorMessage = null)]
    public string Firstname { get; set; }

    [Display(Name = "Surname", ResourceType = typeof(Resources))]
    [MaxLength(50, ErrorMessageResourceName = "Surname", ErrorMessageResourceType = typeof(Validations), ErrorMessage = null)]
    [Required(ErrorMessageResourceName = "Surname", ErrorMessageResourceType = typeof(Validations), ErrorMessage = null)]
    public string Surname { get; set; }
}

that is all fine, and when I check the Model state and there is an error on a model I get something like this: errors:

[{
Key = FirstApplicant.Firstname
Value = ["First name is required field"]
},
{
Key = FirstApplicant.Surname
Value = ["Surname name is required field"]
}].

That is also fine.

Edit: This is the c# ModelState object visualized as JSON object. Real object looks like this:

ModelState
{System.Web.Mvc.ModelStateDictionary}
    Count: 2
    IsReadOnly: false
    IsValid: false
    Keys: Count = 2
    Values: Count = 2
    Results View: Expanding the Results View will enumerate the IEnumerable

However my question is. Is it possible to somehow change the key? I know that the key is created as the name of object and then the name property on that object. So it makes sense, but is there any way how to change this default behavior? Or do I have to change the names of objects?

Edit2:

What I am trying to achieve here is that I have a c# ViewModel and knockout ViewModel. and when you do server side validations you get this dictionary of keys and values which I serialize and send to client. And then I call this function on it on client:

var errors = @Html.Raw(Json.Encode(Model.Errors));
        function showErrors(serializedErrors) {
            var errors = JSON.parse(serializedErrors);
            for (var i = 0; i < errors.length; i++) {
                var error = errors[i];
                var key = error.Key;
                var property = eval("masterModel." + key);
                property.setError(error.Value.ErrorMessage);
                property.isModified(true);
            }
        }
        showErrors(errors);

And this would work fine if the view model property names match on the server and on client. But for example on server side I have a FirstApplicant.FirstName and on a client side it is ApplicantOne.firstname. Thank you all for help and comments. I hope I explained my problem in more detail this time.


Solution

  • in the end I found a solution to this problem. It is a bit complicated but it works.

    First I've created an attribute.

    public class ClientNameAttribute : Attribute, IMetadataAware
    {
        public ClientNameAttribute(string name)
        {
            this.Name = name;
        }
    
        public string Name { get; set; }
    
        public void OnMetadataCreated(ModelMetadata metadata)
        {
            metadata.AdditionalValues["ClientName"] = this.Name;
        }
    }
    

    Notice that this attribute also implements IMetadataAware

    Next step was to create Html helper, so I could call this in a view.

    public static class HtmlHelperExtensions
    {
        public static string CustomModelState<T>(this HtmlHelper<T> helper)
        {
            var errors = helper.ViewData.ModelState.Select(
                m => new { Key = GenerateClientName(m.Key, helper), Value = m.Value.Errors.FirstOrDefault() }).Where(e=> e.Value != null);
    
            return Json.Encode(errors);
        }
    
        private static string GenerateClientName<T>(string key, HtmlHelper<T> helper)
        {
            StringBuilder builder = new StringBuilder();
            int periodIndex = -1;
            do
            {
                periodIndex = key.IndexOf('.', periodIndex + 1);
                string part = key.Substring(0, periodIndex==-1 ? key.Length : periodIndex);
                var partMetadata = ModelMetadata.FromStringExpression(part, helper.ViewData);
    
                object clientName;
                if (builder.Length > 0)
                {
                    builder.Append('.');
                }
    
                if (partMetadata.AdditionalValues.TryGetValue("ClientName", out clientName))
                {
                    builder.Append(clientName);
                }
                else
                {
                    builder.Append(partMetadata.PropertyName);
                }
            }
            while (periodIndex != -1);
    
            return builder.ToString();
        }
    }
    

    CustomModelState is a method that I call in a view.

    like this:

    var errors = @Html.Raw(Html.CustomModelState());
    if (errors.length > 0) {
        showErrors("masterModel",errors);
    }
    

    this will give you nicely formated errors, with your custom names of properties.

    And here are tests for it:

    public class TestModel
    {
        [Required]
        public string Normal { get; set; }
        [ClientName("Other")]
        [Required]
        public string Changed { get; set; }
    
        [ClientName("Complicated")]
        public TestModelTwo TestModelTwo { get; set; }
    }
    
    public class TestModelTwo
    {
        public string PropertyOne { get; set; }
    
        [ClientName("Two")]
        public string PropertyTwo{ get; set; }
    }
    
    [TestClass]
    public class HtmlHelperExtensionsTests
    {
        [TestMethod]
        public void CustomModelStateTests()
        {
            var model = new TestModel();
            var page = new ViewPage();
            page.ViewData.Model = model;
            page.ViewData.ModelState.AddModelError("Normal", "Error1");
            page.ViewData.ModelState.AddModelError("Changed", "Error2");
            HtmlHelper<TestModel> helper = new HtmlHelper<TestModel>(new ViewContext(), page);
            var custom = helper.CustomModelState();
            string expectedResult =
                "[{\"Key\":\"Normal\",\"Value\":{\"Exception\":null,\"ErrorMessage\":\"Error1\"}},{\"Key\":\"Other\",\"Value\":{\"Exception\":null,\"ErrorMessage\":\"Error2\"}}]";
            Assert.AreEqual(expectedResult, custom);
        }
    
    
        [TestMethod]
        public void CustomModelStateTests_ObjectProperty_With_ClientName()
        {
            var model = new TestModel();
            model.TestModelTwo = new TestModelTwo();
            var page = new ViewPage();
            page.ViewData.Model = model;
            page.ViewData.ModelState.AddModelError("TestModelTwo.PropertyOne", "Error1");
            page.ViewData.ModelState.AddModelError("TestModelTwo.PropertyTwo", "Error2");
            HtmlHelper<TestModel> helper = new HtmlHelper<TestModel>(new ViewContext(), page);
            var custom = helper.CustomModelState();
            string expectedResult =
                "[{\"Key\":\"Complicated.PropertyOne\",\"Value\":{\"Exception\":null,\"ErrorMessage\":\"Error1\"}},{\"Key\":\"Complicated.Two\",\"Value\":{\"Exception\":null,\"ErrorMessage\":\"Error2\"}}]";
            Assert.AreEqual(expectedResult, custom);
        }
    }