Let me give you some background on my scenario. I got a multi language site and my cultures are stored in database and got a property that is public bool Active { get; set; }
If you come to my site with lets say a Russian culture on your browser, that is not supported on my site so i need to set the culture to "se" (or whatever).
If you come to my site with a supported culture but its not Active i need to set it to a default one "se" (or whatever).
Now I can do this check easy in my override IHttpHandler GetHttpHandler method. The thing is I dont want to make the call to the database everytime the site makes any call on the site. My thought is that I do a session["firstVisit"] to reduce that check but I kinda don't know how i should go about to that, because my scenario it say that
not set to an instance of an object on the session["firstVisit"] line, so my question is how do I handle this? And what other options I got to go about this?
my thought is something like this
protected override IHttpHandler GetHttpHandler(System.Web.Routing.RequestContext requestContext)
{
String culture = requestContext.RouteData.Values["culture"].ToString();
if (HttpContext.Current.Session["firstVisit"].ToString() == string.Empty)
{
//do the check
}
var ci = new CultureInfo(culture);
Thread.CurrentThread.CurrentUICulture = ci;
Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture(ci.Name);
return base.GetHttpHandler(requestContext);
}
EDIT Doh why did I not think of that, I ended up just doing a check in the protected void Application_Start() to avoid sessions
Use global.asax.cs:
protected void Session_Start( object sender, EventArgs e )
This method is called when the session is first created. You can do the check then, stuff your language info in the session and have it for as long as the session is live through the user's interaction with your site.
Related
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’m working out the concepts for a new project where I need to support for multilingual URL’s. Ideally all URL’s need to be in the native language of the user. So we don’t want to use domain.com/en/contact and domain.com/es/contact but we like domain.com/contact and domain.com/contactar (contactar is Spanish for contact). Internally both should be routed to the same ContactController class.
This could be handled by adding multiple static routes to Global.asax.cs for each language but we’d like to make this very dynamic and would like the user of the system to be able to change the translation of the URL’s through the content management system. So we need some kind of dynamic mapping from URL’s to controllers and actions.
By looking at the source code of MVC3 I figured out that the ProcessRequestInit method of MvcHandler is responsible for determining which controller to create. It simply looks in the RouteData to get the name of the controller. One way to override the default MVC routing would be to create a simple default route that uses a custom RouteHandler. This RouteHandler forces MVC to use my own custom subclassed version of MvcHandler that overrides the ProcessRequestInit method. This overridden method insert my own dynamically found controller and action into the RouteData before calling back to the original ProcessRequestInit.
I’ve tried this:
Global.asax.cs
routes.Add(
new Route("{*url}", new MultilingualRouteHandler())
{
Defaults = new RouteValueDictionary(new { controller = "Default", action = "Default" })
}
);
MultilingualRouteHandler.cs
public class MultilingualRouteHandler : IRouteHandler
{
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
return new MultilingualMVCHandler(requestContext);
}
}
MultilingualMvcHandler.cs
public class MultilingualMVCHandler : MvcHandler
{
public MultilingualMVCHandler(RequestContext context) : base(context)
{
}
protected override void ProcessRequestInit(HttpContextBase httpContext, out IController controller, out IControllerFactory factory)
{
if (RequestContext.RouteData.Values.ContainsKey("controller"))
{
RequestContext.RouteData.Values.Remove("controller");
}
if (RequestContext.RouteData.Values.ContainsKey("action"))
{
RequestContext.RouteData.Values.Remove("action");
}
RequestContext.RouteData.Values.Add("controller", "Product");
RequestContext.RouteData.Values.Add("action", "Index");
base.ProcessRequestInit(httpContext, out controller, out factory);
}
}
In this handler I hardcoded the controller and action for testing purposes to some fixed values but it’s not difficult to make this dynamic. It works but the only problem is that I had to modify the source code of ASP.NET MVC3 to get it working. The problem is that the ProcessRequestInit method of MvcHandler is private and thus cannot be overridden. I’ve modified the source code and changed it to protected virtual which allows me to override it.
This is all great but possibly not the best solution. It’s cumbersome that I would always need to distribute my own version of System.Web.Mvc.dll. It would be much better that it would work with the RTM version.
Am I missing any other possibilities of hooking into ASP.NET MVC that would allow me to dynamically determine the controller and action to launch, depending on the URL? One other way I thought of is to build the RouteCollection dynamically on *Application_Start* but I think that will make it more difficult to change it on the fly.
I would appreciate any tips of hooks that I’ve not yet found.
This is fairly old now, nut just in case anyone else is looking for something similar...
Unless I'm completely misunderstanding what you want to do, it's pretty simple really.
Step 1: Add a new route to global.ascx.cs containing a reference to your personal routing engine
routes.Add(new MyProject.Routing.ContentRoutingEngine());
Make sure that it is in the right place in the list of routes so that other routing engines can catch stuff before it if required, or continue the route search if your engine doesn't handle a particular route. I put it after the ignores, but before the MVC default routes.
Step 2: Create the Content Routing Engine, making sure that it inherites from System.Web.Routing.RouteBase abstract class, and overrides the GetRouteData and GetVirtualPath methods as required e.g.
public class ContentRoutingEngine : RouteBase
{
public override RouteData GetRouteData(HttpContextBase httpContext)
{
var routeHandler = new MvcRouteHandler();
var currentRoute = new Route("{controller}/{action}", routeHandler);
var routeData = new RouteData(currentRoute, routeHandler);
// set your values dynamically here
routeData.Values["controller"] = "Home" ;
// or
routeData.Values.Add("action", "Index");
// return the route, or null to have it passed to the next routing engine in the list
return routeData;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
//implement this to return url's for routes, or null to just pass it on
return null;
}
}
and that should do it. You can change routes as dynamically as you wish within your engine, and no changes to MVC source required. Let the standard MVC RouteHandler actually invoke the controller.
Postscript: Obviously the code above is not production standard - it's written to make it as obvious as possible what's going on.
If you are allowing modification of urls through your CMS, then you will have to keep all old versions of the urls so that you can 301 redirect to the new ones.
The best bet for this will be to put the url tokens eg "contactar" in the db along with its corresponding controller.
query that, and create your routes out of that.
create a route that will handle the 301s
I think that most elegant solution would be using some action filter combined with custom ActionInvoker. That way, you could invoke an action that has specific filters applied. Something like ActionName attribute, only capable to accept multiple values (names).
Edit: Take a look at ActionMethodSelectorAttribute, meybe you don't need a custom ActionInvoker after all.
I'm working with a client that wants the URLs in our web application to be in French. I'm an English developer and we also have English clients. This is an interesting problem but I don't think its something the ASP.NET MVC Framework would support.
Here's the scenario. The route...
Specific EXAMPLE
English URL
www.stackoverflow.com/questions/ask
would also support
French URL
www.stackoverflow.com/problème/poser
Generic EXAMPLE
English URL
http://clientA.product.com/AreaNameEnglish/ControllerNameEnglish/ActionNameEnglish/params
also needs to support
French URL
http://clientB.product.com/AreaNameFrench/ControllerNameFrench/ActionNameFrench/params
So in MVC my Area, Controller and Actions all need to have both English and French translations.
Obviously maintainability would be a HUGE issue if I were to go and hardcode all my Controllers, Views and Action names to French. Is there anyway to localize the route that is presented in the browser without doing this? Keeping in mind there are lots of different routes in the application. A couple Areas each with a handful of Controller each with many Actions?
Thanks,
Justin
EDIT
Thanks to #womp here is what I've come up with so far... Although in the end I took the approach which I posted as an answer.
public class LocalizedControllerFactory : DefaultControllerFactory
{
public override IController CreateController(RequestContext requestContext, string controllerName)
{
if (string.IsNullOrEmpty(controllerName))
throw new ArgumentNullException("controllerName");
if (CultureInfo.CurrentCulture.TwoLetterISOLanguageName == "fr")
{
controllerName = this.ReplaceControllerName(requestContext, controllerName);
this.ReplaceActionName(requestContext);
this.ReplaceAreaName(requestContext);
}
return base.CreateController(requestContext, controllerName);
}
private string ReplaceControllerName(RequestContext requestContext, string controllerName)
{
// would use the language above to pick the propery controllerMapper. For now just have french
Dictionary<string, string> controllerMapper = new Dictionary<string, string>()
{
{"frenchControllerA", "englishControllerA"},
{"frenchControllerB", "englishControllerB"}
};
return this.ReplaceRouteValue(requestContext, "controller", controllerMapper);
}
private void ReplaceAreaName(RequestContext requestContext)
{
// would use the language above to pick the propery areaMapper. For now just have french
Dictionary<string, string> areaMapper = new Dictionary<string, string>()
{
{"frenchAreaX", "englishAreaX"},
{"frenchAreaY", "englishAreaY"}
};
this.ReplaceRouteValue(requestContext, "area", areaMapper);
}
private void ReplaceActionName(RequestContext requestContext)
{
// would use the language above to pick the propery actionMapper. For now just have french
Dictionary<string, string> actionMapper = new Dictionary<string, string>()
{
{"frenchAction1", "englishAction1"},
{"frenchAction2", "englishAction2"}
};
this.ReplaceRouteValue(requestContext, "action", actionMapper);
}
private string ReplaceRouteValue(RequestContext requestContext, string paramName, Dictionary<string, string> translationLookup)
{
if (requestContext.RouteData.Values[paramName] == null)
{
return null;
}
string srcRouteValue = requestContext.RouteData.Values[paramName] as string;
if (srcRouteValue != null && translationLookup.ContainsKey(srcRouteValue))
{
requestContext.RouteData.Values[paramName] = translationLookup[srcRouteValue];
}
return requestContext.RouteData.Values[paramName] as string;
}
}
A decent start. If I localize just the ControllerName and ActionName in the Url it will find and render the proper View. However I have the following problems.
Area Name can't be translated
Localizing the Area means the Controller.View() method fails to find Views.
Even though I've replaced the Area name in the request context the ViewEngineCollection.Find() method doesn't seem to pick it up. Anywhere in my Controller class that does "return View()" fails to find the default view for its action. If I don't localize the Area then the other steps work.
RedirectToAction or Html.ActionLink
Anytime the application calls RedirectToAction or if I use an Html.ActionLink helper or something similiar the Urls generate are the English ones. It looks like I'm going to have to add logic somewhere possibly in multiple spots to convert an English Url to the French (or other language) one.
The following blog contains a complete solution this exact problem. Its actually a very elegant solution which I highly recommend.
https://blog.maartenballiauw.be/post/2010/01/26/translating-routes-(aspnet-mvc-and-webforms).html
Note to get it working for AREAs I had to add the following extension method to his "TranslatedRouteCollectionExtensions.cs" class:
public static Route MapTranslatedRoute(this AreaRegistrationContext areaContext, string name, string url, object defaults, object routeValueTranslationProviders, bool setDetectedCulture)
{
TranslatedRoute route = new TranslatedRoute(
url,
new RouteValueDictionary(defaults),
new RouteValueDictionary(routeValueTranslationProviders),
setDetectedCulture,
new MvcRouteHandler());
route.DataTokens["area"] = areaContext.AreaName;
// disabling the namespace lookup fallback mechanism keeps this areas from accidentally picking up
// controllers belonging to other areas
bool useNamespaceFallback = (areaContext.Namespaces == null || areaContext.Namespaces.Count == 0);
route.DataTokens["UseNamespaceFallback"] = useNamespaceFallback;
areaContext.Routes.Add(route);
return route;
}
However, even with this a translated route with an AREA can be read and interpreted the routes generated always seem to include an English AREA name but localized everything else.
I was directed to a blog via the same question asked on the ASP.NET MVC Forums
The MVC framework supports pretty much any routing scenario you can think of, but not necessarily with the default routing classes.
Most localization solutions I've run across involve using the same Controller and Action method names, but specifying a culture parameter in the route which dictates which translated version of the View is presented. For example,
http://clientA.product.com/AreaName/Controller/Action //en-US
http://clientB.product.com/es-MX/AreaName/Controller/Action // spanish
If you really must have translated URL's though, I don't see much other choice then to maintain a mapping table somewhere. If I understand your question correctly, you need to be able to map all the different language translations of "questions" (controller) and "ask" (action) to the same controller/action method combination.
However, once you've built this table somewhere (resource files?), you can easily override the DefaultControllerFactory that the framework is using, and implement your own logic for determining the controller to instantiate. So instead of just matching the {controller} token from the URL as a simple string comparison, you can implement logic to check it against your mapping table to pick the right controller.
For a walkthrough of creating a custom controller factory, check this great blog post. It's actually a localization example as well, but it's based on the user's culture settings, rather than the language of the URL.
Consider an ASP.NET MVC application that requires a session variable be set. It's used throughout the app. It'll be set by either reading a hashed value on the browser cookie, or after having the user login.
In the WebForms + Master Page model, I'd check the Page_Load() of the master page. Perhaps not the ultimate event, but it was an easy one to find.
How would you check and enforce the existence of a session variable in ASP.NET MVC? Consider that this question might not involve user login details, but some other piece of data (first visit time, perhaps).
Solution Attempts
public void Application_BeginRequest(Object source, EventArgs e)
{
HttpApplication application = (HttpApplication)source;
HttpContext context = application.Context;
context.Session["SomeDateTime"] = DateTime.Now.ToString();
// results in Object reference not set to an instance of an object.
// context.Session is null
}
You have two options.
1.Place logic in base controller's Initialize function
Assuming that all your controllers inherit from a base controller, you can place the logic needed in the override of the Execute() function of the base controller.
public class BaseController : Controller
{
public BaseController()
{
}
protected override void Initialize(System.Web.Routing.RequestContext requestContext)
{
// check if the user has the value here using the requestContext.HttpContext object
}
{
2. Use the Global.asax void Application_PreRequestHandlerExecute(Object source, EventArgs e) function
public void Application_PreRequestHandlerExecute(Object source, EventArgs e)
{
HttpApplication application = (HttpApplication)source;
HttpContext context = application.Context;
// use an if statement to make sure the request is not for a static file (js/css/html etc.)
if(context != null && context.Session != null)
{
// use context to work the session
}
}
Note: The second part works with any ASP.NET application, WebForms or MVC.
As for enforcing that they have a certain session variable, its very open really. You can redirect to a certain page for them to fill out a form or select an option or something. Or maybe just have a default value that is set to a certain session key if it is not found.
EDIT
While playing with this, I noticed a big issue with Application_PreRequestHandlerExecute approach. The event handler is being called for any request done to the server, be it .css/.js/.html files. I'm not sure if this is an issue with the way my workstation is setup, or just how ASP.NET/IIS works, so I would make sure that this isn't being called on all requests when implementing the approach above.
It is for the previous reasons I wrapped the work to be done in the session with an if statement.
Not sure I fully understand the question, but I do this by override the OnActionExecuting method of the controller.
In there you do the code to see if the Session Variable exists. If not, create it, if so then use it.
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (filterContext.HttpContext.Session != null)
{
//TODO: Get value from session etc.
}
base.OnActionExecuting(filterContext);
}
As another alternative, the ControllerActionInvoker class invokes every action method; it gets assigned to the controller via the controller factory. So you could subclass this action invoker, everytime an action is invoked (by overridding the InvokeAction method) check here for this existence...
If I want the default url of my web app to display completely different UIs depending on the user, what is the best way to accomplish this? I don't really want to use the same controller for every type of user. To put it another way, if a user is logged in and goes to http://mysweetapp.com and is an admin user, they should get what they would see the same thing as if they had gone to http://mysweetapp.com/admin. If the user is logged in as a normal user, they should see the same thing as if they had gone to http://mysweetapp.com/normaluser
Should I just make a "redirect" controller as my default and have it send the client to the appropriate controller?
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Redirect", action = "Index", id = "0" });
I've also tried creating my own ControllerFactory, but I don't think I was clear on the concept and couldn't get it to work.
Thanks
The cleanest way in my opinion would be to create a custom route handler to be used by your default route. Then you can separate out which controller to be used if the controller name is your default controller name, in the example below, it is: Home. Then check if the user is an administrator or not and process the request with the controller you would like to use.
Here is the code:
public class CustomHttpHandler : IHttpHandler
{
public RequestContext RequestContext { get; private set; }
public CustomHttpHandler(RequestContext requestContext)
{
try
{
string controllerName = RequestContext.RouteData.GetRequiredString("controller");
if (controllerName.Equals("home", StringComparison.CurrentCultureIgnoreCase))
{
bool isAdmin = RequestContext.HttpContext.User.IsInRole("Admin");
controllerName = isAdmin ? "admin" : "normaluser";
}
IControllerFactory factory = ControllerBuilder.Current.GetControllerFactory();
IController controller = factory.CreateController(RequestContext, controllerName);
if (controller != null)
{
controller.Execute(RequestContext);
}
}
finally
{
factory.ReleaseController(controller);
}
}
}
public class CustomRouteHandler : IRouteHandler
{
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
return new CustomHttpHandler(requestContext);
}
}
// Now use the CustomRouteHandler when you map your default route.
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = "" }
).RouteHandler = new CustomRouteHandler();
Hope this helps.
For simplicity, in your HomeController Index method (or whatever default controller you are using) you could put some code like this and then the links from the AdminIndex view or the Index view can send the users to appropriate areas when they start navigating round your site - that way you have one shared controller and the other controllers can be specific to the user type.
return user.IsAdministrator ? View("AdminIndex") : View("Index");
the user.IsAdministrator call is pseudocode of course - replace this with whatever method you are using to work out if the user is an admin user
If you don't want to use the same controller set up individual controllers and views for each item first - mysweetapp.com/admin and mysweetapp.com/normaluser.
You can then redirect specific users to this page through a default controller based on their logged in role.
if (User.IsInRole("Admin")
{
return RedirectToAction("Index", "admin");
}
else if (User.IsInRole("Standard")
{
return RedirectToAction("Index", "normaluser");
}
What you might want to consider is areas. This would allow you to have separate controllers for each area. Then permit access to those areas based on roles or whatever you wish.
This will give you routes like '/admin/controller/action', '/users/controller/action', etc. The 'pattern' separates all your controllers by namespace, and handles the routing quite well. Separate master pages easily, etc.
It won't give you the (potentially confusing, IMO) '/' and '/admin/' looking the same to an admin user, but it will let you separate the content and controllers.
What you are describing would lead to potentially tons of methods for each controller, something that is generally frowned upon by the MVC/REST crowd. It's not horrible, but its not considered best practice either.
You can read about areas at this blog here. Google 'asp.net mvc areas' for more.
--------edit-----------
To expand a bit:
Without custom routes or some other shenanigans, actions are mapped to controllers by the url. So if you want to keep all admin actions and views different, but on the root url, along with normal user actions, this would lead to one big controller that has to handle all these actions, or some strange "if this role, this view; if that role, that view" sort of nonsense that would have to happen in each action. Kind of a mess to debug potentially.
Similarly, the default view engine finds the views based on the url as well.
This would mean that all of your views are going to sit in one big ugly directory full of all sorts of weird similarly named but differently behaving views.
In short, this would become a potentially horrific maintenance nightmare, depending on complexity of the application.
Could you create a class that extends from DefaultControllerFactory and overrides CreateController?
public class RedirectControllerFactory : DefaultControllerFactory
{
public override IController CreateController(System.Web.Routing.RequestContext requestContext, string controllerName)
{
if (controllerName.Equals("Redirect"))
{
controllerName = requestContext.HttpContext.User.IsInRole("Admin") ? "Admin" : "NormalUser";
}
return base.CreateController(requestContext, controllerName);
}
}
Then in your Application_Start():
protected void Application_Start()
{
// ...
ControllerBuilder.Current.SetControllerFactory(new RedirectControllerFactory());
}