Reputation: 8177
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
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.
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
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