I got a custom ModelBinder and i would like to get the action. Because i want to get the Attributes of the action using reflection, the action name is not enough.
my action method:
[MyAttribute]
public ActionResult Index([ModelBinder(typeof(MyModelBinder))] MyModel model)
{
}
and here a typically ModelBinder
public class MyModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
// here i would like to get the action method and his "MyAttribute"
}
}
any suggestions, other solutions ?
many thanks in advance
No, you cannot with 100% certainty get the current action from a model binder. The model binder is not coupled to the action, but to binding to a model. For example, you can call
TryUpdateMode(model)
In an filter before an action has been chosen. Also note that an action method might not even be a CLR method (see http://haacked.com/archive/2009/02/17/aspnetmvc-ironruby-with-filters.aspx) that can be reflected on.
I think the real question is, what exactly are you trying to accomplish and is this the right way? If you want information from the action to be passed to the model binder (heeding the advice that your model binder should degrade gracefully if the information isn't there), you should use an action filter to put the information in HttpContext.Items (or somewhere like that) and then have your binder retrieve it.
An action filter's OnActionExecuting method receives an ActionExecutingContext which has an ActionDescriptor. You can call GetCustomAttributes on that.
You could try this:
var actionName = controllerContext.RouteData.GetRequiredString("action");
var myAttribute = (MyAttribute) Attribute.GetCustomAttribute(controllerContext.Controller.GetMethod(actionName), typeof(MyAttribute));
You could override ControllerActionInvoker.FindAction() to get the action's attribute and store it in HttpContext.Current.Items as mentioned here, or extendedControllerContext.RequestContext, as follows:
public class MyControllerActionInvoker : ControllerActionInvoker
{
protected override ActionDescriptor FindAction(ControllerContext controllerContext, ControllerDescriptor controllerDescriptor, string actionName)
{
var action = base.FindAction(controllerContext, controllerDescriptor, actionName);
if (action != null)
{
var requestContext = ExtendedRequestContext.Bind(controllerContext);
var attr = action.GetCustomAttributes(typeof(MyAttribute), false).FirstOrDefault();
if (attr != null)
requestContext.CustomAttribute = (MyAttribute)attr;
}
return action;
}
}
public class ExtendedRequestContext : RequestContext
{
public MyAttribute CustomAttribute { get; set; }
public static ExtendedRequestContext Bind(ControllerContext controllerContext)
{
var requestContext = new ExtendedRequestContext
{
HttpContext = controllerContext.RequestContext.HttpContext,
RouteData = controllerContext.RequestContext.RouteData
};
controllerContext.RequestContext = requestContext;
return requestContext;
}
}
The default action invoker is replaced either in your controller's constructor or in a custom controllers factory:
public MyController() : base()
{
ActionInvoker = new MyControllerActionInvoker();
}
By the way, Controller.TempData already contains an item of ReflectedParameterDescriptor type, which gives you access to ActionDescriptor, so the above code may be redundant. However, beware this is implementation specific, so may change over time.
Finally, get the attribute from that storage in your binder class:
var requestContext = (ExtendedRequestContext)controllerContext.RequestContext;
if (requestContext.CustomAttribute != null)
{
// apply your logic here
}
Related
Situation is this:
I can't find a way of getting the viewModel that was passed to the POST action method.
[HttpPost]
public ActionResult Edit(SomeCoolModel viewModel)
{
// Some Exception happens here during the action execution...
}
Inside the overridable OnException for the controller:
protected override void OnException(ExceptionContext filterContext)
{
...
filterContext.Result = new ViewResult
{
ViewName = filterContext.RouteData.Values["action"].ToString(),
TempData = filterContext.Controller.TempData,
ViewData = filterContext.Controller.ViewData
};
}
When debugging the code filterContext.Controller.ViewData is null since the exception occurred while the code was executing and no view was returned.
Anyways I see that filterContext.Controller.ViewData.ModelState is filled and has all the values that I need but I don't have the full ViewData => viewModel object available. :(
I want to return the same View with the posted data/ViewModel back to the user in a central point. Hope you get my drift.
Is there any other path I can follow to achieve the objective?
You could create a custom model binder that inherits from DefaultModelBinder and assign the model to TempData:
public class MyCustomerBinder : DefaultModelBinder
{
protected override void OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
base.OnModelUpdated(controllerContext, bindingContext);
controllerContext.Controller.TempData["model"] = bindingContext.Model;
}
}
and register it in Global.asax:
ModelBinders.Binders.DefaultBinder = new MyCustomerBinder();
then access it:
protected override void OnException(ExceptionContext filterContext)
{
var model = filterContext.Controller.TempData["model"];
...
}
I'm trying to make some of my post methods like this:
Client Side:
$.post("/control/PostMethod",{a:1,b:2,c:[1,2,3],d:{x:1,y:0}})
Server Side:
[HttpPost]
public int PostMethod(dynamic model)
{
//do something with model.a model.b etc.
return 1;
}
The problem is if I did nothing,the model seems to be one simple object with no property,so i tried to write a CustomModelBinder replace DefaultModelBinder and override the CreateModel method by analyse form values.But I found it became very difficult because the properties had been expanded,like the property d became a[d][x],a[d][y] in form values.
So is there any simple way to transfer client post data to a dynamic object in actions?
If you are able to also supply information about the type you could use a custom model binder and supply the necessary type infos. I have a modelbinder that scans for a "type" querystring-value that is used to feed the modelbinder with the information it needs.
public class MyModelBinder : DefaultModelBinder
{
public override object BindModel( ControllerContext controllerContext, ModelBindingContext bindingContext )
{
// Check if the model type is object
if (bindingContext.ModelType == typeof( object ))
{
// Try to get type info from the query string or form-data
var type = controllerContext.HttpContext.Request.QueryString["type"] ??
controllerContext.HttpContext.Request.Form["type"];
if (type != null )
{
// Find a way to get type info, for example
var matchingType = Assembly.GetExecutingAssembly().GetType(type);
// Supply the metadata for our bindingcontext and the default binder will do the rest
bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType( null, matchingType );
}
}
return base.BindModel( controllerContext, bindingContext );
}
}
My controller method looks like this:
public ActionResult Method( string type, object model )
{
}
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);
}
}
Investigating the security of a system I'm building with ASP.NET MVC 2 led me to discover the request validation feature of ASP.NET - a very neat feature, indeed. But obviously I don't just want to present the users with the Yellow Screen of Death when they enter data with HTML in, so I'm out to find a better solution.
My idea is to find all the fields that have invalid data and add them to the ModelStateDictionary before invoking the action such that they automatically appear in the UI as error messages. After googling this a bit it appears that no one have implemented this before which I find puzzling since it seems so obvious. Does anyone here have a suggestion on how to do this? My own idea is to supply a custom ControllerActionInvoker to the controller, as described here, that somehow checks for this and modifies the ModelStateDictionary but I'm stuck on how to do this last bit.
Just catching HttpRequestValidationException exceptions does not seem a useful approach since it does not actually contain all the information I need.
I've answered the question myself but I'd still be very interested to hear of any solutions which are more elegant/robust.
Having looked a bit at how MVC does the model binding I've come up with a solution myself. I extend the Controller class with a custom implementation that overrides the Execute method like so:
public abstract class ExtendedController : Controller
{
protected override void Execute(RequestContext requestContext)
{
ActionInvoker = new ExtendedActionInvoker(ModelState);
ValidateRequest = false;
base.Execute(requestContext);
}
}
For me to control when request validation occurs I've added the following to the web.config:
<httpRuntime requestValidationMode="2.0"/>
The meat of the action happens in the custom implementation of the ControllerActionInvoker class:
public class ExtendedActionInvoker : ControllerActionInvoker
{
private ModelStateDictionary _modelState;
private const string _requestValidationErrorKey = "RequestValidationError";
public ExtendedActionInvoker(ModelStateDictionary modelState)
{
_modelState = modelState;
}
protected override ActionDescriptor FindAction(ControllerContext controllerContext, ControllerDescriptor controllerDescriptor, string actionName)
{
var action = base.FindAction(controllerContext, controllerDescriptor, actionName);
controllerContext.RequestContext.HttpContext.Request.ValidateInput();
return action;
}
protected override object GetParameterValue(ControllerContext controllerContext, ParameterDescriptor parameterDescriptor)
{
try
{
return base.GetParameterValue(controllerContext, parameterDescriptor);
}
catch (HttpRequestValidationException)
{
var fieldName = parameterDescriptor.ParameterName;
_modelState.AddModelError(fieldName, ModelRes.Shared.ValidationRequestErrorMessage);
_modelState.AddModelError(_requestValidationErrorKey, ModelRes.Shared.ValidationRequestErrorMessage);
var parameterType = parameterDescriptor.ParameterType;
if (parameterType.IsPrimitive || parameterType == typeof(string))
{
return GetValueFromInput(parameterDescriptor.ParameterName, parameterType, controllerContext);
}
var complexActionParameter = Activator.CreateInstance(parameterType);
foreach (PropertyDescriptor descriptor in TypeDescriptor.GetProperties(complexActionParameter))
{
object propertyValue = GetValueFromInput(descriptor.Name, descriptor.PropertyType, controllerContext);
if (propertyValue != null)
{
descriptor.SetValue(complexActionParameter, propertyValue);
}
}
return complexActionParameter;
}
}
private object GetValueFromInput(string parameterName, Type parameterType, ControllerContext controllerContext)
{
object propertyValue;
controllerContext.RouteData.Values.TryGetValue(parameterName, out propertyValue);
if (propertyValue == null)
{
propertyValue = controllerContext.HttpContext.Request.Params[parameterName];
}
if (propertyValue == null)
return null;
else
return TypeDescriptor.GetConverter(parameterType).ConvertFrom(propertyValue);
}
}
What this does is to perform request validation after the action has been found. This will not immediate cause an error if the request is invalid but when GetParameterValue is called it will throw an exception. To avoid this I override this method and wrap the base call in a try-catch. If an exception is caught I basically re-implement the model binding (I make no promises about the quality of this code) and add an error to the ModelStateDictionary object for the value.
As a bonus, as I wanted to return an error in a standard format for my ajax methods I also added a custom implementation of the InvokeActionMethod.
protected override ActionResult InvokeActionMethod(ControllerContext controllerContext, ActionDescriptor actionDescriptor, IDictionary<string, object> parameters)
{
if (_modelState.ContainsKey(_requestValidationErrorKey))
{
var errorResult = new ErrorResult(_modelState[_requestValidationErrorKey].Errors[0].ErrorMessage, _modelState);
var type = controllerContext.Controller.GetType();
var methods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);
if (methods.Where(m => m.Name == actionDescriptor.ActionName).First().ReturnType == typeof(JsonResult))
return (controllerContext.Controller as ExtendedControllerBase).GetJson(errorResult);
}
return base.InvokeActionMethod(controllerContext, actionDescriptor, parameters);
}
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.