(ASP.NET) ASP.NET (2007 год)

SiteMap-провайдер

На этой страничке я расскажу как написать СОБСТВЕННЫЙ SiteMap-провайдер. Особо умного я не нашел ничего на эту тему ни в MSDN, ни в букварях - все переписывают друг у друга один и тот же пример со StaticSiteMapProvider, который мне явно не подходит. Поэтому пришлось разбираться самому.


До начала этой работы у меня был примерно вот такой приготовленный мною файлик Web.sitemap. Именно его цепляет стандартный AspNetXmlSiteMapProvider (при отсутствии указаний на другое имя файла в Web-config). Этот файлик обрабатывается на страничке (обычно MasterPage) двумя контролами

MS подготовила для конфигурирования этих контролов преотличный мастер, который можно вызвать и ровно в один клик получить и меню и путь по сайту - вообще не задумываясь что и как там работает за кулисами. Но мы задумаемся и сначала посмотрим на методы и свойства контрола SiteMapPath (и увидим на этом рисунке как это контрол напрямую обращается к стандартному XmlSiteMapProvider), посмотрим на методы и свойства контрола Menu (и увидим, как он биндится на SiteMapDataSource).

Теперь посмотрим на методы и свойства источника данных - SiteMapDatasource и увидим, как он фактически обращается к тому же провайдеру, к которому контрол SiteMapPath обращается непосоредственно. Далее обратимся к провайдеру и посмотрим на саму основную структуру данных SiteMapNode - которую провайдер отдает либо контролу SiteMapPath непосредственно, либо SiteMapDataSource, который формирует из нее иерархический DataView и отдает его контролу Menu. К этому моменту уже становится ясно что именно и должен будет совершить наш провайдер - скомпоновать дерево из узлов SiteMapNode и отдавать их либо контролу либо SiteMapDataSource.


Теперь попробуем понять КАК ИМЕННО он должен это делать. Первое, что очевидно - ОДИН его экземпляр будет работать со всеми Request'ами на сервер. Иначе говоря, это должен быть реентерабельный код, в необходимых местах залоченный по SyncLock (так же как работает собственно операционная система). Попробуем унаследоваться от SiteMapProvider и посмотреть какие именно методы нам обозначили как MustInherit. Это самый начальный момент моей разработки, когда я еще не понимал, какие методы и в какой момент сработают. Конечно, в мультизадачном варианте он некорректен, как минимум надо было лочить Trace1.AppendLine. После трассировки становится более ли менее понятно, что:


Настало время подготовить некоторые тестовые входные данные для нашего провайдера. На самом деле, все конечно обстоит значительно сложнее и реальные данные УЖЕ загружены в базе Дигимейкера, но мы пока не будем зацикливаться на этом и добьемся корректной работы провайдера на тестовых данных. Итак: <


Теперь пора поговорить об атрибутах каждого узла. К этим атрибутам можно будет привязаться непосредственно со странички, ибо нам не просто надо создать такой провайдер, который бы создавал разные планы сайта для разных юзеров (да еще и принимая данные из базы дигимейкера), но и достаточно визуально накрученный. Те некоторые узлы синенькие, некоторые беленькие и так далее. Эти все фишечки мы заделаем с помощью атрибутов, которые будут записаны у нас базе.

Для этого возьмем вот такой мой библиотечный код:

00001: Public Class SoapFormatter
00002:     Dim Buf() As Byte
00003:     Dim Encoder As New System.Text.ASCIIEncoding
00004:     Dim Soap As New System.Runtime.Serialization.Formatters.Soap.SoapFormatter
00005: 
00006:     Public Sub New(ByVal BufSize As Integer)
00007:         Buf = Array.CreateInstance(GetType(System.Byte), BufSize)
00008:     End Sub
00009: 
00010:     Public Function Serialize(ByVal Input As System.Collections.Specialized.NameValueCollection) As String
00011:         Dim MS As New System.IO.MemoryStream(Buf)
00012:         Soap.Serialize(MS, Input)
00013:         Return Encoder.GetString(Buf)
00014:     End Function
00015: 
00016:     Public Function Deserialize(ByVal Input As String) As System.Collections.Specialized.NameValueCollection
00017:         Buf = Encoder.GetBytes(Input)
00018:         Dim MS As New System.IO.MemoryStream(Buf)
00019:         Return Soap.Deserialize(MS)
00020:     End Function
00021: End Class
сформируем им атрибуты нужных узлов И вот так запишем их в базу. Обратите внимание, как именно я прочитал этот атрибут на страничке. Так же легко я его прочитаю и декларативно в коде HTML-разметки, чтобы у меня узлы на плане сайта были еще и разноцветными и условно содержали либо не содержали ссылки - те были бы ЗАГОЛОВКАМИ подразделов ИЛИ ПРОПУСКАМИ на плане сайта.


Те, кто дочитал до этого момента, уже созрел, чтобы увидеть код простейшей заготовки моего провайдера:

00001: Imports Microsoft.VisualBasic
00002: 
00003: Public Class MySiteMapProvider
00004:     Inherits SiteMapProvider
00005: 
00006:     'Dim Trace1 As New System.Text.StringBuilder
00007:     Dim AdminRight As String
00008:     Dim PrividerName As String
00009:     Dim LocalPrefix as string
00010:     Dim Map As dsSiteMap.MapDataTable
00011:     Dim Right As dsSiteMap.RightDataTable
00012:     Dim Tree As System.Web.SiteMapNode 'MySiteMapNode 
00013:     Dim SOAP as New siSMWeb.SoapFormatter(10000)
00014:     Dim Attr as New Collections.Specialized.NameValueCollection
00015: 
00016: 
00017: #Region "Начальная загрузка дерева"
00018: 
00019:     Public Overrides Sub Initialize(ByVal name As String, ByVal attributes As System.Collections.Specialized.NameValueCollection)
00020:         'Trace1.AppendLine("Initialize")
00021:         If Map Is Nothing Then
00022:             SyncLock Me
00023:                 'сначала читаем все из базы
00024:                 PrividerName = name
00025:                 AdminRight = attributes("AdminRight")
00026:                 LocalPrefix = attributes("LocalPrefix")
00027:                 Map = New dsSiteMap.MapDataTable
00028:                 Right = New dsSiteMap.RightDataTable
00029:                 Dim MapTA1 As New dsSiteMapTableAdapters.MapTA
00030:                 Dim RightTA1 As New dsSiteMapTableAdapters.RightTA
00031:                 MapTA1.Fill(Map)
00032:                 RightTA1.Fill(Right)
00033:                 For Each X As Data.DataRow In Map.Rows
00034:                     X.Item("Url") = LocalPrefix & X.Item("Url")
00035:                     X.Item("Key") = LocalPrefix & X.Item("Key")
00036:                     If IsDBNull(X.Item("Title")) Then X.Item("Title") = System.IO.Path.GetFileNameWithoutExtension(X.Item("Url"))
00037:                     If IsDBNull(X.Item("Description")) Then X.Item("Description") = System.IO.Path.GetFileNameWithoutExtension(X.Item("Url"))
00038:                 Next
00039:                 If Map.Rows.Count = 0 Then Throw New Exception("SiteMap Error - No records in SiteMap table")
00040:                 If Right.Rows.Count = 0 Then Throw New Exception("Right Error - No records in UserRight table")
00041:                 '
00042:                 'теперь строим дерево
00043:                 Tree = New System.Web.SiteMapNode(Me, Map.Rows(0).Item("Key").ToString, Map.Rows(0).Item("Url").ToString, Map.Rows(0).Item("Title").ToString, Map.Rows(0).Item("Description").ToString)
00044:                 Tree.ResourceKey = Map.Rows(0).Item("i") '"i" строки в Map для ссылки из дочернего узла 
00045:                 AddChildren(Map.Rows(0).Item("i"), Tree)
00046:                 For I As Integer = 1 To Map.Rows.Count - 1
00047:                     Dim Z As System.Web.SiteMapNode = GetNodesForTag(Tree, Map.Rows(I).Item("i"))
00048:                     If Z IsNot Nothing and  not IsDBNull(Map.Rows(I).Item("Attributes")) then
00049:                         'добавим в узел десериализованные атрибуты
00050:                         attr.Clear
00051:                         try
00052:                             Attr=Soap.Deserialize(Map.Rows(I).Item("Attributes"))
00053:                         Catch ex As Exception
00054:                             Throw new Exception ("Error in Attributes fields in SiteMap table in row <" & Map.Rows(I).Item("i").ToString & ">", ex)
00055:                         End Try
00056:                         for each T as string in Attr
00057:                             z.Item(t)=attr(t)
00058:                         Next
00059:                     End If
00060:                     'и дочерние узлы
00061:                     If Z IsNot Nothing Then AddChildren(Map.Rows(I).Item("i"), Z)
00062:                 Next
00063:                 'дерево построено - служебные маркеры можно удалить
00064:                 For I As Integer = 0 To Map.Rows.Count - 1
00065:                     Dim Z As System.Web.SiteMapNode = GetNodesForTag(Tree, Map.Rows(I).Item("i"))
00066:                     If Z IsNot Nothing Then Z.ResourceKey = Nothing
00067:                 Next
00068:             End SyncLock
00069:             '
00070:             MyBase.Initialize(name, attributes)
00071:         End If
00072: 
00073:     End Sub
00074: 
00075:     'Добавление узла в дерево из таблы
00076:     Private Sub AddChildren(ByVal TableIndex As Integer, ByRef ParentNode As System.Web.SiteMapNode)
00077:         Dim Y As New System.Web.SiteMapNodeCollection
00078:         For I As Integer = TableIndex To Map.Rows.Count - 1
00079:             If Map.Rows(I).Item("ToParent") = TableIndex Then
00080:                 Dim X As New System.Web.SiteMapNode(Me, Map.Rows(I).Item("Key").ToString, Map.Rows(I).Item("Url").ToString, Map.Rows(I).Item("Title").ToString, Map.Rows(I).Item("Description").ToString)
00081:                 X.ResourceKey = Map.Rows(I).Item("i").ToString
00082:                 X.ParentNode = ParentNode
00083:                 Y.Add(X)
00084:             End If
00085:         Next
00086:         If Y.Count > 0 Then ParentNode.ChildNodes = Y
00087:     End Sub
00088: 
00089:     'Рекурсивный обход дерева в поисках нужного тега
00090:     Private Function GetNodesForTag(ByVal StartNode As System.Web.SiteMapNode, ByVal TableIndex As Integer) As System.Web.SiteMapNode
00091:         If StartNode.ResourceKey = TableIndex Then Return StartNode
00092:         If StartNode.ChildNodes IsNot Nothing Then
00093:             For Each X As System.Web.SiteMapNode In StartNode.ChildNodes
00094:                 If X.ResourceKey = TableIndex Then
00095:                     Return X
00096:                 Else
00097:                     'обход в глубину - сразу же проверяем его дочек
00098:                     Dim Y As System.Web.SiteMapNode = GetNodesForTag(X, TableIndex)
00099:                     If Y IsNot Nothing Then Return Y
00100:                 End If
00101:             Next
00102:         End If
00103:     End Function
00104: 
00105:     'Рекурсивный обход дерева в поисках нужного URL
00106:     Private Function GetNodesForURL(ByVal StartNode As System.Web.SiteMapNode, ByVal URL As String) As System.Web.SiteMapNode
00107:         If StartNode.URL = URL Then Return StartNode
00108:         If StartNode.ChildNodes IsNot Nothing Then
00109:             For Each X As System.Web.SiteMapNode In StartNode.ChildNodes
00110:                 If X.Url = URL Then
00111:                     Return X
00112:                 Else
00113:                     'обход в глубину - сразу же проверяем его дочек
00114:                     Dim Y As System.Web.SiteMapNode = GetNodesForURL(X, URL)
00115:                     If Y IsNot Nothing Then Return Y
00116:                 End If
00117:             Next
00118:         End If
00119:     End Function
00120: 
00121: #End Region
00122: 
00123:     Public Overloads Overrides Function FindSiteMapNode(ByVal rawUrl As String) As System.Web.SiteMapNode
00124:         'Trace1.AppendLine("FindSiteMapNode:" & rawUrl & ",Context.Session('ContactID')=" & HttpContext.Current.Session("ContactID"))
00125:         Return GetNodesForURL(Tree, rawUrl)
00126:     End Function
00127: 
00128:     Protected Overrides Function GetRootNodeCore() As System.Web.SiteMapNode
00129:         'Trace1.AppendLine("GetRootNodeCore,Context.Session('ContactID')=" & HttpContext.Current.Session("ContactID"))
00130:         Return Tree
00131:     End Function
00132: 
00133:     'Occurs when the CurrentNode property is called. 
00134:     Private Function MySiteMapProvider_SiteMapResolve(ByVal sender As Object, ByVal e As System.Web.SiteMapResolveEventArgs) As System.Web.SiteMapNode Handles Me.SiteMapResolve
00135:         'Trace1.AppendLine("SiteMapResolve. Context.Session('ContactID')=" & e.Context.Session("ContactID"))
00136:	       Return GetNodesForURL(Tree, e.Context.Current.Request.RawUrl)
00137:     End Function
00138: 
00139: #Region "Неактуально в моей схеме"
00140:     'Апендикс. Сюда выпадает при всех обращениях к ChildNodes (даже при загрузке в Initialize)
00141:     Public Overrides Function GetChildNodes(ByVal node As System.Web.SiteMapNode) As System.Web.SiteMapNodeCollection
00142:         'Trace1.AppendLine("GetChildNodes: " & node.Url)
00143:     End Function
00144:
00145:     'Апендикс. Сюда выпадает код ParentNode у узла Nothing
00146:     Public Overrides Function GetParentNode(ByVal node As System.Web.SiteMapNode) As System.Web.SiteMapNode
00147:         'Trace1.AppendLine("GetParentNode: " & node.Url)
00148:     End Function
00149: #End Region
00150: End Class

Разумеется расширять вышевыложенный шаблон проекта можно В ЛЮБОМ направлении. Ну тут приведен тот самый момент разработки, когда провайдер еще НЕ утратил своей универсальности и применимости для всех и вся. На описанный момент - это полностью работающий универсальный класс. Отсюда этот проект можно развивать в любую сторону. Но я его развил только в определенную, необходимую заказчику. Конечно, полностью коммерческий продукт я описывать тут не буду, ибо права на него принадлежат не мне, но я лишь НАМЕКНУ, как В ПРИНЦИПЕ МОЖНО развивать этот проект дальше. Я упоминал, что одна из целей этого провайдера - разграничить создать разный план сайта у разных юзеров.

Здесь я вижу два пути. В первом варианте докручивания к этот код прав юзеров - нам потребуется создать сам узел дерева с дополнительным свойством - минимальным уровнем прав юзера, чтобы этот узел был виден в контексте юзера. Разумеется тут можно докрутить и группы и прочее, но мой собственный провайдер аутентификации (надстройка над дигимейкером) не поддерживает групп. Поэтому их тут и нет:

00001: Public Class MySiteMapNode
00002:     Inherits System.Web.SiteMapNode
00003: 
00004:     Dim Node_Right as Integer
00005:     Public readonly property NodeRight as Integer
00006:         Get
00007:             return Node_Right
00008:         End Get
00009:     End Property
00010:     Public Sub New(NodeRight as Integer, ByVal Provider As System.Web.SiteMapProvider, ByVal Key As String)
00011:         MyBase.New(Provider, Key)
00012:         Node_Right =NodeRight
00013:     End Sub
00014: 
00015:     Public Sub New(NodeRight as Integer,ByVal Provider As System.Web.SiteMapProvider, ByVal Key As String, Url as string)
00016:         MyBase.New(Provider,Key, Url)
00017:         Node_Right =NodeRight
00018:     End Sub
00019: 
00020:     Public Sub New(NodeRight as Integer,ByVal Provider As System.Web.SiteMapProvider, ByVal Key As String, Url as string, Title as string)
00021:         MyBase.New(Provider,Key, Url, Title)
00022:         Node_Right =NodeRight
00023:     End Sub
00024: 
00025:     Public Sub New(NodeRight as Integer,ByVal Provider As System.Web.SiteMapProvider, ByVal Key As String, Url as string, Title as string, Description as string)
00026:         MyBase.New(Provider,Key, Url, Title, Description)
00027:         Node_Right =NodeRight
00028:     End Sub
00029: 
00030:     Public Sub New(NodeRight as Integer,ByVal Provider As System.Web.SiteMapProvider, ByVal Key As String, Url as string, Title as string, Description as string, Roles as system.Collections.Ilist, Attributes as System.Collections.Specialized.NameValueCollection, ExplicitResourceKeys as System.Collections.Specialized.NameValueCollection, ImplocitResourceKey as string)
00031:         MyBase.New(Provider,Key, Url, Title, Description,  Roles, Attributes , ExplicitResourceKeys , ImplocitResourceKey )
00032:         Node_Right =NodeRight
00033:     End Sub
00034: 
00035: End Class
в этом варианте View дерева сайта надо создавать на ходу для каждого юзера отдельно. Это весьма экономно по расходуемой памяти. Но требует перегрузки не только System.Web.SiteMapNode, но и System.Web.SiteMapNodeCollection.

Второй вариант более быстрый, не требует владения тонкостями обьектного программирования. Но плата за это - ощутимый расход памяти при значительном количестве юзеров с разными правами. В этом варианте можно создать например вот такой словарик

00001: Dim SiteMapList as New System.Collections.Generic.Dictionary(Of Integer, System.Web.SiteMapNode)
в который укладывать все возможные представления о структуре сайта и далее возвращать предстваления юзерам с конкретными правами примерно вот так:
00002: Dim Right as Integer = GetCurrentUserRight
00003: If not SiteMapList.ContainsKey(Right) then
00004:    Dim Tree as system.web.SiteMapNode
00005:    Synclock ME
00006:    'дерева сайта для юзера с такими правами еще нету - надо строить
00007:         Tree= BuildSiteTree(Right)
00008:         SiteMapList.Add(Right,Tree)
00009:    End Synclock
00010:    return Tree
00011: else
00012:    return SiteMapList(Right)
00013: End If

Еще один вектор развития дальнейшего этого проекта - это изменение меню без перезапуска приложения. Это можно сделать и через отдельный административный интерфейс. Впрочем для моего проекта с Дигимейкером это не подходит. Другой вариант - сделать применить встроенный в ASP2 сервис событий изменения рекордсетов. Третий вариант - только для SQL2005 - применить Notification Server. И наконец, четвертый - который мне наиболее близок по духу (и к тому же подходит для работы в среде Дигимейкера) - создать в домене приложения поток, который будет держать открытый коннект к базе. Когда триггер словит изменение в нужном мне рекордсете, он рвет коннект. Висящий поток перезапускает опять коннект на WAITFOR и выдает Refresh в провайдер.

В любом случае вам придется вытащить на форму адрес рефреша провайдера. Делается это с помощью делегатов (это вообще хороший пример их применения). Для этого в самом провайдере делается вот такое объявление делегата:

Public delegate sub SiteMapProvider_Refresh (ByVal name As String, ByVal attributes As System.Collections.Specialized.NameValueCollection)
И затем адрес провайдера запоминается в обьекте Application вот так:

Сам по себе вызов этого делегата с формы выглядит так:


Теперь последний вопрос, который мы тут посмотрим - как привязаться на страничке к данным, сформированным моим провайдерм. Вообще, привязка - это непростая придумка. Особенно когда надо выражения привязки составить самостоятельно. Поэтому я тут покажу, как именно прицепиться на страничке к данным, сформированным моим провайдером. Для этого:


И, наконец, самый последний вопрос, про который я чуть не забыл. Провайдер конфигурится через Web-конфиг, через заведомо предусмотренную секцию конфигурации. При этом имя класса провайдера в контролах можно даже не задавать (если провайдер один).




Разумеется, к этому провайдеру вам придется сделать административную форму, если только вы не захотите предоставить юзеру напрямую ковыряться в базе. Эта админка может, например, выглядеть вот так:




Текст этой админ-формы совершенно тривиален и представляет собой обычную ONLINE-редактируемую сетку и единственное, о чем тут может быть упомянуто - это то, что редактируемый шаблон как видите заполняется существенно по разному для каждой строки. В комбешнике можно установить ТОЛЬКО ДОПУСТИМЫЕ значения для номера узла вышележащего уровня. Жизненный цикл странички позволяет добиться такого эффекта ТОЛЬКО если вынести шаблон в отдельный контрол.



Comments ( )
<00>  <01>  <02>  <03>  <04>  <05>  <06>  <07>  <08>  <09>  <10>  <11>  <12>  <13>  <14>  <15>  <16>  <17>  <18>  <19>  <20>  <21>  <22>  <23
Link to this page: //www.vb-net.com/asp2/22/index.htm
<SITEMAP>  <MVC>  <ASP>  <NET>  <DATA>  <KIOSK>  <FLEX>  <SQL>  <NOTES>  <LINUX>  <MONO>  <FREEWARE>  <DOCS>  <ENG>  <CHAT ME>  <ABOUT ME>  < THANKS ME>