juliano.net
juliano.net

Reputation: 8177

Receive an array of json objects and validate in the controller

I'm sending a JSON like this:

[
{col1: 'value', col2: 'value'}, 
{col1: 'value2', col2: 'value2'},
...
]

The action in my controller has a List parameter that is requiring a custom model binder, like this:

public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            var contentType = controllerContext.HttpContext.Request.ContentType;

            String bodyText;

            Stream stream = null;
            try
            {
                stream = controllerContext.HttpContext.Request.InputStream;
                stream.Seek(0, SeekOrigin.Begin);
                using (var reader = new StreamReader(stream))
                {
                    stream = null;
                    bodyText = reader.ReadToEnd();
                }
            }
            finally
            {
                if (stream != null)
                    stream.Dispose();
            }

            if (string.IsNullOrEmpty(bodyText))
            {
                return null;
            }

            var model = new JavaScriptSerializer().Deserialize<T>(bodyText);
            return model;

            // return base.BindModel(controllerContext, bindingContext);
        }

It's working, except that it is not considering the data annotations for validations (required, range, etc).

How can I get this working with validation?

UPDATE

Controller Action

[HttpPost]
        public ActionResult ActionName([ModelBinder(typeof(JsonArrayValidationModelBinder<List<EntityName>>))]List<EntityName> viewModel)

Entity

public class EntityName
{
        [Display(Name = "Data Entrada")]
        [DataType(DataType.Date)]
        [Required]
        public DateTime? DataEntrada { get; set; }
// ....
}

Upvotes: 2

Views: 1453

Answers (2)

RyanCJI
RyanCJI

Reputation: 464

I have revised my answer. There were a few problems that I ran into when trying to get this working. Detailed below is the problem and the solution I used.

The json: The json you provided did not match the Model you provided. So I assumed the json string should have included something like this:

`DataEntrada: "1/1/2014"`

The model: Your model describes only EntityName. The deserialized json is a list. These are two different things. So I modified the json to be an object that defines EntityNames (list of EntityName), like this:

 `data = { EntityNames: [{ DataEntrada: "1/1/2014" }] };`

and then I implemented this class..this will be the result of deserialization:

public class EntityInfo
{
    public EntityName[] EntityNames { get; set; }
}

and finally, modified the ActionMethod like so:

public JsonResult SaveActionName([ModelBinder(typeof(JsonArrayValidationModelBinder<EntityInfo>))]EntityInfo viewModel)

Validation: Validating EntityNames was not as easy to implement as I thought it would be. I could not get the validation attribute for EntityName to fire during model binding (being a member of a list). So, I implemented a custom validator derived from 'ValidationAttribute' like this:

public class EntityNamesValidation : ValidationAttribute
{
    public override bool IsValid(object value)
    {
        EntityName[] list = (EntityName[])value;
        foreach (EntityName e in list)
        {
            if (string.IsNullOrEmpty(e.DataEntrada.ToString()))
                return false;

            // more checks performed here

        }
        return true;
    }
}

and then I applied EntityNamesValidation attribute to EntityNames and EntityInfo, like so:

[EntityNamesValidation]
    public EntityName[] EntityNames { get; set; }

Incorrect model during bind: The JsonArrayValidationModelBinder was using a bindingContext that did not have an instance of anything. If you debug BindModel before base.BindModel you will see that bindingContext.Model is null. So what I did was set bindingContext.ModelMetadata.Model = model after deserialization and before the call to base.BindModel. I also moved base.BindModel in the code to fire just before model is returned...see below

public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)  
{
    [...]

    bindingContext.ModelMetadata.Model = model;
    base.BindModel(controllerContext, bindingContext);

    return model;
}

Verification: I did not unit test this, but I did place a breakpoint in the ActionMethod. I then used the following json:

data = { EntityNames: [{ DataEntrada: "1/1/2014" }, { DataEntrada: null }] };

when the code reached the breakpoint, ModelState.IsValid is false. I then changed json to this:

data = { EntityNames: [{ DataEntrada: "1/1/2014" }, { DataEntrada: "2/19/2014" }] };

when the code reached the breakpoint, ModelState.IsValid is true.

This approach works, but is not ideal. I think you want validation to occur without creating custom code and use MVC to handle this.

I hope this gets you a step further.

ALL THE CODE

javascript

        data = { EntityNames: [{ DataEntrada: "1/1/2014" }, { DataEntrada: null }] };
        var jsonOfLog = JSON.stringify(data);
        $.ajax({
            type: 'POST',
            dataType: 'text',
            url: "/EntityData/SaveActionName",
            data: jsonOfLog,
            success: function (data) {
                alert(data);
            },
            error: function (result) {
                alert(result);
            }
                ,
            async: false
        });

models

public class EntityInfo
{
    [EntityNamesValidation]
    public EntityName[] EntityNames { get; set; }
}

public class EntityName
{
    [Display(Name = "Data Entrada")]
    [DataType(DataType.Date)]
    [Required]
    public DateTime? DataEntrada { get; set; }
}

custom validator

public class EntityNamesValidation : ValidationAttribute
{
    public override bool IsValid(object value)
    {
        EntityName[] list = (EntityName[])value;
        foreach (EntityName e in list)
        {
            if (string.IsNullOrEmpty(e.DataEntrada.ToString()))
                return false;

            // more checks performed here

        }
        return true;
    }
}

BindModel

    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var contentType = controllerContext.HttpContext.Request.ContentType;
        String bodyText;
        Stream stream = null;
        try
        {
            stream = controllerContext.HttpContext.Request.InputStream;
            stream.Seek(0, SeekOrigin.Begin);
            using (var reader = new StreamReader(stream))
            {
                stream = null;
                bodyText = reader.ReadToEnd();
            }
        }
        finally
        {
            if (stream != null)
                stream.Dispose();
        }
        if (string.IsNullOrEmpty(bodyText))
        {
            return null;
        }
        var model = new JavaScriptSerializer().Deserialize<T>(bodyText);
        bindingContext.ModelMetadata.Model = model;
        base.BindModel(controllerContext, bindingContext);

        return model;
    }

ActionMethod

[HttpPost]
public JsonResult SaveActionName([ModelBinder(typeof(JsonArrayValidationModelBinder<EntityInfo>))]EntityInfo viewModel)

Upvotes: 1

RyanCJI
RyanCJI

Reputation: 464

Deriving from DefaultModelBinder will give you what you are looking for. In your override, call base method, like so

public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        // base BindModel should validate your model 
        base.BindModel(controllerContext, bindingContext);

        // (optional) Capture validation result
        bool ModelIsValid = bindingContext.ModelState.IsValid;

        var contentType = controllerContext.HttpContext.Request.ContentType;

        [...]
    }

Upvotes: 0

Related Questions