SqlSiteMapProvider in VB.NET

UPDATE 6.18.2008: You can download the file from here


Alright, so I wanted to build a dynamically/DB driven role-based menu system utilizing framework 2.  I started with this article… http://msdn.microsoft.com/msdnmag/issues/06/02/WickedCode/

 

And then converted to VB.NET…Not to come across some items that I needed to change…

 

I came across the following errors:

“'System.Web.Caching.CacheItemRemovedCallback' is a delegate type and requires a single 'addressof' expression as the only argument to the constructor”

 

Which required this line “New CacheItemRemovedCallback(AOnSiteMapChanged))” in the BuildSiteMap() function to be changed to “New CacheItemRemovedCallback(AddressOf OnSiteMapChanged))”  No biggy.

 

Then there was some funky stuff going on with NULL values (mainly in the “description” and “parentID” columns”  Turns out there was a bunch of this going on..

 

Dim title As String = IIf(reader.IsDBNull(_indexTitle), Nothing, reader.GetString(_indexTitle).Trim())

 

Which was freaking out because of the NULL values (see http://weblogs.asp.net/psteele/archive/2003/10/09/31250.aspx)

 

So I added a simple function “CheckNullRefs” to account for that. 

 

Lastly, it looks like the “Roles” property from SqlSiteMapProvider was actually giving me the _number_ of roles assigned to a siteMapNode rather than a collection of the actual roles themselves.  So for example roles=”2” instead of

roles{0}=”Users”

roles(1)=”Admin”

 

I added a public array list, and then added the line below to the “CreateSiteMapNodeFromDataReader” function. 

roles = ArrayList.Adapter(rolelist)

 

I’m sure there is a more elegant way to do all of this…but figured this would be helpful since I found little to no info on it by googling. 

 

Update! I had a, uhm, "feature" in my code..the signature of the ReplaceNullRefs was wrong (was accepting ByVal rdrVal As String instead of Integer.  Thanks to Benjamin Howarth for pointing this out!

Update 9/20:  I've updated the ReplaceNullRefs function below, thanks Rob!


This allows me to dynamically build my menu by doing the following in the MenuItemDataBound sub

    Protected Sub PublicMenu_MenuItemDataBound(ByVal sender As Object, ByVal e As MenuEventArgs)

        'Users can be in multiple groups so...if the roles attribute of a siteMapNode contains one of the roles that a user belongs to

        'do not remove it.  Remove all others.

        If (UserRoleName.Length) < 1 Then

            UserRoleName = Roles.GetRolesForUser()

        End If

        Dim blmRemoveRole As Boolean = True

For i As Integer = 0 To UserRoleName.Length - 1

            If CType(e.Item.DataItem, System.Web.SiteMapNode).Roles.Contains(UserRoleName(i)) Then

              

                blmRemoveRole = False

                Exit For

            End If

        Next

 

        If blmRemoveRole Then

            If e.Item.Parent Is Nothing Then

                Me.MainMenu.Items.Remove(e.Item)

            Else

                e.Item.Parent.ChildItems.Remove(e.Item)

            End If

        End If

 

    End Sub

 


 
Imports System

Imports System.Web

Imports System.Data.SqlClient

Imports System.Collections.Specialized

Imports System.Configuration

Imports System.Web.Configuration

Imports System.Collections.Generic

Imports System.Configuration.Provider

Imports System.Security.Permissions

Imports System.Data.Common

Imports System.Data

Imports System.Web.Caching

 

''' 

''' Summary description for SqlSiteMapProvider

''' 

 _

Public Class SqlSiteMapProvider

    Inherits StaticSiteMapProvider

    Private Const _errmsg1 As String = "Missing node ID"

    Private Const _errmsg2 As String = "Duplicate node ID"

    Private Const _errmsg3 As String = "Missing parent ID"

    Private Const _errmsg4 As String = "Invalid parent ID"

    Private Const _errmsg5 As String = "Empty or missing connectionStringName"

    Private Const _errmsg6 As String = "Missing connection string"

    Private Const _errmsg7 As String = "Empty connection string"

    Private Const _errmsg8 As String = "Invalid sqlCacheDependency"

    Private Const _cacheDependencyName As String = "__SiteMapCacheDependency"

 

 

    Private _connect As String

    ' Database connection string

    Private _database As String, _table As String

    ' Database info for SQL Server 7/2000 cache dependency

    Private _2005dependency As Boolean = False

    ' Database info for SQL Server 2005 cache dependency

    Private _indexID As Integer, _indexTitle As Integer, _indexUrl As Integer, _indexDesc As Integer, _indexRoles As Integer, _indexParent As Integer

    Private _nodes As New Dictionary(Of Integer, SiteMapNode)(16)

    Private ReadOnly _lock As New Object()

    Private _root As SiteMapNode

    'Added...Declare an arraylist to hold all the roles this menu item applies to

    Public roles As New ArrayList

 

 

 

    Public Overloads Overrides Sub Initialize(ByVal name As String, ByVal config As NameValueCollection)

 

        ' Verify that config isn't null

        If config Is Nothing Then

            Throw New ArgumentNullException("config")

        End If

 

        ' Assign the provider a default name if it doesn't have one

        If [String].IsNullOrEmpty(Name) Then

            Name = "SqlSiteMapProvider"

        End If

 

        ' Add a default "description" attribute to config if the

        ' attribute doesnt exist or is empty

        If String.IsNullOrEmpty(config("description")) Then

            config.Remove("description")

            config.Add("description", "SQL site map provider")

        End If

 

        ' Call the base class's Initialize method

        MyBase.Initialize(Name, config)

 

        ' Initialize _connect

        Dim connect As String = config("connectionStringName")

 

        If [String].IsNullOrEmpty(connect) Then

            Throw New ProviderException(_errmsg5)

        End If

 

        config.Remove("connectionStringName")

 

        If WebConfigurationManager.ConnectionStrings(connect) Is Nothing Then

            Throw New ProviderException(_errmsg6)

        End If

 

        _connect = WebConfigurationManager.ConnectionStrings(connect).ConnectionString

 

        If [String].IsNullOrEmpty(_connect) Then

            Throw New ProviderException(_errmsg7)

        End If

 

        ' Initialize SQL cache dependency info

        Dim dependency As String = config("sqlCacheDependency")

 

        If Not [String].IsNullOrEmpty(dependency) Then

            If [String].Equals(dependency, "CommandNotification", StringComparison.InvariantCultureIgnoreCase) Then

                SqlDependency.Start(_connect)

                _2005dependency = True

            Else

                ' If not "CommandNotification", then extract database and table names

                Dim info As String() = dependency.Split(New Char() {":"c})

                If info.Length <> 2 Then

                    Throw New ProviderException(_errmsg8)

                End If

 

                _database = info(0)

                _table = info(1)

            End If

 

            config.Remove("sqlCacheDependency")

        End If

 

        ' SiteMapProvider processes the securityTrimmingEnabled

        ' attribute but fails to remove it. Remove it now so we can

        ' check for unrecognized configuration attributes.

 

        If config("securityTrimmingEnabled") IsNot Nothing Then

            config.Remove("securityTrimmingEnabled")

        End If

 

        ' Throw an exception if unrecognized attributes remain

        If config.Count > 0 Then

            Dim attr As String = config.GetKey(0)

            If Not [String].IsNullOrEmpty(attr) Then

                Throw New ProviderException("Unrecognized attribute: " + attr)

            End If

        End If

    End Sub

 

    Public Overloads Overrides Function BuildSiteMap() As SiteMapNode

        SyncLock _lock

            ' Return immediately if this method has been called before

            If _root IsNot Nothing Then

                Return _root

            End If

 

            ' Query the database for site map nodes

            Dim connection As New SqlConnection(_connect)

 

            Try

                Dim command As New SqlCommand("proc_GetSiteMap", connection)

                command.CommandType = CommandType.StoredProcedure

 

                ' Create a SQL cache dependency if requested

                Dim dependency As SqlCacheDependency = Nothing

 

                If _2005dependency Then

                    dependency = New SqlCacheDependency(command)

                ElseIf Not [String].IsNullOrEmpty(_database) AndAlso Not String.IsNullOrEmpty(_table) Then

                    dependency = New SqlCacheDependency(_database, _table)

                End If

 

                connection.Open()

                Dim reader As SqlDataReader = command.ExecuteReader()

                _indexID = reader.GetOrdinal("ID")

                _indexUrl = reader.GetOrdinal("Url")

                _indexTitle = reader.GetOrdinal("Title")

                _indexDesc = reader.GetOrdinal("Description")

                _indexRoles = reader.GetOrdinal("Roles")

                _indexParent = reader.GetOrdinal("Parent")

 

                If reader.Read() Then

                    ' Create the root SiteMapNode and add it to the site map

                    _root = CreateSiteMapNodeFromDataReader(reader)

                    AddNode(_root, Nothing)

 

                    ' Build a tree of SiteMapNodes underneath the root node

                    While reader.Read()

                        ' Create another site map node and add it to the site map

                        Dim node As SiteMapNode = CreateSiteMapNodeFromDataReader(reader)

                        AddNode(node, GetParentNodeFromDataReader(reader))

                    End While

 

                    ' Use the SQL cache dependency

                    If dependency IsNot Nothing Then

                        HttpRuntime.Cache.Insert(_cacheDependencyName, New Object(), dependency, Cache.NoAbsoluteExpiration, Cache.NoSlidingExpiration, CacheItemPriority.NotRemovable, _

                         New CacheItemRemovedCallback(AddressOf OnSiteMapChanged))

                    End If

                End If

            Finally

                connection.Close()

            End Try

 

            ' Return the root SiteMapNode

            Return _root

        End SyncLock

    End Function

 

    Protected Overloads Overrides Function GetRootNodeCore() As SiteMapNode

        SyncLock _lock

            BuildSiteMap()

            Return _root

        End SyncLock

    End Function

 

    ' Helper methods

    Private Function CreateSiteMapNodeFromDataReader(ByVal reader As DbDataReader) As SiteMapNode

        ' Make sure the node ID is present

        If reader.IsDBNull(_indexID) Then

            Throw New ProviderException(_errmsg1)

        End If

 

        ' Get the node ID from the DataReader

        Dim id As Integer = reader.GetInt32(_indexID)

 

        ' Make sure the node ID is unique

        If _nodes.ContainsKey(id) Then

            Throw New ProviderException(_errmsg2)

        End If

 

        ' Get title, URL, description, and roles from the DataReader

        Dim title As String = ReplaceNullRefs(reader, _indexTitle)

        Dim url As String = ReplaceNullRefs(reader, _indexUrl)

 

        'Eliminated...see http://weblogs.asp.net/psteele/archive/2003/10/09/31250.aspx

        Dim description As String = ReplaceNullRefs(reader, _indexDesc)

 

        'Changed variable name from 'roles' to 'rolesN' and added line 230 to dump all roles into an arrayList

        Dim rolesN As String = IIf(reader.IsDBNull(_indexRoles), Nothing, reader.GetString(_indexRoles).Trim())

       

 

        Dim rolelist As String() = Nothing

        If Not [String].IsNullOrEmpty(rolesN) Then

            rolelist = rolesN.Split(New Char() {","c, ";"c}, 512)

        End If

        roles = ArrayList.Adapter(rolelist)

 

 

        ' Create a SiteMapNode

        Dim node As New SiteMapNode(Me, id.ToString(), url, title, description, rolelist, _

         Nothing, Nothing, Nothing)

 

        ' Record the node in the _nodes dictionary

        _nodes.Add(id, node)

 

        ' Return the node       

        Return node

    End Function

 

    Private Function ReplaceNullRefs(ByVal rdr As DbDataReader, ByVal rdrVal As Integer) As String

        If Not (rdr.IsDBNull(rdrVal)) Then

            ' Thanks Rob Johnston

            Return rdr.GetString(rdrVal)

        Else

            Return String.Empty

        End If

    End Function

 

    Private Function GetParentNodeFromDataReader(ByVal reader As DbDataReader) As SiteMapNode

        ' Make sure the parent ID is present

        If reader.IsDBNull(_indexParent) Then

            '**** Commented out throw, added exit function ****

            Throw New ProviderException(_errmsg3)

            'Exit Function

        End If

 

        ' Get the parent ID from the DataReader

        Dim pid As Integer = reader.GetInt32(_indexParent)

 

        ' Make sure the parent ID is valid

        If Not _nodes.ContainsKey(pid) Then

            Throw New ProviderException(_errmsg4)

        End If

 

        ' Return the parent SiteMapNode

        Return _nodes(pid)

    End Function

 

    Private Sub OnSiteMapChanged(ByVal key As String, ByVal item As Object, ByVal reason As CacheItemRemovedReason)

        SyncLock _lock

            If key = _cacheDependencyName AndAlso reason = CacheItemRemovedReason.DependencyChanged Then

                ' Refresh the site map

                Clear()

                _nodes.Clear()

                _root = Nothing

            End If

        End SyncLock

    End Sub

End Class
You can download the file from here kick it on DotNetKicks.com

Print | posted @ Tuesday, August 07, 2007 11:10 AM

Comments on this entry:

Gravatar # re: SqlSiteMapProvider in VB.NET w/some updates
by Benjamin Howarth at 8/14/2007 1:37 PM

Amazing stuff!!
I looked up the C# article that you refer to and subsequently translated into VB.NET - I think it's great that finally someone has come up with a database-powered SiteMap function for people using VB.NET!
Great work, I'll be using this in many sites to come in the future!

Benjamin
aka medicineworker
aka MPDDK-mm4
Gravatar # re: SqlSiteMapProvider in VB.NET
by Martyn at 10/3/2007 1:33 PM

Thanks, was just starting to attempt convert the C# example to VB when I found this page. You have saved me a lot of work!
Gravatar # re: SqlSiteMapProvider in VB.NET
by Earl Kimble at 6/18/2008 11:04 AM

This is great! I suppose I've put off learning C# long enough and this just gave me a reason to push it off more. Is there a place to download the final code?
Gravatar # re: SqlSiteMapProvider in VB.NET
by Sanjay at 6/18/2008 11:51 AM

Updated the original posting - just for sake of clarity...you can download it from here http://www.mediafire.com/?1vqezm2ezat

enjoy!
Sanjay
Gravatar # re: SqlSiteMapProvider in VB.NET
by Chris Love at 8/28/2008 4:31 PM

Good job on this. On the use of IIF, you should update that to use the terinary if(statement, true, false). IIF has been deprecaded and is not as robust as the terninary if statement.
Gravatar # re: SqlSiteMapProvider in VB.NET
by gc at 9/17/2008 11:05 AM

Still getting this error, any ideas. I am a newbie, so all your patience will be greatly appreciated.

Data is Null. This method or property cannot be called on Null values.
Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.

Exception Details: System.Data.SqlTypes.SqlNullValueException: Data is Null. This method or property cannot be called on Null values.

Source Error:


Line 222:
Line 223: 'Changed variable name from 'roles' to 'rolesN' and added line 230 to dump all roles into an arrayList
Line 224: Dim rolesN As String = IIf(reader.IsDBNull(_indexRoles), Nothing, reader.GetString(_indexRoles).Trim())
Line 225:
Line 226:


Source File: C:\Visual Studio 2008\TestApp\TestApp\App_Code\SqlSiteMapProvider.vb Line: 224
Gravatar # re: SqlSiteMapProvider in VB.NET
by Martin at 12/31/2008 3:21 PM

I am testing your SqlSiteMapProvider but am having a little difficulty.

I have managed to get it to provide the site map from the data stored in the database but can only seem to display a single root node.

The output of my stored proc is as follows:

ID, Title, Description, Url, Roles, Parent
0, Home, <NULL>, Home.aspx, *, <NULL>
1, Menu1, test1 link, test1.aspx, *, 0
2, Menu1_1, test1_1 link, test1_1.aspx, *, 1
3, Menu1_2, test1_2 link, test1_2.aspx, *, 1
4, Menu2, test2 link, test2.aspx, *, 0
5, Menu1_3, test1_3 link, test1_3.aspx, *, 1

With this I get a "Home" root node with everything nested below it.

What I would like to get is nodes Menu1 and Menu2 as the first nodes displayed in the menu rather than Home.

I have tried to remove the Home entry and set the parent IDs for Menu1 and Menu2 to NULL but get an error.

Please can you provide some guidance as to where I am gong wrong

Thanks in advance.
Gravatar # re: SqlSiteMapProvider in VB.NET
by SanjayU at 12/31/2008 6:00 PM

Martin,
I've responded via e-mail. Let me know if my suggestion does the trick.

Cheers,
Sanjay
Gravatar # re: SqlSiteMapProvider in VB.NET
by Martin at 1/2/2009 4:26 AM

Sanjay,

Your solution worked a treat. Thank you very much for taking the time to solve my problem.

Martin
Gravatar # re: SqlSiteMapProvider in VB.NET
by Kevin at 3/7/2009 10:20 PM

If somebody can help me a bit...
I am using a statement in static item template to change the look of a menu item...
before I would use If Menu1.SelectedValue.Equals(value) Then ... Where "value" = Eval("Text")

With code above, Menu1.SelectedValue always = NULL

What can I do?

Gravatar # re: SqlSiteMapProvider in VB.NET
by Justin Harrison at 3/30/2009 6:09 PM

Hi,
This is a great class, and shows great potential for me, but i have struggled to get it working. I have a problem i thought you might be able to help with. The navigation works, but sometimes it renders fine... sometimes it is invisible. Mostly it is invisible. I have it inside a .ascx control file if that makes a difference. Any ideas??

Thanks for your help.
Justin
Gravatar # re: SqlSiteMapProvider in VB.NET
by SanjayU at 3/31/2009 11:18 AM

Jason,
I have responded via e-mail as well, but, check to see if the menu is rendering at all by doing a view source. I'd like to make sure this isn't a CSS issue. Alternatively, e-mail/post your code and I'll take a look.

Cheers,
Sanjay
Gravatar # re: SqlSiteMapProvider in VB.NET
by Justin at 3/31/2009 1:50 PM

SQLSiteMapProvider with SQL Server 2008 Express

Getting SQLSMP working with SQL 2008 Express is tricky. However i followed the steps laid out in Jeff Proise article in conjunction with the advice given on this blog, and with a great deal of help from sanjay.

SQL EX 2008 would seem to be pretty much the same as SQL EX 2005, however i was not able to get the menu control to render with sqlcachedependency. Sometimes it would render, most of the time it would not render at all.

I had to change the web.config file in order to get it to work

<system.web>

<siteMap enabled="true" defaultProvider="SqlSiteMapProvider">
<providers>
<add name="SqlSiteMapProvider"
type="SqlSiteMapProvider"
securityTrimmingEnabled="true"
connectionStringName="YOURCONNECTIONSTRING"
/>
</providers>
</siteMap>
<caching>
<sqlCacheDependency enabled="false" />
</caching>

Note: sqlCacheDependency is false and there is no CommandNotification in my SqlSiteMapProvider details.

Once i did this it worked fine.

Thanks again to Sanjay for your help.

Justin
Gravatar # re: SqlSiteMapProvider in VB.NET
by Thore at 4/8/2009 11:32 AM

Hi!

I need a solution on the Null-value problem that are mentioned over

Thore
Gravatar # re: SqlSiteMapProvider in VB.NET
by Obed at 8/28/2009 11:13 AM

Hi, I've tried to build my sitemap using your code, but I cann't, sorry for my english,I speak Spanish.
I'm using a repeater as menu in my Master Page, and I've field my sitemap table in the database with this:

1 HOME NULL ~/Default.aspx * NULL
10 News NULL NULL * 1
11 Local News from greater Seattle ~/Summary.aspx?CategoryID=0 * 1
12 World News from around the world ~/Summary.aspx?CategoryID=2 * 1
20 Sports NULL NULL * 1
21 Baseball What's happening in baseball ~/Summary.aspx?CategoryID=3 * 1

So, when I tried to load the page, I just can to see the first item, i mean, I just can to see Home, but no one else,PLease help me! I'd like to know what i've done wrong,.... and is it necessary work with roles?..
Thanks
Obed
Gravatar # re: SqlSiteMapProvider in VB.NET
by Obed at 8/28/2009 11:31 AM

HI this is me again.... I've just read the martin's problem, and I see hi had the same problem as me, so, please SanJayU, I need your help!!!.. please!
I'm newbie, sorry :(.
Obed
Gravatar # re: SqlSiteMapProvider in VB.NET
by dferraro at 10/7/2009 5:28 PM

Hi All,
I'm using this provided class for the most part. (I did run into some issues and had to make some code changes)

However I am having one issue
In the code I have the following:

If dependency IsNot Nothing Then
HttpRuntime.Cache.Insert(_cacheDependencyName, New Object(), dependency, DateTime.Now.AddMilliseconds(60000), Cache.NoSlidingExpiration, CacheItemPriority.NotRemovable, _
New CacheItemRemovedCallback(AddressOf OnSiteMapDeleted))
End If

This should mean the Cache should invalidate after 60 seconds, no?

I first run my app with the config set to sqlCacheDependency="CommandNotification"
The above code executes, which states that the data should expire after 60 seconds... ?

Then I run my code again this time sqlCacheDependency="", which means that the above code should not run...

Shouldn't my sitemap refresh after 60 seconds then? It does not. In order for it to re-fresh, I have to end my debugging session / start debugging again (or I guess re-start my webserver). I *am* using IIS and not VS Web Server..

Any idea what I'm doing wrong? Why if I set the cache to 60 seconds expiration does it seem to just stay cached forever until I re-start the debug session / web server? Am I misunderstanding something here?
Many thanks in advance,
Drew
Post A Comment
Title:
Name:
Email:
Website:
Comment:
Verification:
 
 
Twitter