Pergunta

I've got a POCO that I'm using as an argument to an action in MVC3. Something like this:

My Type

public class SearchData
{
    public string Property1 { get; set; }
    public string Property2 { get; set; }
    public string Property3 { get; set; }
}

My Action

public ActionResult Index(SearchData query)
{
    // I'd like to be able to do this
    if (query == null)
    {
        // do something
    }
}

Currently, query is passed as an instance of SearchData with all of the properties as null. I'd prefer that i get a null for query so I can just do the null check that I have in the above code.

I could always look at ModelBinder.Any() or just the various keys in ModelBinder to see if it got any of the properties for query, but I don't want to have to use reflection to loop over the properties of query. Also, I can only use the ModelBinder.Any() check if query is my only parameter. As soon as I add additional parameters, that functionality breaks.

With the current model binding functionality in MVC3, is it possible to get the behavior of returning null for POCO argument to an action?

Foi útil?

Solução

You'll need to implement a custom modelbinder to do this. You can just extend DefaultModelBinder.

public override object BindModel(
    ControllerContext controllerContext, 
    ModelBindingContext bindingContext)
{
    object model = base.BindModel(controllerContext, bindingCOntext);
    if (/* test for empty properties, or some other state */)
    {
        return null;
    }

    return model;
}

Specific Implementation

This is the actual implementation of the binder that will return null for the model if all of the properties are null.

/// <summary>
/// Model binder that will return null if all of the properties on a bound model come back as null
/// It inherits from DefaultModelBinder because it uses the default model binding functionality.
/// This implementation also needs to specifically have IModelBinder on it too, otherwise it wont get picked up as a Binder
/// </summary>
public class SearchDataModelBinder : DefaultModelBinder, IModelBinder
{
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        // use the default model binding functionality to build a model, we'll look at each property below
        object model = base.BindModel(controllerContext, bindingContext);

        // loop through every property for the model in the metadata
        foreach (ModelMetadata property in bindingContext.PropertyMetadata.Values)
        {
            // get the value of this property on the model
            var value = bindingContext.ModelType.GetProperty(property.PropertyName).GetValue(model, null);

            // if any property is not null, then we will want the model that the default model binder created
            if (value != null)
                return model;
        }

        // if we're here then there were either no properties or the properties were all null
        return null;
    }
}

Adding this as a binder in global.asax

protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();
    ModelBinders.Binders.Add(typeof(SearchData), new SearchDataModelBinder());
    RegisterGlobalFilters(GlobalFilters.Filters);
    RegisterRoutes(RouteTable.Routes);

    MvcHandler.DisableMvcResponseHeader = true;
}

Outras dicas

in the route try

new { controller = "Articles", action = "Index", query = UrlParameter.Optional }

Implement the custom model binder as an attribute on the parameter.

NOTE: All properties on your model must be nullable

  1. Here is the ModelBinderClass as above

    public class NullModelBinder : DefaultModelBinder
    {
        public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
             // use the default model binding functionality to build a model, we'll look at each property below
             object model = base.BindModel(controllerContext, bindingContext);
    
             // loop through every property for the model in the metadata
             foreach (ModelMetadata property in bindingContext.PropertyMetadata.Values)
             {
                 // get the value of this property on the model
                 var value = bindingContext.ModelType.GetProperty(property.PropertyName).GetValue(model, null);
    
                 // if any property is not null, then we will want the model that the default model binder created
                 if (value != null) return model;
             }
    
             // if we're here then there were either no properties or the properties were all null
             return null;
         }
    }
    
  2. Create an Attribute

    public class NullModelAttribute : CustomModelBinderAttribute
    {
        public override IModelBinder GetBinder()
        {
            return new NullModelBinder();
        }
    }
    
  3. Use Attribute on controller method

    public ActionResult Index([NullModel] SearchData query)
    {
        // I'd like to be able to do this
        if (query == null)
        {
            // do something
        }
    }
    

I do not know the answer to your specific question, but I can think of a workaround. Why not just add a method to the SearchData class?

public bool IsEmpty(){
  return Property1 == null 
      && Property2 == null 
      && Property3 == null;
}

Of course, if you have more than one type you're trying to do this on, it may get tedious.

Implement a custom modelbinder but use an interface to determine if the object is null. I prefer this pattern for 2 reasons:

  1. Using reflection on every binding could be very costly
  2. It encapsulates the logic of how to determine if an object is null to that object.

    public class NullValueModelBinder : DefaultModelBinder, IModelBinder {
    
      public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {
    
         object model = base.BindModel(controllerContext, bindingContext);
    
         if (model is INullValueModelBindable && (model as INullValueModelBindable).IsNull()){
             return null;
         }
    
         return model;
      }
    }
    
    public interface INullValueModelBindable {
        bool IsNull();
    }
    

I found that the SetProperty of the DefaultModelBinder only gets called when it finds the property and tries to set it.

With that in mind this is my NullModelBinder.

public class NullModelBinder : DefaultModelBinder
{
    public bool PropertyWasSet { get; set; }

    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        object model = base.BindModel(controllerContext, bindingContext);
        if (!PropertyWasSet)
        {
            return null;
        }

        return model;
    }

    protected override void SetProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor, object value)
    {
        base.SetProperty(controllerContext, bindingContext, propertyDescriptor, value);
        PropertyWasSet = true;
    }
}

So only if the framework found the property in the request and tries to set it to the model I return the Model created by BindModel.

Note:

My approach differs from the NullBinders of previous answers because it only goes once through every property while in the worst case scenario the other NullBinders go twice.

In this code snnipet:

public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
    object model = base.BindModel(controllerContext, bindingContext);

    // loop through every property for the model in the metadata
    //CODE HERE
}

When the base.BindModel gets called .Net goes through every property on the Model trying to find them and set them on the Model is creating.

Then the CustomModelBinder goes again though every property until it finds one present in the request, in which case returns the model created by .Net, otherwise returns null.

Thus, if no property is set, we would effectively go through each property of the Model twice.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top