Is it possible to define routes in various parts of a website.
Example, if I wanted to break functionality of my website out into modules, and each module would define the routes it needs.
Is this possible? How?
Consider using ASP.NET MVC's built-in Areas.
An "Area" is essentially your module, and Areas allow you to register routes for each specific area.
If you've not used them before there is an MSDN walkthrough here:
http://msdn.microsoft.com/en-us/library/ee671793.aspx
Essentially, you have a directory for each area, which contains all your area-specific controllers and views, and in the route of that directory you place a file that registers the routes for that particular area like so:
public class MyAreaRegistration : AreaRegistration
{
public override string AreaName
{
get { return "My Area"; }
}
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"news-articles",
"my-area/articles/after/{date}",
new {controller = "MyAreaArticles", action = "After"}
);
// And so on ...
}
}
In global.asax.cs you need to register all these extra areas, along with your other primary routes:
public static void RegisterRoutes(RouteCollection routes)
{
AreaRegistration.RegisterAllAreas();
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Products",
"products/show/{name}",
new {controller = "Products", action = "Show", name = UrlParameter.Optional}
);
...
}
You can put a DefineRoutes(RouteCollection routes) method in each module, then call them all in Global.asax.cs.
Related
i already have one MVC website in english language. Now I need to add arabic language. To do that i want to use structure below to store my localized .cshtml files
English Views
~/Views/index.cshtml
~/Views/about.cshtml
Arabic Views
~/ar/Views/index.cshtml
~/ar/Views/about.cshtml
English URL (It's working)
www.samplesite.com/home/index
www.samplesite.com/home/index
Arabic URL (It's not working)
www.samplesite.com/ar/home/index
www.samplesite.com/ar/home/index
I need to return english view if url with /en or no language parameter and if URL starts with /ar i need to return view in ar/Views folder. What should i change in route config and actions to get the correct view based on URL ?
In order to localize the ASP.NET MVC app with different views for different languages and cultures, you need to at least do:
Save language code from URL to route data collection, if exists
Set current culture based on the language code, if exists
Route the request to localized views, if language code appears
1. Save language code from URL to route data collection, if exists
You need to define the route mapping so that whenever there is a language code on the URL, it would be put to the route data collection.
You can define the extra route mapping in the RouteConfig:
namespace DL.SO.Globalization.DotNet.Web.UI
{
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.LowercaseUrls = true;
routes.MapRoute(
name: "DefaultWithLang",
url: "{lang}/{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
constraints: new { lang = new LanguageRouteConstraint() }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
}
We will come back to the route constraint, but the idea here is to run this "DefaultWithLang" mapping first, and see if the incoming request has the language code attached on it or not. If yes, save it to the route collection with the key lang. Otherwise, just process it as normal.
Route constraints
Imagine we have those 2 mappings defined, and the incoming request looks like /home/index. Without that route constraint, this would be mapped to the "DefaultWithLang" with lang being "home", which is incorrect.
We will need a way to test whether the language code is a valid 2-letter code. To do that, you can create a route constraint:
using System.Text.RegularExpressions;
using System.Web;
using System.Web.Routing;
namespace DL.SO.Globalization.DotNet.Web.UI.RouteConstraints
{
public class LanguageRouteConstraint : IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route, string parameterName,
RouteValueDictionary values, RouteDirection routeDirection)
{
return Regex.IsMatch((string)values[parameterName], #"^[a-z]{2}$");
}
}
}
With this in placed, the request "/home/index" will first come in with "home" being the language code, but it will fail this route constraint because it has more than 2 letters. This would result no matching for "DefaultWithLang" map. Then the MVC will continue to use the 2nd mapping and see if the request is a valid request, which it is!
2. Set current culture based on the language code, if exists
Now we know the language code will be put into the route data collection. We can set the current culture and the current UI culture of the current thread based on that.
To do so, we can create an ActionFilter:
namespace DL.SO.Globalization.DotNet.Web.UI.Filters
{
public class LanguageFilter : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var values = filterContext.RouteData.Values;
string languageCode = (string)values["lang"] ?? "en";
var cultureInfo = new CultureInfo(languageCode);
Thread.CurrentThread.CurrentCulture = cultureInfo;
Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(cultureInfo.Name);
}
}
}
Here I am reading the language code value from the route data collection, or it defaults to "en" if it doesn't exist.
To enable it for the whole application, you can add it to FilterConfig.cs:
namespace DL.SO.Globalization.DotNet.Web.UI
{
public class FilterConfig
{
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new LanguageFilter());
filters.Add(new HandleErrorAttribute());
}
}
}
3. Route the request to localized views, if language code appears
Similar to Step #2, now we have the language code, we need to create a custom view engine to route the request to different views:
namespace DL.SO.Globalization.DotNet.Web.UI
{
public class GlobalizationRazorViewEngine : RazorViewEngine
{
protected override IView CreatePartialView(ControllerContext controllerContext,
string partialPath)
{
partialPath = GetGlobalizeViewPath(controllerContext, partialPath);
return base.CreatePartialView(controllerContext, partialPath);
}
protected override IView CreateView(ControllerContext controllerContext,
string viewPath, string masterPath)
{
viewPath = GetGlobalizeViewPath(controllerContext, viewPath);
return base.CreateView(controllerContext, viewPath, masterPath);
}
private string GetGlobalizeViewPath(ControllerContext controllerContext,
string viewPath)
{
var request = controllerContext.HttpContext.Request;
var values = controllerContext.RouteData.Values;
string languageCode = (string)values["lang"];
if (!String.IsNullOrWhiteSpace(languageCode))
{
string localizedViewPath = Regex.Replace(viewPath,
"^~/Views/",
String.Format("~/{0}/Views/", languageCode)
);
if (File.Exists(request.MapPath(localizedViewPath)))
{
viewPath = localizedViewPath;
}
}
return viewPath;
}
}
}
The logic here should be straight forward: if the language code exists, try to append it to the original view path the MVC app was going to use to find the corresponding view.
Note: you would need to copy _viewStart.cshtml as well as web.config from the original Views folder to the new localized Views folder. You might consider using different folder structure to contain all localized views. One possible way is to put them all inside the ~/Views folder.
To use this custom view engine, you would need to add it to the view engine collection at Application_Start():
namespace DL.SO.Globalization.DotNet.Web.UI
{
public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new GlobalizationRazorViewEngine());
AreaRegistration.RegisterAllAreas();
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
}
}
}
Screenshots
With zh/ on the URL:
The folder structure:
Github source code: https://github.com/davidliang2008/DL.SO.Globalization.DotNet/tree/master/DL.SO.Globalization.DotNet.Web.UI
My MVC application is set up with controllers at the root/global level. These have no explicit Area. I also have a single "Admin" area. URLs that begin with /admin/ are routed to the matching controller in the Admin area. Other URLs are routed to the matching global controller. This is working fine for the most part.
The issue I'm having is that in the case when a URL matches a controller in the Admin area when one of the same name doesn't exist on the global area, the request is incorrectly routed to the controller in Admin area. I know this is happening because I put a breakpoint on the matching action in the relevant controller.
For example, I have a controller called CalendarController in the Admin area. When I visit /admin/calendar, it works because it finds the action and the corresponding view in Areas/Admin/Views/Calendar/Index.cshtml. The problem occurs when I visit /calendar. I do not have a controller named Calendar at the root level, but for some reason it routes the request to the Admin area's CalendarController, which I don't want. I want it to return a 404 because no CalendarController exists at the root level. Instead, I get an error because it's searching for the view at the root level (at /Views/Calendar/Index.cshtml) even though the matching controller was in the Admin area.
How can I prevent the Admin area from being searched for matching controllers except when the URL has /admin in it?
Here's the relevant route code, which is basically stock except for the namespaces addition. The issue still happens without the namespace. There are more routes in the actual application, but I'm getting the same behavior in a brand new MVC project.
public class AdminAreaRegistration : AreaRegistration
{
public override string AreaName
{
get
{
return "Admin";
}
}
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"Admin_default",
"Admin/{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional },
new[] { "AreaProblem.Areas.Admin.Controllers" }
);
}
}
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
What I mean by a controller at the "root level" is Controllers/HomeController in this screenshot. I want URLs that don't start with /admin to only look at those controllers. The problem is that it's also searching in Areas/Admin/Controllers.
So the MVC routing engine will look in different namespaces to try and find a matching controller. You can solve this by specifying a namespace (like you did for the admin area). You can also specify that a route not search other namespaces using DataTokens.
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
namespaces: new [] { "AreaProblem.Controllers" }
).DataTokens["UseNamespaceFallback"] = false;
I have a blog that I have built. It uses a web api in c# .NET.
If you click here: http://www.judsondesigns.com/api/blogapi/17
You will see it return an entry from the server. How can I easily rewrite the url to use the blog title instead of the ID?
So instead you can access it via: http://www.judsondesigns.com/api/blogapi/my_blog_tite_here
I have done this with isapi rewrites in the past on linux, but wasnt clear how to in .NET. I have heard different way but would like the less is more approach here. Thanks in advance. -Judson
What you want to do is create a custom RouteBase. This code review post is a good place to start.
The jist of it is:
public class MyRoute : RouteBase
{
public override RouteData GetRouteData(HttpContextBase httpContext)
{
// parse url and turn into route
}
public override VirtualPathData GetVirtualPath(
RequestContext requestContext,
RouteValueDictionary values)
{
// create url from route
}
}
Which you then register along with any other routes like
routes.Add(new MyRoute());
By editing the route configuration:
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{blogTitle}",
defaults: new { controller = "Home", action = "Index", blogTitle = UrlParameter.Optional }
);
}
}
or rewrite the action to use the name instead and using a named parameter
http://www.judsondesigns.com/api/blogapi/?blogtitle=my_blog_tite_here
I'm having difficulties with my controllers that are in an Area answering requests on routes that aren't for the area. So I have a setup like this (Extra stuff cut):
/Areas/Security/Controllers/MembersController.cs
/Areas/Security/SecurityAreaRegistration.cs
/Controllers/HomeController.cs
I have my area for security defined:
namespace MyApp.Web.Areas.Security
{
public class SecurityAreaRegistration : AreaRegistration
{
public override string AreaName
{
get
{
return "Security";
}
}
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"Security_default",
"Security/{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional }
);
}
}
}
And my global routing rules:
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.IgnoreRoute("{*robotstxt}", new { robotstxt = #"(.*/)?robots.txt(/.*)?" });
routes.IgnoreRoute("{*favicon}", new { favicon = #"(.*/)?favicon.ico(/.*)?" });
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
namespaces: new string[] { "MyApp.Web.Controllers" }
);
In my global asax I'm doing quite a few things but the relevant part is that I call AreaRegistration.RegisterAllAreas(); then I call the routing function that does the above.
But my problem is that requests for "/Members/" are hitting my Members controller using my "Default" route... even though the controller's not in the namespace I specified. Then when it tries to run it can't find it's Views cause they're defined in the Area and it's trying to find them in the overall Views folders. I tried making the route namespace "Weird.Namespace.With.No.Content" and it STILL hits the Members controller - I can't find any way to make it not use that controller. How do I make it not answer requests that aren't in it's area?
Ended up finding a solution by changing the Route to:
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
namespaces: new string[] { "MyApp.Web.Controllers" }
).DataTokens["UseNamespaceFallback"] = false;
For some reason with that unset it seemed to always find my Controllers no matter where they were and totally disregard my namespaces - even from other referenced assemblies. Looking through the ILSpy of DefaultControllerFactory it looks like GetControllerType eventually falls back to searching absolutely every controller if it doesn't find the controller in the namespaces you asked for...
This flag seems to be set automatically on the Routes I made in a specific area, but not on the ones I made globally. When I set it on the global ones they began behaving how I had originally expected. I have no idea why you'd ever want to turn this on...
You should register the namespace on your area as well.
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"Security_default",
"Security/{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional },
new []{ "MyApp.Web.Areas.Security.Controllers"},
);
}
And then make sure all your controllers are in their appropriate namespaces.
Are you calling register all areas after registering your main route?
This is the default snippet from the MVC template (note how area regisration goes first)
AreaRegistration.RegisterAllAreas();
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
By default, the #RenderBody in _Layout.cshtml in an MVC3 app points to ~/Views/Home/Index.
#RenderBody()
Where is this set and how do I change it to point to ~/Views/Account/Logon? Or wherever I want. Thanks
It doesn't point to that view, it merely renders the view that it is given
Your app starts up and goes to the default action on the routing which can be found in Global.asax
You can modify that to default to /Account/LogOn if you wish
public class MvcApplication : System.Web.HttpApplication {
public static void RegisterGlobalFilters(GlobalFilterCollection filters) {
filters.Add(new HandleErrorAttribute());
}
public static void RegisterRoutes(RouteCollection routes) {
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
//routes.IgnoreRoute("{*favicon}", new { favicon = #"(.*/)?favicon.ico(/.*)?" });
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Account", action = "LogOn", id = UrlParameter.Optional } // Parameter defaults
);
}
protected void Application_Start() {
AreaRegistration.RegisterAllAreas();
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
}
}
RenderBody doesn't point by default to ~/Views/Home/Index. It renders the view that was returned by the controller action that was executed. And since in your Global.asax in the routing definition the default action is configured to be Index, it is this view that is rendered.
So all you have to do is modify your routing configuration so that the default action is Logon on the Account controller:
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Account", action = "LogOn", id = UrlParameter.Optional } // Parameter defaults
);
Now when you navigate to /, the LogOn action of the Account controller will be executed which itself will render the ~/Views/Account/LogOn.cshtml view.
You should use #RenderPage instead. Follow this link for more information.