MVC Model Binding to a collection where collection

2019-02-04 21:49发布

问题:

I'm trying to perform remote validation on a property of an item within a collection. The validation works OK on the first item of the collection. The http request to the validation method looks like:

/Validation/IsImeiAvailable?ImeiGadgets[0].ImeiNumber=123456789012345

However on the 2nd item where the url looks like below, the validation doesn't work

/Validation/IsImeiAvailable?ImeiGadgets[1].ImeiNumber=123456789012345

Now I'm pretty sure the reason for this, is that binding wont work on a collection that doesn't begin with a zero index.

My validation method has a signature as below:

public JsonResult IsImeiAvailable([Bind(Prefix = "ImeiGadgets")] Models.ViewModels.ImeiGadget[] imeiGadget)

Because I'm passing an item within a collection I have to bind like this yet what I'm really passing is just a single value.

Is there anyway I can deal with this other than just binding it as a plain old query string.

Thanks

Edit: This is the quick fix to get the Imei variable but I'd rather use the model binding:

string imeiNumber = Request.Url.AbsoluteUri.Substring(Request.Url.AbsoluteUri.IndexOf("=")+1);

Edit: Here is my ImeiGadget class:

public class ImeiGadget
{
    public int Id { get; set; }

    [Remote("IsImeiAvailable", "Validation")]
    [Required(ErrorMessage = "Please provide the IMEI Number for your Phone")]
    [RegularExpression(@"(\D*\d){15,17}", ErrorMessage = "An IMEI number must contain between 15 & 17 digits")]
    public string ImeiNumber { get; set; }
    public string Make { get; set; }
    public string Model { get; set; }
}

回答1:

You could write a custom model binder:

public class ImeiNumberModelBinder : IModelBinder
{
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var modelName = bindingContext.ModelName;
        var request = controllerContext.HttpContext.Request;
        var paramName = request
            .Params
            .Keys
            .Cast<string>()
            .FirstOrDefault(
                x => x.EndsWith(modelName, StringComparison.OrdinalIgnoreCase)
            );

        if (!string.IsNullOrEmpty(paramName))
        {
            return bindingContext
                .ValueProvider
                .GetValue(request[paramName])
                .AttemptedValue;
        }

        return null;
    }
}

and then apply it to the controller action:

public ActionResult IsImeiAvailable(
    [ModelBinder(typeof(ImeiNumberModelBinder))] string imeiNumber
)
{
    return Json(!string.IsNullOrEmpty(imeiNumber), JsonRequestBehavior.AllowGet);
}

Now the ImeiGadgets[xxx] part will be ignored from the query string.



回答2:

If you are posting the whole collection, but have a nonsequential index, you could consider binding to a dictionary instead http://www.hanselman.com/blog/ASPNETWireFormatForModelBindingToArraysListsCollectionsDictionaries.aspx

If you're only posting a single item or using a GET link, then you should amend to

/Validation/IsImeiAvailable?ImeiNumber=123456789012345

and

public JsonResult IsImeiAvailable(string imeiNumber)


回答3:

If you are sending up a single value to the server for validation, then your Action Method should only accept a scalar (single-value) parameter, not a collection. Your URL would then look like this (assuming default routing table for {controller}/{action}/{id}:

/Validation/IsImeiAvailable?ImeiNumber=123456789012345

the corresponding action method signature could look like this:

/* note that the param name has to matchthe prop name being validated */
public ActionResult IsImeiAvailable(int ImeiNumber)

EDIT: which you could then use to lookup whether that particular ID is available.

if you want to change the name of the parameter, you can modify the routing table, but that's a different topic.

The long story short of it is that if you wanted to do validate a collection of ImeiGadget, you'd GET or POST that full collection. For a single value, it doesn't make much sense to send up or to expect an entire collection.

UPDATE: Based on new info, I would look at where the remote validation attribute is being placed. It sounds like it might be placed on something like an IEnumerable<IMEiGadgets>, like this:

[Remote("IsImeiAvailable", "Validation", "'ImeiNumber' is invalid"]
public IEnumerable<ImeiGadget> ImeiGadgets { get; set;}

Would it be possible to move that attribute and modify it to be on the ImeiGadget class instead, to be something like this?

[Remote("IsImeiAvailable", "Validation", "'ImeiNumber is invalid"]
public int ImeiNumber { get; set;}

In theory, you shouldn't have to change anything on your HTML templates or scripts to get this working if you also make the change suggested in my answer above. In theory.



回答4:

Unless you need this binding feature in many places and you control the IsImeiAvailable validation method then I think creating a custom model binder is an over-head.

Why don't you try a simple solution like this,

// need little optimization?
public JsonResult IsImeiAvailable(string imeiNumber)
{
  var qParam = Request.QueryString.Keys
     .Cast<string>().FirstOrDefault(a => a.EndsWith("ImeiNumber"));

  return Json(!string.IsNullOrEmpty(imeiNumber ?? Request.QueryString[qParam]), JsonRequestBehavior.AllowGet);
}