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

Add HTML helpers for Previous() and Next() nodes #363

Open
borekb opened this issue Oct 9, 2014 · 6 comments
Open

Add HTML helpers for Previous() and Next() nodes #363

borekb opened this issue Oct 9, 2014 · 6 comments

Comments

@borekb
Copy link

borekb commented Oct 9, 2014

Would be nice if there was an easy way to generate links to next and previous nodes in HTML, using something like:

Html.MvcSiteMap().Previous()
Html.MvcSiteMap().Next()

I had a quick look at the API but didn't find anything that would expose this directly.

@NightOwl888
Copy link
Collaborator

The SiteMap does not keep track individual user's movements. It only builds a set of links based on the user's current position within the map. This functionality is not build into the API because there would be no practical way of determining this without some kind of session tracking.

I have written an article titled How to Make MvcSiteMapProvider Remember a User's Position to explain why this is not possible with this design. Here is the introduction from that post:

Today I am going to talk about something that many people who are new to MvcSiteMapProvider struggle with – making the breadcrumb trail remember the last position the user was at – including the ID.

In short, you can’t do this. MvcSiteMapProvider doesn’t remember a user’s position at all – that simply isn’t how it works. In fact, there is no usage of session state, cookies, or anything user related that can remember a user’s position. Everything that MvcSiteMapProvider does is site wide. It is a site wide map, or sitemap that can tell where the current request is by looking it up in the map. This makes it useful for both end users and for search engines.

@borekb
Copy link
Author

borekb commented Oct 10, 2014

Just to make sure we are talking about the same thing: as there is SiteMap.CurrentNode, I'd like something like NextNode and PreviousNode as well. I don't need to keep track where the user was the last time, I just want previous and next nodes in the dynamic node collection so that in my documentation site, I can point to the next and previous doc topic.

@NightOwl888
Copy link
Collaborator

Ok, but it is still a bit unclear what you are trying to achieve. The current node corresponds to the current HTTP request. But what should determine a "next node" and a "previous node"? If you put your nodes into an Mvc.sitemap XML format and post them here and tell me which one is the "current node" and what node you are expecting to get to as the "next node" and "previous node" as well as what HTML output you are expecting from such an HTML helper, I might be able to provide you some direction.

For example, if you were creating a wizard, you would probably want to model your nodes in such a way that they build a breadcrumb trail that shows all of the previous steps.

Start > User Info > Company Info > Clients > Survey > Finish

And if you were currently on the "Comany Info" step, your breadcrumb would look like this:

Start > User Info > Company Info

In this case, you would probably want to make a named SiteMapPath that is only used for your wizard, so the nodes don't show up on any of your regular navigation.

@Html.MvcSiteMap().SiteMapPath(new { name = "SignupWizard" })

You would model such a wizard like this in XML:

  <mvcSiteMapNode title="Home" controller="Home" action="Index" visibility="!SignupWizard">
    <mvcSiteMapNode title="Sign Up" controller="SignupWizard" action="Index" visibility="!SignupWizard">
      <mvcSiteMapNode title="Start" action="Start">
        <mvcSiteMapNode title="User Info" action="UserInfo">
          <mvcSiteMapNode title="Company Info" action="CompanyInfo">
            <mvcSiteMapNode title="Clients" action="Clients">
              <mvcSiteMapNode title="Survey" action="Survey">
                <mvcSiteMapNode title="Finish" action="Finish"/>
              </mvcSiteMapNode>
            </mvcSiteMapNode>
          </mvcSiteMapNode>
        </mvcSiteMapNode>
      </mvcSiteMapNode>
    </mvcSiteMapNode>
  </mvcSiteMapNode>

So your "previous node" would be the parent node of the current node. The "next node" would be the first child of the next level in this case, but what if you wanted to branch your wizard at a certain step depending on what the user selects?

  <mvcSiteMapNode title="Home" controller="Home" action="Index" visibility="!SignupWizard">
    <mvcSiteMapNode title="Sign Up" controller="SignupWizard" action="Index" visibility="!SignupWizard">
      <mvcSiteMapNode title="Start" action="Start">
        <mvcSiteMapNode title="User Info" action="UserInfo">
          <mvcSiteMapNode title="Company Info" action="CompanyInfo">
            <mvcSiteMapNode title="Clients" action="CompanyClients">
              <mvcSiteMapNode title="Survey" action="CompanySurvey">
                <mvcSiteMapNode title="Finish" action="CompanyFinish"/>
              </mvcSiteMapNode>
            </mvcSiteMapNode>
          </mvcSiteMapNode>
          <mvcSiteMapNode title="Personal Info" action="PersonalInfo">
            <mvcSiteMapNode title="Survey" action="PersonalSurvey">
              <mvcSiteMapNode title="Finish" action="PersonalFinish"/>
            </mvcSiteMapNode>
          </mvcSiteMapNode>
        </mvcSiteMapNode>
      </mvcSiteMapNode>
    </mvcSiteMapNode>
  </mvcSiteMapNode>

Now the user can either select to signup for their company or for their personal use. The "next node" at the "User Info" step would be conditional depending on what the user's input is. Therefore, logic that determines the "next node" would be completely dependent on the design of the wizard.

@NightOwl888 NightOwl888 reopened this Oct 10, 2014
@borekb
Copy link
Author

borekb commented Oct 10, 2014

I have a docs site with a structure like this:

- Home
- Getting started
    - Intro
    - Installation
    - Usage
- Details
    - Feature 1
    - Feature 2
- Other
    - License
    - Support

When I am on the Home node, there would be no "previous" topic and the next one would be Getting started. When I am on Getting started node, the previous one would be Home and the next one would be Intro. When I am on Intro, the previous one would be Getting started and the next one would be Installation. Etc.

So it's not a wizard, it's a navigation in a hierarchical structure that despite being hierarchical always has a well-defined "previous" and "next" nodes.

@NightOwl888
Copy link
Collaborator

I have created a demo project showing how you can extend the functionality of MvcSiteMapProvider by making your own HTML helpers. For the sake of consistency, I made them all templated HTML helpers (just like the built-in HTML helpers) but there is no reason why you couldn't make HTML helpers that output the HTML elements you want as a string instead of referring to a template.

The real magic happens in the GetNextNode and GetPreviousNode methods:

private static ISiteMapNode GetNextNode(ISiteMapNode startingNode, IDictionary<string, object> sourceMetadata)
{
    ISiteMapNode nextNode = null;
    if (startingNode.HasChildNodes)
    {
        // Get the first child node
        nextNode = startingNode.ChildNodes[0];
    }
    else if (startingNode.ParentNode != null)
    {
        // Get the next sibling node
        nextNode = startingNode.NextSibling;
        if (nextNode == null)
        {
            // If there are no more siblings, the next position
            // should be the parent's next sibling
            var parent = startingNode.ParentNode;
            if (parent != null)
            {
                nextNode = parent.NextSibling;
            }
        }
    }

    // If the node is not visible or accessible, run the operation recursively until a visible node is found
    if (nextNode != null && !(nextNode.IsVisible(sourceMetadata) || nextNode.IsAccessibleToUser()))
    {
        nextNode = GetNextNode(nextNode, sourceMetadata);
    }

    return nextNode;
}

private static ISiteMapNode GetPreviousNode(ISiteMapNode startingNode, IDictionary<string, object> sourceMetadata)
{
    ISiteMapNode previousNode = null;

    // Get the previous sibling
    var previousSibling = startingNode.PreviousSibling;
    if (previousSibling != null)
    {
        // If there are any children, go to the last descendant
        if (previousSibling.HasChildNodes)
        {
            previousNode = previousSibling.Descendants.Last();
        }
        else
        {
            // If there are no children, return the sibling.
            previousNode = previousSibling;
        }
    }
    else
    {
        // If there are no more siblings before this one, go to the parent node
        previousNode = startingNode.ParentNode;
    }

    // If the node is not visible or accessible, run the operation recursively until a visible node is found
    if (previousNode != null && !(previousNode.IsVisible(sourceMetadata) || previousNode.IsAccessibleToUser()))
    {
        previousNode = GetPreviousNode(previousNode, sourceMetadata);
    }

    return previousNode;
}

The rest of the code is mostly just boilerplate stuff to make the HTML helper work and to provide ways to override the text that is displayed, template used, node that is considered the "current node", etc.

I think this would make a good thing to add to MvcSiteMapProvider as a feature, but I need to consider how to make it clear that this is not the same thing as the forward and back buttons of the browser (which is what people normally expect). Maybe I could call them @Html.MvcSiteMap().DocumentOutline().Next() and @Html.MvcSiteMap().DocumentOutline().Previous() or something along those lines. That would pave the way for making @Html.MvcSiteMap().Wizard().Previous() and perhaps a @Html.MvcSiteMap().Wizard().Next() that does an HTTP post, since the business logic is completely different in that case. Maybe there could even be a @Html.MvcSiteMap().Session().Previous() and @Html.MvcSiteMap().Session().Next().

@borekb
Copy link
Author

borekb commented Oct 12, 2014

This is seriously awesome, thanks for the example!

As for the naming, in my mind, I want a "next / previous node in a site map" so methods called NextNode() and PreviouNode() on MvcSiteMap() would make perfect sense to me. The word "node" in them clearly hint to something static in the site's structure and I wouldn't, personally, confuse that with browser's back/forward buttons.

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