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