DataAnnotation Validations and Custom ModelBinder
Asked Answered
S

3

6

I've been running some experiments with ASP.NET MVC2 and have run into an interesting problem.

I'd like to define an interface around the objects that will be used as Models in the MVC app. Additionally, I'd like to take advantage of the new DataAnnotation functionally by marking up the members of this interface with validation attributes.

So, if my site has a "Photo" object, I'll define the following interface:

public interface IPhoto 
{ 
 [Required]
 string Name { get; set; }

 [Required]
 string Path { get; set; }
}

And I'll define the following implementation:

public class PhotoImpl : IPhoto 
{
 public string Name { get; set; }
 public string Path { get; set; }
}

My MVC App controller might include methods like:

public class PhotoController : Controller
{
 [HttpGet]
 public ActionResult CreatePhoto()
 {
  return View(); 
 }

 [HttpPost]
 public ActionResult CreatePhoto(IPhoto photo)
 {
  if(ModelState.IsValid)
  {
   return View(); 
  }
  else
  {
   return View(photo);
  }

 }
}

And finally, in order to bind PhotoImpls to the parameters in these action methods, I might implement the following extensions to the DefaultModelBinder:

public class PhotoModelBinder : DefaultModelBinder
{
 public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
  if(bindingContext.ModelType == typeof(IPhoto))
  {
   IPhoto photo = new PhotoImpl();
   // snip: set properties of photo to bound values
   return photo; 
  }

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

Everything appears to working great, except that the ModelState.IsValid property in my controller doesn't appear to notice invalid values (say, null) in the [Required] properties of the IPhoto implementation.

I suspect that I'm neglecting to set some important piece of state in my ModelBinder implementation. Any hints?

Scampi answered 8/1, 2010 at 19:17 Comment(0)
S
8

After inspecting the source for System.Web.MVC.DefaultModelBinder, it looks like this can be solved using a slightly different approach. If we rely more heavily on the base implementation of BindModel, it looks like we can construct a PhotoImpl object while still pulling the validation attributes from IPhoto.

Something like:

public class PhotoModelBinder : DefaultModelBinder
{
    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        if (bindingContext.ModelType == typeof(IPhoto))
        {
            ModelBindingContext newBindingContext = new ModelBindingContext()
            {
                ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(
                    () => new PhotoImpl(), // construct a PhotoImpl object,
                    typeof(IPhoto)         // using the IPhoto metadata
                ),
                ModelState = bindingContext.ModelState,
                ValueProvider = bindingContext.ValueProvider
            };

            // call the default model binder this new binding context
            return base.BindModel(controllerContext, newBindingContext);
        }
        else
        {
            return base.BindModel(controllerContext, bindingContext);
        }
    }
}
Scampi answered 12/1, 2010 at 19:44 Comment(0)
S
9

I had the same issue. The answer is instead of overriding BindModel() in your custom model binder, override CreateModel()...

protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, System.Type modelType)
{
    if (modelType == typeof(IPhoto))
    {
        IPhoto photo = new PhotoImpl();
        // snip: set properties of photo to bound values
        return photo;
    }

    return base.CreateModel(controllerContext, bindingContext, modelType);
}

You can then let the base BindModel class do its stuff with validation :-)

Signboard answered 27/5, 2010 at 10:45 Comment(1)
I've been chasing around for a solution to binding with a complex ViewModel with DataAnnotations and this is perfect thanks!Venosity
S
8

After inspecting the source for System.Web.MVC.DefaultModelBinder, it looks like this can be solved using a slightly different approach. If we rely more heavily on the base implementation of BindModel, it looks like we can construct a PhotoImpl object while still pulling the validation attributes from IPhoto.

Something like:

public class PhotoModelBinder : DefaultModelBinder
{
    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        if (bindingContext.ModelType == typeof(IPhoto))
        {
            ModelBindingContext newBindingContext = new ModelBindingContext()
            {
                ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(
                    () => new PhotoImpl(), // construct a PhotoImpl object,
                    typeof(IPhoto)         // using the IPhoto metadata
                ),
                ModelState = bindingContext.ModelState,
                ValueProvider = bindingContext.ValueProvider
            };

            // call the default model binder this new binding context
            return base.BindModel(controllerContext, newBindingContext);
        }
        else
        {
            return base.BindModel(controllerContext, bindingContext);
        }
    }
}
Scampi answered 12/1, 2010 at 19:44 Comment(0)
S
0

Have you tried placing the [Required] attribute on your model and retesting? It may be having difficulty applying the attribute to an interface.

Sensor answered 9/1, 2010 at 22:32 Comment(1)
Thanks for the answer. It doesn't seem to be an issue with the location of the [Required] attribute. The problem continues if I move the attributes to the PhotoImpl, change the Controller to operate on a PhotoImpl, and change the ModelBinder to act on requests for a PhotoImpl. Conversely, if I instruct my ModelBinder to not act on the PhotoImpl and to fall back to the default ModelBinder implementation, the validations function correctly.Scampi

© 2022 - 2024 — McMap. All rights reserved.