One custom model binder to CreateModel and another to BindModel? - asp.net-mvc

Background:
In my MVC post back action methods I am receiving command objects rather than view models. The idea is that these command objects (which roughly equate to transaction scripts) will be set up and ready to execute upon entering the action method, with the model binder having set parameters which are used during the execution process:
public class MyCommand : IMyCommand
{
// In this case Value1 and Value2 are being set by the default model binder from posted form values - wonderful :)
public String Value1 { get; set; }
public String Value2 { get; set; }
public CommandResult ExecuteCommand()
{
// Does something awesome...
}
}
To make things a little more complex, my command objects have dependencies (services, repositories etc) which are required in their respective constructors; so I had to create a custom model binder which used the default DependencyResolver (which was already set up with my IoC container) to construct the model objects:
public class DependencyModelBinder : DefaultModelBinder
{
protected override Object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
{
return DependencyResolver.Current.GetService(modelType);
}
}
And set up in Global.asax.cs like so:
ModelBinders.Binders.DefaultBinder = new DependencyModelBinder();
Again this all works fine, the dependencies are injected into the constructor and then the default model binder takes over to set the properties as usual.
The Issue:
The problem I have is that all of my command objects have a 'SessionId' GUID parameter (which comes from a cookie), and the first thing they do is try to resolve a session object from this id using an injected service.
public class MyCommand : IMyCommand
{
public MyCommand (ISessionRepository sessionRepository) { ... }
public Guid SessionId { get; set; } // Set by model binder from a cookie...
public CommandResult Execute()
{
Session session = SessionRepository.Get(SessionId);
if (session == null)
// Do something not so awesome...
}
}
I wanted to remove this repetition, so I created a second model binder which would take care of this lookup in the repository, meaning my command objects could have a Session property directly (removing the constructor dependency for the session repository).
public class SessionModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var sessionRepository = DependencyResolver.Current.GetService<ISessionRepository>();
return sessionRepository.Get((Guid)controllerContext.HttpContext.Request["SessionId"]);
}
}
My Global.asax.cs file now looking like so:
ModelBinders.Binders.DefaultBinder = new DependencyModelBinder();
ModelBinders.Binders.Add(typeof(Session), new SessionModelBinder());
Having tested the SessionModelBinder in isolation, I know it works. However when using it in conjunction with the DependencyModelBinder, it is never called. How can I get MVC to use my DependencyModelBinder when constructing model objects, but have it use my SessionModelBinder when binding session properties on them? Or does anyone know a better approach to this?

You could use the GetPropertyValue method in your original model binder to provide a value for the Session property:
public class DependencyModelBinder : DefaultModelBinder
{
protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
{
return DependencyResolver.Current.GetService(modelType);
}
protected override object GetPropertyValue(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, IModelBinder propertyBinder)
{
if (propertyDescriptor.Name == "Session")
{
var sessionRepository = DependencyResolver.Current.GetService<ISessionRepository>();
return sessionRepository.Get(controllerContext.HttpContext.Request["SessionId"]);
}
return base.GetPropertyValue(controllerContext, bindingContext, propertyDescriptor, propertyBinder);
}
}

Related

argument for custom model binder MVC

For the code below, I am wondering how Employee type affects by prefixed [] brackets
public ActionResult SaveEmployee([ModelBinder(typeof(MyEmployeeModelBinder))]Employee e, string BtnSubmit)
{
// some logic
}
MyEmployeeModelBinder is a custom model binder, that will execute instead of default model binder in Employee e.
public class MyEmployeeModelBinder: DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {
...
}
}
The Employee type affects bindingContext properties in BindModel(), like bindingContext.ModelMetadata and bindingContext.ModelType, that contains information about the model. Take a look at ModelMetaData: https://msdn.microsoft.com/pt-br/library/system.web.mvc.modelmetadata%28v=vs.108%29

Using the BindAttribute Globally

In a ASP.NET MVC (4) application, I am using a 3rd party Javascript library that auto-magically sends back a query string parameter with a poor name (IMO). I can intercept this value in model binding with the following:
public ActionResult MyAction([Bind(Prefix="rp")] int pageSize = 50)
{
}
However, this code inside all the places where I use paging gets tiresome pretty quickly.
Is it possible inside ASP.NET MVC to globally set a [BindAttribute] with a specific prefix/replacement combination?
A workaround would be to modify the Javascript library (undesirable); or get the parameter out of the Request.QueryString property manually; but I was hoping to keep things cleaner.
You could use a view model:
public class MyViewModel
{
public int PageSize { get; set; }
}
in your controller action(s):
public ActionResult MyAction(MyViewModel model)
{
...
}
and then write a custom model binder for this view model:
public class MyViewModelBinder: DefaultModelBinder
{
protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor)
{
bindingContext.ModelName = "rp";
base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
}
}
that will be registered in Application_Start:
ModelBinders.Binders.Add(typeof(MyViewModel), new MyViewModelBinder());

Custom Model Binder Not Validating Model

I started to play around with knockout.js and in doing so I used the FromJsonAttribute (created by Steve Sanderson). I ran into an issue with the custom attribute not performing model validation. I put together a simple example-- I know it looks like a lot of code-- but the basic issue is how to force the validation of the model within a custom model binder.
using System.ComponentModel.DataAnnotations;
namespace BindingExamples.Models
{
public class Widget
{
[Required]
public string Name { get; set; }
}
}
and here is my controller:
using System;
using System.Web.Mvc;
using BindingExamples.Models;
namespace BindingExamples.Controllers
{
public class WidgetController : Controller
{
public ActionResult Index()
{
return View();
}
[HttpPost]
public ActionResult Index(Widget w)
{
if(this.ModelState.IsValid)
{
TempData["message"] = String.Format("Thanks for inserting {0}", w.Name);
return RedirectToAction("Confirmation");
}
return View(w);
}
[HttpPost]
public ActionResult PostJson([koListEditor.FromJson] Widget w)
{
//the ModelState.IsValid even though the widget has an empty Name
if (this.ModelState.IsValid)
{
TempData["message"] = String.Format("Thanks for inserting {0}", w.Name);
return RedirectToAction("Confirmation");
}
return View(w);
}
public ActionResult Confirmation()
{
return View();
}
}
}
My issue is that the model is always valid in my PostJson method. For completeness here is the Sanderson code for the FromJson attribute:
using System.Web.Mvc;
using System.Web.Script.Serialization;
namespace koListEditor
{
public class FromJsonAttribute : CustomModelBinderAttribute
{
private readonly static JavaScriptSerializer serializer = new JavaScriptSerializer();
public override IModelBinder GetBinder()
{
return new JsonModelBinder();
}
private class JsonModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var stringified = controllerContext.HttpContext.Request[bindingContext.ModelName];
if (string.IsNullOrEmpty(stringified))
return null;
var model = serializer.Deserialize(stringified, bindingContext.ModelType);
return model;
}
}
}
}
Description
The FromJsonAttribute only binds to the model and does, like you said, no validation.
You can add validation to the FromJsonAttribute in order to validate the model's against his DataAnnotations attributes.
This can be done using the TypeDescriptor class.
TypeDescriptor Provides information about the characteristics for a component, such as its attributes, properties, and events.
Check out my solution. I have tested it.
Solution
private class JsonModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var stringified = controllerContext.HttpContext.Request[bindingContext.ModelName];
if (string.IsNullOrEmpty(stringified))
return null;
var model = serializer.Deserialize(stringified, bindingContext.ModelType);
// DataAnnotation Validation
var validationResult = from prop in TypeDescriptor.GetProperties(model).Cast<PropertyDescriptor>()
from attribute in prop.Attributes.OfType<ValidationAttribute>()
where !attribute.IsValid(prop.GetValue(model))
select new { Propertie = prop.Name, ErrorMessage = attribute.FormatErrorMessage(string.Empty) };
// Add the ValidationResult's to the ModelState
foreach (var validationResultItem in validationResult)
bindingContext.ModelState.AddModelError(validationResultItem.Propertie, validationResultItem.ErrorMessage);
return model;
}
}
More Information
TypeDescriptor Class
System.ComponentModel.DataAnnotations Namespace
Thank you, thank you, dknaack!! Your answer was exactly what I was looking for, except I want to validate after each property is bound b/c I have properties that are dependent on other properties, and I don't want to continue binding if a dependent property is invalid.
Here's my new BindProperty overload:
protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor){
// if this is a simple property, bind it and return
if(_simplePropertyKeys.ContainsKey(propertyDescriptor.Name)){
this.BindSimpleProperty(bindingContext, propertyDescriptor);
// if this is complex property, only bind it if we don't have an error already
} else if (bindingContext.ModelState.IsValid){
this.BindComplexProperty(bindingContext, propertyDescriptor);
}
// add errors from the data annotations
propertyDescriptor.Attributes.OfType<ValidationAttribute>()
.Where(a => a.IsValid(propertyDescriptor.GetValue(bindingContext.Model)) == false)
.ForEach(r => bindingContext.ModelState.AddModelError(propertyDescriptor.Name, r.ErrorMessage));
}
First of all, I'm only starting to learn ASP.NET so don't take my solution seriously. I found this article and as you, tried to do a custom model binder. There was no validation. Then i just replaced IModelBinder interface with DefaultModelBinder and voula, it works. Hope I could help someone

ASP.NET MVC UpdateModel with interface

I am trying to get UpdateModel to populate a model that is set as only an interface at compile-time. For example, I have:
// View Model
public class AccountViewModel {
public string Email { get; set; }
public IProfile Profile { get; set; }
}
// Interface
public interface IProfile {
// Empty
}
// Actual profile instance used
public class StandardProfile : IProfile {
public string FavoriteFood { get; set; }
public string FavoriteMusic { get; set; }
}
// Controller action
public ActionResult AddAccount(AccountViewModel viewModel) {
// viewModel is populated already
UpdateModel(viewModel.Profile, "Profile"); // This isn't working.
}
// Form
<form ... >
<input name='Email' />
<input name='Profile.FavoriteFood' />
<input name='Profile.FavoriteMusic' />
<button type='submit'></button>
</form>
Also note that I have a custom model binder that inherits from DefaultModelBinder being used that populates IProfile with an instance of StandardProfile in the overriden CreateModel method.
The problem is that FavoriteFood and FavoriteMusic are never populated. Any ideas? Ideally this would all be done in the model binder, but I'm not sure it is possible without writing a completely custom implementation.
Thanks, Brian
I would have to check the ASP.NET MVC code (DefaultModelBinder) but I'm guessing that its reflecting on the type IProfile, and not the instance, StandardProfile.
So it looks for any IProfile members it can try to bind, but its an empty interface, so it considers itself done.
You could try something like updating the BindingContext and changing the ModelType to StandardProfile and then calling
bindingContext.ModelType = typeof(StandardProfile);
IProfile profile = base.BindModel(controllerContext, bindingContext);
Anyways, having an empty Interface is weird~
Edit: just want to add that code above is just pseudo code, you would need to check DefaultModelBinder to see exactly what you want to write.
Edit#2:
Can you do:
public class ProfileModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {
{
bindingContext.ModelType = typeof(StandardProfile);
return base.BindModel(controllerContext, bindingContext);
}
}
No need to make a model binder for AccountView, that one works fine.
Edit #3
Tested it out, the above binder works, just need to add:
ModelBinders.Binders[typeof(IProfile)] = new ProfileModelBinder();
Your action looks like:
public ActionResult AddAccount(AccountViewModel viewModel) {
// viewModel is fully populated, including profile, don't call UpdateModel
}
You can use IOC when setting the model binder (have the type constructor injected for instance).
Not inspecting the actual type behind the interface was discussed here: http://forums.asp.net/t/1348233.aspx
That said, I found a hackish way around the problem. Since I already had a custom model binder for this type, I was able to add some code to it to perform the binding for me. Here's what my model binder looks like now:
public class AccountViewModelModelBinder : DefaultModelBinder
{
private readonly IProfileViewModel profileViewModel;
private bool profileBound = false;
public AccountViewModelModelBinder(IProfileViewModel profileViewModel)
{
this.profileViewModel = profileViewModel;
}
protected override void OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
// Bind the profile
if (profileBound)
return;
profileBound = true;
bindingContext.ModelType = profileViewModel.GetType();
bindingContext.Model = profileViewModel;
bindingContext.ModelName = "Profile";
BindModel(controllerContext, bindingContext);
}
protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, System.Type modelType)
{
var model = new AccountViewModel();
model.Profile = profileViewModel;
return model;
}
}
Basically, when the model binder is "done" binding the main AccountViewModel, I then alter the binding context (as suggested by eyston) and call BindModel once again. This then binds my profile. Note that I called GetType on the profileViewModel (which is supplied by the IOC container in the constructor). Also notice that I include a flag to indicate if the profile model has been bound already. Otherwise there would be an endless loop of OnModelUpdated being called.
I'm not saying this is pretty, but it does work well enough for my needs. I'd still love to hear about other suggestions.

ASP.NET MVC - Mixing Custom and Default Model Binding

I have a type:
public class IssueForm
{
Order Order {get; set;}
Item Item {get; set;}
Range Range {get; set;}
}
I created a custom model binder due to requirements on Order and Item, but Range could still use the Default Model Binder.
Is there a way from within my custom model binder to call the default model binder to return a Range object? I think I just have to just setup ModelBindingContext correctly, but I don't know how.
EDIT
Looking at the first comment and answer -- it seems like inheriting from the default model binder could be useful.
To add more specifics for my setup so far I have:
public IssueFormModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
Order = //code to pull the OrderNumber from the context and create an Order
Item = //code to pull the ItemNumber from the context and create an Item
IssueForm form = IssueFormFactory.Create(Order, Item);
form.Range = // ** I'd like to replace my code with a call to the default binder **
return form
}
}
This might be a stupid way of doing it... this is my first model binder. Just pointing out my current implementation.
EDIT #2
So the answers to override BindProperty will work if I can hook into like a "I'm all done binding" method and call the Factory method with the properties.
I guess I really should look at the DefaultModelBinder implementation and quit being stupid.
override the BindProperty from the DefaultModelBinder:
public class CustomModelBinder:DefaultModelBinder
{
protected override void BindProperty( ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor )
{
if (propertyDescriptor.PropertyType == typeof(Range))
{
base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
}
// bind the other properties here
}
}
Try something like this:
public class CustomModelBinder : DefaultModelBinder {
protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor) {
if(propertyDescriptor.Name == "Order") {
...
return;
}
if(propertyDescriptor.Name == "Item") {
...
return;
}
base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
}
}
I think I would have registered two different custom model binders, one for Order and one for Item, and let the default model binder handle the Range and the IssueForm.

Resources