MvcSiteMapProvider Node URL rendered as /# - mvcsitemapprovider

As the title states, the URL for one of the nodes in the breadcrumb is not correct. I just get a http://localhost/#
Obviously I have something wrong.
I have other, similar structures in the sitemap that are working. Can you tell from this whats missing?
I can post more info if needed.
SiteMap :
<mvcSiteMapNode title="ISP" controller="xxx" action="Index">
<mvcSiteMapNode title="PC" action="Details" preservedRouteParameters="pcId">
<mvcSiteMapNode title="SGD" controller="yyy" action="Details" preservedRouteParameters="pcId, yyyId, editable">
<mvcSiteMapNode title="ESGN" controller="yyy" action="Title" preservedRouteParameters="pcId, yyyId, editable" />
</mvcSiteMapNode>
Actions:
[HttpGet]
[Route("xxx/{pcId:int}/yyy/{yyyId:int}/Details/{editable:bool}")]
public virtual ActionResult Details(int pcId, int yyyId, bool editable)
{
[HttpGet]
[Route("xxx/{pcId:int}/yyy/{yyyId:int}/Title")]
public virtual ActionResult Title(int pcId, int yyyId)
{
Route Map:
routes.MapRoute(
name: "xxx",
url: "xxx/{action}/{pcId}",
defaults: new
{
controller = "xxx",
action = "Index",
pcId = UrlParameter.Optional
}
);
Update: When removing the "editable" parameter it started to work.
Could there be an issue with more than 2 params? or possibly the type or name of the parameter?
Update following debug advice from NightOwl88:
The urlHelper does generate the correct url's
This is my controller code:
[HttpGet]
[Route("TransactionDetails/File/{fileId:int}")]
public virtual ActionResult Index(int fileId)
{
{
var urlHelper = new UrlHelper(new System.Web.Routing.RequestContext(this.HttpContext, this.RouteData));
var url = urlHelper.Action("Index", "Transaction",
new System.Web.Routing.RouteValueDictionary { { "id", 678 } });
System.Diagnostics.Debug.WriteLine(url);
}
{
var urlHelper = new UrlHelper(new System.Web.Routing.RequestContext(this.HttpContext, this.RouteData));
var url = urlHelper.Action("Index", "File",
new System.Web.Routing.RouteValueDictionary {{"fileId", 123}});
System.Diagnostics.Debug.WriteLine(url);
}
I get:
/AdministratorConsole/TransactionDetails/678
and
/AdministratorConsole/TransactionDetails/File/123
So the helper is able to generate a url for me but MvcSiteMapProvider is still not happy.
SiteMap is:
<mvcSiteMapNode title="Transaction Log" controller="TransactionLog" action="Index">
<mvcSiteMapNode title="Transaction Details" controller="Transaction" action="Index" preservedRouteParameters="id">
<mvcSiteMapNode title="File Details" controller="File" action="Index" preservedRouteParameters="id, fileId"> <!--TODO link back to parent not working-->

A # indicates the URL could not be resolved based on the information provided (between the current request and what is in the node configuration). See Why does Default UrlResolver throw an exception for an explanation.
MvcSiteMapProvider resolves the URL through MVC's UrlHelper class, so if you are having trouble, you should use the UrlHelper explicitly to troubleshoot. If you put the below code into a controller and edit it to match the request that is generating a #, you will be able to determine how to resolve the URL correctly. However, unlike in MvcSiteMapProvider, the UrlHelper will return null if the URL cannot be resolved. The most likely reason is that you are missing a route value that you have set as required by the route.
// Using controller and action
var urlHelper = new UrlHelper(new System.Web.Routing.RequestContext(this.HttpContext, this.RouteData));
var url = urlHelper.Action("View", "Videos", new System.Web.Routing.RouteValueDictionary { { "id", 123 } });
// Using controller, action, and route name (similar to #Html.RouteLink())
var urlHelper = new UrlHelper(new System.Web.Routing.RequestContext(this.HttpContext, this.RouteData));
var url = urlHelper.RouteUrl("RouteName", new System.Web.Routing.RouteValueDictionary { { "controller", "Videos" }, { "action", "View" }, { "id", 123 } });

Related

struggling with an ASP.NET MVC5 routing issue

So, I have an MVC5 site that uses the default routing template {controller}/{action}/{id} and this works fine. Most everything in the site requires a login (i.e. [Authorize] attribute is used almost everywhere), and this works fine.
Well, now I have a need to allow anonymous access to select pages when a certain kind of link pattern is used: App/{token}/{action}. The {token} is a random string associated with something in my database. I can issue and deactivate these tokens at will.
I got this new App/{token}/{action} routing working by implementing a custom RouteBase that parses the incoming URL for these tokens, and, crucially, adds the the token value to the RouteData.DataTokens so that my App controller can make use of it without needing an explicit action argument for it. So, I added this new route to the route table ahead of the default routing like this:
// new route here
routes.Add("AppToken", new AnonAppAccessRoute());
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
Here is the problem/question: adding this now route has now made my default route stop working -- everything is now going through AnonAppAccessRoute which of course is meant to work only for a few things. I don't understand how to make my AnonAppAccessRoute apply only to URLs with a certain pattern. The MapRoute method accepts a URL pattern, but Adding a route doesn't seem to let you put a filter on it. What am I missing? I've looked around quite a bit at various blogs and documentation about routing, but I've not found good info about using the DataTokens collection (which I feel is important to my approach), and I'm not seeing a good explanation of the difference between Adding a route explicitly vs calling MapRoute.
Here's the code of my custom RouteBase:
public class AnonAppAccessRoute : RouteBase
{
public override RouteData GetRouteData(HttpContextBase httpContext)
{
RouteData result = null;
string[] pathElements = httpContext.Request.Path.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
if (pathElements.Length > 0)
{
string token = TryGetArrayElement(pathElements, 1);
if (!string.IsNullOrEmpty(token))
{
result = new RouteData(this, new MvcRouteHandler());
result.DataTokens.Add("appToken", token);
result.Values.Add("controller", "App");
result.Values.Add("action", TryGetArrayElement(pathElements, 2, "Index"));
}
}
return result;
}
private string TryGetArrayElement(string[] array, int index, string defaultValue = null)
{
try
{
return array[index];
}
catch
{
return defaultValue;
}
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
return null;
}
}
I got this to work by dropping the custom RouteBase and instead used this MapRoute call like this:
routes.MapRoute(
name: "AppAnon",
url: "App/{token}/{action}",
defaults: new { controller = "App", action = "Index" }
);
Then, in my App controller, I did this in the Initialize override:
protected AppToken _appToken = null;
protected override void Initialize(RequestContext requestContext)
{
base.Initialize(requestContext);
string token = requestContext.RouteData.Values["token"]?.ToString();
_appToken = Db.FindWhere<AppToken>("[Token]=#token", new { token });
if (!_appToken?.IsActive ?? false) throw new Exception("The token is not found or inactive.");
}
This way, my "token" is available to all controller actions via the _appToken variable, and already validated. I did not need to use RouteData.DataTokens. Note that my Db.FindWhere statement is ORM-specific and not really related to the question -- it's just how I look up a database record.

MVC 5 Pretty URL

Hi there
I'm working on a system where I have been asked to change the URL in the address line.
To take the short version, we have a profile page for all our lorries, let's say we have a lorry named SuperTransport, so I've made a routing that allows us to access his profile page by typing http: //app.fragtopgaver.dk/SuperTransport, problems are now that when you come to his profile page, something else says in the URL, which says http://app.fragtopgaver.dk/getindex/?slug=supertransport
I need that it still says http://app.fragtopgaver.dk/SuperTransport in the URL when landing on the page.
My routing looks like this:
routes.MapRoute(
name: "slug",
url: "{slug}",
defaults: new { controller = "Home", action = "show" },
constraints: new { slug = ".+" });
And in my Home Controller
public async Task<ActionResult> Show(string slug)
{
return RedirectToRoute(ProfileControllerRoute.GetIndex, new { slug = slug});
}
and my Profile Controller
[Route("GetIndex", Name = ProfileControllerRoute.GetIndex)]
public ActionResult Index(int? page, string slug = null)
Hope someone can give me a hint of what i can do about this.
The Answer was really simple, just had to add
[Route("{slug}")]
to the controller

Optional parameter "Id" is same for all nodes in MvcSiteMapProvide

following is my sitemap code :
<mvcSiteMapNode title="Partner" controller="Partner" key="Partner" action="ShowPartners" >
<mvcSiteMapNode title="ISP" controller="ISP" key="ISP" action="ShowPartnersIsps" preservedRouteParameters="Id" >
<mvcSiteMapNode title="Operator" controller="Operator" key="Operator" action="ShowIspsOperators" preservedRouteParameters="Id" >
<mvcSiteMapNode title="Subscriber" controller="Subscriber" key="Subscriber" action="ShowOperatorsSubscribers" preservedRouteParameters="Id" >
<mvcSiteMapNode title="Router" controller="Router" key="Router" action="ShowSubscribersRouters" preservedRouteParameters="Id" />
</mvcSiteMapNode>
</mvcSiteMapNode>
</mvcSiteMapNode>
</mvcSiteMapNode>
following is my route.config
routes.MapRoute(
"GetPartnerRoute",
"Partner/ShowPartners/{search}",
new { controller = "Partner", action = "ShowPartners", Search = UrlParameter.Optional }
);
routes.MapRoute(
"GetISPRoute",
"ISP/ShowPartnersIsps/{Id}/{Search}",
new { controller = "ISP", action = "ShowPartnersIsps", Id = UrlParameter.Optional, Search = UrlParameter.Optional }
);
routes.MapRoute(
"GetOperatorRoute",
"Operator/ShowIspsOperators/{Id}/{Search}",
new { controller = "Operator", action = "ShowIspsOperators", Id = UrlParameter.Optional, Search = UrlParameter.Optional }
);
routes.MapRoute(
"GetSubscriberRoute",
"Subscriber/ShowOperatorsSubscribers/{Id}/{Search}",
new { controller = "Subscriber", action = "ShowOperatorsSubscribers", Id = UrlParameter.Optional, Search = UrlParameter.Optional }
);
routes.MapRoute(
"GetRouterRoute",
"Router/ShowSubscribersRouters/{Id}/{Search}",
new { controller = "Router", action = "ShowSubscribersRouters", Id = UrlParameter.Optional, Search = UrlParameter.Optional }
);
the id parameters is not same for all node.
in above situation.
each node has different "Id" value. which not similar for every node. by renaming "Id" i can achieve what i expected. but i cant change the name of "Id".
so when i goes to child node which having parameter "Id" it sets similar value to its parent node.
following is my code after inspect :
Home
>
Partner
>
ISP
>
Operator
>
Subscriber
>
"268e4984-0923-4db7-8dd3-78564663e4d1" is similar for every node. which should be different.
how can i achieve this. please help
When using preservedRouteParameters, the values are "preserved" from the current request. By definition that means a route value can only be used for a singular purpose (although that purpose can span multiple nodes).
There are 2 ways to overcome this:
Use a different route key for each purpose ({ispId}, {operatorId}, etc).
Instead of using preservedRouteParameters, use the default behavior of MvcSitemapProviderwhich expects a unique node perid`. You can build up the parent-child relationships by using a dynamic node provider.
The first option is more scalable. The second option gives you more control (but only scales to about 10,000 nodes).
Do note that for preservedRouteParameters to work right, you need to put all of the ancestor's route values into the current request. So, for example you must provide ispId for the Operator node. Even though your action method may not require it, the parent node ISP still does for it to build the URL correctly.
See demos of both of these options here: https://github.com/NightOwl888/MvcSiteMapProvider-Remember-User-Position

Custom routes and namespaces in MVC5

I'm trying to implement some domain name logic in my existing MVC5 app. The problem I'm running in to is if I try to use my custom subclass from Route, it doesn't respect the Namespaces field and throws an error because I have 2 different User controllers.
As a control, this works perfectly fine:
routes.MapRoute("Login",
"login/",
new { controller = "User", action = "Login" },
new[] { "Quotes.Web.Controllers" });
My DomainRoute class inherits from Route and just adds a Domain property. Here is the relevant constructor:
public DomainRoute(string domain, string url, object defaults, string[] namespaces = null)
: base(url, new RouteValueDictionary(defaults), new MvcRouteHandler())
{
Domain = domain;
DataTokens = new RouteValueDictionary {["Namespaces"] = namespaces};
}
and I register it like:
var loginRoute = new DomainRoute(
domain,
"login/",
new { controller = "User", action = "Login" },
new[] { "Quotes.Web.Controllers" });
routes.Add("Login", loginRoute);
DataTokens looks identical between the working version and my broken version yet it seems to ignore the fact that my DomainRoute has a Namespace entry
Multiple types were found that match the controller named 'User'. This can happen if the route that services this request ('login/') does not specify namespaces to search for a controller that matches the request. If this is the case, register this route by calling an overload of the 'MapRoute' method that takes a 'namespaces' parameter.
What am I missing?
I think,this will help you, i had the same issue, solved this by adding the below code
var dataTokens = new RouteValueDictionary();
var ns = new string[] {"MyProject.Controllers"};
dataTokens["Namespaces"] = ns;
routes.Add("Default", new CultureRoute(
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
null /*constraints*/,
dataTokens
));
I switched my DomainRoute class with the much improved version found here: https://gist.github.com/IDisposable/77f11c6f7693f9d181bb
Now my route creation is just:
var clientRoutes = new DomainRouteCollection("mydomain",
"Quotes.Web.Controllers",
routes);
clientRoutes.MapRoute("Login", "login/", new { controller = "User", action = "Login" });
...which is more concise and even more importantly, it works.

MVC 5 routing issue with new url for subcategories

In my productcontroller I have two actionresult returning methods:
[Route("Shop/{brand}/{category}/{subcategory?}/{page:int?}")]
public ActionResult Index(string brand, string category, string subcategory, int? page, SortOptions currentSort = SortOptions.SinceDesc)
{ //...
and
[HttpPost]
[Route("Shop/{brand}/{category}/{subcategory?}/{page:int?}")]
public ActionResult Index(ProductsViewModel pvm)
{ //...
And this is my razor view:
#using (#Html.BeginForm("Index", "Products", FormMethod.Post))
{
#Html.DropDownListFor(x => x.SubCatID, Model.SubCategoriesSelectList, new { #class = "multiselect" })
}
When I submit the page it hits the httppost method but the url is still: Shop/nike/shoes even when I selected the subcategory runningshoes from the dropdown.
I would like to have urls like:
shop/nike/shoes
shop/nike/shoes/runningshoes
Being more of a webform-guy I am having a hard time to navigate to new url's and using viewmodel properties as parameters.
edit posted my UI:
to explain my ui:
the first dropdown should get to a subcategory. for instance: shop/nike/shoes/runningshoes
The second should post 'back' to sort the products.
The price slider should post back because it should filter. (would filter client side if there was no paging)
The paging should get so you can deeplink to a certain page: shop/nike/shoes/runningshoes/page2 etc.
In your BeginForm(...) you end up having to pass a route values dictionary with Subcategory = "runningshoes"
This does mix values being passed by GET, aka in the querystring through the route values dictionary, and POST, which will be the values from the form, but will accomplish what you are trying to do. More can be read about the BeginForm(..) overload Here on MSDN
You should end up with:
#using (#Html.BeginForm("Index", "Products", new { subcategory = "runningshoes" }, FormMethod.Post))
{
#Html.DropDownListFor(x => x.SubCatID, Model.SubCategoriesSelectList, new { #class = "multiselect" })
}
EDIT
Just realized you want the value from the form post to be in the QueryString on the response. Instead of returning the view directly from your MVC method for the Form Post what you could possibly do is a return RedirectToAction("ActionName", "ControllerName", new { subcategory = request.SubCategory }); which you would have an action that would support this redirect specifically.
Additional information on redirect to action can be found here on MSDN

Resources