asp .net mvc routing url with custom literal - asp.net-mvc

Is it possible to make url with custom literal separator that can have default parameters ?
context.MapRoute(
"Forums_links",
"Forum/{forumId}-{name}",
new { area = "Forums", action = "Index", controller = "Forum" },
new[] { "Jami.Web.Areas.Forums.Controllers" }
);
I have this as you see im using to dash to separate id from name so I can have url like:
/Forum/1-forum-name
Instead of:
/Forum/1/forum-name
I see the problem is I'm using multiple dashes. And routing engine don't know which one to separate. But overalll it doesn't change my question because I want to use multiple dashes anyway.

Very interesting question.
The only way I could come up with is much like Daniel's, with one extra feature.
context.MapRoute(
"Forums_links",
"Forum/{forumIdAndName}",
new { area = "Forums", action = "Index", controller = "Forum" },
new { item = #"^\d+-(([a-zA-Z0-9]+)-)*([a-zA-Z0-9]+)$" } //constraint
new[] { "Jami.Web.Areas.Forums.Controllers" }
);
That way, the only items that will get matched to this route are ones formatted in the pattern of:
[one or more digit]-[zero or more repeating groups of string separated by dashes]-[final string]
From here you would use the method Daniel posted to parse the data you need from the forumIdAndName parameter.

One way to achieve this could be by combining id and name into the same route value:
context.MapRoute(
"Forums_links",
"Forum/{forumIdAndName}",
new { area = "Forums", action = "Index", controller = "Forum" },
new[] { "Jami.Web.Areas.Forums.Controllers" }
);
And then extract the Id from it:
private static int? GetForumId(string forumIdAndName)
{
int i = forumIdAndName.IndexOf("-");
if (i < 1) return null;
string s = forumIdAndName.Substring(0, i);
int id;
if (!int.TryParse(s, out id)) return null;
return id;
}

Related

MVC Attribute routing with Url.Action not resolving route

I cannot get #Url.Action to resolve to the url I am expecting based on the attribute route I have applied:
My action (SearchController but with [RoutePrefix("add")])
[Route("{searchTerm}/page/{page?}", Name = "NamedSearch")]
[Route("~/add")]
public ActionResult Index(string searchTerm = "", int page = 1)
{
...
}
Call to Url.Action
#Url.Action("Index", new { controller = "Search", searchTerm = "replaceMe", page = 1 })
This results in a url of
/add?searchTerm=replaceMe&page=1
I would expect
/add/replaceMe/page/1
If I type the url manually then it resolves to the correct action with the correct parameters. Why doesn't #Url.Action resolve the correct url?
Since you have a name for your pretty route definition, you may use the RouteUrl method.
#Url.RouteUrl("NamedSearch", new { searchTerm = "replaceMe", page = 1})
And since you need add in the url, you should update your route definition to include that in the url pattern.
[Route("~/add")]
[Route("~/add/{searchTerm?}/page/{page?}", Name = "NamedSearch")]
public ActionResult Index(string searchTerm = "", int page = 1)
{
// to do : return something
}
Routes are order sensitive. However, attributes are not. In fact, when using 2 Route attributes on a single action like this you may find that it works on some compiles and not on others because Reflection does not guarantee an order when analyzing custom attributes.
To ensure your routes are entered into the route table in the correct order, you need to add the Order property to each attribute.
[Route("{searchTerm}/page/{page?}", Name = "NamedSearch", Order = 1)]
[Route("~/add", Order = 2)]
public ActionResult Index(string searchTerm = "", int page = 1)
{
return View();
}
After you fix the ordering problem, the URL resolves the way you expect.
#Url.Action("Index", new { controller = "Search", searchTerm = "replaceMe", page = 1 })
// Returns "/add/replaceMe/page/1"
To return full URL use this
#Url.Action("Index", new { controller = "Search", searchTerm = "replaceMe", page = 1}, protocol: Request.Url.Scheme)
// Returns "http://yourdomain.com/add/replaceMe/page/1"
Hope this helps someone.

How to customize route to map url dynamically to a composed named controller in ASP.NET MVC3

I need to map URLs like this:
/stock/risk -->StockRiskController.Index()
/stock/risk/attr -->StockRiskController.Attr()
/srock/risk/chart -->StockRiskController.Chart()
...
/bond/performance -->BondPerformanceController.Index()
/bond/performance/attr -->BondPerformanceController.Attr()
/bond/performance/chart -->BondPerformanceController.Chart()
...
The first part is dynamic but enumerable, the second part has only two options(risk|performance).
For now I know only two ways:
customized a ControllerFactory(seems overkilled or complicated)
hard code all the combinations because they are enumerable(ugly).
Can I use routes.MapRoute to achieve this? Or any other handy way?
There is a nice solution based on IRouteConstraint. First of all we have to create new route mapping:
routes.MapRoute(
name: "PrefixedMap",
url: "{prefix}/{body}/{action}/{id}",
defaults: new { prefix = string.Empty, body = string.Empty
, action = "Index", id = string.Empty },
constraints: new { lang = new MyRouteConstraint() }
);
Next step is to create our Constraint. Before I will introduce some way how to check relevance as mentioned above - two list with possible values, but logic could be adjusted
public class MyRouteConstraint : IRouteConstraint
{
public readonly IList<string> ControllerPrefixes = new List<string> { "stock", "bond" };
public readonly IList<string> ControllerBodies = new List<string> { "risk", "performance" };
...
And now the Match method, which will adjust the routing as we need
public bool Match(System.Web.HttpContextBase httpContext
, Route route, string parameterName, RouteValueDictionary values
, RouteDirection routeDirection)
{
// for now skip the Url generation
if (routeDirection.Equals(RouteDirection.UrlGeneration))
{
return false;
}
// try to find out our parameters
string prefix = values["prefix"].ToString();
string body = values["body"].ToString();
var arePartsKnown =
ControllerPrefixes.Contains(prefix, StringComparer.InvariantCultureIgnoreCase) &&
ControllerBodies.Contains(body, StringComparer.InvariantCultureIgnoreCase);
// not our case
if (!arePartsKnown)
{
return false;
}
// change controller value
values["controller"] = prefix + body;
values.Remove("prefix");
values.Remove("body");
return true;
}
You can play with this method more, but the concept should be clear now.
NOTE: I like your approach. Sometimes it is simply much more important to extend/adjust routing then go to code and "fix names". Similar solution was working here: Dynamically modify RouteValueDictionary

MVC RouteDatas are confused

I am using asp.net mvc for my website project. I think i have wrong things in my routedata but i am not sure it is wrong or ok. i will explain the situation.
I am caching my action results (html outputs) in Cache with a generated key
public static string GetKeyFromActionExecutingContext(ControllerContext filterContext)
{
StringBuilder keyBuilder = new StringBuilder();
if (filterContext.IsChildAction)
keyBuilder.Append("C-");
else
keyBuilder.Append("P-");
foreach (var item in filterContext.RouteData.Values)
{
keyBuilder.AppendFormat("{0}={1}.", item.Key, item.Value);
}
return keyBuilder.ToString();
}
ex: For HomePage , generated cache key is P-Controller=Home.Action=Index and
I have also childactions in my sitemaster like LoginBox(It is in MembershipController/LoginBox)
Its cache key is C-Controller=Membership.Action=LoginBox.
Everything is okey till now.
I have also subcategories in my website like
domain/category1
domain/category1/subcategory1
domain/category1/subcategory2
domain/category2
When i am browsing a sub category from domain/category1
My generated keys are failed because my routedatas are wrong
filterContext.RouteData.Values:
Controller = Membership
Action = LoginBox
ctg1 = category1
ctg2 = ""
ctg3 = ""
Why these are mixed. It is using the "Category" routemapping but I think it must use "Default" routemapping.
My global.asax like below
routes.MapRoute(
"Category",
"{ctg0}/{ctg1}/{ctg2}/{ctg3}",
new
{
controller = "Category",
action = "Index",
ctg0 = "",
ctg1 = "",
ctg2 = "",
ctg3 = ""
},
new
{
ctg0 = new CategoryRouteConstraint(),
}
);
routes.MapRoute(
"Default",
"{controller}/{action}/{id}", new { controller = "Home", action = "Index", id = "" },
new { controller = #"[^\.]*" }
);
Also my CategoryRouteConstraint Method it is checking from db that ctg0 value is a category name
public class CategoryRouteConstraint : IRouteConstraint
{
public Boolean Match(
HttpContextBase httpContext,
Route route,
String sParameterName,
RouteValueDictionary values,
RouteDirection routeDirection
)
{
if ((routeDirection == RouteDirection.IncomingRequest))
{
if (values["ctg0"] != null && !string.IsNullOrEmpty(values["ctg0"].ToString()))
return Category.IsRoutingForCategory(values["ctg0"].ToString());
return false;
}
return false;
}
}
Hopefully this may help you, it will show you which routes a url matches, I was a little confused by the question.
http://haacked.com/archive/2008/03/13/url-routing-debugger.aspx

Ambient values in mvc2.net routing

I have following two routes registered in my global.asax file
routes.MapRoute(
"strict",
"{controller}.mvc/{docid}/{action}/{id}",
new { action = "Index", id = "", docid = "" },
new { docid = #"\d+"}
);
routes.MapRoute(
"default",
"{controller}.mvc/{action}/{id}",
new { action = "Index", id = "" },
new { docConstraint = new DocumentConstraint() }
);
and I have a static "dashboard" link in my tabstrip and some other links that are constructed from values in db here is the code
<ul id="globalnav" class = "t-reset t-tabstrip-items">
<li class="bar" id = "dashboard">
<%=Html.ActionLink("dash.board", "Index", pck.Controller, new{docid =string.Empty,id = pck.PkgID }, new { #class = "here" })%>
</li>
<%
foreach (var md in pck.sysModules)
{
%>
<li class="<%=liClass%>">
<%=Html.ActionLink(md.ModuleName, md.ActionName, pck.Controller, new { docid = md.DocumentID}, new { #class = cls })%>
</li>
<%
}
%>
</ul>
Now my launching address is localhost/oa.mvc/index/11 clearly matching the 2nd route. But when I visit any page that has mapped to first route and then come back to dash.board link it shows me localhost/oa.mvc/7/index/11 where 7 is docid and picked from previous Url.
I understand that my action method is after docid and changing it would not clear the docid.
My question here is, can I remove docid in this scenario without changing the route?
I have the same "not clearing out" value problem...
I've stepped into source code and I don't understand the reason for being of segment commented as : // Add all current values that aren't in the URL at all
# System\Web\Routing\ParsedRoute.cs, public BoundUrl Bind(RouteValueDictionary currentValues, RouteValueDictionary values, RouteValueDictionary defaultValues, RouteValueDictionary constraints) method from line 91 to line 100
While the clearing process is correctly handled in method preceding steps, this code "reinjects" the undesired parameter into acceptedValues dictionary!?
My routing is defined this way:
routes.MapRoute(
"Planning",
"Plans/{plan}/{controller}/{action}/{identifier}",
new { controller = "General", action = "Planning", identifier = UrlParameter.Optional },
new { plan = #"^\d+$" }
);
// default application route
routes.MapRoute(
"Default",
"{controller}/{action}/{identifier}",
new {
controller = "General",
action = "Summary",
identifier = UrlParameter.Optional,
plan = string.Empty // mind this default !!!
}
);
This is very similar to what you're using. But mind my default route where I define defaults. Even though my default route doesn't define plan route value I still set it to string.Empty. So whenever I use Html.ActionLink() or Url.Action() and I want plan to be removed from the URL I call it the usual way:
Url.Action("Action", "Controller", new { plan = string.Empty });
And plan is not included in the URL query string any more. Try it out yourself it may work as well.
Muhammad, I suggest something like this :
(written 5 mn ago, not tested in production)
public static class MyHtmlHelperExtensions {
public static MvcHtmlString FixActionLink(this HtmlHelper htmlHelper, string linkText, string actionName, string controllerName, object routeValues, object htmlAttributes) {
var linkRvd = new RouteValueDictionary(routeValues);
var contextRvd = htmlHelper.ViewContext.RouteData.Values;
var contextRemovedRvd = new RouteValueDictionary();
// remove clearing route values from current context
foreach (var rv in linkRvd) {
if (string.IsNullOrEmpty((string)rv.Value) && contextRvd.ContainsKey(rv.Key)) {
contextRemovedRvd.Add(rv.Key, contextRvd[rv.Key]);
contextRvd.Remove(rv.Key);
}
}
// call ActionLink with modified context
var htmlString = htmlHelper.ActionLink(linkText, actionName, controllerName, routeValues, htmlAttributes);
// restore context
foreach (var rv in contextRemovedRvd) {
contextRvd.Add(rv.Key, rv.Value);
}
return htmlString;
}
}
This is such a frustrating problem and I would venture to say that it is even a bug in ASP.Net MVC. Luckily it's an easy fix using ActionFilters. If you are using MVC3 then I would just put this as a global attribute to clear out ambient values. I made this attribute discriminatory, but you can change it to clear all attributes.
The assumption here is that by the time the Result is executing (your view most likely), you have already explicitly specified all your ActionLinks and Form Actions. Thus this will execute before they (the links) are evaluated, giving you a new foundation to generate them.
public class ClearAmbientRouteValuesAttribute : ActionFilterAttribute
{
private readonly string[] _keys;
public ClearAmbientRouteValuesAttribute(params string [] keys)
{
if (keys == null)
_keys = new string[0];
_keys = keys;
}
public override void OnResultExecuting(ResultExecutingContext filterContext)
{
foreach (var key in _keys) {
// Why are you sticking around!!!
filterContext.RequestContext.RouteData.Values.Remove(key);
}
}
}
// Inside your Global.asax
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new HandleErrorAttribute());
filters.Add(new ClearAmbientRouteValuesAttribute("format"));
}
Hope this helps someone, cause it sure helped me. Thanks for asking this question.
In this particular scenario I have two recommendations:
Use named routes. The first parameter to the MapRoute method is a name. To generate links use Html.RouteLink() (and other similar APIs). This way you'll always choose the exact route that you want and never have to wonder what gets chosen.
If you still want to use Html.ActionLink() then explicitly set docid="" to clear out its value.
Here's how I solved my problem, it may take a little adapting to get it to work, but I felt like I could get what I needed and just use routing more or less normally:
Excerpted from Apress Pro ASP.Net.MVC 3 Framework:
A value must be available for every segment variable defined in the URL pattern.
To find values for each segment variable, the routing system looks first at the
values we have provided (using the properties of anonymous type), then the
variable values for the current request, and finally at the default values defined in
the route. (We return to the second source of these values later in this chapter.)
None of the values we provided for the segment variables may disagree with the
default-only variables defined in the route. These are variables for which default
values have been provided, but which do not occur in the URL pattern. For
example, in this route definition, myVar is a default-only variable:
routes.MapRoute("MyRoute", "{controller}/{action}",
new { myVar = "true" });
For this route to be a match, we must take care to not supply a value for myVar or to make
sure that the value we do supply matches the default value.
The values for all of the segment variables must satisfy the route constraints. See
the “Constraining Routes” section earlier in the chapter for examples of different
kinds of constraints.
Basically I used the rule about a route not matching if it doesn't define a segment, but has a default variable used to give me a little more control over whether a route was chosen for outbound routing or not.
Here's my fixed routes, notice how I specify a value for category that would never be valid and don't specify a segment for category. This means that route will be skipped if I have a category, but will use it if I only have a page:
routes.MapRoute(null, "receptionists/faq/{page}", new { controller = "Receptionist", action = "Faq", page = 1, category = (Object)null }, new { page = #"^\d+$" });
routes.MapRoute(null, "receptionists/faq/{category}/{page}", new { controller = "Receptionist", action = "Faq", page = 1 }, new { category = #"^\D+$", page = #"^\d+$" });
For Category Links
#Html.ActionLink("All", "Faq", new { page = 1 })
#foreach (var category in Model.Categories)
{
#Html.ActionLink(category.DisplayName, "faq", new { category = category.DisplayName.ToLower(), page = 1 })
}
For Page Links
#for (var p = 1; p <= Model.TotalPages; p++)
{
#Html.ActionLink(p.ToString(), "Faq", new { page = p, category = Model.CurrentCategory})
}

ASP.NET MVC: Route to URL

What's the easiest way to get the URL (relative or absolute) to a Route in MVC? I saw this code here on SO but it seems a little verbose and doesn't enumerate the RouteTable.
Example:
List<string> urlList = new List<string>();
urlList.Add(GetUrl(new { controller = "Help", action = "Edit" }));
urlList.Add(GetUrl(new { controller = "Help", action = "Create" }));
urlList.Add(GetUrl(new { controller = "About", action = "Company" }));
urlList.Add(GetUrl(new { controller = "About", action = "Management" }));
With:
protected string GetUrl(object routeValues)
{
RouteValueDictionary values = new RouteValueDictionary(routeValues);
RequestContext context = new RequestContext(HttpContext, RouteData);
string url = RouteTable.Routes.GetVirtualPath(context, values).VirtualPath;
return new Uri(Request.Url, url).AbsoluteUri;
}
What's a better way to examine the RouteTable and get a URL for a given controller and action?
Use the UrlHelper class: http://msdn.microsoft.com/en-us/library/system.web.mvc.urlhelper.aspx
You should be able to use it via the Url object in your controller. To map to an action, use the Action method: Url.Action("actionName","controllerName");.
A full list of overloads for the Action method is here: http://msdn.microsoft.com/en-us/library/system.web.mvc.urlhelper.action.aspx
so your code would look like this:
List<string> urlList = new List<string>();
urlList.Add(Url.Action("Edit", "Help"));
urlList.Add(Url.Action("Create", "Help"));
urlList.Add(Url.Action("Company", "About"));
urlList.Add(Url.Action("Management", "About"));
EDIT: It seems, from your new answer, that your trying to build a sitemap.
Have a look at this Codeplex project: http://mvcsitemap.codeplex.com/. I haven't used it myself, but it looks pretty solid.
How about this (in the controller):
public IEnumerable<SiteMapEntry> SiteMapEntries
{
get
{
var entries = new List<SiteMapEntry>();
foreach (var route in this.Routes)
{
entries.Add(new SiteMapEntry
(
this.Url.RouteUrl(route.Defaults),
SiteMapEntry.ChangeFrequency.Weekly,
DateTime.Now,
1F));
}
return entries;
}
}
Where the controller has member:
public IEnumerable<Route> Routes
Take note of:
this.Url.RouteUrl(route.Defaults)

Resources