How do I enforce proper globalization behavior for view models containing date properties in MVC3 - asp.net-mvc-3

I am attempting to pass a complex view model into a controller action. The object passed is of type Goal and contains among other things a datetime property (Goal.moddate). In my case the string representation of the date is following es-MX. Therefore February 29th, 2012 is represented as "29/02/2012" (I have the same issue with other dates).
The controller action is also annotated with an [CultureAwareAction] attribute - this one sets the culture info based on user preferences. In this case (updated to make the solution clearer)
public class CultureAwareActionAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
base.OnActionExecuting(filterContext);
Thread.CurrentThread.CurrentCulture = new CultureInfo("es-MX");
Thread.CurrentThread.CurrentUICulture = new CultureInfo("es-MX");
}
}
My preferred way to handle the action would be the following:
[HttpPost]
[CultureAwareAction]
[ValidateAntiForgeryToken]
public ActionResult Edit(Goal goal)
{
try
{
if (ModelState.IsValid)
{
{
...update logic ...
}
}
}
catch (DataException)
{
... error handling ....
}
return View();
}
Using this approach ModelState.IsValid returns false due to the date string not being parsed. Changing the controller action to the following I encounter no errors:
[HttpPost]
[CultureAwareAction]
[ValidateAntiForgeryToken]
public ActionResult Edit(int id, FormCollection formcollection)
{
try
{
Goal goal = unitOfWork.GoalRepository.GetByID(id);
if (TryUpdateModel(goal,formcollection))
{
{
... update logic ....
}
}
}
catch (DataException)
{
... error handling ...
}
return View();
}
My goal is to enforce proper globalization behavior on the first case as there are significant advantages going with that approach. It seems like this should work unless model binding happens before setting the user preferred culture using the [CultureAwareAction] attribute.

The model binder uses the CurrentCulture, not the CurrentUICulture when parsing dates. Also you haven't shown the code of this CultureAwareAction but chances are that it executes after the model binding so you are setting the culture too late.
If you want to ensure that it executes before model binding you could implement the IAuthorizationFilter interface:
public class CultureAwareActionAttribute : ActionFilterAttribute, IAuthorizationFilter
{
public void OnAuthorization(AuthorizationContext filterContext)
{
// That's for displaying in the UI, the model binder doesn't use it
Thread.CurrentThread.CurrentUICulture = new CultureInfo("es-MX");
// That's the important one for the model binder
Thread.CurrentThread.CurrentCulture = new CultureInfo("es-MX");
}
}

Related

Injecting Non-User-Submitted Data For Use During Validation

From what I can tell, ASP.Net Core performs model state validation before calling the relevant controller action method. This means that code in the action method isn't given an opportunity to add data to the model before it is validated.
What is the ASP.Net Core way of giving a view model access to additional, non-user-submitted data prior to validation?
Example
What I'm trying to do (doesn't work).
The view model's Validate method expects data to be in ValidOptions. However, since validation occurs before the controller can set this property, validation causes the view model to throw an ArgumentNullException.
// From the Controller
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Process([Bind("SelectedId")]ViewModels.Import details)
{
// data needed for validation
details.ValidOptions = await service.ImportTypes.ToListAsync();
if (ModelState.ValidationState != ModelValidationState.Valid) {
// ...
}
}
// From ViewModels.Import
public IEnumerable<Option> ValidOptions { get; set; }
public int SelectdId {get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
// throws ArgumentNullException because ValidOptions hasn't been set when this is executed
var option = ValidOptions.Single(t => t.Id == SelectdId);
//...
}
Probably many ways to skin a cat here. But the easiest for you is probably custom model binders. It's a way to "supplement" or change the binding of your model before it hits the controller. I will say that some see it as extremely bad practice to call an external service/repository at the point of model binding, but it does work and can come in handy.
You need to implement a class that inherits from IModelBinder.
public class MyViewModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
//Bind here. Including calling external services if you want.
}
}
Then you need to implement a provider, this essentially says "when" to bind.
public class MyViewModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context.Metadata.ModelType == typeof(MyViewModel))
return new MyViewModelBinder();
return null;
}
}
In your configure method of your startup.cs, you need to add the provider to the ModelBinderProviders list.
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddMvc(config =>
config.ModelBinderProviders.Add(new MyViewModelBinderProvider())
);
}
Further Documentation :
http://dotnetcoretutorials.com/2016/12/28/custom-model-binders-asp-net-core/
http://intellitect.com/custom-model-binding-in-asp-net-core-1-0/
I don't think the official documentation has an article on custom model binders yet unfortunately.

Use Action method selector to differentiate between Ajax and non-ajax request instead of relying on if(Request.isAjaxRequest)?

I'm following a book called 'Asp.Net MVC4 in Action'. And now at certain point they say, Instead of relying on if statement within our code to check if the request is Ajax or not, we could use an action method selector to differentiate it. And what they have done is create a class AcceptAjaxAttribute with following code
using System;
using System.Reflection;
using System.Web.Mvc;
namespace CustomAjax
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class AcceptAjaxAttribute : ActionMethodSelectorAttribute
{
public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo)
{
return controllerContext.HttpContext.Request.IsAjaxRequest();
}
}
}
and the function in controller which looked like this before
var speaker = _repository.FindSpeaker(id);
if(Request.IsAjaxRequest())
{
return Json(speaker, JsonRequestBehaviour.AllowGet);
}
return View();
has changed to something like this
[AcceptAjax]
public ActionResult Details(int id)
{
var speaker = _repository.FindId(id);
return Json(speaker, JsonRequestBehavior.AllowGet);
}
[ActionName("Details")]
public ActionResult Details_NonAjax(int id)
{
var speaker = _repository.FindId(id);
return View();
}
To be honest I have no idea what is being done or why we created new class and used that[AcceptAjax] thingy. Can someone may be explain it to me.
Before you had one action with an if inside, after the refactoring you have 2 actions each returning a different type of result. The ActionMethodSelectorAttribute is used to select the proper action based on some condition. Since the 2 actions have the same name (Details), ASP.NET MVC will use this attribute to select the one or the other based on whether an AJAX request is being used.
But honestly I don't like this approach at all. You now have 2 actions and have repeated the var speaker = _repository.FindId(id); call twice which is not very DRY. Wouldn't it be better if you had this?
[AcceptAjax]
public ActionResult Details(int id)
{
var speaker = _repository.FindId(id);
return View(speaker);
}
If you are like me and think that this is better, then simply replace this AcceptAjaxAttribute you got from the book with an action filter:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class AcceptAjaxAttribute : ActionFilterAttribute
{
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
if (filterContext.RequestContext.HttpContext.Request.IsAjaxRequest())
{
var result = filterContext.Result as ViewResultBase;
if (result != null && result.Model != null)
{
filterContext.Result = new JsonResult
{
Data = result.Model,
JsonRequestBehavior = JsonRequestBehavior.AllowGet
};
}
}
}
}
The OnActionExecuted method will be invoked after the controller action has finished running and returned some result. Inside this method we verify whether the controller action returned a ViewResultBase (ViewResult or PartialViewResult) and whether a model has been passed. If this is the case we simply replace this result by a JsonResult.
And if you want to avoid decorating all your controller actions with this [AcceptAjax] attribute, you could register it as a global action filter in your ~/App_Start/FilterConfig.cs:
public class FilterConfig
{
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new HandleErrorAttribute());
filters.Add(new AcceptAjaxAttribute());
}
}

How can I apply an attribute that is already a global filter to a method without it running twice?

I have extended HandleErrorAttribute in MVC by creating my own attribute that handles exceptions raised during Ajax requests. That attribute is here:
public class AjaxAwareHandleErrorAttribute : HandleErrorAttribute
{
public string PartialViewName { get; set; }
public override void OnException(ExceptionContext filterContext)
{
ExceptionHelper.HandleException(filterContext.Exception, false); // custom static class and method
base.OnException(filterContext);
// Use partial view in case of AJAX request
if (filterContext.HttpContext.Request.IsAjaxRequest())
{
var result = new PartialViewResult {ViewName = PartialViewName ?? "~/Views/Errors/DefaultAjaxError.cshtml"};
filterContext.Result = result;
}
}
}
As you can see, there is default behavior in the case when PartialViewName is null. This is because I have it registered as a global filter in my Global.asax file:
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new AjaxAwareHandleErrorAttribute());
}
So the way this is set up, I have default behavior when an exception is raised during an Ajax call, namely that DefaultAjaxError.cshtml will be returned. In cases where I want a custom message, I create a view and pass it as PartialViewName to an attribute directly on the action, like this:
[AjaxAwareHandleError(PartialViewName = "~/Views/Errors/CustomAjaxError.cshtml")]
public ActionResult SomeAjaxControllerAction()
The problem in cases where I have a controller decorated with the controller action is that the code in the AjaxAwareHandleErrorAttribute class is running twice, presumably because it's registered globally and because it's on the method as well. Is there a way I can prevent it from running twice?
Thanks.
Set the filterContext.ExceptionHandled property to true.
Then, at the top of your override, check whether that property is true and return immediately.

Switch off request validation in the view

is it possible to switch request validation in the view.
I would like to switch it off in the view because of this bit of code in the base controller
protected override void Execute(System.Web.Routing.RequestContext requestContext)
{
if (!this.CurrentStore.IsDefault)
{
IStoreRepository storeRepo = ObjectFactory.GetInstance<IStoreRepository>();
IStoreEntity store = storeRepo.GetById(this.CurrentStore.Id);
this.CurrentStore = store;
}
base.Execute(requestContext);
}
it fails in base.Execute(requestContext);
N.B I do not wish to switch it off for my entire site. I would like to switch it off in a few action methods.
In MVC you don't do it at the view level, you do it at the controller or a method of the controller level. You can use ValidateInput attribute for that, for example:
public class HomeController : Controller
{
[ValidateInput(false)] // prevent validation on this method
public ActionResult Index()
{
return View();
}
}

MVC Model is null in OnExecuted action filter ... or a more elegant way to set the model?

I have an ActionFilter with an override on the OnActionExecuted method. The filterContext.Controller.ViewData.Model is always null on a POST operation. I did find the following article that seems to be saying that it should not be null but this must have been an earlier version of MVC. This is MVC3. What should I be getting?
Model availability inside ActionFilter
UPDATE:
I've figured out the answer to the original question. I had a custom ActionResult that outputs JSON with a custom date formatter. The problem was that the model is not being set in the controller.
In my custom ActionResult the ExecuteResult method get passed the ControllerContext which would be nice if I could set the Model there:
context.Controller.ViewData.Model = _data;
But this is to late in the cycle and the result is still null in the ActionFilter. This seems to mean that I need to manually set the model in the controller:
ControllerContext.Controller.ViewData.Model = model;
Or
View(model);
Which then means I need to remember to do this every time I use this custom ActionResult. Is there a more elegant way?
YET ANOTHER UPDATE:
I found a way to do this it just isn't as elegant as I hoped.
In my constructor for the comstom ActionResult I sending in the controller, that way at least it will alway be consistent:
public JsonNetResult(object data, Controller controller) {
SerializerSettings = new JsonSerializerSettings();
_data = data;
controller.ControllerContext.Controller.ViewData.Model = _data;
}
Another approach is to use a base controller to automatically handle the storing of the action parameters collection for later use:
public class BaseController : Controller
{
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
filterContext.HttpContext.Items["ActionParms"] = filterContext.ActionParameters.ToDictionary(p => p.Key, p => p.Value);
base.OnActionExecuting(filterContext);
}
}
then in your attribute:
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
var dictionary = filterContext.HttpContext.Items["ActionParms"] as Dictionary<string, object>;
if (dictionary != null)
{
foreach (var o in dictionary.Keys)
{
// do something here
}
}
base.OnActionExecuted(filterContext);
}
It uses HttpContext items which is not very nice but I don't know that you can access your ViewBag or ViewData in the attribute.
In order to decide whether you want to handle the request in your attribute, you can interrogate the action name and other parameter information:
var action = filterContext.ActionDescriptor.ActionName;
var parms = filterContext.ActionDescriptor.GetParameters();
foreach (var parameterDescriptor in parms)
{
// do something here
}
I found a solution like yours using the OnModelUpdated event to set that property before.
I have the ModelBinder:
public class CustomModelBinder: DefaultModelBinder
{
protected override void OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
controllerContext.Controller.ViewData.Model = bindingContext.Model;
base.OnModelUpdated(controllerContext, bindingContext);
}
}
After that, you need to set the default binder to your new model binder in Application_Start() section in Global.asax:
ModelBinders.Binders.DefaultBinder = new CustomModelBinder();
Finally you can access your Model in an ActionFilter:
public class TraceLog : ActionFilterAttribute
{
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
//filterContext.Controller.ViewData.Model now isn't null
base.OnActionExecuted(filterContext);
}
}

Resources