How to change routes globally? - asp.net

I want to switch my views in MVC 3 between two languages - PL and EN. I've created two folders in Views- EN and PL. So after clicking appropriate language link at any site I want my route change from:
routes.MapRoute(
"pl", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "PL", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
to:
routes.MapRoute(
"en", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "EN", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
When I click appropriate link (language switcher) it changes CultureInfo which is persistent to all threads.
_Layout View with switcher:
<ul>
<li>#Html.ActionLink("En", "ChangeCulture", null, new { lang = "en"}, null)</li>
<li>#Html.ActionLink("Pl", "ChangeCulture", null, new { lang = "pl"}, null)</li>
</ul>
and controller (which sets also static variable lang that can be seen in every controller's method and be persistent between requests):
public ActionResult ChangeCulture(string lang)
{
PLController.lang = lang;
CultureSettings setCulture = new CultureSettings();
setCulture.InitializeCulture(lang);
cookie.Value = CultureInfo.CurrentCulture.Name;
this.ControllerContext.HttpContext.Response.Cookies.Add(cookie);
return View("Index");
}
InitializeCulture method is overriden from Page class as follows:
public class CultureSettings : Page{
public void InitializeCulture(string culture)
{
String selectedLanguage;
if(culture == null)
{
selectedLanguage = "pl";
}
else
{
selectedLanguage = culture;
}
UICulture = selectedLanguage;
Culture = selectedLanguage;
Thread.CurrentThread.CurrentCulture =
CultureInfo.CreateSpecificCulture(selectedLanguage);
Thread.CurrentThread.CurrentUICulture = new
CultureInfo(selectedLanguage);
base.InitializeCulture();
}
}
It sets CultureInfo properly. Now I want (according to current CultureInfo) switch routes for every navigation links and change route pattern from mysite.com/PL/{controller}/{action} to mysite.com/EN/{controller}/{action}.
Does anyone has any ideas or maybe better approach for this problem? But condition is that address must be looking like this mysite.com/EN or mysite.com/PL - not different (i.e. en.mysite.com)

The first thing that you must decide is where to store the current user language. There are different possibilities:
part of every url
cookie
session
IMHO for SEO purposes it's best to have it as part of the url.
So I would recommend writing a custom route which will parse the language from the url and set the current thread culture:
public class LocalizedRoute : Route
{
public LocalizedRoute()
: base(
"{lang}/{controller}/{action}/{id}",
new RouteValueDictionary(new
{
lang = "en-US",
controller = "home",
action = "index",
id = UrlParameter.Optional
}),
new RouteValueDictionary(new
{
lang = #"[a-z]{2}-[a-z]{2}"
}),
new MvcRouteHandler()
)
{
}
public override RouteData GetRouteData(HttpContextBase httpContext)
{
var rd = base.GetRouteData(httpContext);
if (rd == null)
{
return null;
}
var lang = rd.Values["lang"] as string;
if (string.IsNullOrEmpty(lang))
{
// pick a default culture
lang = "en-US";
}
var culture = new CultureInfo(lang);
Thread.CurrentThread.CurrentCulture = culture;
Thread.CurrentThread.CurrentUICulture = culture;
return rd;
}
}
We could now register this custom route in Global.asax:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.Add("Default", new LocalizedRoute());
}
Alright, now let's have a model:
public class MyViewModel
{
[DisplayFormat(DataFormatString = "{0:d}")]
public DateTime Date { get; set; }
}
A controller:
public class HomeController : Controller
{
public ActionResult Index()
{
return View(new MyViewModel
{
Date = DateTime.Now
});
}
public ActionResult Test()
{
return Content(DateTime.Now.ToLongDateString());
}
}
And a view:
#model MyViewModel
#Html.DisplayFor(x => x.Date)
<ul>
<li>#Html.ActionLink("switch to fr-FR", "index", new { lang = "fr-FR" })</li>
<li>#Html.ActionLink("switch to de-DE", "index", new { lang = "de-DE" })</li>
<li>#Html.ActionLink("switch to en-US", "index", new { lang = "en-US" })</li>
</ul>
#Html.ActionLink("Test culture", "test")
Now when you click on the links we are changing the language and this language is now part of the routes. Notice how once you have chosen the language this language is being persisted in the routes for the test link.
Scott Hanselman also wrote a nice blog post on localization and globalization in ASP.NET that's worth checking out.

This solution will not solve this issue:
english: /en/home
french: /fr/accueil
english: /en/contactus
french: /fr/contacteznous
It will just do
/en/home
/fr/home
You should also localize the routes in a dictionary and do a lookup.

Related

Having issues generating URLs for custom routes to include the users culture in the RouteValues

I'm trying to create a custom route which will include the users culture in the route (RouteValues). Using default routing convention everything works fine.
I have the following controller:
public class HomeController : Controller
{
public HomeController()
{
}
public IActionResult Index()
{
return View();
}
public IActionResult Privacy()
{
return View();
}
}
I believe I've configured localization in the app correctly as follows:
public void ConfigureServices(IServiceCollection services)
{
services.Configure<RequestLocalizationOptions>(opts =>
{
opts.SupportedCultures = new[] { new CultureInfo("en"), new CultureInfo("fr") };
opts.SupportedUICultures = opts.SupportedCultures;
opts.SetDefaultCulture("en");
opts.DefaultRequestCulture = new RequestCulture("en");
opts.RequestCultureProviders.Insert(0, new RouteDataRequestCultureProvider);
opts.ApplyCurrentCultureToResponseHeaders = opts.ApplyCurrentCultureToResponseHeaders;
});
services.AddControllersWithViews(opts =>
{
opts.Filters.Add(new CultureFilter("en"));
});
services.AddLocalization();
services.AddMvc();
}
And I have an ActionFilter that sets the users culture based on the route value.
public class CultureFilter : IAuthorizationFilter
{
private readonly string defaultCulture;
public CultureFilter(string defaultCulture)
{
this.defaultCulture = defaultCulture;
}
public void OnAuthorization(AuthorizationFilterContext context)
{
var values = context.RouteData.Values;
string culture = (string)values["culture"] ?? this.defaultCulture;
CultureInfo ci = new CultureInfo(culture);
Thread.CurrentThread.CurrentCulture = ci;
Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(ci.Name);
}
}
Using the default routing convention I get the desired result (more or less).
Thus for the following routes:
endpoints.MapControllerRoute(
name: "culture-default",
pattern: "{culture=en}/{controller=Home}/{action=Index}");
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}");
Thus while on the url "http://localhost" I get:
"#Url.ActionLink("Index", "Home")" = "http://localhost"
"#Url.ActionLink("Privacy", "Home")" = "http://localhost/home/privacy"
And while on the url "http://localhost/fr" I get:
"#Url.ActionLink("Index", "Home")" = "http://localhost/fr"
"#Url.ActionLink("Privacy", "Home")" = "http://localhost/fr/home/privacy"
Ok - so far so goood....
But when I add a custom route for the Privacy ActionMethod I can't seem to get the correct culture in the generated URL.
Thus for the following routes:
endpoints.MapControllerRoute(
name: "culture-privacy",
pattern: "{culture}/h/p",
defaults: new { culture = "en", controller = "Home", action = "Privacy" });
endpoints.MapControllerRoute(
name: "default-privacy",
pattern: "h/p",
defaults: new { controller = "Home", action = "Privacy" });
Thus while on the url "//localhost" I get:
"#Url.ActionLink("Index", "Home")" = "//localhost"
"#Url.ActionLink("Privacy", "Home")" = "//localhost/h/p"
And while on the url "//localhost/fr" I get:
"#Url.ActionLink("Index", "Home")" = "//localhost/fr"
"#Url.ActionLink("Privacy", "Home")" = "//localhost/en/h/p"
Presumably this is because I included the "culture = 'en'" in the default for the "culture-privacy" route, but shouldn't the default value of 'en' only be used if the culture is not otherwise specified in the route?
You are right, the reason is that the default value "en" is overriding the value passed in the route data. If you want to use the culture which user typed, try to use the culture value passed in the route data instead of a fixed value. Here is the code sample:
endpoints.MapControllerRoute(
name: "culture-privacy",
pattern: "{culture}/h/p",
defaults: new { culture = "{culture}", controller = "Home", action = "Privacy" });

Language-specific Default URL using MVC's Attribute Routing and RouteLocalization.mvc

I would like to be able to create a succinct language-specific default URL for my website so that when someone browses to:
somesite.com
They get redirected to a language-culture page such as:
somesite.com/en-US/
somesite.com/sp-MX/
somesite.com/fr-FR/
Specifically, I do not want /Home/Index appended to the URLs:
somesite.com/en-US/Home/Index
somesite.com/sp-MX/Home/Index
somesite.com/fr-FR/Home/Index
I am committed to making this site using RouteLocalization.mvc
Dresel/RouteLocalization
Translating Your ASP.NET MVC Routes
And I would like to use MVC Attribute Routing to the extent feasible.
I am having trouble figuring out how to cause the Start() method redirect to a language-culture specific URL without the addition of something like "index".
Samples of what I have attempted follow:
using RouteLocalization.Mvc;
using RouteLocalization.Mvc.Extensions;
using RouteLocalization.Mvc.Setup;
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.Clear();
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapMvcAttributeRoutes(Localization.LocalizationDirectRouteProvider);
const string en = "en-us";
ISet<string> acceptedCultures = new HashSet<string>() { en, "de", "fr", "es", "it" };
routes.Localization(configuration =>
{
configuration.DefaultCulture = en;
configuration.AcceptedCultures = acceptedCultures;
configuration.AttributeRouteProcessing = AttributeRouteProcessing.AddAsNeutralAndDefaultCultureRoute;
configuration.AddCultureAsRoutePrefix = true;
configuration.AddTranslationToSimiliarUrls = true;
}).TranslateInitialAttributeRoutes().Translate(localization =>
{
localization.AddRoutesTranslation();
});
CultureSensitiveHttpModule.GetCultureFromHttpContextDelegate = Localization.DetectCultureFromBrowserUserLanguages(acceptedCultures, en);
var defaultCulture = System.Threading.Thread.CurrentThread.CurrentUICulture.Name;
routes.MapRoute(
name: "DefaultLocalized",
url: "{culture}/{controller}/{action}/{id}",
constraints: new { culture = #"(\w{2})|(\w{2}-\w{2})" },
defaults: new { culture = defaultCulture, controller = "Home", action = "Index", id = UrlParameter.Optional }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
And my home controller:
public class HomeController : Controller
{
[HttpGet]
[Route]
public RedirectToRouteResult Start()
{
return RedirectToAction("Home", new { culture = Thread.CurrentThread.CurrentCulture.Name });
}
[Route("Index", Name = "Home.Index")]
public ActionResult Index()
{
return View();
}
public ActionResult Contact()
{
return View();
}
public ActionResult About()
{
return View();
}
}
My Global.asax file:
public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
AreaRegistration.RegisterAllAreas();
}
}
Redirecting is a separate concern than routing. Since your goal of redirecting any URL to its localized counterpart is a cross-cutting concern your best bet is to make a global filter.
public class RedirectToUserLanguageFilter : IActionFilter
{
private readonly string defaultCulture;
private readonly IEnumerable<string> supportedCultures;
public RedirectToUserLanguageFilter(string defaultCulture, IEnumerable<string> supportedCultures)
{
if (string.IsNullOrEmpty(defaultCulture))
throw new ArgumentNullException("defaultCulture");
if (supportedCultures == null || !supportedCultures.Any())
throw new ArgumentNullException("supportedCultures");
this.defaultCulture = defaultCulture;
this.supportedCultures = supportedCultures;
}
public void OnActionExecuting(ActionExecutingContext filterContext)
{
var routeValues = filterContext.RequestContext.RouteData.Values;
// If there is no value for culture, redirect
if (routeValues != null && !routeValues.ContainsKey("culture"))
{
string culture = this.defaultCulture;
var userLanguages = filterContext.HttpContext.Request.UserLanguages;
if (userLanguages.Length > 0)
{
foreach (string language in userLanguages.SelectMany(x => x.Split(';')))
{
// Check whether language is supported before setting it.
if (supportedCultures.Contains(language))
{
culture = language;
break;
}
}
}
// Add the culture to the route values
routeValues.Add("culture", culture);
filterContext.Result = new RedirectToRouteResult(routeValues);
}
}
public void OnActionExecuted(ActionExecutedContext filterContext)
{
// Do nothing
}
}
Usage
public class FilterConfig
{
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new RedirectToUserLanguageFilter("en", new string[] { "en", "de", "fr", "es", "it" }));
filters.Add(new HandleErrorAttribute());
}
}
Note also that your routing is misconfigured. The route setup is run one time per application startup, so setting the default culture to that of the current thread is meaningless. In fact, you should not be setting a default culture at all for your culture route because you want it to miss so your Default route will execute if there is no culture set.
routes.MapRoute(
name: "DefaultLocalized",
url: "{culture}/{controller}/{action}/{id}",
constraints: new { culture = #"(\w{2})|(\w{2}-\w{2})" },
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);

Asp .net MVC 5 Owin authentication with multiple login pages

I have an asp.net MVC 5 site with OWIN authentication. I would like to have two different log in pages, one for regular users and one for Admin users. The login redirect path is currently set in Start.cs when setting up UseCookieAuthentication. I am pretty sure I could parse the returnUrl and look for /admin/ in the path, but that seems really hacky and prone to errors (what if there is no returnUrl?). Is there a better way? I see stuff online about using an authentication filter, but not sure if that is workable when using OWIN.
#AntP is right, I had the same problem and I solved using Authentication Filter. (I am also using OWIN Authentication).
In my case I wanted the user to be redirected to a different Login page according to the area.
public class AreaAuthorizationAttribute : FilterAttribute, IAuthenticationFilter
{
string clientRole = "Client"; // can be taken from resource file or config file
string adminRole = "Admin"; // can be taken from resource file or config file
string currentArea = "";
public void OnAuthentication(AuthenticationContext context)
{
var area = context.RouteData.DataTokens["area"];
if (context.HttpContext.User.Identity.IsAuthenticated)
{
if (context.HttpContext.User.IsInRole(clientRole) && !(area.ToString().Equals("Client")))
{
context.Result = new HttpUnauthorizedResult();
currentArea = "Client";
}
if (context.HttpContext.User.IsInRole(adminRole) && !(area.ToString().Equals("Admin")))
{
context.Result = new HttpUnauthorizedResult();
currentArea = "Admin";
}
}
else
{
if (area.ToString().Equals("Client"))
{
context.Result = new HttpUnauthorizedResult();
currentArea = "Client";
} else if (area.ToString().Equals("Admin"))
{
context.Result = new HttpUnauthorizedResult();
currentArea = "Admin";
}
}
}
public void OnAuthenticationChallenge(AuthenticationChallengeContext context)
{
if (context.Result == null)
{
Debug.WriteLine("Context null");
context.Result = new RedirectToRouteResult(new System.Web.Routing.RouteValueDictionary(
new { area = "Client", controller = "Client", action = "Login", returnUrl = context.HttpContext.Request.RawUrl }));
}
if (context.Result is HttpUnauthorizedResult)
{
if ((currentArea.Equals("Client")))
{
context.Result = new RedirectToRouteResult(new System.Web.Routing.RouteValueDictionary(
new { area = "Client", controller = "Client", action = "Login", returnUrl = context.HttpContext.Request.RawUrl }));
}
else if(currentArea.Equals("Admin"))
{
context.Result = new RedirectToRouteResult(new System.Web.Routing.RouteValueDictionary(
new { area = "Admin", controller = "Admin", action = "Login", returnUrl = context.HttpContext.Request.RawUrl }));
}
}
}
}
Then I used my custom attribute as followes:
[AreaAuthorization]
public class AdminController : Controller //controller in Admin area
{
.... //actions
}
[AreaAuthorization]
public class ClientController : Controller //controller in Client area
{
.... //actions
}
This is my source (for custom authorize attributes): http://www.dotnetfunda.com/articles/show/2935/creating-custom-authentication-filter-in-aspnet-mvc
You could put a constraint in your routing rules. This constraint will check is the user is of type Admin and allow (or reject) the routing.
You need to set a IRouteConstraint class. For example:
public class IsAdmin : IRouteConstraint
{
public IsAdmin()
{
}
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
bool isAdmin = false;
// TO DO, detect if current user is Admin and return the value
// ...
return isAdmin;
}
}
Then in your routing rules you add that as a constraint
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "home", action = "index", id = UrlParameter.Optional },
constraints: new { controller = new Common.Constraints.IsAdmin() }
);
It is a rather simple approach. I have used to prevent non admin users from accessing anything on the /admin section of my website and it works like a charm.

ASP.NET MVC3 Linking Area "Resource cannot be found"

I just barely created a new Area to organize my code.
But currently I am having trouble actually linking it from my "base" or "root" index page, to my new area page.
#Html.ActionLink("Tube Record Form", "BearingAssemblyForm", "_HiCT", new { area = "HICT" }, null)
public class HICTAreaRegistration : AreaRegistration
{
public override string AreaName
{
get
{
return "HICT";
}
}
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"HICT_default",
"HICT/{controller}/{action}/{id}",
new {controller = "_HiCT", action = "BearingAssemblyForm", id = UrlParameter.Optional }
);
}
}
The resource cannot be found.
And it seems its linked wrongly
Requested URL: /HICT/HiCT/BearingAssemblyForm
Controller: HiCT,
View/Action: BearingAssemblyForm, Area: HICT.
How would I like this?
Thank you so much.
Try this:
#Html.ActionLink("LinkText",
"ActionName",
"ControllerName",
new { area = "HICT" }, null)
I think you're not using the correct #Html.ActionLink method overload.
First off all, see if your area is registered correctly:
public class Routes : AreaRegistration
{
public override string AreaName
{
get
{
return "HICT";
}
}
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"HICT_default",
"HICT/{controller}/{action}/{id}",
new { controller = "_HiCT", action = "BearingAssemblyForm", id = UrlParameter.Optional }
);
}
Make sure you're calling RegisterAllAreas inside the Global.asax.cs file:
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
...
}
/Master/BearingAssemblyForm
usually, Master is the controller and the 2nd part is action, so it seems your controller name is different than your route.
Its been a wile but this should work.
#Html.ActionLink("Tube Record Form", "action", "Area/controller")
Are you calling:
AreaRegistration.RegisterAllAreas();
on Application_Start in your Global.asax? What server are you using for development Cassini, IISExpress, IIS?
Edit after reviewing more detailed information.
In your Admin Area Registration File if you have this code
context.MapRoute(
"Admin_default",
"Admin/{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
new string[] { "CoolProject.Web.Areas.Admin.Contollers" }
);
I think there is a typo in CoolProject.Web.Areas.Admin.Contollers and it should be CoolProject.Web.Areas.Admin.Controllers?

asp.net mvc routing id parameter

I am working on a website in asp.net mvc. I have a route
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional }
// Parameter defaults
);
which is the default route. And I have a method
public ActionResult ErrorPage(int errorno)
{
return View();
}
Now if I want to run this code with http://something/mycontroller/Errorpage/1
it doesn't work. But if I change the parameter name to id from errorno
it works.
Is it compulsory to have same parameter name for this method? Or do I need to create separate routes for such situations?
So, you have a parameter named errorno, and you want it to have a value from parameter id. This is obviously the binding problem.
How to solve it:
create a class for model binder:
public class ParameterBinder : IModelBinder
{
public string ActualParameter { get; private set; }
public ParameterBinder(string actualParameter)
{
this.ActualParameter = actualParameter;
}
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
object id = controllerContext.RouteData.Values[this.ActualParameter];
return id;
}
}
create a custom attribute for custom model binding:
[AttributeUsage(AttributeTargets.Parameter)]
public class BindParameterAttribute : CustomModelBinderAttribute
{
public string ActualParameter { get; private set; }
public BindParameterAttribute(string actualParameter)
{
this.ActualParameter = actualParameter;
}
public override IModelBinder GetBinder()
{
return new ParameterBinder(this.ActualParameter);
}
}
apply the new attribute to your action parameters as needed:
public ActionResult ErrorPage(
[BindParameter("id")]
int errorno)
{
return View();
}
Now your errorno will have the value, which was passed as id for your url.
Note: you can remove the paramter id from the example above, if you are sure you need it solved only for id.
Leaving this way will allow you bind other parameters too.
Option 1
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
public ActionResult ErrorPage(int id)
{
return View();
}
Option 2
routes.MapRoute(
"Default",
"{controller}/{action}/{errorno}",
new { controller = "Home", action = "Index", errorno = UrlParameter.Optional }
);
public ActionResult ErrorPage(int errorno)
{
return View();
}
Option 3
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
public ActionResult ErrorPage(int id)
{
int errorno = id;
return View();
}
use the bind attribute prefix:
public ActionResult Customer([Bind(Prefix = "id")]string cname) {}
#Parminder
The default route can handle all action with one parameter "id". And I think not every action need this parameter. So I change my default route
routes.MapRoute(
"Default",
"{controller}/{action}",
new { controller = "Home", action = "Index"}
);
and you can add a new route:
routes.MapRoute("errorpage", "yourcontroller/errorpage/{errorno}",
new {controller="controllername", action="errorpage"});
this just handle your controll name is "controllername". If you want to handle all controller, you can add this:
routes.MapRoute("errorpage", "{controller}/errorpage/{errorno}",
new {controller="controllername", action="errorpage"});
This method will create very much code in global.asax if you need a lot of custom route.
You could either rename the parameter in the default root (which probably is not a good idea) or rename it in the action method. Adding another root will not help because it will be the same as the default one and given an url the routing engine cannot distinguish between the two and will always take the first one in the list.
try to use the same name of parameter in action method as in in the route table url parameter.
global.asx
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
myController
public ActionResult ErrorPage(int id)
{
return View();
}

Resources