Developer forum

Forum » CMS - Standard features » Custom UrlProvider

Custom UrlProvider

Claus Ørum-Petersen
Reply

Hi,

Is it possible the write your own provider for the new functionality in 9.7 that would skip the pages and keep the ecom structure ?

There is not a lot of documentation on the subject yet - this namespace seems like the right place ?

https://doc.dynamicweb.com/api/html/f8b73970-6a7c-83f0-cb5d-66b69829456f.htm


Replies

 
Nicolai Pedersen
Reply

It is possible. But the shipped URL provider does that already, see attached screen dumps - also find our own provider attached.

 

And our provider code:

Imports System.Collections.Concurrent
Imports System.IO
Imports Dynamicweb.Data
Imports Dynamicweb.Ecommerce.International
Imports Dynamicweb.Ecommerce.Products
Imports Dynamicweb.Ecommerce.Shops
Imports Dynamicweb.Ecommerce.Variants
Imports Dynamicweb.Extensibility.AddIns
Imports Dynamicweb.Extensibility.Editors
Imports Dynamicweb.Extensibility.Notifications
Imports Dynamicweb.Frontend.UrlHandling
Imports Dynamicweb.SystemTools

Namespace Frontend.UrlHandling
    <AddInName("Ecommerce")>
    Public Class ShopUrlDataProvider
        Inherits UrlDataProvider
        Implements IDropDownOptions

        <AddInLabel("Shop")>
        <AddInParameter(NameOf(IncludeFrom)), AddInParameterEditor(GetType(RadioParameterEditor), "SortBy=Key")>
        Public Property IncludeFrom As String = "ContextShop"

        <AddInLabel("Selected shop")>
        <AddInParameter(NameOf(SelectedShopId)), AddInParameterEditor(GetType(DropDownParameterEditor), "")>
        Public Property SelectedShopId As String

        <AddInLabel("Language")>
        <AddInParameter(NameOf(IncludeLanguage)), AddInParameterEditor(GetType(RadioParameterEditor), "SortBy=Key")>
        Public Property IncludeLanguage As String = "ContextLanguage"

        <AddInLabel("Selected language")>
        <AddInParameter(NameOf(SelectedLanguageId)), AddInParameterEditor(GetType(DropDownParameterEditor), "")>
        Public Property SelectedLanguageId As String

        <AddInLabel("Include")>
        <AddInParameter(NameOf(Include)), AddInParameterEditor(GetType(RadioParameterEditor), "SortBy=Key")>
        Public Property Include As String = "GroupsProductsVariants"

        <AddInLabel("Groups")>
        <AddInParameter(NameOf(GroupPath)), AddInParameterEditor(GetType(RadioParameterEditor), "")>
        Public Property GroupPath As String = "Tree"

        <AddInLabel("Products")>
        <AddInParameter(NameOf(ProductPath)), AddInParameterEditor(GetType(RadioParameterEditor), "")>
        Public Property ProductPath As String = "Tree"

        Private Shared lazyGroupProductRelationIndex As New Lazy(Of ConcurrentDictionary(Of String, List(Of String)))(AddressOf InitializeGroupProductRelationIndex)
        Private Shared ReadOnly Property GroupProductRelationIndex As ConcurrentDictionary(Of String, List(Of String))
            Get
                Return lazyGroupProductRelationIndex.Value
            End Get
        End Property

        Private Shared lazyProductUrlDataIndex As New Lazy(Of ConcurrentDictionary(Of String, Dictionary(Of String, UrlDataNode)))(AddressOf InitializeProductUrlDataIndex)
        Private Shared ReadOnly Property ProductUrlDataIndex As ConcurrentDictionary(Of String, Dictionary(Of String, UrlDataNode))
            Get
                Return lazyProductUrlDataIndex.Value
            End Get
        End Property

        Public Overrides Function GetUrlDataNodes(parent As UrlDataNode, dataContext As UrlDataContext) As IEnumerable(Of UrlDataNode)

            Dim nodes As New List(Of UrlDataNode)

            Dim shop As Shop = GetShop(dataContext)

            If shop Is Nothing Then
                Return nodes
            End If

            Dim language As Language = GetLanguage(dataContext)

            If language Is Nothing Then
                Return nodes
            End If

            Dim groups As ICollection(Of Group) = GetTopLevelGroups(shop, language)

            If groups Is Nothing Then
                Return nodes
            End If

            Dim settings As UrlDataSettings = GetUrlDataSettings()

            For Each group As Group In groups
                AddNodesRecursive(nodes, parent, group, settings)
            Next

            Return nodes
        End Function

        Private Function GetUrlDataSettings() As UrlDataSettings
            Dim settings As New UrlDataSettings()

            Select Case Include
                Case "Groups"
                    settings.IncludeProducts = False
                    settings.IncludeVariants = False
                Case "GroupsProducts"
                    settings.IncludeProducts = True
                    settings.IncludeVariants = False
                Case Else
                    settings.IncludeProducts = True
                    settings.IncludeVariants = True
            End Select

            Select Case GroupPath
                Case "Root"
                    settings.PlaceGroupsInRoot = True
                Case Else
                    settings.PlaceGroupsInRoot = False
            End Select

            Select Case ProductPath
                Case "Root"
                    settings.PlaceProductsInRoot = True
                Case Else
                    settings.PlaceProductsInRoot = False
            End Select

            Return settings
        End Function

        Private Function GetTopLevelGroups(shop As Shop, language As Language) As ICollection(Of Group)
            Return shop.TopLevelGroups(language.LanguageId)
        End Function

        Private Function GetLanguage(dataContext As UrlDataContext) As Language
            Dim languageId As String = Nothing

            If String.Equals(IncludeLanguage, "ContextLanguage", StringComparison.OrdinalIgnoreCase) Then
                languageId = dataContext.EcomLanguageId
            ElseIf String.Equals(IncludeLanguage, "SelectedLanguage", StringComparison.OrdinalIgnoreCase) Then
                languageId = SelectedLanguageId
            End If

            Dim language As Language = Nothing
            If Not String.IsNullOrEmpty(languageId) Then
                language = Services.Languages.GetLanguage(languageId)
            End If

            Return language
        End Function

        Private Function GetShop(dataContext As UrlDataContext) As Shop
            Dim shopId As String = Nothing

            If String.Equals(IncludeFrom, "ContextShop", StringComparison.OrdinalIgnoreCase) Then
                shopId = dataContext.EcomShopId
            ElseIf String.Equals(IncludeFrom, "SelectedShop", StringComparison.OrdinalIgnoreCase) Then
                shopId = SelectedShopId
            End If

            Dim shop As Shop = Nothing

            If Not String.IsNullOrEmpty(shopId) Then
                shop = Services.Shops.GetShop(shopId)
            End If

            Return shop
        End Function

        Private Sub AddNodesRecursive(entries As IList(Of UrlDataNode), parent As UrlDataNode, group As Group, settings As UrlDataSettings)
            Dim groupUrlData As UrlDataNode = CreateGroupUrlData(parent, group)

            If settings.PlaceGroupsInRoot Then
                groupUrlData.IgnoreParentPath = True
            End If

            entries.Add(groupUrlData)

            Dim subGroups As IEnumerable(Of Group) = GetSubGroups(group)
            For Each subGroup As Group In subGroups
                AddNodesRecursive(entries, groupUrlData, subGroup, settings)
            Next

            If settings.IncludeProducts Then
                Dim productIds As List(Of String) = Nothing
                If GroupProductRelationIndex.TryGetValue(group.Id, productIds) Then
                    Dim indexByProductId As Dictionary(Of String, UrlDataNode) = Nothing
                    If ProductUrlDataIndex.TryGetValue(group.LanguageId, indexByProductId) Then

                        For Each productId As String In productIds
                            Dim indexedProductUrlData As UrlDataNode = Nothing
                            If indexByProductId.TryGetValue(productId, indexedProductUrlData) Then
                                ' clone and set unique id
                                Dim productUrlData As UrlDataNode = CloneUrlData(indexedProductUrlData)
                                productUrlData.Id = $"{indexedProductUrlData.Id}_{groupUrlData.Id}"
                                productUrlData.ParentId = groupUrlData.Id

                                If settings.PlaceProductsInRoot Then
                                    productUrlData.IgnoreParentPath = True
                                End If

                                entries.Add(productUrlData)

                                If settings.IncludeVariants Then
                                    Dim variantCombinations As IList(Of VariantCombination) = Services.VariantCombinations.GetVariantCombinations(productId)
                                    For Each variantCombination In variantCombinations
                                        Dim variantUrlData As UrlDataNode = CreateProductVariantUrlData(productUrlData, group.LanguageId, variantCombination)

                                        If settings.PlaceProductsInRoot Then
                                            variantUrlData.PathName = productUrlData.PathName & "-" & variantUrlData.PathName
                                            variantUrlData.IgnoreParentPath = True
                                        End If

                                        entries.Add(variantUrlData)
                                    Next
                                End If
                            End If
                        Next
                    End If
                End If
            End If
        End Sub

        Private Shared Function CloneUrlData(urlData As UrlDataNode) As UrlDataNode
            Dim urlDataClone As New UrlDataNode With {
                                                .Id = urlData.Id,
                                                .ParentId = urlData.ParentId,
                                                .PathName = urlData.PathName,
                                                .IgnoreInChildPath = urlData.IgnoreInChildPath,
                                                .IgnoreParentPath = urlData.IgnoreParentPath,
                                                .PathExact = urlData.PathExact,
                                                .QueryStringParameter = urlData.QueryStringParameter,
                                                .QueryStringValue = urlData.QueryStringValue,
                                                .QueryStringExact = urlData.QueryStringExact,
                                                .IgnoreParentQuerystring = urlData.IgnoreParentQuerystring
                                            }
            Return urlDataClone
        End Function

        Private Function CreateGroupUrlData(parent As UrlDataNode, group As Group) As UrlDataNode
            Dim nodeData As New UrlDataNode With {
                                .Id = $"PRODUCTGROUP_{group.Id}_{parent.Id}",
                                .ParentId = parent.Id,
                                .PathName = group.Name,
                                .IgnoreInChildPath = False,
                                .IgnoreParentPath = group.Meta.UrlIgnoreParent,
                                .PathExact = group.Meta.Url,
                                .QueryStringParameter = "GroupID",
                                .QueryStringValue = group.Id,
                                .QueryStringExact = String.Empty,
                                .IgnoreParentQuerystring = False
                            }

            Return nodeData
        End Function

        Private Function CreateProductVariantUrlData(parent As UrlDataNode, languageId As String, variantCombination As VariantCombination) As UrlDataNode
            Dim nodeData As New UrlDataNode With {
                                    .Id = $"PRODUCTVARIANT_{variantCombination.VariantId}_{parent.Id}",
                                    .ParentId = parent.Id,
                                    .PathName = variantCombination.GetVariantName(languageId),
                                    .IgnoreInChildPath = False,
                                    .IgnoreParentPath = False,
                                    .PathExact = String.Empty,
                                    .QueryStringParameter = "VariantID",
                                    .QueryStringValue = variantCombination.VariantId,
                                    .QueryStringExact = String.Empty,
                                    .IgnoreParentQuerystring = False
                                }

            Return nodeData
        End Function

        Private Function GetSubGroups(group As Group) As ICollection(Of Group)
            Dim childRelations As GroupRelationCollection = GroupRelation.GroupRelationsByParentId(group.Id)
            Dim children As New List(Of Group)

            Dim cachedGroups As Dictionary(Of String, Group) = Services.ProductGroups.GetAllGroupsByLanguageId(group.LanguageId)

            For Each childRelation As GroupRelation In childRelations
                Dim theGroup As Group = Nothing
                If cachedGroups.TryGetValue(childRelation.Id, theGroup) AndAlso theGroup IsNot Nothing Then
                    children.Add(theGroup)
                End If
            Next

            Return children
        End Function

        Private Shared Function InitializeGroupProductRelationIndex() As ConcurrentDictionary(Of String, List(Of String))
            Dim result As New ConcurrentDictionary(Of String, List(Of String))

            Using reader = Database.CreateDataReader("SELECT [GroupProductRelationGroupId],[GroupProductRelationProductId] FROM [EcomGroupProductRelation]")
                While reader.Read()
                    Dim groupId As String = reader.GetString(0)
                    Dim productId As String = reader.GetString(1)

                    Dim list As List(Of String) = Nothing
                    If Not result.TryGetValue(groupId, list) Then
                        list = New List(Of String)
                        result.TryAdd(groupId, list)
                    End If

                    list.Add(productId)
                End While
            End Using

            Return result
        End Function

        ''' <summary>
        ''' UrlData by languageid, productid
        ''' </summary>
        Private Shared Function InitializeProductUrlDataIndex() As ConcurrentDictionary(Of String, Dictionary(Of String, UrlDataNode))
            Dim indexByLanguageId As New ConcurrentDictionary(Of String, Dictionary(Of String, UrlDataNode))

            Using reader = Database.CreateDataReader("SELECT [ProductID], [ProductLanguageID], [ProductName], [ProductMetaUrl] FROM [EcomProducts] WHERE ([ProductVariantID] IS NULL OR [ProductVariantID] = '')")
                While reader.Read()
                    Dim productId As String = Convert.ToString(reader.GetString(0))
                    Dim productLanguageId As String = Convert.ToString(reader.GetString(1))
                    Dim productName As String = Convert.ToString(reader(2))
                    Dim productMetaUrl As String = Convert.ToString(reader(3))

                    Dim indexByProductId As Dictionary(Of String, UrlDataNode) = Nothing
                    If Not indexByLanguageId.TryGetValue(productLanguageId, indexByProductId) Then
                        indexByProductId = New Dictionary(Of String, UrlDataNode)
                        indexByLanguageId.TryAdd(productLanguageId, indexByProductId)
                    End If

                    Dim nodeData As New UrlDataNode With {
                                    .Id = $"PRODUCT_{productId}",
                                    .PathName = productName,
                                    .IgnoreInChildPath = False,
                                    .IgnoreParentPath = False,
                                    .PathExact = productMetaUrl,
                                    .QueryStringParameter = "ProductID",
                                    .QueryStringValue = productId,
                                    .QueryStringExact = String.Empty,
                                    .IgnoreParentQuerystring = False
                                }

                    indexByProductId.Add(productId, nodeData)
                End While
            End Using

            Return indexByLanguageId
        End Function

        Public Function GetOptions(dropdownName As String) As Hashtable Implements IDropDownOptions.GetOptions
            Dim options As New Hashtable()

            Select Case dropdownName
                Case NameOf(Include)
                    options.Add("GroupsProductsVariants", Translate.Translate("Groups, products, variants"))
                    options.Add("GroupsProducts", Translate.Translate("Groups, products"))
                    options.Add("Groups", Translate.Translate("Groups"))
                Case NameOf(IncludeFrom)
                    options.Add("ContextShop", Translate.Translate("Current website shop"))
                    options.Add("SelectedShop", Translate.Translate("Selected shop"))
                Case NameOf(SelectedShopId)
                    For Each shop As Shop In Services.Shops.GetShops()
                        options.Add(shop.Id, shop.Name)
                    Next
                Case NameOf(IncludeLanguage)
                    options.Add("ContextLanguage", Translate.Translate("Current website language"))
                    options.Add("SelectedLanguage", Translate.Translate("Selected language"))
                Case NameOf(SelectedLanguageId)
                    For Each language As Language In Services.Languages.GetLanguages()
                        options.Add(language.LanguageId, language.Name)
                    Next
                Case NameOf(GroupPath)
                    options.Add("Tree", Translate.Translate("Tree path"))
                    options.Add("Root", Translate.Translate("Root path"))
                Case NameOf(ProductPath)
                    options.Add("Tree", Translate.Translate("Tree path"))
                    options.Add("Root", Translate.Translate("Root path"))
                Case Else
            End Select

            Return options
        End Function

        Public Overrides Sub RenderAdditionalContent(output As TextWriter)
            MyBase.RenderAdditionalContent(output)

            ' Generate script to conditionally show/hide parameter controls based on selected options

            output.WriteLine("<script>")

            ' shops
            output.WriteLine("  var shopSelector = document.getElementById('SelectedShopId').closest('.form-group');")
            output.WriteLine("  var includeFromOptions = document.getElementsByName('IncludeFrom');")
            output.WriteLine("  handleShowHide(includeFromOptions, showHideShopSelector);")

            output.WriteLine("  function showHideShopSelector() {")
            output.WriteLine("      if (this.value == 'SelectedShop') {")
            output.WriteLine("          shopSelector.style.display = '';")
            output.WriteLine("      } else {")
            output.WriteLine("          shopSelector.style.display = 'none';")
            output.WriteLine("      }")
            output.WriteLine("  }")

            ' languages
            output.WriteLine("  var languageSelector = document.getElementById('SelectedLanguageId').closest('.form-group');")
            output.WriteLine("  var includeLanguageOptions = document.getElementsByName('IncludeLanguage');")
            output.WriteLine("  handleShowHide(includeLanguageOptions, showHideLanguageSelector);")

            output.WriteLine("  function showHideLanguageSelector() {")
            output.WriteLine("      if (this.value == 'SelectedLanguage') {")
            output.WriteLine("          languageSelector.style.display = '';")
            output.WriteLine("      } else {")
            output.WriteLine("          languageSelector.style.display = 'none';")
            output.WriteLine("      }")
            output.WriteLine("  }")

            ' helper methods
            output.WriteLine("function handleShowHide(options, handler) {")
            output.WriteLine("  for (var i = 0; i < options.length; i++) {")
            output.WriteLine("      options[i].addEventListener('change', handler);")
            output.WriteLine("      if (options[i].checked) {")
            output.WriteLine("          if ('createEvent' in document) {")
            output.WriteLine("              var evt = document.createEvent('HTMLEvents');")
            output.WriteLine("              evt.initEvent('change', false, true);")
            output.WriteLine("              options[i].dispatchEvent(evt);")
            output.WriteLine("          } else {")
            output.WriteLine("              options[i].fireEvent('onchange');")
            output.WriteLine("          }")
            output.WriteLine("      }")
            output.WriteLine("  }")
            output.WriteLine("}")

            output.WriteLine("</script>")
        End Sub

        Private Class UrlDataSettings
            Public Property IncludeProducts As Boolean
            Public Property PlaceProductsInRoot As Boolean
            Public Property IncludeVariants As Boolean
            Public Property PlaceGroupsInRoot As Boolean
        End Class

        <Subscribe(Notifications.Ecommerce.Product.ProductGroupRelationAfterSave)>
        <Subscribe(Notifications.Ecommerce.Product.ProductGroupRelationDeleted)>
        Public Class ProductGroupRelationChangedObserver
            Inherits NotificationSubscriber

            Public Overrides Sub OnNotify(notification As String, args As NotificationArgs)
                lazyGroupProductRelationIndex = New Lazy(Of ConcurrentDictionary(Of String, List(Of String)))(AddressOf InitializeGroupProductRelationIndex)
                UrlHelper.ResetPageIndex()
            End Sub

        End Class

        <Subscribe(Notifications.Ecommerce.Product.AfterSave)>
        <Subscribe(Notifications.Ecommerce.Product.AfterDelete)>
        Public Class ProductChangedObserver
            Inherits NotificationSubscriber

            Public Overrides Sub OnNotify(notification As String, args As NotificationArgs)
                lazyProductUrlDataIndex = New Lazy(Of ConcurrentDictionary(Of String, Dictionary(Of String, UrlDataNode)))(AddressOf InitializeProductUrlDataIndex)
                UrlHelper.ResetPageIndex()
            End Sub

        End Class

        <Subscribe(Notifications.Ecommerce.Group.AfterSave)>
        <Subscribe(Notifications.Ecommerce.Group.Deleted)>
        Public Class GroupChangedObserver
            Inherits NotificationSubscriber

            Public Overrides Sub OnNotify(notification As String, args As NotificationArgs)
                UrlHelper.ResetPageIndex()
            End Sub

        End Class

        <Subscribe(Notifications.Ecommerce.Group.RelationUpdated)>
        <Subscribe(Notifications.Ecommerce.Group.RelationUpdated)>
        Public Class GroupRelationChangedObserver
            Inherits NotificationSubscriber

            Public Overrides Sub OnNotify(notification As String, args As NotificationArgs)
                UrlHelper.ResetPageIndex()
            End Sub

        End Class

        <Subscribe(Dynamicweb.Notifications.Standard.Area.OnAreaSaved)>
        <Subscribe(Dynamicweb.Notifications.Standard.Area.OnAfterAreaDeleted)>
        Public Class AreaChangedObserver
            Inherits NotificationSubscriber

            Public Overrides Sub OnNotify(notification As String, args As NotificationArgs)
                UrlHelper.ResetPageIndex()
            End Sub

        End Class

    End Class

End Namespace
Capture.PNG Capture1.PNG Capture2.PNG
 
Nicolai Pedersen
Reply
This post has been marked as an answer

And by the way, with the new URL handling, you can just use the "Do not include URL in subpage URLs" checkbox on the product catalog page - it now also works for groups added as subpages using the new URLDataProviders.

The providers you have found in the documentation are the old providers.

BR Nicolai

Capture.PNG
Votes for this answer: 1
 
Claus Ørum-Petersen
Reply

Thanks Nicolai,

That seems to have done it. Prette awesome and thanks for the provider code smiley

 

You must be logged in to post in the forum