Reputation: 19446
I've got a POCO that I'm using as an argument to an action in MVC3. Something like this:
public class SearchData
{
public string Property1 { get; set; }
public string Property2 { get; set; }
public string Property3 { get; set; }
}
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?
Upvotes: 4
Views: 2756
Reputation: 5208
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.
Upvotes: 0
Reputation: 11
Implement the custom model binder as an attribute on the parameter.
NOTE: All properties on your model must be nullable
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;
}
}
Create an Attribute
public class NullModelAttribute : CustomModelBinderAttribute
{
public override IModelBinder GetBinder()
{
return new NullModelBinder();
}
}
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
}
}
Upvotes: 1
Reputation: 4413
Implement a custom modelbinder but use an interface to determine if the object is null. I prefer this pattern for 2 reasons:
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();
}
Upvotes: 0
Reputation: 5187
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;
}
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;
}
}
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
ModelBinders.Binders.Add(typeof(SearchData), new SearchDataModelBinder());
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
MvcHandler.DisableMvcResponseHeader = true;
}
Upvotes: 6
Reputation: 12174
in the route try
new { controller = "Articles", action = "Index", query = UrlParameter.Optional }
Upvotes: 1
Reputation: 6259
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.
Upvotes: 0