GetVirtualPathForArea returns wrong VirtualPath - c#

I am using Martijn Bolands paging and I have an issue with GetVirtualPathForArea method.
I have two pages: /Page1 and /Page2.
I've set a MapRoute for /Page1 and is accessed as /Page1Name
routes.MapRoute(
"Page1 name", // Route name
"Page1Name", // URL with parameters
new { controller = "ContollerName", action = "ActionName" } // Parameter defaults
);
If I try to get the Vitrual Path from Page1Name page, it gives me the correct one.
But!! If I try to get the Virtual Path from page2, it gives me again the Page1Name.
I haven't set any Route for Page2 and If do this, then in Page1 I will get the VirtualPath of Page2.
Call and Methods
The call is through an HtmlHelper as follows
public static HtmlString Pager(this HtmlHelper htmlHelper, int pageSize, int currentPage, int totalItemCount, string actionName, RouteValueDictionary valuesDictionary, string controllerName) {
if (valuesDictionary == null) {
valuesDictionary = new RouteValueDictionary();
}
if (actionName != null) {
if (valuesDictionary.ContainsKey("action")) {
throw new ArgumentException("The valuesDictionary already contains an action.", "actionName");
}
valuesDictionary.Add("action", actionName);
}
var pager = new Pager(htmlHelper.ViewContext, pageSize, currentPage, totalItemCount, valuesDictionary, null, controllerName);
return pager.RenderHtml();
}
This is from RenderHtml() method
It gets the viewContext from htmlHelper and this is the call to get the virtualPath
var pageLinkValueDictionary = new RouteValueDictionary(linkWithoutPageValuesDictionary) { { "page", pageNumber } };
var virtualPathForArea = RouteTable.Routes.GetVirtualPathForArea(viewContext.RequestContext, pageLinkValueDictionary);
Is it a known issue? Have I set something in a wrong way?
Thanks and I'll appreciate any comment.

Response from microsoft:
The behavior you’re mentioning here is actually “by design”, if a route URL (such as your first route: FirstPage) has no parameters, no defaults, and no constraints, the call to RouteTable.Routes.GetVirtualPathForArea() will match this route (when it gets to it as it tries to match the route one by one in the order they are added).
The general fix is to make one of those conditions false OR to use named routes.
You can read more about this in a blog post by Phil Haack here: http://haacked.com/archive/2010/11/21/named-routes-to-the-rescue.aspx

Normally, a route that is registered could look like
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "ControllerName", action = "ActionName", id = UrlParameter.Optional } // Parameter defaults,
);
meaning that doing a Url.Action("Index","Home", new {id = 1}) would result in the url /Home/Index/1; the {controller}/{action}/{id} are placeholders for both interpreting a request (which controller, action and parameters to use) and generating url's.
If you look at your route defined (if it is the only one, or defined as the first) whatever you do with a Url.Action(..) it will always return /Page1Name
If you want to do as in the link provided
Internally, the pager uses RouteTable.Routes.GetVirtualPath() to render the url’s so the page url’s can be configured via routing to create nice looking url’s like for example /Categories/Shoes/Page/1 instead of /Paging/ViewByCategory?name=Shoes&page=1.
Then you can register your route like:
routes.MapRoute(
"PagingRoute", // Route name
"Categories/{type}/Page/{page}", // URL with parameters
new { controller = "PagingControllerName",
action = "Index",
type = UrlParameter.Optional,
page = UrlParameter.Optional } // Parameter defaults,
);
and the matching method in the PagingController would be
public ActionResult Index(string type, int? page) { // do something}
update
If you are using Martijn Bolands pager, look at the second route he maps/registers:
routes.MapRoute("SimplePaging", "SimplePaging/{page}",
new { controller = "Paging", action = "Index", page = UrlParameter.Optional },
new[] { "MvcPaging.Demo.Controllers" });
then yours should be about the same (i.e. adding the UrlParameter page). But that would mean your url will always look like /Page1Name/[pagenumber]. If on the other hand Page1Name is your actual action, you should define your Route like:
routes.MapRoute("Page1 name", "{action}/{page}",
new { controller = "Paging", action = "Page1Name", page = UrlParameter.Optional },
);

Related

Redirect to Area without changing the URL and TempData

In my project I have to redirect from one controller to another controller which is present inside the Areas named SIP. If use the following method the redirection works successfully and also the TempData value is passed to the other controller:
TempData["sipModel"] = 1;
return RedirectToAction("Index", "Home", new { area = "SIP" });
But in this case the URL gets changed while my requirement is to keep the same URL, to achieve that I went though other answers and used the method TransferToAction() mentioned
in this answer
This works perfectly and I'm able to redirect to the other area without changing the URL with the following code:
TempData["sipModel"] = 1;
return this.TransferToAction("Index", "Home", new { area = "SIP"});
However, in this case the TempData value is not retained and I get Null Reference Exception while trying to read the same.
I tried to use the following code mentioned in the other answer:
public static TransferToRouteResult TransferToAction(this System.Web.Mvc.Controller controller, string actionName, string controllerName, object routeValues)
{
controller.TempData.Keep();
controller.TempData.Save(controller.ControllerContext, controller.TempDataProvider);
return new TransferToRouteResult(controller.Request.RequestContext, actionName, controllerName, routeValues);
}
But this doesn't work out. Can somebody please suggest me how can I fix this or any other better approach to achieve this result. Thanks.
Edited:
The URL is like:
https://myproject/Home/Index?cid=ABC-1234&pid=xyz123456abc
I have a complex data in a class which also needs to be passed from the one controller to the other (which is present in the Area SIP), for that I've been using TempData, I've used an integer here just as a sample.
In the first controller method I've if-else condition, so:
if (companyCode = 'X')
return View();
else
TempData["sipModel"] = 1;
return RedirectToAction("Index", "Home", new { area = "SIP" }); OR (this.TransferToAction("Index", "Home", new { area = "SIP"});)
Server.TransferRequest is completely unnecessary in MVC. This is an antiquated feature that was only necessary in ASP.NET because the request came directly to a page and there needed to be a way to transfer a request to another page. Modern versions of ASP.NET (including MVC) have a routing infrastructure that can be customized to route directly to the resource that is desired. There is no point of letting the request reach a controller only to transfer it to another controller when you can simply make the request go directly to the controller and action you want.
So, given your example is not a complete set of requirements I will make the following assumptions. Adjust these as necessary for your requirements.
If there are no query string parameters passed to the home page, it will stay on the home page.
If there is a query parameter cid or pid on the home page, we will send the request to the Index action of the HomeController of the SID area.
We will pass a metadata parameter "sipModel" with value 1 in the latter case and omit the parameter in the first case.
First of all, we subclass RouteBase and put our custom logic there. A more complete scenario might have dependent services and options passed in through the constructor, and even have its own MapRoute extension methods to wire it together.
public class CustomHomePageRoute : RouteBase
{
public override RouteData GetRouteData(HttpContextBase httpContext)
{
RouteData result = null;
// Only handle the home page route
if (httpContext.Request.Path == "/")
{
var cid = httpContext.Request.QueryString["cid"];
var pid = httpContext.Request.QueryString["pid"];
result = new RouteData(this, new MvcRouteHandler());
if (string.IsNullOrEmpty(cid) && string.IsNullOrEmpty(pid))
{
// Go to the HomeController.Index action of the non-area
result.Values["controller"] = "Home";
result.Values["action"] = "Index";
// NOTE: Since the controller names are ambiguous between the non-area
// and area route, this extra namespace info is required to disambiguate them.
// This is not necessary if the controller names differ.
result.DataTokens["Namespaces"] = new string[] { "WebApplication23.Controllers" };
}
else
{
// Go to the HomeController.Index action of the SID area
result.Values["controller"] = "Home";
result.Values["action"] = "Index";
// This tells MVC to change areas to SID
result.DataTokens["area"] = "SID";
// Set additional data for sipModel.
// This can be read from the HomeController.Index action by
// adding a parameter "int sipModel".
result.Values["sipModel"] = 1;
// NOTE: Since the controller names are ambiguous between the non-area
// and area route, this extra namespace info is required to disambiguate them.
// This is not necessary if the controller names differ.
result.DataTokens["Namespaces"] = new string[] { "WebApplication23.Areas.SID.Controllers" };
}
}
// If this isn't the home page route, this should return null
// which instructs routing to try the next route in the route table.
return result;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
var controller = Convert.ToString(values["controller"]);
var action = Convert.ToString(values["action"]);
if (controller.Equals("Home", StringComparison.OrdinalIgnoreCase) &&
action.Equals("Index", StringComparison.OrdinalIgnoreCase))
{
// Route to the Home page URL
return new VirtualPathData(this, "");
}
return null;
}
}
To wire this into MVC, we just edit the RouteConfig as follows:
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
// Add the custom route to the static routing collection
routes.Add(new CustomHomePageRoute());
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
namespaces: new string[] { "WebApplication23.Controllers" }
);
}
}
This passes an extra route value sipModel to the PID area's HomeController.Index method. So, we need to adjust the method signature to accept that parameter.
namespace WebApplication23.Areas.SID.Controllers
{
public class HomeController : Controller
{
// GET: SID/Home
public ActionResult Index(int sipModel)
{
return View();
}
}
}
As you can see, there really is no reason to use TempData, either. TempData relies on session state by default. It has its uses, but you should always think twice about using session state in MVC as it can often be avoided entirely.

Tenant area route with default route

So I have a site that has basically an 'area' per tenant. so it will show up as www.site.com/ and that will go to that groups page using an area.
Thing is I also have a default route for outside the area so you can go to www.site.com/ which will take you to the actual ~/Views/Home/Index page. However if you try to type www.site.com/Home/Index or say the page to create a new group www.site.com/Group/Create it thinks it needs to go to the area which that doesn't exist and gives the 404 resource cannot be found.
Here is the default route in the RouteConfig.cs
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
new[] { "TicketSystem.Controllers" }
);
Here is the route config for the area:
context.MapRoute(
"Group_default",
"{group}/{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
new[] { "TicketSystem.Areas.Group.Controllers" });
so the {group} is whatever group you are currently visiting and then it goes to the regular controller/action for that group. However for the default route it still seems to go to the area route instead no matter what.
I was thinking that there could be a fallback. So when it tries to go to the area and it can't find the correct controller/action it will check the default route next. If it still can't find anything it will give the 404 error resource cannot be found. Though I am not exactly sure how to do this.
So to make www.site.com/ to work and allow www.site.com/Home/Index to work.
The problem is, When you try to access /Home/Index The route engine does not know by "Home" , you meant the controller name or a groupName!
To solve this, you can create a custom route constraint which checks whether the group value in the request url is a valid controller name in your app. If yes, The request won't be handled by the area route registration definition.
public class GroupNameConstraint : IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route, string parameterName,
RouteValueDictionary values, RouteDirection routeDirection)
{
var asm = Assembly.GetExecutingAssembly();
//Get all the controller names
var controllerTypes = (from t in asm.GetExportedTypes()
where typeof(IController).IsAssignableFrom(t)
select t.Name.Replace("Controller", ""));
var groupName = values["group"];
if (groupName != null)
{
if (controllerTypes.Any(x => x.Equals(groupName.ToString(),
StringComparison.OrdinalIgnoreCase)))
{
return false;
}
}
return true;
}
}
Register this constraint when you register your area route.
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"Group_default",
"{group}/{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
new { anything = new GroupNameConstraint() }
);
}
This should work assuming you will never have a groupName same as your controller name (Ex : Home )

RouteValueDictionary coming across as null in Extension method

Using bootstrap I ran into the problem of having to set the active class on certain links. I found this helper method out on the interwebs and was adding some functionality to it. Basically I wanted to add RouteValueDictionary and Html items to it. For some reason, my view is passing a null RouteValueDictionary instead of the values I'm populating.
I think my problem is I am missing something on the view side, the HTML rendered might not actually produce a route value the way it should.
Extension method:
public static MvcHtmlString MenuLink(this HtmlHelper pHtmlHelper, string pLinkText, string pActionName, string pControllerName, RouteValueDictionary pRouteValues)
{
var vCurrentAction = pHtmlHelper.ViewContext.RouteData.GetRequiredString("action");
var vCurrentController = pHtmlHelper.ViewContext.RouteData.GetRequiredString("controller");
var vBuilder = new TagBuilder("li") { InnerHtml = pHtmlHelper.ActionLink(pLinkText, pActionName, pControllerName, pRouteValues, null).ToHtmlString() };
if (pControllerName == vCurrentController && pActionName == vCurrentAction) vBuilder.AddCssClass("active");
return new MvcHtmlString(vBuilder.ToString());
}
View (portion):
#Html.MenuLink("Account", "Details", "Dashboard", new RouteValueDictionary(new {id="Account"}))
Rendered HTML:
Account
So again, the problem I'm having is MenuLink gets a null RouteValueDictionary, I feel like it should contain id!
(Note... pHtmlHelper.ViewContext.RouteData actually contains the id tag in it's RouteData Values....)
Your code is working as expected assuming you have a route that looks like (depending on the order of the routes):
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters*
new { controller = "Home", action = "Index",
id = UrlParameter.Optional }
);
So the code:
pHtmlHelper.ActionLink(
pLinkText, // Link Text
pActionName, // Action
pControllerName, // Controller
pRouteValues, // Route Values
null)
.ToHtmlString() };
The Url would be
/Dashboard/Details/{id}
Route Values:
{
id="Account"
}
becomes:
/Dashboard/Details/Account

MVC3 Route/redirect problem

My application is multilingual and I wrote the following route in order to handle the languages:
routes.MapRoute(
"Default", // Route name
"{language}/{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index",
language = "pt", id = UrlParameter.Optional }, // Parameter defaults
new { language = #"(pt)|(es)|(en)" }
);
This works for domain.com and domain.com/pt/home/index. However, if I type domain.com/home/index it fails (404).
The desired behavior would be it being redirected to domain.com/pt/home/index (pt is the default language).
Whats the best way to achieve this? I've been reading a lot about routes and ActionFilters but nothing seems quite right.
i would suggest using custom route handler like following
public class LanguageRouteHandler : IRouteHandler
{
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
IRouteHandler handler = new MvcRouteHandler();
var vals = requestContext.RouteData.Values;
if(vals["language"] == null)
{
vals["language"] = "pt";
}
return handler.GetHttpHandler(requestContext);
}
}
and have another route without language route value and set its route handler (global.asax)
routes.MapRoute(
"Default2", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
).RouteHandler = new LanguageRouteHandler();
This will not redirect Home/index to pt/home/index yet it will provide language = "pt" to your index action method (and all others). if you want to redirect you can implement an actionfilter but redirecting will create problems with post requests. For example when you post a form to /home/index and suppose it is redirected by action filter, the redirected request will lose posted form data
You need two routes, the other one without the language, or add the language parameter at the end
Try this
routes.MapRoute(
"Default", // Route name
"{language}/{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional }, // Parameter defaults
new { language = #"(pt)|(es)|(en)" }
);
and modify your action in this way
public ActionResult Index([DefaultValue("pt")] string language)
{
...
}

ASP.NET MVC 2 Routing with additional consistent parameters (not just controller & action)

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" })

Categories