I have a site with controllers and two other areas with the respective controllers for each. One of the controllers within the area has a language contraint code say en. By default it works perfectly fine. But when I try to use the Route specification in the controllers it is building the routes in misleading way.
The RouteConfig.cs file looks like below
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapMvcAttributeRoutes();
routes.LowercaseUrls = true;
AreaRegistration.RegisterAllAreas();
routes.MapRoute(
name: "DefaultWithLanguageAndOrg",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
namespaces: new[] { "MyProj.Website.WebApp.Controllers" }
);
}
Part of Area registration file looks like below:-
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"Test_default",
"{lang}/Test/{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
new { lang = new LanguageRouteConstraint() },
new[] { "MyProj.Website.WebApp.Areas.Test.Controllers" }
);
}
And controller looks like below:-
[RouteArea("Test")]
[RoutePrefix("certificate")]
public class CertificationsController : Controller
{
[Route("Home")]
public ActionResult Home()
{
return View();
}
}
My expecation is to have the URL structure like site/en/Test/certificate/Home but I'm not able to add the prefix en before RouteArea.
Note:-
Tried adding en into the RouteArea like [RouteArea("en/Test")] it executes the action but expects the views folder to be moved inside en. That is not a proper solution, other routes without the Route specification will not work.
Tried adding Area and language contraint within the RoutePrefix like [RoutePrefix("{lang}/Test/certificate/Home")], it executes the action but not renders the view. It searches the view in the path like ~/Views/Certifications/Home.cshtml where Area Test is missing, it should be like ~/Test/Views/Certifications/Home.cshtml. And this format as well [RoutePrefix("en/{area}/certificate")] no luck.
You can override the View() of the Controller
like -
protected override ViewResult View(string ViewName, string masterName, object model)
{
return PrepareView(ViewName, masterName, model);
}
private ViewResult PrepareView(string ViewName, string masterName, object model)
{
renderview = base.View("~/Views/Shared/" + ViewName + ".cshtml", masterName, model);
return base.View(ViewName, masterName, model);
}
This is just a example code, similar to this you can override the path using your languagecode.
Related
I'm having problems with the routings in my MVC project not working...
I want all my views in the Views > Shared folder like this:
Error.cshtml (default)
Index.cshtml (default)
Overview.cshtml (custom that I made)
Recordings.cshtml (custom that I made)
I've then created one shared controller to handle all views like this:
public class SharedController : Controller
{
public ActionResult Index()
{
return View();
}
public ActionResult Error()
{
return View();
}
public ActionResult Overview()
{
return View();
}
public ActionResult Recordings()
{
return View();
}
}
My RouteConfig.cs looks like this:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
// Map to specific pages under Shared controller:
routes.MapRoute("SharedPages", "{action}/{id}",
new { controller = "Shared", action = #"Overview|Recordings", id = UrlParameter.Optional });
// Use the default rout for all other pages:
routes.MapRoute("Default", "{controller}/{action}/{id}",
new { controller = "Shared", action = "Index", id = UrlParameter.Optional }
);
// Show the Error page for anything else (404):
routes.MapRoute("Error", "{*url}",
new { controller = "Shared", action = "Error" }
);
}
I want the routing to work like this:
://(url)/ (root - no action specified) --> Shared/Index.cshtml
://(url)/Index --> Shared/Index.cshtml
://(url)/Overview --> Shared/Overview.cshtml
://(url)/Recordings --> Shared/Recordings.cshtml
://(url)/whatever (or if an error occurs) --> Shared/Error.cshtml
But it's not working as expected. If I go to ://(url)/ (root), I get a HTTP 404 - The resource cannot be found. If I go to for example ://(url)/Overview, it's working fine.
How can I make it work like I want?
The order of how you map route is important and first matched route wins. That means that even if there is no resource there one it matches the route it will use it.
public static void RegisterRoutes(RouteCollection routes)
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
// Use specific rout for all other pages:
routes.MapRoute("WhateverA", "WhateverA/{action}/{id}",
new { controller = "WhateverA", action = "Index", id = UrlParameter.Optional }
);
routes.MapRoute("WhateverB", "WhateverB/{action}/{id}",
new { controller = "WhateverB", action = "Index", id = UrlParameter.Optional }
);
// Map to specific pages under Shared controller:
routes.MapRoute("RootPages", "{action}/{id}",
new { controller = "Shared", action = "Index", id = UrlParameter.Optional });
// Show the Error page for anything else (404):
routes.MapRoute("Error", "{*url}",
new { controller = "Shared", action = "Error" }
);
}
The problem with the Default and SharedPages routes is that they conflict with each other. You may need to provide specific routes for other controllers if they exist. Other wise the other option is to use Attribute Routing for the other controllers and convention-based routing for your root routes and error
public static void RegisterRoutes(RouteCollection routes)
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
//Attribute routing
routes.MapMvcAttributeRoutes();
// Map to specific pages under Shared controller:
routes.MapRoute("RootPages", "{action}/{id}",
new { controller = "Shared", action = "Index", id = UrlParameter.Optional });
// Show the Error page for anything else (404):
routes.MapRoute("Error", "{*url}",
new { controller = "Shared", action = "Error" }
);
}
With controllers decorated accordingly
[RoutePrefix("WhatEver")]
public class WhatEverController : Controller {
//GET whatever
[HttpGet]
[Route("")]
public ActionResult Index() { ... }
}
In my project there is an action
public ActionResult Lead(int leadId)
{
return View();
}
and in the View an ActionLink was created like this
#Html.ActionLink("Old Link", "Lead", "Home", new { leadId = 7 }, null)
But after some time, to make clean URL, I have changed the name of parameter of that action
public ActionResult Lead(int id)
{
return View();
}
And ActionLink change accordingly
#Html.ActionLink("New Link", "Lead", "Home", new { id = 5 }, null)
But old link was shared in multiple social network sites. Whenever anyone clicks on that old link, he is redirect to the page www.xyx.com/Home/Lead?leadId=7
But now in my application, no such URL exists.
To handle this problem, I was thinking of overloading, but MVC action doesn't support overloading.
I have created another Action with same name with extra parameter, and redirect to new action, but it doesn't work.
public ActionResult Lead(int leadId, int extra=0)
{
return RedirectToAction("Lead", "Home", new { id = leadId });
}
I have found one link to handle such situation, but It is not working in my case.
ASP.NET MVC ambiguous action methods
One possibility to handle this would be to write a custom route:
public class MyRoute : Route
{
public MyRoute() : base(
"Home/Lead/{id}",
new RouteValueDictionary(new
{
controller = "Home",
action = "Lead",
id = UrlParameter.Optional,
}),
new MvcRouteHandler()
)
{
}
public override RouteData GetRouteData(HttpContextBase httpContext)
{
var rd = base.GetRouteData(httpContext);
if (rd == null)
{
return null;
}
var leadId = httpContext.Request.QueryString["leadid"];
if (!string.IsNullOrEmpty(leadId))
{
rd.Values["id"] = leadId;
}
return rd;
}
}
that you will register before the default one:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.Add(new MyRoute());
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
and now you could only have a single action:
public ActionResult Lead(int id)
{
return View();
}
Now both the following urls will work as expected:
www.xyx.com/Home/Lead/7
www.xyx.com/Home/Lead?leadId=7
I have problem with routing. I have many pages on my site generated dynamically from database.
First thing which I want to accomplish is to route to these pages like that:
"How to repair a car"
www.EXAMPLE.com/How-to-repair-a-car
For now it works like that: www.EXAMPLE.com/Home/Index/How-to-repair-a-car
Secondly my default page have to be like that: www.EXAMPLE.com
On the Start Page will be news with pagging, so if someone click for instance in the "page 2" button, the address should looks: www.EXAMPLE.com/page =2
CONCLUSION:
default page -> www.EXAMPLE.com (with page = 0)
default page with specific page of news -> www.EXAMPLE.com/page=12
article page -> www.EXAMPLE.com/How-to-repair-car (without parameter 'page') routing sholud point to article or error404
PS: sorry for my english
Try to create route for articles in routing config, like this:
Routing config:
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(null, "{article}",
new {controller = "Home", action = "Article" });
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
HomeController:
public class HomeController : Controller
{
public ActionResult Index(int? page)
{
var definedPage = page ?? 0;
ViewBag.page = "your page is " + definedPage;
return View();
}
public ActionResult Article(string article)
{
ViewBag.article = article;
return View();
}
}
/?page=10 - works
/How-to-repair-car - works
That approach excellent works.
Here is a basic routing example for www.example.com/How-to-repair-car
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
namespace Tipser.Web
{
public class MyMvcApplication : HttpApplication
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.MapRoute(
"ArticleRoute",
"{articleName}",
new { Controller = "Home", Action = "Index", articleName = UrlParameter.Optional },
new { userFriendlyURL = new ArticleConstraint() }
);
}
public class ArticleConstraint : IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
var articleName = values["articleName"] as string;
//determine if there is a valid article
if (there_is_there_any_article_matching(articleName))
return true;
return false;
}
}
}
}
I have a controller that looks like this:
public class PageController : Controller
{
public ActionResult Render(string url)
{
//this is just for testing!
return Content("url was " + url);
}
}
I'm trying to pass in the value of the url into the controller. For example:
http://www.site.com/products/something/else
Would pass "products/something/else" into my Render action of the PageController.
This is because we are using "products/something/else" as a unique key for a record in the database (legacy system, don't ask)
So, my resultant query would be something along the lines of this:
select * from foo where urlKey = 'products/something/else'
So far I have this in my RegisterRoutes section on Global.asax:
routes.MapRoute("pages", "{*url}", new { controller = "Page", action = "Render", url="/" });
But this isn't working as expected...
By visiting www.site.com/products/something/else, the value passed into the controller is "home/index/0"
The only route defined in RegisterRoutes is that described in the question.
The below class matches every route but you can modify as per your needs.
public class LegacyRoute : RouteBase
{
public override RouteData GetRouteData(HttpContextBase httpContext)
{
RouteData result = null;
string url = httpContext.Request.RawUrl.Substring(1);
result = new RouteData(this, new MvcRouteHandler());
result.Values.Add("controller", "Page");
result.Values.Add("action", "Render");
result.Values.Add("url", url);
return result;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
return null;
}
}
In Global.asax.cs
routes.Add(new LegacyRoute());
Hope this helps, one of our routes does something similar and this is the code:
routes.MapRoute(
name: "Standard",
url: "{controller}/{action}/{id}",
defaults: new { id = UrlParameter.Optional, action = ControllersAndActions.TypicalController.IndexAction, page = 1 },
constraints: new
{
controller = ControllersAndActions.ControllerConstraintExpression
}
);
I have a bit of a problem. I have an area called Framed. This area has a home controller. The default for the site also has a home controller.
What I'm trying to do with this is have a version of each controller/action that is suitable for an IFrame, and a version that is the normal site. I do this through Master pages, and the site masterpage has many different content place holders than the framed version. For this reason I can't just swap the master page in and out. For example, http://example.com/Framed/Account/Index will show a very basic version with just your account info for use in an external site. http://example.com/Account/Index will show the same data, but inside the default site.
My IoC container is structuremap. So, I found http://odetocode.com/Blogs/scott/archive/2009/10/19/mvc-2-areas-and-containers.aspx and http://odetocode.com/Blogs/scott/archive/2009/10/13/asp-net-mvc2-preview-2-areas-and-routes.aspx. Here's my current setup.
Structuremap Init
ObjectFactory.Initialize(x =>
{
x.AddRegistry(new ApplicationRegistry());
x.Scan(s =>
{
s.AssembliesFromPath(HttpRuntime.BinDirectory);
s.AddAllTypesOf<IController>()
.NameBy(type => type.Namespace + "." + type.Name.Replace("Controller", ""));
});
});
The problem here that I found through debugging is that because the controllers have the same name (HomeController), it only registers the first one, which is the default home controller. I got creative and appended the namespace so that it would register all of my controllers.
Default Route
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { area = "", controller = "Home", action = "Index", id = UrlParameter.Optional }, // Parameter defaults
new[] { "MySite.Controllers" }
);
Area route
context.MapRoute(
"Framed_default",
"Framed/{controller}/{action}/{id}",
new { area = "Framed", controller = "Home", action = "Index", id = UrlParameter.Optional },
new string[] { "MySite.Areas.Framed.Controllers" }
);
As recommended by Phil Haack, I am using the namespaces as the 4th parameter
app start, just to prove the order of initialization
protected void Application_Start()
{
InitializeControllerFactory();
AreaRegistration.RegisterAllAreas();
RouteConfiguration.RegisterRoutes();
}
Controller Factory
protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
{
IController result = null;
if (controllerType != null)
{
result = ObjectFactory.GetInstance(controllerType)
as IController;
}
return result;
}
So, when I hit /Home/Index, it passes in the correct controller type. When I hit /Framed/Home/Index, controllerType is null, which errors because no controller is returned.
It's as if MVC is ignoring my area altogether. What's going on here? What am I doing wrong?
In case anyone tries to do something similar, I used the idea from this post: Categories of controllers in MVC Routing? (Duplicate Controller names in separate Namespaces) I had to dump using areas altogether and implement something myself.
I have Controllers/HomeController.cs and Controllers/Framed/HomeController.cs
I have a class ControllerBase which all controllers in /Controllers inherit from. I have AreaController which inherits from ControllerBase which all controllers in /Controllers/Framed extend from.
Here's my Area Controller class
public class AreaController : ControllerBase
{
private string Area
{
get
{
return this.GetType().Namespace.Replace("MySite.Controllers.", "");
}
}
protected override ViewResult View(string viewName, string masterName, object model)
{
string controller = this.ControllerContext.RequestContext.RouteData.Values["controller"].ToString();
if (String.IsNullOrEmpty(viewName))
viewName = this.ControllerContext.RequestContext.RouteData.Values["action"].ToString();
return base.View(String.Format("~/Views/{0}/{1}/{2}.aspx", Area, controller, viewName), masterName, model);
}
protected override PartialViewResult PartialView(string viewName, object model)
{
string controller = this.ControllerContext.RequestContext.RouteData.Values["controller"].ToString();
if (String.IsNullOrEmpty(viewName))
viewName = this.ControllerContext.RequestContext.RouteData.Values["action"].ToString();
PartialViewResult result = null;
result = base.PartialView(String.Format("~/Views/{0}/{1}/{2}.aspx", Area, controller, viewName), model);
if (result != null)
return result;
result = base.PartialView(String.Format("~/Views/{0}/{1}/{2}.ascx", Area, controller, viewName), model);
if (result != null)
return result;
result = base.PartialView(viewName, model);
return result;
}
}
I had to override the view and partialview methods. This way, the controllers in my "area" can use the default methods for views and partials and support the added folder structures.
As for the Views, I have Views/Home/Index.aspx and Views/Framed/Home/Index.aspx. I use the routing as shown in the post, but here's how mine looks for reference:
var testNamespace = new RouteValueDictionary();
testNamespace.Add("namespaces", new HashSet<string>(new string[]
{
"MySite.Controllers.Framed"
}));
//for some reason we need to delare the empty version to support /framed when it does not have a controller or action
routes.Add("FramedEmpty", new Route("Framed", new MvcRouteHandler())
{
Defaults = new RouteValueDictionary(new
{
controller = "Home",
action = "Index",
id = UrlParameter.Optional
}),
DataTokens = testNamespace
});
routes.Add("FramedDefault", new Route("Framed/{controller}/{action}/{id}", new MvcRouteHandler())
{
Defaults = new RouteValueDictionary(new
{
//controller = "Home",
action = "Index",
id = UrlParameter.Optional
}),
DataTokens = testNamespace
});
var defaultNamespace = new RouteValueDictionary();
defaultNamespace.Add("namespaces", new HashSet<string>(new string[]
{
"MySite.Controllers"
}));
routes.Add("Default", new Route("{controller}/{action}/{id}", new MvcRouteHandler())
{
Defaults = new RouteValueDictionary(new
{
controller = "Home",
action = "Index",
id = UrlParameter.Optional
}),
DataTokens = defaultNamespace
});
Now I can go /Home/Index or /Framed/Home/Index on the same site and get two different views with a shared control. Ideally I'd like one controller to return one of 2 views, but I have no idea how to make that work without 2 controllers.
I had a similar issue using Structuremap with Areas. I had an Area named Admin and whenever you tried to go to /admin it would get to the StructureMap Controller Factory with a null controller type.
I fixed it by following this blog post:
http://stephenwalther.com/blog/archive/2008/08/07/asp-net-mvc-tip-30-create-custom-route-constraints.aspx
Had to add a constraint on the default route to not match if the controller was admin.
Here's my default route definition:
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "MyController", action = "AnAction", id = UrlParameter.Optional },
new { controller = new NotEqualConstraint("Admin")},
new string[] {"DailyDealsHQ.WebUI.Controllers"}
);
and here's the implementation of the NotEqualConstraint:
public class NotEqualConstraint : IRouteConstraint
{
private string match = String.Empty;
public NotEqualConstraint(string match)
{
this.match = match;
}
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
return String.Compare(values[parameterName].ToString(), match, true) != 0;
}
}
There's probably other ways to solve this problem, but this fixed it for me :)