Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SiteMapPath Issue with passing sourceMetadata parameters #383

Closed
fenildesai opened this issue Feb 9, 2015 · 10 comments
Closed

SiteMapPath Issue with passing sourceMetadata parameters #383

fenildesai opened this issue Feb 9, 2015 · 10 comments

Comments

@fenildesai
Copy link

Left hand nav works fine with sourceMetadata parameters
i.e. having something like
switch (condition)
{
case Condition1:
@Html.MvcSiteMap().Menu(new { name = "Condition1" })
case Condition2:
@Html.MvcSiteMap().Menu(new { name = "Condition2" })
}

the above gives the correct left hand navigation menu.

But when i use the same in SiteMapPath
i.e
switch (condition)
{
case Condition1:
@Html.MvcSiteMap().SiteMapPath(new { name = "Condition1" })
case Condition2:
@Html.MvcSiteMap().SiteMapPath(new { name = "Condition2" })
}

The above always shows the first mvcSiteMapNode from the sitemapFile & not the one depending on the name passed.

Sample sitemap is below:

<mvcSiteMapNode title="ProductName1" controller="Product" action="ProductDetails" productType="productname" visibility="Home,SomeOtherCondition,Condition1,SiteMapPathHelper,!*">
  <mvcSiteMapNode title="Choose your type" controller="Product" action="ChooseType" productType="productName" visibility="Home,SomeOtherCondition,Condition1,SiteMapPathHelper,!*">
    <mvcSiteMapNode title="Product1" controller="Product" action="Action1" visibility="Home,Condition1,SiteMapPathHelper,!*">
      <mvcSiteMapNode title="Product1Child" controller="Product" action="Acion2" visibility="Home,Condition1, SiteMapPathHelper,!*"/>
    </mvcSiteMapNode>
  </mvcSiteMapNode>
</mvcSiteMapNode>

<mvcSiteMapNode title="ProductName2" controller="Product" action="ProductDetails" productType="productname" visibility="Home,SomeOtherCondition,Condition2,SiteMapPathHelper,!*">
  <mvcSiteMapNode title="Choose your type" controller="Product" action="ChooseType" productType="productName" visibility="Home,SomeOtherCondition,Condition2,SiteMapPathHelper,!*">
    <mvcSiteMapNode title="Product2" controller="Product" action="Action1" visibility="Home,Condition2,SiteMapPathHelper,!*">
      <mvcSiteMapNode title="Product2Child" controller="Product" action="Acion2" visibility="Home,Condition2, SiteMapPathHelper,!*"/>
    </mvcSiteMapNode>
  </mvcSiteMapNode>

</mvcSiteMapNode>

The thing is when i come to the product page at that time i have a routeValues but when i go to the ProductChild pages then i don't have the routeValues passed.

Any help would be appreciated.

@NightOwl888
Copy link
Collaborator

Multiple nodes with exactly the same route values are not supported. When 2 nodes with the exact same route values are configured, the first match always wins - this is normal and expected behavior.

However, I can't tell from your comment exactly what you are trying to achieve. You are using named menu instances, but it isn't really necessary if you just want 2 menus with the exact same node structure. Also, I it appears that you intend some conditional logic, but since your visibility providers are showing the exact same structure I don't see where a condition would be useful. Please explain your use case.

@fenildesai
Copy link
Author

The structure is same but the products are different & this are just a few for an example scenario, have a few more as well.

The routeValues are different it will be productType = "product1", "product2", so on.

Depending on the condition, the product name in the left hand nav & breadcrumb should change that's what i want to achieve.

But seems like since the main productName1 page has a productType parameter while the Product1 doesn't, it is not working as expected.

If i pass a productType parameter to Product1, then it works. But ideally it should work without it as well, since the left hand nav is working without the parameter passed.

@NightOwl888
Copy link
Collaborator

There are 2 ways to handle custom route values as explained in the documentation:

  1. Use a dynamic node provider to add a node for every route value combination.
  2. Use preservedRouteParameters (in conjunction with SiteMapTitleAttribute and visibility providers).

Or you can combine both techniques (on different route values on the same node).

The first option works without any special hacks, but it doesn't scale to more than about 10,000 - 15,000 nodes. The second option scales much better, but requires that you include the custom route data (that is, any route key that is not "area", "controller", or "action") for parent nodes in each of the child nodes, and the keys must keep a consistent meaning for the same ancestry.

But I think the crux of your problem is that a) you are configuring multiple nodes with the same route combination, b) you aren't accounting for all of the route parameters, and c) you are putting a condition on which HTML helper instance to show, which is what visibility providers and/or custom HTML helpers are intended for.

The idea is that each route will match a different node, and the values in the menu and breadcrumb change according to which node is the "current" one.

Unless you are using preservedRouteParameters, in which case you need to make the title change dynamically. When you use preservedRouteParameters, it means that MvcSiteMapProvider will not automatically build the URLs for the menu. You have to use a table or a list of some kind to generate the hyperlinks, and then the breadcrumb will change according to which pages you visit. That is why you generally need to use a visibility provider to exclude the one node that matches all of the products (and leave its parent node) in the Menu and SiteMap.

There is a blog post with a downloadable demo that goes into detail about using the above techniques, although I haven't yet created a demo for a wizard if that is what you are doing.

@fenildesai
Copy link
Author

Thanks for the explanation, but not able to get much out of it.
Had a look at the demo but it is with id as parameter & in my case i don't have it.

Also I am not able to get why the left hand nav is working fine & the breadcrumbs are not.

By wizard you mean to say the breadcrumbs?

@NightOwl888
Copy link
Collaborator

By wizard, I mean that by your example it looks like you want the user to visit a product page, then visit a customization page for the product, then do two other actions on the product before perhaps submitting the whole build to some other controller (perhaps a shopping cart).

Here is your configuration (at least the part that makes sense):

Breadcrumb URL (assuming you are using the default MVC route)
Home > ProductName1 /Product/ProductDetails?productType=productname
Home > ProductName1 > Select your type /Product/ChooseType?productType=productname
Home > ProductName1 > Select your type > Product1 /Product/Action1
Home > ProductName1 > Select your type > Product1 > ProductChild /Product/Action2

The Product2 nodes have exactly the same route values as the Product1 nodes, so they will never match a request. I have removed them from this list because that is not a valid configuration.

But, you really haven't explained the workflow enough for me to give you any more than general advice. I can't tell if you intended your navigation structure to be like this or if you have something else in mind, but I suspect this isn't what you intended. And you haven't described your desired URL/Routing structure either, which is what drives the SiteMap.

It is unclear what Action1 and Action2 are - are these additional steps in a wizard? Are you editing different details on a product, and if so, why are they nested below the "Select your type" node instead of siblings to "Select your type"? Why do you have your nodes set up to show "Product1" twice in same the breadcrumb? Why is there no node to select from a list or category of products in your configuration (which is usually the first step whether you are browsing or editing products)? Typically selecting product options occurs on the product page, so what reason do you have to make "Select your type" as a separate page from the product page (if not making a multi-step wizard)? If you can show me a table of URLs and desired breadcrumbs for those URLs, I can show you a configuration that works with that structure, but unless you have an idea of where you are going I can't tell you how to get there.

Just so you understand, you are not building menus and breadcrumbs one at a time, you are building a map of nodes (representing URLs) that all of the navigation controls will be driven from. The design is very similar to using the ASP.NET 2.0 SiteMap Providers, except that you typically configure matching routes for MVC instead of matching URLs. The menus in your example show up fine because they do not depend on matching the current node to be visible, but the breadcrumb does. The route values that you enter into the node must match the route values in the request, then you will get a match, and the breadcrumbs will be visible. Understanding how to make the URL match the node is critical to get it working (which is why I sent you to the blog post that describes it in some detail).

And the key of the route value parameter is not important - MvcSiteMapProvider treats the key "id" the same as "productType" as in your example. The only condition is that they need to match the routes in the request. Those routes are configured in MVC. Note that I added the home page to your example because it is almost never omitted in a typical navigation scenario (although you might want to use a visibility provider to hide it in a wizard).

<mvcSiteMapNode title="Home" controller="Home" action="Index">
    <mvcSiteMapNode title="ProductName1" controller="Product" action="ProductDetails" productType="productname" visibility="Home,SomeOtherCondition,Condition1,SiteMapPathHelper,!*">
      <mvcSiteMapNode title="Choose your type" controller="Product" action="ChooseType" productType="productName" visibility="Home,SomeOtherCondition,Condition1,SiteMapPathHelper,!*">
        <mvcSiteMapNode title="Product1" controller="Product" action="Action1" visibility="Home,Condition1,SiteMapPathHelper,!*">
          <mvcSiteMapNode title="Product1Child" controller="Product" action="Acion2" visibility="Home,Condition1, SiteMapPathHelper,!*"/>
        </mvcSiteMapNode>
      </mvcSiteMapNode>
    </mvcSiteMapNode>
</mvcSiteMapNode>

Let's single out this node:

<mvcSiteMapNode title="ProductName1" controller="Product" action="ProductDetails" productType="productname" visibility="Home,SomeOtherCondition,Condition1,SiteMapPathHelper,!*">

So, taking the above example, you have configured the values

Route Key Route Value
controller Product
action ProductDetails
productType productname

This means that to match that node (for the breadcrumb), your incoming route needs the same keys and values. If you have the default route configured in MVC (and no other routes):

public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

    routes.MapRoute(
        name: "Default",
        url: "{controller}/{action}/{id}",
        defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
    );
}

The URL you need to match the node is /Product/ProductDetails?productType=productname (it has to be this exactly), and when it is, your breadcrumb will be:

Home > ProductName1

Because the route values that MVC generates from that URL based on this route configuration are:

Route Key Route Value
controller Product
action ProductDetails
productType productname

If you wanted to clean up the URL, you could add a new route:

public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

    routes.MapRoute(
        name: "Product",
        url: "Product/{action}/{productType}",
        defaults: new { controller = "Product", action = "Index" }

    routes.MapRoute(
        name: "Default",
        url: "{controller}/{action}/{id}",
        defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
    );
}

And now to match the same node, you can use the URL /Product/ProductDetails/productname. Note that the route values are still exactly the same as the table above, and the breadcrumb will be the same, only the URL has changed.

If you configure another node to match the case where you have no route value, it will work.

<mvcSiteMapNode title="Home" controller="Home" action="Index">
    <mvcSiteMapNode title="ProductName1" controller="Product" action="ProductDetails"/>
    <mvcSiteMapNode title="ProductName1" controller="Product" action="ProductDetails" productType="productname" visibility="Home,SomeOtherCondition,Condition1,SiteMapPathHelper,!*">
      <mvcSiteMapNode title="Choose your type" controller="Product" action="ChooseType" productType="productName" visibility="Home,SomeOtherCondition,Condition1,SiteMapPathHelper,!*">
        <mvcSiteMapNode title="Product1" controller="Product" action="Action1" visibility="Home,Condition1,SiteMapPathHelper,!*">
          <mvcSiteMapNode title="Product1Child" controller="Product" action="Acion2" visibility="Home,Condition1, SiteMapPathHelper,!*"/>
        </mvcSiteMapNode>
      </mvcSiteMapNode>
    </mvcSiteMapNode>
</mvcSiteMapNode>

This is a valid configuration, because you have a different route value combination. However, this doesn't make a lot of sense because there is usually a separate controller action to handle listing all of the products.

<mvcSiteMapNode title="Home" controller="Home" action="Index">
    <mvcSiteMapNode title="Product List" controller="Product" action="Index"/>
    <mvcSiteMapNode title="ProductName1" controller="Product" action="ProductDetails" productType="productname" visibility="Home,SomeOtherCondition,Condition1,SiteMapPathHelper,!*">
      <mvcSiteMapNode title="Choose your type" controller="Product" action="ChooseType" productType="productName" visibility="Home,SomeOtherCondition,Condition1,SiteMapPathHelper,!*">
        <mvcSiteMapNode title="Product1" controller="Product" action="Action1" visibility="Home,Condition1,SiteMapPathHelper,!*">
          <mvcSiteMapNode title="Product1Child" controller="Product" action="Acion2" visibility="Home,Condition1, SiteMapPathHelper,!*"/>
        </mvcSiteMapNode>
      </mvcSiteMapNode>
    </mvcSiteMapNode>
</mvcSiteMapNode>

Also, it is normal for the product details to be nested inside of the list node, but I am not sure what you intend to do with your other nodes. You have not told your controller what product you are dealing with in "Action1" or "Action2", so I don't see why they are nested below "Product1" - are they related to Product1 or not?

<mvcSiteMapNode title="Home" controller="Home" action="Index">
    <mvcSiteMapNode title="Product List" controller="Product" action="Index">
      <mvcSiteMapNode title="ProductName1" controller="Product" action="ProductDetails" productType="productname" visibility="Home,SomeOtherCondition,Condition1,SiteMapPathHelper,!*"/>
    </mvcSiteMapNode>
</mvcSiteMapNode>

@fenildesai
Copy link
Author

Hi, below is the sitemap, this resembles same as what I am using:

<mvcSiteMapNode title="HeadingPage" url="http://www.mysitename.com/sportsstore/" visibility="Home,SiteMapPathHelper,!*">
  <mvcSiteMapNode title="Products" url="http://www.mysitename/sportsstore/products/" visibility="Home,Mens,MensClothing,WomensClothing,SiteMapPathHelper,!*">

    <mvcSiteMapNode title="Mens" controller="Product" action="ProductDetails" id="Mens" visibility="Home,Mens,MensClothing,SiteMapPathHelper,!*">
      <mvcSiteMapNode title="Choose your product type" controller="Product" action="ChooseType" visibility="Home,Mens,MensClothing,SiteMapPathHelper,!*">
        <mvcSiteMapNode title="Clothing" controller="Clothing" action="MensClothing" visibility="Home,MensClothing,SiteMapPathHelper,!*">
          <mvcSiteMapNode title="Search results" controller="Clothing" action="SearchList" visibility="Home,MensClothing, SiteMapPathHelper,!*"/>
        </mvcSiteMapNode>
      </mvcSiteMapNode>
    </mvcSiteMapNode>

    <mvcSiteMapNode title="Womens" controller="Product" action="ProductDetails" id="Womens" visibility="Home,Womens,WomensClothing,SiteMapPathHelper,!*">
      <mvcSiteMapNode title="Choose your product type" controller="Product" action="ChooseType" visibility="Home,WomensClothing,SiteMapPathHelper,!*">
        <mvcSiteMapNode title="Clothing" controller="Clothing" action="WomensClothing" visibility="Home,WomensClothing,SiteMapPathHelper,!*">
          <mvcSiteMapNode title="Search results" controller="Clothing" action="SearchList" visibility="Home,WomensClothing, SiteMapPathHelper,!*"/>
        </mvcSiteMapNode>
      </mvcSiteMapNode>
    </mvcSiteMapNode>

    <mvcSiteMapNode title="Kids" controller="Product" action="ProductDetails" id="Kids" visibility="Home,Kids,KidsClothing,SiteMapPathHelper,!*">
      <mvcSiteMapNode title="Choose your product type" controller="Product" action="ChooseType" visibility="Home,Kids,KidsClothing,SiteMapPathHelper,!*">
        <mvcSiteMapNode title="Clothing" controller="Clothing" action="KidsClothing" visibility="Home,Kids,KidsClothing,SiteMapPathHelper,!*">
          <mvcSiteMapNode title="Search results" controller="Clothing" action="SearchList" visibility="Home,Kids,KidsClothing,SiteMapPathHelper,!*">
        </mvcSiteMapNode>
      </mvcSiteMapNode>
    </mvcSiteMapNode>

  </mvcSiteMapNode>
</mvcSiteMapNode>

Below is the RouteConfig.cs:

public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

routes.MapRoute(
    name: "Product",
    url: "Product/{action}/{productType}",
    defaults: new { controller = "Product", action = "ProductDetails", productType = UrlParameter.Optional }

routes.MapRoute(
    name: "Default",
    url: "{controller}/{action}/{id}",
    defaults: new { controller = "Default", action = "Index", id = UrlParameter.Optional }
);

}

What I am doing is:

First you select the product i.e either Mens, Womens or Kids when you go to - Homepage/HeadingPage/Products

Suppose you select Mens it goes to http://localhost:56267/Product/ProductDetails/Mens

Left hand nav should be:

Products
Mens
Choose your product type

Breadcrumb should be:

Homepage > HeadingPage > Products > Mens

At this point we store the id i.e Mens in session & for further use it from session.

From that page user selects the product type i.e Clothing, footwear, etc.

So user goes to http://localhost:56267/Product/ChooseType

In the action method, we get the id i.e Mens from Session & set that value in the Model & in the view based on the Model value Mens/Womens/Kids, pass the

SourceMetaData parameter as name = "MensClothing"

When user selects Clothing, it redirects to http://localhost:56267/Clothing/MensClothing

So based on that the left hand nav should be:

Products
Mens
Choose your product type
Clothing

Breadcrumb should be:

Homepage > HeadingPage > Products > Mens > Choose your type > Clothing

Now on this page I have search functionality, so user enters search text in a textbox which redirects to Search results page.

i.e http://localhost:56267/CLothing/SearchList

Same as in above, we get id value from session & pass it to View.

So based on that the left hand nav should be:

Products
Mens
Choose your product type
Clothing
Search results

Breadcrumb should be:

Homepage > HeadingPage > Products > Mens > Choose your type > Clothing > Search results

Same is followed in case of Womens & Kids.

Hope this information might help you in assisting me.

Thanks.

@NightOwl888
Copy link
Collaborator

Thanks, yes this is helpful for understanding your scenario.

Just so you understand, the SiteMap is completely stateless. It can be made to read information from session state, but the breadcrumb will break if the user goes directly to the page, such as when going from search engine results page > http://www.mysite.com/Clothing/MensClothing. It will also break when your session times out, and will be broken when search engines crawl your website.

In those cases, you basically have 2 choices

  1. Don't show any breadcrumb
  2. Show the breadcrumb, but any links to pages that rely on session state will need some default value for the missing session state

Either way, this will seriously negatively impact your search engine placement because search engines can't properly index pages that rely on session state variables. In my opinion, if you expect your pages to be search engine indexed, you should not use session state. And there is no reason to for this scenario.

If you were to always put the "id" information into the URL, it will fix both the issue with navigating directly to the page and also with search engine indexing. You can also improve your indexing (and navigation) situation by making deep URLs that progressively show the hierarchy of pages. For example:

/Products
/Products/Mens
/Products/Mens/ChooseType
/Products/Mens/Clothing
/Products/Mens/Clothing/Search?query=red
/Products/Mens/Footwear
/Products/Mens/Footwear/Search?query=red
/Products/Womens
/Products/Womens/ChooseType
/Products/Womens/Clothing/
/Products/Womens/Clothing/Search?query=red
/Products/Womens/Footwear
/Products/Womens/Footwear/Search?query=red

On the other hand, if your user has to login to see these pages, this doesn't apply to search engines, but users who bookmark URLs without having the corresponding state information will still have broken navigation if you don't put the "state" information into the URL.

Either way, it is better to put your "id" information in the URL than in session state - that will ensure the navigation will work 100% of the time instead of breaking in certain cases, and make it possible to use preservedRouteParameters, which will give your SiteMap a much smaller memory footprint on your server.

You can use URLs without the state information, but in addition to the SEO and navigation problems, with MvcSiteMapProvider you are limited to about 10,000 URLs for the whole site.

With that in mind, I have a few more questions I need answers for:

  1. Do you want to optimize for search engine placement as shown above? Or similar to what is above? (please provide sample)
  2. Are you likely to have more than 5,000-10,000 URLs on the site (now or in the future)?
  3. Are any of these pages behind a login?
  4. Is your site nested in a virtual directory /sportsstore, and if so, is will this virtual directory be registered as an an application in IIS?
  5. Is your "Left Menu" going to have other items in it than what are shown? Or perhaps you have a top menu that will?

@fenildesai
Copy link
Author

  1. Search engine optimization not needed currently.
  2. No
  3. No
  4. Yes
  5. Yes

Also the main point is I don't want to pass Id in the URL for all pages. This is because we also have a functionality of basket, so suppose a user has gone to - /Products/Mens/Clothing/Search?query=red
& added some items to the basket, now from the URL if he changes it to /Products/Mens/Footwear/Search?query=red, then we will lose the basket items as the category has been changed to Footwear. This is how the requirement is the basket is dependent on the category & hence we are not in favour of passing the ID in the URL for all pages. As I told you earlier as well, passing ID, everything works fine.

So I need an alternative solution. Hope it helps.

@NightOwl888
Copy link
Collaborator

I have created a demo project that shows the configuration that works for your scenario. Note that you will either need to use external DI as shown in the demo or you will need to make a custom fork of MvcSiteMapProvider (and a custom build) with the changes from the /MvcSiteMapProvider directory in order to get it to use session state. I tried to do it by altering the route values with an action filter so external DI wouldn't be necessary, but it didn't seem to work.

I used Ninject in the example, but there is no reason why you can't use another DI container.

The way it works is that it copies over the specified session state keys in the copySessionToRoute to the incoming request route values just before comparing the node to the route. If the value that is in that key for session matches the value that is in that key for the current node, it will match. Then just before resolving the URL, the values (that are configured in the node, not the values from session) with the keys in copySessionToRoute are removed from the request's routeValues. That part is done using a custom URL resolver, which needs to be applied to every node where the copySessionToRoute attribute is applied. In addition, cacheResolvedUrl needs to be set to false on those nodes.

Note that the nodes still require unique route value combinations, but this is altering the request in order to match those unique combinations. Even though the URL of the request is not unique, the URL + session state values are.

You may still need to do some styling and/or make HTML layout changes for your "left nav". You can make HTML changes by editing or creating new templates in the /Views/Shared/DisplayTemplates directory.

@NightOwl888
Copy link
Collaborator

There hasn't been any new information posted for some time on this, so I am closing it for now. Feel free to reopen if the proposed solution doesn't work out.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants