I've an ASP.NET MVC blog, in order to show the posts and comments dates in client timezone a cookie is used, the cookie contains the client timezone offset. When the server receives a request it will read the offset value from cookie and changes all the dates accordingly before sending to browser. My question how I can store the cookie in a global variable on every request so that it can be accessed by any where for date adjustment.
Generally, the more controller and action depend on values supplied from outside, the more unit testable and robust they become. I would do it this way
First, create model that holds settings for timezone
public class ClientTimeZoneSettings
{
public string TimeZoneName {get; set;} // or whatever
}
Then, create model binder. That Model binder will be used to extract values from cookie
public class ClientTimeZoneSettingsModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (controllerContext.RequestContext.HttpContext.Request.Cookies.AllKeys.Contains("timeZoneName"))
{
bindingContext.Model = new ClientTimeZoneSettings {TimeZoneName = controllerContext.RequestContext.HttpContext.Request.Cookies["timeZoneName"]; }
}
}
}
Register that model binder in Global.asax
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
ModelBinders.Binders.Add(typeof(ClientTimeZoneSettings), new ClientTimeZoneSettingsModelBinder());
}
And the main point. In all your actions that require those settings, you can directly use ClientTimeZoneSettings as a parameter
public ActionResult ShowComments(ClientTimeZoneSettings settings)
{
// use settings
}
UPDATE: Significantly simpler approach:
Install MvcFutures from nuget. It contains CookieValueProviderFactory that will automatically inspect cookies for values when model binding. To use it, simply add into ValueProviderFactories
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
ValueProviderFactories.Factories.Add(new CookieValueProviderFactory());
}
And then name your parameter accorting to cookie name
public ActionResult ShowComments(string timeZoneName)
{
// timeZoneName will contain your cookie value
return View();
}
You can use a session variable if you don't want to use the cookie every time
session["MyVarName"] = mycookievalue
then you can access the session every time needed.
You can also think of implementing e custom modelbinder so you can bind your session's value to a model. (for example a class UserSettingsModel)
Related
I'm working on a web application that contains a sequence of views which will allow the user to submit different pieces of data which will have to be submitted to a 3rd party web service on completion.
e.g
[Select Account Type] --> [Submit User Info] --> [Submit Billing
Details]
Each view is generated under a different Controller.
What is the best way to store the data between different views? As MVC is stateless, I can't just store it all in a global object until I'm ready to submit to the web service, and I think a database is overkill because I don't want to store the data any longer than it takes to submit all of it.
What is the best way to store the data until the end of the session, without e.g. storing it in the Session object?
The best way is to store the user data is in session. as session is a data dictionary held by server across application against per user session. so any activity done by user including user details must be store in session, this will allow you to access the user object any where in the application across different views controller actions.
MVC gives you advantage to use Model Binding technique, which allows you access session variable in different controller's action across application.
for example on login .....
Store user in session
create model binder
register in global.asax
access it anywhere in across the application like simple parameter.
enjoy
*********************Login Controller********************
public ActionResult Login(LoginModel model, string returnUrl)
{
//store use object in session once
Session["UserProfile"] = user;
}
*******************Create Model Binder**************************
public class UserModelBinder:IModelBinder
{
private const string sessionKey = "UserProfile";
public object BindModel(ControllerContext controllerContext,
ModelBindingContext bindingContext) {
// get the Cart from the session
EntityUserProfile User = (EntityUserProfile)controllerContext.HttpContext.Session[sessionKey];
// create the Cart if there wasn't one in the session data
if (User == null){
User = new EntityUserProfile();
controllerContext.HttpContext.Session[sessionKey] = User;
}
// return the cart
return User;
}
}
*******************Register Model binder in global.asax***************
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
WebApiConfig.Register(GlobalConfiguration.Configuration);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
ControllerBuilder.Current.SetControllerFactory(new NinjectControllerFactory());
ModelBinders.Binders.Add(typeof(EntityUserProfile), new UserModelBinder());
}
********************Access model binder any where in application **************
//following is 2nd paramter using model binder in one of my controller action
[HttpPost]
public JsonResult EmailWorkOrderInvoiceToClient(string OrderNumber, EntityUserProfile user)
{
// following is the user object from the session using model binder..
var myuser=user;
}
Keeping true to MVC
I would recommend having these linked views in the same controller. You could then easily persist your earlier model data in your later models.
For example, your SubmitBillingDetailsModel model could have a SubmitUserInfoModel property etc.
E.g:
public ActionResult SubmitUserInfo (SubmitUserInfoModel model)
{
return View ("SubmitBillingDetails", new SubmitBillingDetailsModel
{
SubmitUserInfoModel = model
});
}
You could even use one model to represent all separate views and cumulate the posted values between the separate views.
E.g:
public ActionResult SelectAccountType (CompleteModel model)
{
return View ("SubmitBillingDetails", model);
}
Just make sure that you persist the all values in your later views using hidden fields (maybe make a partial to do this for the whole model).
Use JavaScript localStorage proivided it is key/value pairs in strings
In MVC an OutputCacheAttribute is capable to prevent the execution of the action it is decorating (if the relative cache exists)
How to implement the same mechanism with a custom attribute?
In other works I would like to be able to decorate an action, and based on a logic internal to the attribute it will decide whether the action should carry on executing or not.
Additions
I've implemented a mechanism by which if a request to an action arrives with a querystring like flushaction=flush_silent, the custom attribute (which is extending the OutputCacheAttribute) invalidates the cache.
What I would also like to do, is not to execute the Action:
[JHOutputCache(CacheProfile = "ContentPageController.Index")]
public ActionResult Index(string url)
{
//custom code that should not execute when flushing the cache
}
As JHOutputCache extends OutputCacheAttribute, which derives from ActionFilterAttribute, halting execution of the underlying action is quite simple:
public class JHOutputCacheAttribute : OutputCacheAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (condition)
filterContext.Result = new EmptyResult();
else
base.OnActionExecuting(filterContext);
}
}
You can return any valid ActionResult here, including any custom ActionResult you may have derived.
This is what I'm trying to achieve:
I want to extend the HandleErrorAttribute so I can redirect to an appropriate error page. I had a look at the implementation of HandleErrorAttribute and noticed that essentially it just returns a ViewResult, that points to a view, that can be defined per Exception type.
I'd like to use the same system as HandleErrorAttribute, except:
I'd like to define a Controller for Error pages
I want to maintain the Exception as model (HandleErrorInfo)
I could obviously use a RedirectToRouteResult, but I can't pass through the Exception as model, since it's an actual Response.Redirect. And I'd like to avoid having to cache the model server side.
For now I'm just overwriting the ViewResult and manually setting controller. But that still just returns the view and doesn't actually execute the controller.
public class ErrorViewResult : ViewResult
{
public ControllerBase Controller { get; set; }
public string ControllerName { get; set; }
protected override ViewEngineResult FindView(ControllerContext context)
{
context.Controller = Controller;
context.RouteData.Values["controller"] = ControllerName;
return base.FindView(context);
}
}
I have to somehow return a result, that restarts the whole pipeline starting with the Controller.
Any ideas?
Thanks!
Be careful with TempData functionality, it will store your values only till the next request, and if in between these requests you will do any others or if you use mvc to handle client resources (like dynamically combined css and js files) then you will loose your data.
If you want to start controller manually (with all nested actions) then look at that:
RouteData data = new RouteData();
data.Values.Add("controller", "error");
data.Values.Add("action", "Handle500");
data.Values.Add("area", "");
data.Values.Add("exception", sb.ToString());
var controller = new MTool.BusinessLogic.Controllers.ErrorController();
controller.ControllerContext = new ControllerContext([HttpContextInstance], data, controller);
controller.ActionInvoker.InvokeAction(controller.ControllerContext, "Handle500");
Why not use TempData?
TempData allows you to store data that can be read on the very next request. If they refresh the page after being redirected it will be gone.
TempData["exception"] = exception;
return RedirectToAction("Index");
Now in the get you have access to TempData["exception"] for the first GET only, then it is gone. Sounds like what you need.
I need to retrieve the data from cookie in every request in ASP.NET MVC and store it in a global variable so that it'll be available throughout the application.
I've two questions here is there any event-handler in ASP.NET MVC where I can get the data from cookie in every request and what kind of global variable I can use to store this cookie value so it is available in all places?
You can use a filter to get the cookie in every request. Create for example a class MyFilter
public class MyFilter : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
HttpCookie cookie = filterContext.HttpContext.Request.Cookies["myCookie"];
//do something with cookie.Value
if (cookie!=null) filterContext.HttpContext.Session["myCookieValue"] = cookie.Value;
// or put it in a static class dictionary ...
base.OnActionExecuting(filterContext);
}
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
base.OnActionExecuted(filterContext);
}
}
Mark every controller class with [MyFilter] attribute.
This example takes the cookie and puts the value in the session so it's available in all the views and all the controllers. Else you can put the cookie value in a static class that contains a static Dictionary and use the session ID as key. There are many way to store the cookie value and access it in every part of the application.
You can use Attributes on each request or make a custom Controller and handle "OnActionExecuting" (override)
You could go old school and handle the onrequest event in the asax file. That way you could abstract the code out to an httpmodule if you need to reuse the approach in another app. The filters approach is probably better though.
Apparently it is possible to dynamically attach DataAnnotation attributes to object properties at runtime and as such achieve dynamic validation.
Can someone provide code sample on this?
MVC has a hook to provide your own ModelValidatorProvider. By default MVC 2 uses a sub class of ModelValidatorProvider called DataAnnotationsModelValidatorProvider that is able to use System.DataAnnotations.ComponentModel.ValidationAttribute attributes for validation.
The DataAnnotationsModelValidatorProvider uses reflection to find all the ValidationAttributes and simply loops through the collection to validate your models. All you need to do is override a method called GetValidators and inject your own attributes from whichever source you choose. I use this technique to do convention validations, the properties with DataType.Email attribute always gets passed through a regex, and use this technique to pull information from the database to apply more restrictive validations for "non-power" users.
The following example simply says "always make any FirstName properties required":
public class CustomMetadataValidationProvider : DataAnnotationsModelValidatorProvider
{
protected override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context, IEnumerable<Attribute> attributes)
{
//go to db if you want
//var repository = ((MyBaseController) context.Controller).RepositorySomething;
//find user if you need it
var user = context.HttpContext.User;
if (!string.IsNullOrWhiteSpace(metadata.PropertyName) && metadata.PropertyName == "FirstName")
attributes = new List<Attribute>() {new RequiredAttribute()};
return base.GetValidators(metadata, context, attributes);
}
}
All you have to do is register the provider in your Global.asax.cs file:
protected void Application_Start()
{
ModelValidatorProviders.Providers.Add(new CustomMetadataValidationProvider());
AreaRegistration.RegisterAllAreas();
RegisterRoutes(RouteTable.Routes);
}
The end result:
with this model:
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime Birthday { get; set; }
}
In your global.asax you have to clear the ModelValidatorProviders before adding the new one. Otherwise it will add every annotation two times which will give you a "Validation type names in unobtrusive client validation rules must be unique."-error.
protected void Application_Start()
{
ModelValidatorProviders.Providers.Clear();
ModelValidatorProviders.Providers.Add(new CustomMetadataValidationProvider());
AreaRegistration.RegisterAllAreas();
RegisterRoutes(RouteTable.Routes);
}
The approach of using a custom MetadataValidationProvider with an overridden GetValidators has a few weaknesses:
Some attributes such as DisplayAttribute aren't related to validation, so adding them at the validation stage doesn't work.
It may not be future-proof; a framework update could cause it to stop working.
If you want your dynamically-applied data annotations to work consistently, you can subclass DataAnnotationsModelMetadataProvider and DataAnnotationsModelValidatorProvider. After doing this, replace the framework's ones via ModelMetadataProviders.Current and ModelValidatorProviders.Providers at application start-up. (You could do it in Application_Start.)
When you subclass the built-in providers, a systematic and hopefully future-proof way to apply your own attributes is to override GetTypeDescriptor. I've done this successfully, but it involved creating an implementation of ICustomTypeDescriptor and PropertyDescriptor, which required a lot of code and time.
I don't think you can add attributes to members at runtime, but you could probably use a custom metadata provider to handle this for you.
You should check out this blog post.