-[Danny Chen]- Blog of an ASP.NET QA tester

Tips and info about Site Navigation, ImageMap, Menu and other cool ASP.NET v2.0 features.
Menu Control doesn't work after migrating from VS2003 and ASP.NET 1.1

I noticed this issue quite a bit on the forums.  A lot of users are finding that after moving their ASP.NET 1.1 site to ASP.NET 2.0 and then adding a Menu Control to their app, the Menu doesn't work. 

The reason this doesn't work is that an extra tag was added to web.config <xhtmlConformance mode="Legacy" />  This flag was meant for 2.0 to act a lot more like 1.1 and one of the major differences is the naming convention of controls.  This naming change causes the quirky behavior with Menu in IE.

There are 2 fixes for this issue:

1) Remove the xhtmlConformance tag from web.config
Personally if you can do it, I recommend this option.  If you're using ASP.NET 2.0 features, you should try and stick with the new rendering modes.

2) If you can't do #1, then give your masterpage an ID
To do this, add a line to your Page_Load for your Masterpage:

public partial class MasterPage : System.Web.UI.MasterPage
{
    protected void Page_Load(object sender, EventArgs e)
    {
        this.ID = "Master1";
    }
}

Hacking the SiteMapPath, changing it's data source.

The SiteMapPath control is designed to work directly against a SiteMapProvider.  However, there is sometimes that rare
occasion where a developer might want to take advantage of the formatting of a SiteMapPath but take the data from somewhere
other than a SiteMapProvider (such as another control).  In that case, obviously, the developer would be inclined to build a
custom control that inherits from SiteMapPath.  However, since the SiteMapPath works directly with a provider and not with a
datasource, the process seems trickier.  This MSDN help article helps describes some of the process:

http://msdn2.microsoft.com/en-us/library/system.web.ui.webcontrols.sitemappath.createcontrolhierarchy.aspx

As mentioned in the article, there are two primary places a developer is likely to override.  InitializeItem where completely
custom rendering can be injected and CreateControlHierarchy where the controls are generated.  This article will focus on
CreateControlHierarchy because that is what needs to be overridden to change the data feed.

The purpose of CreateControlHierarchy is to take the data from a source (normally a SiteMapProvider) and convert it into a
series of SiteMapNodeItem controls which are added to the Controls collection of the SiteMapPath.  In the process, InitializeItem
is called on each item which populates it with any templating/style information that has been set.  It also is responsible for firing any
Databinding and Creation events. 

Here's an example of a custom CreateControlHierarchy function which takes it's data from a TreeView.  The idea in this case is to
take a populated TreeView with a selected node and render out a path from the root of the tree to that node.  There's a link to a
working control listing at the end of the article.

        Protected Overrides Sub CreateControlHierarchy()
            
Dim item As SiteMapNodeItem = Nothing

            ' Get the current selected tree node
            If Tree Is Nothing Then
                Throw New Exception("Tree or TreeViewID is not valid")
            
End If
            Dim node As TreeNode = Tree.SelectedNode()
            
If node Is Nothing Then
                Return
            Else
                ' Create the current node
                AddItemFromTreeNode(item, node, SiteMapNodeItemType.Current)
                node = node.Parent

                
' Create the parent nodes and separators
                While node IsNot Nothing AndAlso node.Parent IsNot Nothing
                    AddItemFromTreeNode(item, Nothing, SiteMapNodeItemType.PathSeparator)
                    AddItemFromTreeNode(item, node, SiteMapNodeItemType.Parent)
                    node = node.Parent
                
End While

                ' Create the root node (unless there is only one node)
                If node IsNot Nothing Then
                    AddItemFromTreeNode(item, Nothing, SiteMapNodeItemType.PathSeparator)
                    AddItemFromTreeNode(item, node, SiteMapNodeItemType.Root)
                
End If
            End If
        End Sub

The function AddItemFromTreeNode will add SiteMapNodeItems to the Controls collection that reflect the passed in
TreeNode.  The code itself is very straight forward, add the current node, any number of parent nodes, and then the root node, all
the while looping through the parent node hierarchy of the TreeView.

 Link to VB Code Listing

Setting the DataSource property of a Menu or TreeView to an array

Here is a neat "trick" you can do with the Data controls.  If you have an array of data you want to format, you can just
directly assign it to the Datasource property kind of like this:

 <%@ Page Language="VB" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<
script runat="server">
   Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs)
      DataList1.DataSource =
New String() {"one", "two", "three"}
      DataList1.DataBind()
   End Sub
</
script>
<
html xmlns="http://www.w3.org/1999/xhtml" >
   <
head runat="server">
      <title>Untitled Page</title>
   </
head>
<
body>
<form id="form1" runat="server">
<div>
   <asp:DataList ID="DataList1" runat="server">
      <ItemTemplate>
         <%# Container.DataItem %>
      </ItemTemplate>
   </asp:DataList>
</div>
</form>
</
body>
</
html>

However, if you've ever tried this with the Menu or Treeview, you'd quickly figure out it doesn't work.  This is
because the HierarchicalDataSourceView requires a new type called a IHierarchicalEnumerable which enumerates
a type called IHierarchyData.  The solution would be to implement these two interfaces as minimally as possible.

 

 Public Class HierarchicalData
   Implements IHierarchicalEnumerable



   
Private data As ArrayList
   Public Sub New(ByVal values() As String)
      data =
New ArrayList()
      For Each value As String In values
         data.Add(
New HierarchicalDataElement(value))
      Next
   End Sub

   Public
Function GetEnumerator() As System.Collections.IEnumerator Implements System.Collections.IEnumerable.GetEnumerator
      Return data.GetEnumerator()
   End Function
   Public Function GetHierarchyData(ByVal enumeratedItem As Object) As System.Web.UI.IHierarchyData Implements System.Web.UI.IHierarchicalEnumerable.GetHierarchyData
      Return CType(enumeratedItem, HierarchicalDataElement)
   End Function
End
Class


Public
Class HierarchicalDataElement
   Implements IHierarchyData    
   
Public _data As String
   Public Sub New(ByVal data As String)
      _data = data
   End Sub
   Public Function GetChildren() As System.Web.UI.IHierarchicalEnumerable Implements System.Web.UI.IHierarchyData.GetChildren
      Return Nothing
   End Function
   Public Function GetParent() As System.Web.UI.IHierarchyData Implements System.Web.UI.IHierarchyData.GetParent
      Return Nothing
   End Function
   Public ReadOnly Property HasChildren() As Boolean Implements System.Web.UI.IHierarchyData.HasChildren
   Get
      Return False
   End Get
   End Property
   Public ReadOnly Property Item() As Object Implements System.Web.UI.IHierarchyData.Item
   Get
      Return _data
   End Get
   End Property
   Public ReadOnly Property Path() As String Implements System.Web.UI.IHierarchyData.Path
   Get
      Return _data
   End Get
   End Property
   Public ReadOnly Property Type() As String Implements System.Web.UI.IHierarchyData.Type
   Get
      Return _data.GetType().ToString()
   End Get
   End Property
   
    Public
Overrides Function ToString() As String
       Return _data
    End Function
End
Class
 

And to use it, declare a Menu on your form and just add the following code.  There is no need to bind to
Container.DataItem in a template because the MenuItems get their text value from the IHierarchyData.ToString() overload. 

 

<script runat="server">
   Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs)
      Menu1.DataSource = New HierarchicalData(New String() {"one", "two", "three"})
      Menu1.DataBind()
   End
Sub
</
script
>
Customizing Individual Menu Items

One question I see very frequently is how to customize the styles on an individual menu item. Declaratively, this isn't possible, although it is on the list of features for a future release. However, that doesn't mean it can't be done at all. It does take a little more work.  So, here are a couple variations on a theme to help customize various items. The ideas here can be extended on easily and I expect we might see some pretty interesting Menu's out there in the future.

Here's the most straight forward brute force way of setting an individual item style:

 <script runat="server">
Shared colorWheel As System.Collections.Generic.List(Of System.Drawing.Color)
Shared currentColor As System.Collections.Generic.List(Of System.Drawing.Color).Enumerator
Public Function GetBackColor() As System.Drawing.Color
   If colorWheel Is Nothing Then
      populatecolors()
   End If
   currentColor.MoveNext()
   Return currentColor.Current
End Function
Private Sub populatecolors()
   colorWheel =
New System.Collections.Generic.List(Of System.Drawing.Color)
   colorWheel.Add(Drawing.Color.Purple)
   colorWheel.Add(Drawing.Color.Green)
   colorWheel.Add(Drawing.Color.Brown)
   colorWheel.Add(Drawing.Color.Chartreuse)
   currentColor = colorWheel.GetEnumerator()
End Sub
</
script>

<asp:Menu ID="Menu1" runat="server" Orientation="Horizontal">

   <Items>

      <asp:MenuItem Text="one" Value="one"></asp:MenuItem>

      <asp:MenuItem Text="two" Value="two"></asp:MenuItem>

      <asp:MenuItem Text="three" Value="three"></asp:MenuItem>

      <asp:MenuItem Text="four" Value="four"></asp:MenuItem>

   </Items>

   <StaticItemTemplate>

      <asp:Label runat="server" ID="Label1" Text='<%# Eval("Text") %>'

         BackColor='<%# GetBackColor() %>' />

   </StaticItemTemplate>

</asp:Menu>

 

Now, this isn't the most ideal code for a number of reasons, but it gets the job done. So lets make 2 improvements. First, lets use the Style property so that we're not limited to a single style. Second, lets get away from relying on matching the order in our style list and the order of declared items.

<script runat="server">
Shared
styles As System.Collections.Generic.Dictionary(Of String, String)
Public Function GetStyle(ByVal value As String) As String
   If styles Is Nothing Then
      populateStyles()
   End If
   Return styles(value)
End Function
Private Sub populateStyles()
   styles =
New System.Collections.Generic.Dictionary(Of String, String)
   styles(
"one") = "background-color:Blue; color:White;"
   styles("two") = "background-color:Black; color:Yellow;"
   styles("three") = "background-color:Purple; color:Black;"
   styles("four") = "background-color:Green; color:Red;"
End Sub
</script>

<asp:Menu ID="Menu1" runat="server" Orientation="Horizontal">

   <Items>

      <asp:MenuItem Text="one" Value="one"></asp:MenuItem>

      <asp:MenuItem Text="two" Value="two">

         <asp:MenuItem Text="three" Value="three"></asp:MenuItem>

         <asp:MenuItem Text="four" Value="four"></asp:MenuItem>

      </asp:MenuItem>

   </Items>

   <StaticItemTemplate>

      <asp:Label runat="server" ID="Label1" Text='<%# Eval("Text") %>'

         Style='<%# GetStyle( Eval("Value") ) %>' />

   </StaticItemTemplate>

   <DynamicItemTemplate>

      <asp:Label runat="server" ID="Label1" Text='<%# Eval("Text") %>'

         Style='<%# GetStyle( Eval("Value") ) %>' />

   </DynamicItemTemplate>

</asp:Menu>

 

With this improvement, we have a lot control. Dynamic items also aren't a problem (although technically they weren't with the first method). With a bit of a variation, we can also make this work with a databound Menu like this:

 <script runat="server">
Shared styles As New System.Collections.Generic.Dictionary(Of String, String)
Protected Sub Menu1_MenuItemDataBound(ByVal sender As Object, _
                                      
ByVal e As System.Web.UI.WebControls.MenuEventArgs)
   styles(e.Item.ValuePath) =
CType(e.Item.DataItem, SiteMapNode)("style")
End Sub </script>
<asp:Menu ID="Menu1" runat="server" DataSourceID="SiteMapDataSource1"
   OnMenuItemDataBound="Menu1_MenuItemDataBound">
   <StaticItemTemplate>
      <asp:Label runat="server" ID="Label1" Text='<%# Eval("Text") %>'
         
Style='<%# styles( Eval("ValuePath") ) %>' />
   </StaticItemTemplate>
   <DynamicItemTemplate>
      <asp:Label runat="server" ID="Label1" Text='<%# Eval("Text") %>'
         
Style='<%# styles( Eval("ValuePath") ) %>' />
   </DynamicItemTemplate>
</asp:Menu>



[web.sitemap]
<siteMap xmlns="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0" >
   <
siteMapNode url="root.aspx" title="root" style="">
      <
siteMapNode url="one.aspx" title="one" style="background-color:Blue; color:White;" />
      <
siteMapNode url="two.aspx" title="two" style="background-color:Black; color:Yellow;">
         <
siteMapNode url="three.aspx" title="three" style="background-color:Purple; color:Black;" />
         <
siteMapNode url="four.aspx" title="four" style="background-color:Green; color:Red;" />
      </
siteMapNode>
   </
siteMapNode>
</
siteMap>
Using Device Filters (and making Menu work with Safari)

Well, as much as I would hate to admit it, there are a few bugs in the Menu control.  For the most part, I'm OK with that.  Some of the bugs are a matter of interpretation and those kind will always exist.  But one really sticks as a thorn in my side. 

  If you specify a DynamicMenuStyle (or LevelSubMenuStyles) on the Menu control, sub menus seem to disappear on "downlevel" browsers.  Well it's really there (if you look at the source) but it's styles make it hidden. 

There are very few workarounds for this bug.

1) Remove runat="server" from <head>
   Cons: HoverStyles are no longer allowed even in rich renderings

2) Remove the DynamicMenuStyle
   Cons: No Dynamic styles allowed even in rich renderings

3) Use device filters to selectively apply styles
   Cons: Still limited in what can be done but uplevel is not affected


note: This bug is being fixed and will eventually be availible through a patch, update, service pack, or etc in the future. Looks like I spoke too soon, this may not get fixed, what you should do instead is see if you can use the CSS Adaptors:  http://www.asp.net/cssadapters/


What are device filters?

Device filters are a somewhat lesser known feature of ASP.NET.  It isn't a feature I actively work with but since there seems to be little information about it, I'll give a brief description of how it works.

In the folder <SysDrive>\Windows\Framework\v2.0.50727\CONFIG\Browsers is a series of .browser files.  These contain some information about every type of web browsing device that ASP.NET knows about.  This information is used for a number of things, among which, choosing whether to render the 'downlevel' menu or the 'uplevel' menu based on which browser is being used. 

Device filters can use the browser information to selectively apply properties to a control depending on which browser is viewing the page.  Here's an example:

<asp:Label runat="server" id="Label1"
  ie:Text="Hello Internet Explorer browser
  Mozilla:Text="Hello Mozilla Browser"
  Text="Hello other browser" />

  This works as you might predict: the IE user gets a message about IE, a Mozilla user gets a message about Mozilla and other browsers, like Opera or Netscape, get the standard message.  One interesting observation is that IE is considered a Mozilla browser.  However, since the IE: filter is more specific, it wins over the Mozilla filter.

  Device filters can only be used declaratively and there is no designer support for them.  The syntax is:  browsername:Property=<value>.  browsername comes from the id field of each recognized browser in the .browsers files.  Any property (that isn't explicitly set as Filterable(false) through an attribute in the control code) can be filtered.  This includes collections and templates:

<asp:Menu runat="server" id="Menu1">
  <ie:Items>
   <asp:MenuItem Text="IE Item" />
  </ie:Items>

  <Items>
    <asp:MenuItem Text="Other Item" />
  </Items>

  <ie:StaticItemTemplate>
     This item is templated only in IE: <%# Eval("Text") %>
  </ie:StaticItemTemplate>

</asp:Menu>

So, can we work around this Menu bug with Device Filters? Kind of.

Ok, so we really can't, but what we can do is limit the damage that any of the workarounds I listed above would do to the uplevel rendering.  After all, they work fine.  The most basic improved workaruond is to recognize that IE 5 and Safari are the two browsers affected by this bug (for some reason Netscape is ok).  So, we could simply not apply a DynamicMenuStyle to those browsers:

<asp:Menu ... >
 <DynamicMenuStyle [my styles here] />
 <Safari:DynamicMenuStyle />
 <IE5:DynamicMenuStyle />
</asp:Menu>

But even better.  It's possible in many cases to produce a less desirable but still functional and styled menu using only DynamicMenuItemStyles. 

<asp:Menu ... >
 <DynamicMenuStyle [my styles here] />
 <Safari:DynamicMenuStyle />
 <IE5:DynamicMenuStyle />
 
 <Safari:DynamicMenuItemStyle ... />
 <IE5:DynamicMenuItemStyle ... />

</asp:Menu>

Displaying Sibling Nodes in a Tree or Menu

I got an email with this question and a request that I post the solution on my blog so here it is:

Consider a sitemap with the following structure: 

The nodes "c", "e", and "h" (highlighted in yellow) are considered sibling nodes because they have the same parent.

Q: Can I display all the sibling nodes of the current node in a TreeView or Menu?

The answer is yes, and it's easy: 

<asp:SiteMapDataSource ID="SiteMapDataSource1" runat="server" ShowStartingNode="False" StartFromCurrentNode="True" StartingNodeOffset="-1" />

However, a more challenging question is:  Can I display all the nodes at the same depth as the current node (lets assume this is node "c")?  This would include the nodes "c", "e", "h" but also "r", and "u" (all the ones highlighted in green).

The answer is yes (of course) but it has to be done after-the-fact in the Tree or Menu.  There are a couple "tricks", however.  Here's the code for a TreeView:

    ' Databound event handler for a TreeView   
    Protected Sub TreeView_DataBound(ByVal sender As Object, ByVal e As System.EventArgs)
        Dim Tree As TreeView = sender
       
        ' Only prune the tree if a node is selected
        If Tree.SelectedNode IsNot Nothing Then
           
            Dim childNodes As New Generic.List(Of TreeNode)()
            Dim childNodeCollection As New TreeNodeCollection()
           
            FindNodesAtDepth(Tree.Nodes, Tree.SelectedNode.Depth, childNodes)
                       
            ' Copy from List(of TreeNode) to TreeNodeCollection
            For Each node As TreeNode In childNodes
                childNodeCollection.Add(node)
            Next
           
            ' Clear the TreeView nodes and add the nodes we found
            Tree.Nodes.Clear()
            CloneTreeNodeHierarchyRecursive(childNodeCollection, Tree.Nodes)
           
        End If
       
    End Sub

  This function, conceptually, is pretty straight forward.  Collect a list of the Nodes at the depth of the selected node, clear the nodes in the tree and add them back into the Tree.  However, in practice, the helpers needed make it a bit more complicated. 

 

    ' Recursively searches the tree for all nodes at a given depth
    ' and adds them to a list of TreeNodes
    Private Sub FindNodesAtDepth(ByVal collection As TreeNodeCollection, _
                                 ByVal targetDepth As Integer, _
                                 ByVal targetNodes As Generic.List(Of TreeNode))
       
        For Each node As TreeNode In collection
            If node.Depth = targetDepth Then
                targetNodes.Add(node)
            ElseIf node.Depth < targetDepth Then
                FindNodesAtDepth(node.ChildNodes, targetDepth, targetNodes)
            End If
        Next
    End Sub

There's not much to say about this function, it's pretty simple.  It's a depth-first recursive function that creates a list of all the nodes with Depth=targetDepth.

 

    ' Recursively creates a clone of a given TreeNode Hierarchy   
    Private Sub CloneTreeNodeHierarchyRecursive(ByVal inCollection As TreeNodeCollection, _
                                                ByVal outCollection As TreeNodeCollection)
       
        For Each node As TreeNode In inCollection
            Dim clonedNode As TreeNode = CType(node, ICloneable).Clone()           
            outCollection.Add(clonedNode)
           
            If node.ChildNodes.Count > 0 Then
                CloneTreeNodeHierarchyRecursive(node.ChildNodes, clonedNode.ChildNodes)
            End If                       
        Next
    End Sub

This function is interesting and I had some fun writing it.  It's also a depth first recursive function.  It's job is to create a copy of the hierarchy. 

Some quick Q&A about this solution:

Q: Why did I copy all the nodes into a list and then to a collection, why not just pass the collection into FindNodesAtDepth()?

A: TreeNodes (and MenuItems) can only live in one collection at a time.  When they are added to a collection, they are removed from any collection they previously were in.  This causes the initial collection the Node was in to change and .NET does not allow a collection to change while it's being enumerated.

Q: Why did I go through all the business of recursively cloning all the nodes instead of adding them directly to the Tree?

A: This is because, currently, TreeNodes (and MenuItems) cache the Depth property once it is calculated.  At this time, there is no straight forward way to tell an entire hierarchy to recalculate their depth except to create a new node which hasn't calculated it's depth.  If I hadn't done this, the TreeNodes would actually render themselves deeper than expected. 

Link to VB Code Listing
Link to C# Code Listing

Adding items to the end of a SiteMapPath

This question came up on the ASP.NET forums and I thought it was worth mentioning here.  There are some instances when it would be useful to add an extra bit of detail to the end of a SiteMapPath.  For example, lets say there is a MultiView on the page and you'd like the Path to look like:

Home >> Parent Page >> Current Page >> Current View

A SiteMapResolve event handler wouldn't really work here since we need intimate knowledge about the current page's controls.  Here's a couple solutions.

The "cheater" way (easiest) :

        <asp:SiteMapPath ID="SiteMapPath2" runat="server" PathSeparator=" | " >
            <CurrentNodeTemplate>
                <asp:Label ID="Label1" runat="server" Text='<%# Eval("Title") %>'></asp:Label>
                <asp:Label ID="Label3" runat="server" Text='<%# SiteMapPath2.PathSeparator %>'></asp:Label>
                <asp:Label ID="Label2" runat="server" Text='<%# MultiView1.GetActiveView().ID %>'></asp:Label>
            </CurrentNodeTemplate>       
        </asp:SiteMapPath>


The "hack" (minimum code) :

    Protected Sub SiteMapPath1_PreRender(ByVal sender As Object, ByVal e As System.EventArgs)
        Dim sep As New Literal()
        sep.Text = SiteMapPath1.PathSeparator
        Dim view As New Literal()
        view.Text = MultiView1.GetActiveView().ID
        SiteMapPath1.Controls.AddAt(-1, sep)
        SiteMapPath1.Controls.AddAt(-1, view)       
    End Sub


The "nearly complete" (most involved) :

    Protected Sub SiteMapPath1_PreRender(ByVal sender As Object, ByVal e As System.EventArgs)
        Dim sepItem As New SiteMapNodeItem(-1, SiteMapNodeItemType.PathSeparator)
        Dim sepTemplate As ITemplate = SiteMapPath1.PathSeparatorTemplate
        If sepTemplate Is Nothing Then
            Dim separator As New Literal()
            separator.Text = SiteMapPath1.PathSeparator
            sepItem.Controls.Add(separator)           
        Else
            sepTemplate.InstantiateIn(sepItem)       
        End If
        sepItem.ApplyStyle(SiteMapPath1.PathSeparatorStyle)
       
        Dim viewItem As New SiteMapNodeItem(-1, SiteMapNodeItemType.Current)
        Dim viewName As New Literal()
        viewName.Text = MultiView1.GetActiveView().ID
        viewItem.Controls.Add(viewName)
        viewItem.ApplyStyle(SiteMapPath1.CurrentNodeStyle)
       
        SiteMapPath1.Controls.AddAt(-1, sepItem)
        SiteMapPath1.Controls.AddAt(-1, viewItem)
        
    End Sub


Each of these has some pros and cons.  The primary trade off's are complexity vs integration with the SiteMapPath.  Clearly the first two solutions don't take into account SeparatorTemplates.  They also make it difficult to apply the controls styles to the new elements.  However, even the nearly complete way won't be completely seamless with the SiteMapPath.  Primarily the SiteMapPath merges NodeStyle and CurrentNodeStyle to create the style that actually gets applied to the CurrentNode (I'm assuming here that we'll just apply CurrentNodeStyle again).  Since that  merged style is internal and generated on the fly in SiteMapPath, we can't access it.  However, this doesn't stop someone from applying their own styles through code and properties instead.

Two posts in one week!  I'm on a roll.

I wrote a custom provider and now autoselecting nodes in Menu and TreeView doesn't work?

Here's a question I get quite often.  The problem: Menu and TreeView won't autoselect the current node as you're navigating around the site.  It usually happens two ways.  First: someone decided to bind the Menu or TreeView through DataSource instead of DataSourceID or Second: Someone wrote a custom SiteMapProvider.  So what's happening?

Actually, there are two problems here.  In order for Menu/TreeView (here on lets just pretend it's for Menu since the code in TreeView is identical for all intensive purposes and this post applies to both equally) to autoselect a node it must satisfy two conditions: 

   1) Must be bound to a SiteMapDataSource through DataSourceID
   2) The url of the Current Node has to match the key of the item

Ok, so that 2nd one is really an oversight on our part (and should eventually get fixed) but it's important to know about it so that we can make sites work Today.  It so happens that XmlSiteMapProvider found it useful to set the key to the url so this problem is avoided.  Do we recommend it as a best practice?  Not necessarily, if your data comes from a Sql DB, for example, you might have an identity field that would be better suited for the key.

So what can you do?  
For Problem #1) Why are you using DataSource instead of DataSourceID for a SiteMapDataSource?  Add the DataSource to the page, give it an ID and use DataSourceID (and you can still do this all in code)
For problem #2) You could adjust your provider to mate the key and url and store whatever data was in Key in a custom attribute.
(what do you do if there is no url? come up with something unique, XmlSiteMapProvider uses a GUID)


And, if you really don't want to do either of these, then you can also write custom databinding code like this:

C#:
    private string currentKey = SiteMap.CurrentNode.Key;
    protected void Menu1_MenuItemDataBound(object sender, MenuEventArgs e)
    {
       
        if( string.Equals(e.Item.DataPath, currentKey, StringComparison.OrdinalIgnoreCase ) )
            e.Item.Selected = true;
    }

VB:
    Protected Sub Menu1_MenuItemDataBound(ByVal sender As Object, ByVal e As System.Web.UI.WebControls.MenuEventArgs)
        Static currentKey As String = SiteMap.CurrentNode.Key
        If String.Equals(e.Item.DataPath, currentKey, StringComparison.OrdinalIgnoreCase) Then
            e.Item.Selected = True
        End If
    End Sub

 

New NullableBoolean types for TreeView TreeNode.Expanded and TreeNode.ShowCheckBox properties

  Two properties on the TreeView TreeNodes are especially tricky.  They are the Expanded and the ShowCheckBox properties.  And they've changed between Beta2 and the RTM.  The reason they are tricky is that their behavior can be explicitly set or implicitly set through the parent TreeView control.  Here's an example.  Lets say you have a tree that looks like this:



And you want it expanded to a depth of 2 except that "Node 2.1" should not be expanded.  Then to do this, you would set the TreeView to to an expand depth of 2 and that particular nodes expanded to false in the node editor 

And you get this:



It may not seem obvious but in order to accomplish this we need a tri-state variable type.  This is a type that has 3 values:  True, False, and {other}.  In Beta2 we rolled-our-own special type for this called an OptionalBoolean and {other} was called "NotSet".  But this was a lot of extra work, custom to only the TreeView and, as it turns out, .NET 2.0 already supports a tri-state type called a NullableBoolean.  So, for the RTM, the decision was made to use it instead, now {other} is "Null" or "Nothing". 

It looks like this:



However, this does cause a couple issues for some C# code. (I'm showing Expanded here but ShowCheckBox will act similarly)



It is interesting to point out that the above code won't have this limitation in VB.  VB can implicitly cast the True or False values of a NullableBoolean while C# cannot.  However, the code construct isn't really robust in VB.  The nodes Expanded and ShowCheckBox state initially start out as Nothing and then later on get set to their appropriate values.  You could still get an exception testing it like that.




So, what should you do?  There are a couple of solutions but the very best one in this case is to explicitly specify the test:

VB:

If TreeView1.Nodes(0).Expanded = True Then
 'some work here
End IF

'in this case we're explicitly testing for if Expanded is True and therefore Nothing and False will fall through.

C#:

if( TreeView1.Nodes[0].Expanded == false )
{
 //some work here
}

// in this case we're explicitly testing for if Expanded is false and therefore null and true would fall through.


However, since it is a .NET Framework Nullable type, you can use any of the nullable operators and operations on it.  Here's are a couple links to msdn articles about Nullable types:

http://msdn.microsoft.com/vcsharp/2005/overview/language/nullabletypes/
http://winfx.msdn.microsoft.com/library/default.asp?url=/library/en-us/dv_csref/html/0bacbe72-ce15-4b14-83e1-9c14e6380c28.asp

Working with Roles and Windows Authentication

  This post will be pretty short and simple but when I starting thinking about putting up this blog, this was the first topic I came up with. The reason is simply that it took me a lot longer to figure out all the details than I had expected it to (and I have direct access to the devs!).
  In forms authentication, users and roles are very straight-forward. The Web Admin Tool allows web-admins to create users, create roles, put users in roles, and it's done. But with Windows Authentication, it's a little vague. Sure, we have "users" but what exactly are "Roles"? Well, the anti-climatic answer is pretty much: Roles are "groups" either local or domain.

Lets say we had two users: "User1" and "User2" who are in two groups (and therefore in two roles) "UsersGroup" and "Group1" like so:

And to show the correlation, I'll also include a simple web.sitemap that looks like this:

<?xml version="1.0" encoding="utf-8" ?>
<siteMap xmlns="
http://schemas.microsoft.com/AspNet/SiteMap-File-1.0" >
    <siteMapNode title="Root Node, Everyone can see it" roles="*">
        <siteMapNode title="UsersGroup can see this node" roles="dannych-02\UsersGroup" />
        <siteMapNode title="Only Group 1 can see this node" roles="dannych-02\Group1" />
    </siteMapNode>
</siteMap>
Here is User1's page:

 

And here is User2's page:

 

  There is one more little side note. In this example, I didn't put any urls on the siteMap for demonstration purposes. However, putting this into practice takes a little more effort than I've shown. In Forms Authentication, different parts of the site are secured with location tags in web.config. These tags will implicitly filter a site map (so usually roles attributes usually necessary except to expand visibility). With Windows authentication, the security is also dictated by the file authorization. Denied file access can also implicitly filter a site map in addition to the way location tags do it.

Link to source code

More Posts « Previous page - Next page »