I'm trying to implement localization with routes
I have the following:
routes.MapRoute( "DefaultLocalized",
"{lang}/{controller}/{action}/{id}",
new { controller = "Home",
action = "Index",
id = "",
lang = "en" }
);
routes.MapRoute( "Default",
"{controller}/{action}/{id}",
new { controller = "Home",
action = "Index",
id = "" }
);
When I call my page domain/en/home/index, it works fine but when i call domain/home/index I get error 404: resource cannot be found.
Also when I'm at domain/en/home/index and I click a secured page I get redirected to domain/Account/login how can I be redirected to domain/en/Account/login?
Also when I get an application error how can I be redirected to domain/en/home/error?
The real question is how can I implement localization with language as a route parameter?
The routes will match, by default, left-to-right, so "domain/home/index" will match first to lang=domain, controller=index, action (default to index), id (default to 0/null).
To fix this, I believe you can specify a regex on the MapRoute (matching, for example, languages with exactly 2 characters) - it has changed at some point, though... (sorry, no IDE at the moment, so I can't check exactly).
From memory, it might be:
routes.MapRoute( "DefaultLocalized",
"{lang}/{controller}/{action}/{id}",
new { controller = "Home",
action = "Index",
id = "",},
new { lang = "[a-z]{2}" }
);
Note that you probably aren't going to want every action to take a "string lang", so you should handle the "lang" part of the route either in a base-controller, or an action-filter (in either case, presumably add the info to the ViewData).
I know this is a very old question, but having just had to solve the complete set of related issues, I thought I would share my solution.
Below is a complete solution, including a few extra tricks to allow easy changing of language. It allows for specific cultures, not just specific languages (but only the language part is retained in this example).
Features include:
Fallback to browser locale in determining language
Uses cookies to retain language across visits
Override language with url
Supports changing language via link (e.g. simple menu options)
Step 1: Modify RegisterRoutes in RouteConfig
This new routing includes a constraint (as others also suggest) to ensure the language route does not grab certain standard paths. There is no need for a default language value as that is all handled by the LocalisationAttribute (see step 2).
public static void RegisterRoutes(RouteCollection routes)
{
...
// Special localisation route mapping - expects specific language/culture code as first param
routes.MapRoute(
name: "Localisation",
url: "{lang}/{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
constraints: new { lang = #"[a-z]{2}|[a-z]{2}-[a-zA-Z]{2}" }
);
// Default routing
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
Step 2: Create a Localisation attribute
This will look at controller requests, before they are handled, and change the current culture based on the URL, a cookie, or the default browser culture.
// Based on: http://geekswithblogs.net/shaunxu/archive/2010/05/06/localization-in-asp.net-mvc-ndash-3-days-investigation-1-day.aspx
public class LocalisationAttribute : ActionFilterAttribute
{
public const string LangParam = "lang";
public const string CookieName = "mydomain.CurrentUICulture";
// List of allowed languages in this app (to speed up check)
private const string Cultures = "en-GB en-US de-DE fr-FR es-ES ro-RO ";
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
// Try getting culture from URL first
var culture = (string)filterContext.RouteData.Values[LangParam];
// If not provided, or the culture does not match the list of known cultures, try cookie or browser setting
if (string.IsNullOrEmpty(culture) || !Cultures.Contains(culture))
{
// load the culture info from the cookie
var cookie = filterContext.HttpContext.Request.Cookies[CookieName];
var langHeader = string.Empty;
if (cookie != null)
{
// set the culture by the cookie content
culture = cookie.Value;
}
else
{
// set the culture by the location if not specified - default to English for bots
culture = filterContext.HttpContext.Request.UserLanguages == null ? "en-EN" : filterContext.HttpContext.Request.UserLanguages[0];
}
// set the lang value into route data
filterContext.RouteData.Values[LangParam] = langHeader;
}
// Keep the part up to the "-" as the primary language
var language = culture.Split(new char[] { '-' }, StringSplitOptions.RemoveEmptyEntries)[0];
filterContext.RouteData.Values[LangParam] = language;
// Set the language - ignore specific culture for now
Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(language);
// save the locale into cookie (full locale)
HttpCookie _cookie = new HttpCookie(CookieName, culture);
_cookie.Expires = DateTime.Now.AddYears(1);
filterContext.HttpContext.Response.SetCookie(_cookie);
// Pass on to normal controller processing
base.OnActionExecuting(filterContext);
}
}
Step 3: Apply localisation to all controllers
e.g.
[Localisation] <<< ADD THIS TO ALL CONTROLLERS (OR A BASE CONTROLLER)
public class AccountController : Controller
{
Step 4: To change language (e.g. from a menu)
This is where it got a little tricky and required some workarounds.
Add a ChangeLanguage method to your account controller. This will strip out any existing language code from the "previous path" to allow the new language to take effect.
// Regex to find only the language code part of the URL - language (aa) or locale (aa-AA) syntax
static readonly Regex removeLanguage = new Regex(#"/[a-z]{2}/|/[a-z]{2}-[a-zA-Z]{2}/", RegexOptions.Compiled);
[AllowAnonymous]
public ActionResult ChangeLanguage(string id)
{
if (!string.IsNullOrEmpty(id))
{
// Decode the return URL and remove any language selector from it
id = Server.UrlDecode(id);
id = removeLanguage.Replace(id, #"/");
return Redirect(id);
}
return Redirect(#"/");
}
Step 5: Add language menu links
The menu options consist of a link with the new language specified as a route parameter.
e.g. (Razor example)
<li>#Html.ActionLink("English", "ChangeLanguage", "Account", new { lang = "en", id = HttpUtility.UrlEncode(Request.RawUrl) }, null)</li>
<li>#Html.ActionLink("Spanish", "ChangeLanguage", "Account", new { lang = "es", id = HttpUtility.UrlEncode(Request.RawUrl) }, null)</li>
The return URl is the current page, encoded so that it can become the id parameter of the URL. This means that you need to enable certain escape sequences that are otherwise refused by Razor as a potential security violation.
Note: for non-razor setups you basically want an anchor that has the new language, and the current page relative URL, in a path like:
http://website.com/{language}/account/changelanguage/{existingURL}
where {language} is the new culture code and {existingURL} is a URLencoded version of the current relative page address (so that we will return to the same page, with new language selected).
Step 6: Enable certain "unsafe" characters in URLs
The required encoding of the return URL means that you will need to enable certain escape characters, in the web.config, or the existing URL parameter will cause an error.
In your web.config, find the httpRuntime tag (or add it) in <system.web> and add the following to it (basically remove the % that is in the standard version of this attribute):
requestPathInvalidCharacters="<,>,&,:,\,?"
In your web.config, find the <system.webserver> section and add the following inside it:
<security>
<requestFiltering allowDoubleEscaping="true"/>
</security>
Add a contraint as new {lang = "[a-z]{2}"}.
Additionally, drop the default lang = "en". If you don't, the routing will grab the language rule when you are browsing it without it. So if you are looking at domain and you select About, it would use domain/en/Home/About instead of the more simple domain/Home/About
You could also introduce a constraint even tighter than Marc Gravell and Freddy Rios.
something like "en|de|fr|es". This would mean hardcoding the languages but usually these are few and known.
Related
I am creating an ASP.NET MVC application which needs to be translated to French. I have never implemented localization using resource files before. Currently I have two resource files, one each for English and french. The user currently has the ability to choose the language in the home screen by either clicking the English/French button. How do I load the resource file dynamically at runtime based on the language selected by the user? I don't need to take in to account of the language setting of the browser, the resources should be loaded only based on the language selected in the app. Any ideas on how this could be achieved please? I had a quick look online for solutions and couldn't find anything that loads resource files based on language selection within the app and ignoring the browser preferences.
Eidt
It is enough to name the resource file with the same name but with an extension which makes difference in the language, for example
here is support for three languages (English, which is the file which has no special extension, Arabic with ar.resx extension, and Turkish with tr.resx extension)
You may also to control the properties of the resource file to make the files which are dedicated to another language has No Code Generated property. You can change this property from the top when you open any resource file
Short Answer
you have to change the Culture of the thread like this
Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo(lang);
Long Answer
you need some code which is executed each time a new HTTP request is initiated. I am using the following code in my projects
public class LocalizedControllerActivator : IControllerActivator
{
public const string _DefaultLanguage = "en";
public const string _DefaultLanguageKey = "lang";
public IController Create(RequestContext requestContext, Type controllerType)
{
string lang = GetCurrentLanguageOrDefaultFromRouteData(requestContext);
if (lang != _DefaultLanguage)
{
try
{
Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo(lang);
}
catch (Exception)
{
Thread.CurrentThread.CurrentUICulture = new CultureInfo(_DefaultLanguage);
}
}
return DependencyResolver.Current.GetService(controllerType) as IController;
}
private string GetCurrentLanguageOrDefaultFromRouteData(RequestContext requestContext)
{
// Get the {language} parameter from the RouteData
RouteValueDictionary routeData = requestContext.RouteData.Values;
if (!routeData.ContainsKey(_DefaultLanguageKey))
routeData[_DefaultLanguageKey] = _DefaultLanguage;
return routeData[_DefaultLanguageKey].ToString();
}
}
and you have to consider changing the route to make it accepting the required language
routes.MapRoute(
name: "Default",
url: "{lang}/{controller}/{action}/{id}",
constraints: new { lang = "en|fr" },
defaults: new { lang = "en", controller = "Home", action = "Index", id = UrlParameter.Optional }
);
and last you need to register the LocalizedContollerActivator when the Web Application start
protected void Application_Start()
{
ControllerBuilder.Current.SetControllerFactory(new DefaultControllerFactory(new LocalizedControllerActivator()));
}
the url should be look like this
http://yourDomainOrLocalhost/en/controllerName/actionName
So I'm having a little problem here with routing.
There are two parts to this web application:
1. Brochure / Display Website
2. Internal Site / Client Application
We wanted a way to release changes for the brochure without having to do a whole release of said Web application.
Visiting existing named views will take the user to a brochure page, however if it doesn't exist, it will act like they are a client and will redirect them to their company's login screen.
Global.asax:
//if view doesnt exist then url is a client and should be redirected
routes.MapRoute(
name: "Brochure",
url: "{id}",
defaults: new { controller = "brochure", action = "Brochure", id = "Index" },
namespaces: new[] { "Web.Areas.Brochure.Controllers" }
);
//This is home page
routes.MapRoute(
name: "HomeDefault",
url: "{client}/{action}",
defaults: new { controller = "home", action = "index" },
namespaces: new string[] { "Web.Controllers" }
);
Controller:
/// <summary> Check if the view exists in our brochure list </summary>
private bool ViewExists(string name) {
ViewEngineResult result = ViewEngines.Engines.FindView(ControllerContext, name, null);
return (result.View != null);
}
/// <summary> Generic action result routing for all pages.
/// If the view doesn't exist in the brochure area, then redirect to interal web
/// This way, even when we add new pages to the brochure, there is no need to re-compile & release the whole Web project. </summary>
public ActionResult Brochure(string id) {
if (ViewExists(id)) {
return View(id);
}
return RedirectToRoute("HomeDefault", new { client = id });
}
This code works fine up until we log in and go to the landing page. It seems to keep the Brochure action in the route and doesn't want to go to the subsequent controller which results in a 500 error.
e.g. 'domain/client/Brochure' when it needs to be: 'domain/client/Index'
Things tried but not worked:
Changing RedirectToRoute() to a RedirectToAction() - this results in a
finite loop of going back to the ActionResult Brochure(). So
changing controllers through that didn't work.
Create an ActionResult called Brochure() inside the 'HomeController'. It
doesn't even get hit.
Passed in namespaces for RedirectToRoute() as an attribute. I knew this would probably not work, but it was worth a try.
So the question is:
How can I get the route to act properly?
If you can restrict id to some subset of all values you can add that constraints to route (i.e. numbers only) to let default handle the rest.
routes.MapRoute(
name: "Brochure",
url: "{id}",
defaults: new { controller = "brochure", action = "Brochure", id = "Index" },
namespaces: new[] { "Web.Areas.Brochure.Controllers" }
constraints : new { category = #"\d+"}
);
If you can't statically determine restrictions - automatically redirecting in your BrochureController similar to your current code would work. The only problem with sample in the question is it hits the same route again and goes into infinite redirect loop - redirect to Url that does not match first rule:
// may need to remove defaults from second route
return RedirectToRoute("HomeDefault", new { client = id, action = "index" });
If standard constraints do not work and you must keep single segment in url - use custom constraints - implement IRouteConstraint and use it in first route. See Creating custom constraints.
There are several issues with your configuration. I can explain what is wrong with it, but I am not sure I can set you on the right track because you didn't provide the all of the URLs (at least not all of them from what I can tell).
Issues
Your Brouchure route, which has 1 optional URL segment named {id}, will match any URL that is 0 or 1 segments (such as / and /client). The fact that it matches your home page (and you have another route that is named HomeDefault that will never be given the chance to match the home page) leads me to believe this wasn't intended. You can make the {id} value required by removing the default value id = "Index".
The Brouchure route has a namespace that indicates it is probably in an Area. To properly register the area, you have to make the last line of that route ).DataTokens["area"] = "Brochure"; or alternatively put it into the /Areas/Brouchure/AreaRegistration.cs file, which will do that for you.
The only way to get to the HomeDefault route is to supply a 2 segment URL (such as /client/Index, which will take you to the Index method on the HomeController). The example URLs you have provided have 3 segments. Neither of the routes you have provided will match a URL with 3 segments, so if these URLs are not getting 404 errors they are obviously matching a route that you haven't provided in your question. In other words, you are looking for the problem in the wrong place.
If you provide your entire route configuration including all Area routes and AttributeRouting routes (including the line that registers them), as well as a complete description of what URL should go to what action method, I am sure you will get more helpful answers.
So the question is:
How can I get the route to act properly?
Unknown. Until you describe what properly is.
Related: Why map special routes first before common routes in asp.net mvc?
Two ways I could have solved this issue:
Way 1
I reviewed the redirect and just passed in an action in order to get a route that has 2 segments in the url. i.e. client/Index. The Index action now handles logins - going past a custom controller.
public class HomeController : CustomController
public ActionResult Brochure(string id, string action) {
if (ViewExists(id)) {
return View(id);
}
return RedirectToAction("Index", "Home", new { client = id, action = "Index" });
}
Way 2
(from #Alexei_Levenkov)
Create a custom Route constraint so the route will be ignored if the view cannot be found.
namespace Web.Contraints {
public class BrochureConstraint : IRouteConstraint {
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection) {
//Create our 'fake' controllerContext as we cannot access ControllerContext here
HttpContextWrapper context = new HttpContextWrapper(HttpContext.Current);
RouteData routeData = new RouteData();
routeData.Values.Add("controller", "brochure");
ControllerContext controllerContext = new ControllerContext(new RequestContext(context, routeData), new BrochureController());
//Check if our view exists in the folder -> if so the route is valid - return true
ViewEngineResult result = ViewEngines.Engines.FindView(controllerContext, "~/Areas/Brochure/Views/Brochure/" + values["id"] + ".cshtml", null);
return result.View != null;
}
}
}
namespace Web {
public class MvcApplication : System.Web.HttpApplication {
//If view doesnt exist then url is a client so use the 'HomeDefault' route below
routes.MapRoute(
name: "Brochure",
url: "{id}",
defaults: new { controller = "brochure", action = "Brochure", id = "Index" },
namespaces: new[] { "Web.Areas.Brochure.Controllers" },
constraints: new { isBrochure = new BrochureConstraint() }
);
//This is home page for client
routes.MapRoute(
name: "HomeDefault",
url: "{client}/{action}",
defaults: new { controller = "home", action = "index" },
namespaces: new string[] { "Web.Controllers" }
);
}
}
I hope this helps someone else out there.
I am trying to prepare a 301 redirect for a typo I made 'recieved'
I am struggling to find a way of getting the url from the action and controller names.
I am aware of UrlHelper.Action but it does not exist within Global.asax. How do I gain access to this method?:
// Add permanent redirection for retired pages (Application_BeginRequest())
if (HttpContext.Current.Request.Url.LocalPath.ToLower().StartsWith("/blah/listrecieved"))
{
HttpContext.Current.Response.RedirectPermanent(/*Need url generated from action and controller*/);
}
Alternatively I have created a route, if that's how I should be getting the string, this is also fine but I am unsure of how:
routes.MapRoute(
name: "blah-list-received",
url: "blah/list-received",
defaults: new { controller = "Blah", action = "ListReceived" }
);
for example, it might look like this:
// Add permanent redirection for retired pages
if (HttpContext.Current.Request.Url.LocalPath.ToLower().StartsWith("/blah/listrecieved"))
{
HttpContext.Current.Response.RedirectPermanent(routes.GetUrl( "blah-list-received" ) );
}
You need to construct the UrlHelper yourself:
var url = new UrlHelper(HttpContext.Current.Request.RequestContext, RouteTable.Routes)
.Action("YourAction",
"YourController",
new { paramName = paramValue });
See MSDN
I’m trying to set up some routes for my ASP.NET MVC 5 project.
I defined custom routes to get nice blog post permalinks – those
seem to be working fine
I added a XmlRpc Handler (similar to how it’s done
in Mads' Miniblog and Scott’s post)
Now I have some strange behavior:
/Home/About is routed correctly
/Home/Index gets routed to /XmlRpc?action=Index&controller=Blog
/HOme/Index works (yes I
discovered that due to a typo) – I always thought routes are case
insensitive?
Using Url.Action("Foo","Bar") also creates /XmlRpc?action=Foo&controller=Bar
This is my RouteConfig file:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.Add("XmlRpc", new Route("XmlRpc", new MetaWeblogRouteHandler()));
routes.MapRoute("Post", "Post/{year}/{month}/{day}/{id}", new {controller = "Blog", action = "Post"}, new {year = #"\d{4,4}", month = #"\d{1,2}", day = #"\d{1,2}", id = #"(\w+-?)*"});
routes.MapRoute("Posts on Day", "Post/{year}/{month}/{day}", new {controller = "Blog", action = "PostsOnDay"}, new {year = #"\d{4,4}", month = #"\d{1,2}", day = #"\d{1,2}"});
routes.MapRoute("Posts in Month", "Post/{year}/{month}", new {controller = "Blog", action = "PostsInMonth"}, new {year = #"\d{4,4}", month = #"\d{1,2"});
routes.MapRoute("Posts in Year", "Post/{year}", new {controller = "Blog", action = "PostsInYear"}, new {year = #"\d{4,4}"});
routes.MapRoute("Post List Pages", "Page/{page}", new {controller = "Blog", action = "Index"}, new {page = #"\d{1,6}"});
routes.MapRoute("Posts by Tag", "Tag/{tag}", new {controller = "Blog", action = "PostsByTag"}, new {id = #"(\w+-?)*"});
routes.MapRoute("Posts by Category", "Category/{category}", new {controller = "Blog", action = "PostsByCategory"}, new {id = #"(\w+-?)*"});
routes.MapRoute("Default", "{controller}/{action}/{id}", new {controller = "Blog", action = "Index", id = UrlParameter.Optional});
}
And that’s the definition of MetaWeblogRouteHandler:
public class MetaWeblogRouteHandler : IRouteHandler
{
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
return new MetaWeblog();
}
}
Basically I’d like to have the usual ASP.NET MVC routing behavior (/controller/action) + my defined custom routes for permalinks + XML-RPC handling via the XmlRpc handler only at /XmlRpc.
Since the parameters are the same that are defined in the Default route I tried to remove the route, but without success.
Any ideas?
Update:
When calling /Home/Index the AppRelativeCurrentExecutionFilePath is set to "~/XmlRpc" so the XmlRpc route is legally chosen. Something seems to be messing around with the request?
Update2: The problem fixed itself in every but one case: when starting IE via Visual Studio for Debug it still fails. In every other case it now works (yes I checked browser cache and even tried it on a different machine to be sure; IE started from VS = fail, all other combinations are fine). Anyway, since it will now work for the end user I'm satisfied for the moment ;)
When you execute Url.Action("Foo","Bar"), MVC will create a collection of route values from your inputs (In that case action=Foo, controller=Bar) and it will then look at your routes, trying to match one that matches based on its segments and default values.
Your XmlRpc route has no segments and no default values, and is the first one defined. This means it will always be the first match when generating urls using #Url.Action, #Html.ActionLink etc.
A quick way to prevent that route from being matched when generating urls would be adding a default controller parameter (using a controller name that you are sure you will never use). For example:
routes.Add("XmlRpc", new Route("XmlRpc", new RouteValueDictionary() { { "controller", "XmlRpc" } }, new MetaWeblogRouteHandler()));
Now when you execute Url.Action("Foo","Bar"), you will get the expected /Bar/Foo url, as "Bar" doesn´t match the default controller value in the route definition, "XmlRpc".
However that seems a bit hacky.
A better option would be creating your own RouteBase class. This willonly care for the url /XmlRpc, which will then be served using MetaWeblogRouteHandler and will be ignored when generating links using the Html and Url helpers:
public class XmlRpcRoute : RouteBase
{
public override RouteData GetRouteData(HttpContextBase httpContext)
{
//The route will only be a match when requesting the url ~/XmlRpc, and in that case the MetaWeblogRouteHandler will handle the request
if (httpContext.Request.AppRelativeCurrentExecutionFilePath.Equals("~/XmlRpc", StringComparison.CurrentCultureIgnoreCase))
return new RouteData(this, new MetaWeblogRouteHandler());
//If url is other than /XmlRpc, return null so MVC keeps looking at the other routes
return null;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
//return null, so this route is skipped by MVC when generating outgoing Urls (as in #Url.Action and #Html.ActionLink)
return null;
}
}
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
//Add the route using our custom XmlRpcRoute class
routes.Add("XmlRpc", new XmlRpcRoute());
... your other routes ...
}
However, in the end you are creating a route just to run an IHttpHandler outside the MVC flow, for a single url. You are even struggling to keep that route from interferring with the rest of the MVC components, like when generating urls using helpers.
You could then just add directly a handler for that module in the web.config file, also adding an ignore rule for /XmlRpc in your MVC routes:
<configuration>
...
<system.webServer>
<handlers>
<!-- Make sure to update the namespace "WebApplication1.Blog" to whatever your namespace is-->
<add name="MetaWebLogHandler" verb="POST,GET" type="WebApplication1.Blog.MetaWeblogHandler" path="/XmlRpc" />
</handlers>
</system.webServer>
</configuration>
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
//Make sure MVC ignores /XmlRpc, which will be directly handled by MetaWeblogHandler
routes.IgnoreRoute("XmlRpc");
... your other routes ...
}
Using either of these 3 approaches, this is what I get:
/Home/Index renders the Index view of the HomeController
/ renders the Index view of the BlogController
#Url.Action("Foo","Bar") generates the url /Bar/Foo
#Html.ActionLink("MyLink","Foo","Bar") renders the following html: MyLink
/XmlRcp renders the a view describing the MetaWeblogHandler and its available methods, where there is a single method available (blog.index, taking no parameters and returning a string)
In order for testing this, I have created a new empty MVC 5 application, adding the NuGet package xmlrpcnet-server.
I have created a HomeController and a BlogController, both with an index action, and I have created the following MetaWeblog classes:
public interface IMetaWeblog
{
[XmlRpcMethod("blog.index")]
string Index();
}
public class MetaWeblogHandler : XmlRpcService, IMetaWeblog
{
string IMetaWeblog.Index()
{
return "Hello World";
}
}
public class MetaWeblogRouteHandler : IRouteHandler
{
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
return new MetaWeblogHandler();
}
}
Currently, I have URLs that look like this:
http://www.example.com/user/create
http://www.example.com/user/edit/1
But now, I have to support multiple organizations and their users. I need to have something like this:
http://www.example.com/org-name/user/create
http://www.example.com/org-name/user/edit/1
I was having trouble getting the routes to work just perfectly, so I had to add a token to the beginning of the organization name so that routing wouldn't confuse it with a controller/action pair. Not a huge deal but my URLs look like this now:
http://www.example.com/o/org-name/user/create
http://www.example.com/o/org-name/user/edit/1
That's fine. I can live with that.
Here's where I'm running into trouble:
When I generate URLs once I have an organization selected, it's not persisting the organization name. So when I'm here:
http://www.example.com/o/org-name
...and I use Url.Action("User", "Create") to generate a URL, it outputs:
/user/create
...rather than what I want:
/o/org-name/user/create
This is what my routes look like (in order):
routes.MapRouteLowercase(
"DefaultOrganization",
"{token}/{organization}/{controller}/{action}/{id}",
new { id = UrlParameter.Optional },
new { token = "o" }
);
routes.MapRouteLowercase(
"OrganizationDashboard",
"{token}/{organization}/{controller}",
new { controller = "Organization", action = "Dashboard" },
new { token = "o" }
);
routes.MapRouteLowercase(
"DefaultSansOrganization",
"{controller}/{action}/{id}",
new { controller = "Core", action="Dashboard", id = UrlParameter.Optional }
);
It's similar to this question ASP.NET MVC Custom Routing Long Custom Route not Clicking in my Head.
I have a feeling this is going to end up being obvious but it's Friday and it's not happening right now.
EDIT:
Womp's suggested worked but would this be the best way to automate this?
public static string ActionPrepend(this UrlHelper helper, string actionName, string controllerName)
{
string currentUrl = helper.RequestContext.RouteData.Values["url"] as string;
string actionUrl = string.Empty;
if (currentUrl != null)
{
Uri url = new Uri(currentUrl);
if (url.Segments.Length > 2 && url.Segments[1] == "o/")
actionUrl = string.Format("{0}{1}{2}{3}", url.Segments[0], url.Segments[1], url.Segments[2],
helper.Action(actionName, controllerName));
}
if(string.IsNullOrEmpty(actionUrl))
actionUrl = helper.Action(actionName, controllerName);
return actionUrl;
}
EDIT:
Fixed my routes to work rather than hacking it together. The final solution didn't need the stupid {token} in the URL. Maybe this'll help someone else:
routes.MapRouteLowercase(
"Organization",
"{organization}/{controller}/{action}/{id}",
new { controller = "Organization", action = "Dashboard", id = UrlParameter.Optional },
new { organization = #"^(?!User|Account|Report).*$" }
);
routes.MapRouteLowercase(
"Default",
"{controller}/{action}/{id}",
new { controller = "Core", action = "Dashboard", id = UrlParameter.Optional }
);
Url.Action uses route values to generate the actual URL's by querying the virtual path provider and attempting to match the most specific route. In the form that you are using, you are supplying values for the controller and the action, which is as deep as most simple websites go, hence the convenient form of the method. When Url.Action queries the routing system, it only has a "controller" and an "action" segment to match.
If you give the method the rest of the routing information it needs, it will properly match the route that you desire, and will return the correct URL. Try this:
Url.Action("User", "Create", new { token = "o", organization = "organization" })