About this article

Written by:
Posted:
18/08/2010 13:38:08

About the author

Imar Spaanjaars is the owner of De Vier Koeden, a company specializing in consultancy and development on the Microsoft .NET platform and DynamicWeb.

Interested in custom development or consultancy on DynamicWeb or .NET? Then contact De Vier Koeden through the Contact page.

If you want to find out more about Imar Spaanjaars, check out his About page on this web site or check out his personal website.

Extending the Sitemap with Dynamicweb Content

In an earlier article I showed you how to extend the sitemap.xml file with links to content coming from a custom module. However, you're not limited to extending the sitemap.xml file. You can use the same principles to extend the navigation that is used on the web site.

In this article I'll show you how to use a NavigationProvider to implement the following features:

  • Display the latest X number of news items from the News module as sub nodes of a main menu.
  • Link each article using a SEO friendly URL to the relevant details page
  • Let a content manager determine the details page in a Settings module.

Extending the Navigation System

Just as I showed you in the previous article, you need to use a NavigationProvider to dynamically inject sub nodes in the navigation tree. The standard Dynamicweb navigation system then automatically displays the child nodes on the page, using the built-in menus, or using XSLT files to process the tree.

In the ProcessTree method of the NavigationProvider you have access to a parameter called rootNode. You can loop over this node's child elements (recursively if you need that), find the node you want to extend, and then add child elements to them. Here's a simple example:

public class AddNewsItemsToMenu : NavigationProvider
{
public override void ProcessTree(RootNavigationItem rootNode, Frontend.XMLNavigation.NavigationType navigationType)
{
var node = rootNode.ChildNodes.Where(no => no.ID == 12).FirstOrDefault(); if (node != null) { node.AddChild(new PageNavigationItem() { FriendlyHref = "SomeUrl.aspx", Title = "Your title", MenuText = "Menu Text", AllowClick = true });
} } }

This code is triggered when the navigation tree is being build. In this example, I use a LINQ query to find a specific menu item (the News page in my sample web site with an ID of 12) and then add a child element to it using AddChild.

To add news items to this node instead of a hard code item, you need to find a way to get news items from the Dynamicweb database. There are at least two ways to do this, each with their own pros and cons.

Using the API

There's a convenient method called GetAllNewsValidAndActiveAndNotArchive on the NewsItem class that gives you all valid and active and not archived messages. Calling it is as simple as this:

NewsItemCollection newsItems = NewsItem.GetAllNewsValidAndActiveAndNotArchive();

You can then use LINQ to get only the first few items like this:

var top3 = (from n in newsItems
orderby n.NewsActiveFrom descending
select n).Take(3);

While easy to call, GetAllNewsValidAndActiveAndNotArchive has one drawback: it returns all active items. So, depending on the size of your news module, you may retrieve hundreds of articles. When this is the case, you may be better of using the DbHandler class as discussed next

Using DbHandler

With the DbHandler class you can execute a SQL statement to get the news items you're looking for. Since it's custom SQL, you can easily limit the number of items using TOP, like this:

NewsItemCollection newsItems = 
     DbHandler.ExecuteList<NewsItemCollection, NewsItem>(
     string.Format("SELECT TOP 3 * FROM [News] WHERE ([NewsActive] = {0}) AND 
     (NewsArchive <> {1}) AND ({2} > [NewsActiveFrom]) AND ({2} <= [NewsActiveTo])
     ORDER BY [NewsDate] DESC", DbHandler.SqlBool(true), DbHandler.SqlBool(true), 
     DbHandler.SqlDate(DateTime.Now)), "Dynamic.mdb");

This gives you the latest three news items. The risk of this solution is that in the future the structure of the News table may change, causing the SQL statement to break. However, I don't think this is very likely, so I would use this solution when the number of active news items is large to improve performance.

Once you have a NewsCollection, the next step is to add the items to the navigation node. Most of this is simple, except for the FriendlyHref attribute. To correctly display a news item, Dynamicweb needs to know the page ID and paragraph ID of the paragraph that displays your news using the News V2 module. I'll show you a hard coded version first but later in the article I'll show you a way to make this user configurable with a simple custom module.

string url = "Default.aspx?ID=53#70";
string[] urlSplit = url.Split(new char[] { '#' });
string newsPage = urlSplit[0];
string paragraphId = urlSplit[1];
foreach (var item in top3)
{
  string seoUrl = SearchEngineFriendlyURLs.getSearchFriendlyUrl(
         string.Format("{0}&M=NewsV2&PID={1}&Action=1&NewsId={2}", 
         newsPage, paragraphId, item.ID), item.NewsHeading);
  node.AddChild(new PageNavigationItem()
  {
    FriendlyHref = seoUrl, 
    MenuText = item.NewsHeading, 
    AllowClick = true 
  });
}	    

There are a few important pieces in this code. First, the assignment of the url variable: Default.aspx?ID=53#70. Here, 53 refers to the ID of the page that shows your news details, while 70 refers to the paragraph that contains the news module. Again, you'll see how to make this dynamic later. The ID of 53 may, or may not, be the ID of the page that is being extended. In my case, the two are different as I display my news details on a separate page.

I then split the url variable based on the hash symbol to get the paragraph ID. This ID is injected in the URL for the news page's PID variable. So, for a news item with an ID of 3, the string that is passed to SearchEngineFriendlyURLs.getSearchFriendlyUrl ends up like this:

"Default.aspx?ID=53?M=NewsV2&PID=70&Action=1&NewsId=3"

Once processed by the getSearchFriendlyUrl method, the final link could look like this:

/news/all-news/[M/NewsV2/PID/70/Action/1/NewsId/3]/My-news-title.aspx

When all items are added to the correct news node, and rendered to a CSS menu using some XSLT style sheet, they could end up like this:

The News Item with extra child nodes

Each item in the sub menu now correctly links to the news details page and passes the ID of the paragraph and news ID in the address.

Improving the Maintainability of this Solution

Clearly, hard coding page and paragraph IDs like this is asking for trouble. You can be sure that just minutes after you deployed this solution your client will change the content of the site, move the news page to a different location and expect things to keep working. Oooops. You now need to change your code, compile and then redeploy the application. Fortunately, it's pretty easy to make the destination page user configurable. All you need to do is create a simple page in the Administration section, add a dw:LinkManager control to it, make the page save its own state, and then register the module as a custom module without paragraph access. In my sample application, I have a page called /CustomModules/ManageSettings/Default.aspx that contains code like this:

<dw:LinkManager DisableFileArchive="true"  ID="NewsDetail" 
   DisableParagraphSelector="false" EnablePageSelector="false" runat="server" />

You need a recent version of Dynamicweb (later than 19.1.0.5) in order for the DisableParagraphSelector and EnablePageSelector properties to show up and function correctly.

I store the selected value and prepopulate the control in Code Behind using a custom Entity Framework model that has a SiteSettings entity. Saving the settings can be as easy as this:

using (SamplesEntities db = new Lib.Model.SamplesEntities())
{
SiteSetting settings = db.SiteSettings.FirstOrDefault();
if (settings == null)
{
settings = new SiteSetting();
db.SiteSettings.AddObject(settings);
}
settings.NewsPage = NewsDetail.Value;
db.SaveChanges();
}

I can then access the site settings in my NavigationProvider to make the news destination page dynamic:

using (SamplesEntities db = new SamplesEntities())
{
SiteSetting settings = db.SiteSettings.FirstOrDefault();
if (settings == null)
{
return;
}
string url = settings.NewsPage; ... rest of the code goes here
}

Extending even Further

Once you have this in place, it's easy to come up with other ideas to extend the concepts presented in this article. You could, for example, filter the list of articles to a specific language area. Here's one possible implementation:

  1. Create a custom field in the news module, called LanguageArea for example. Make it a DropDown type and supply values for each language area culture, such as en-US, nl-NL and so on.
  2. Assign the appropriate culture to each news item on the custom fields tab.
  3. Modify the code that queries the items using GetAllNewsValidAndActiveAndNotArchive as follows:

    var newsItems = NewsItem.GetAllNewsValidAndActiveAndNotArchive().
                OrderByDescending(n => n.NewsActiveFrom);
    NewsItemCollection temp = new NewsItemCollection();
    string culture = PageView.Current().Area.Values["areaculture"].ToString();
    foreach (var item in newsItems)
    {
      item.FillCustomFields();
      if (item.CustomFields.Any(c => c.Value == culture))
      {
        temp.Add(item);
      }
      if (temp.Count == 3)
      {
        break;
      }
    }      
    You need to loop over the newsItems collection and call FillCustomFields because the custom fields are not loaded by default. (Hint to Dynamicweb: an overload of GetAllNewsValidAndActiveAndNotArchive that automatically loads the custom fields in one fell swoop would be nice, to avoid this slightly nasty N+1 anti-pattern.)
  4. If you're using the SQL route instead of calling GetAllNewsValidAndActiveAndNotArchive, you need to look in the table AccessCustomFieldRow and join on the CustomFieldRowItemID as that tables contains the custom field values for the news items.

Instead of filtering by culture, you could also filter by category ID, which is a lot easier:

var newsItems = NewsItem.GetAllNewsValidAndActiveAndNotArchive().
            OrderByDescending(n => n.NewsActiveFrom);
var temp = newsItems.Where(n => n.NewsCategoryID == 2); 

To make this a bit more dynamic, you can make the selected category ID(s) user configurable in a custom settings module.

With the code presented in this article, you can extend the navigation system in any way you see fit. Did you come up with a different use case for extending the navigation system? I am interested in what and how you extended it. Contact me through my Contact page and if needed, I'll update this article with other custom solutions.

Happy extending!

By clicking 'Accept All' you consent that we may collect information about you for various purposes, including: Statistics and Marketing