Hi Adrian
You can create your own URL provider - not sure it will help you in this case as it does not look into primary group pageid.
This is the version we use - you can modify it. Do NOT get an instance of a product, page or something crazy in it - your system will break.
using Dynamicweb.Core;
using Dynamicweb.Data;
using Dynamicweb.Ecommerce.International;
using Dynamicweb.Ecommerce.Products;
using Dynamicweb.Ecommerce.Shops;
using Dynamicweb.Ecommerce.Variants;
using Dynamicweb.Extensibility.AddIns;
using Dynamicweb.Extensibility.Editors;
using Dynamicweb.Extensibility.Notifications;
using Dynamicweb.Frontend.UrlHandling;
using Dynamicweb.SystemTools;
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace Dynamicweb.Ecommerce.Frontend.UrlHandling
{
[AddInName("Ecommerce")]
public class ShopUrlDataProvider : UrlDataProvider, IDropDownOptionActions
{
[AddInLabel("Shop")]
[AddInParameter(nameof(IncludeFrom))]
[AddInParameterEditor(typeof(RadioParameterEditor), "SortBy=Key")]
public string IncludeFrom { get; set; } = "ContextShop";
[AddInLabel("Selected shop")]
[AddInParameter(nameof(SelectedShopId))]
[AddInParameterEditor(typeof(DropDownParameterEditor), "")]
public string SelectedShopId { get; set; }
[AddInLabel("Language")]
[AddInParameter(nameof(IncludeLanguage))]
[AddInParameterEditor(typeof(RadioParameterEditor), "SortBy=Key")]
public string IncludeLanguage { get; set; } = "ContextLanguage";
[AddInLabel("Selected language")]
[AddInParameter(nameof(SelectedLanguageId))]
[AddInParameterEditor(typeof(DropDownParameterEditor), "")]
public string SelectedLanguageId { get; set; }
[AddInLabel("Include")]
[AddInParameter(nameof(Include))]
[AddInParameterEditor(typeof(RadioParameterEditor), "SortBy=Key")]
public string Include { get; set; } = "GroupsProductsVariants";
[AddInLabel("Groups")]
[AddInParameter(nameof(GroupPath))]
[AddInParameterEditor(typeof(RadioParameterEditor), "")]
public string GroupPath { get; set; } = "Tree";
[AddInLabel("Products")]
[AddInParameter(nameof(ProductPath))]
[AddInParameterEditor(typeof(RadioParameterEditor), "")]
public string ProductPath { get; set; } = "Tree";
private static Lazy<ConcurrentDictionary<string, List<string>>> lazyGroupProductRelationIndex = new Lazy<ConcurrentDictionary<string, List<string>>>(InitializeGroupProductRelationIndex);
private static ConcurrentDictionary<string, List<string>> GroupProductRelationIndex => lazyGroupProductRelationIndex.Value;
private static Lazy<ConcurrentDictionary<string, Dictionary<string, UrlDataNode>>> lazyProductUrlDataIndex = new Lazy<ConcurrentDictionary<string, Dictionary<string, UrlDataNode>>>(InitializeProductUrlDataIndex);
private static ConcurrentDictionary<string, Dictionary<string, UrlDataNode>> ProductUrlDataIndex => lazyProductUrlDataIndex.Value;
public override IEnumerable<UrlDataNode> GetUrlDataNodes(UrlDataNode parent, UrlDataContext dataContext)
{
var nodes = new List<UrlDataNode>();
var shop = GetShop(dataContext);
if (shop is null)
{
return nodes;
}
var language = GetLanguage(dataContext);
if (language is null)
{
return nodes;
}
var groups = GetTopLevelGroups(shop, language);
if (groups is null)
{
return nodes;
}
var settings = GetUrlDataSettings();
foreach (Group group in groups)
AddNodesRecursive(nodes, parent, group, settings);
return nodes;
}
private UrlDataSettings GetUrlDataSettings()
{
var settings = new UrlDataSettings();
switch (Include ?? "")
{
case "Groups":
{
settings.IncludeProducts = false;
settings.IncludeVariants = false;
break;
}
case "GroupsProducts":
{
settings.IncludeProducts = true;
settings.IncludeVariants = false;
break;
}
default:
{
settings.IncludeProducts = true;
settings.IncludeVariants = true;
break;
}
}
switch (GroupPath ?? "")
{
case "Root":
{
settings.PlaceGroupsInRoot = true;
break;
}
default:
{
settings.PlaceGroupsInRoot = false;
break;
}
}
switch (ProductPath ?? "")
{
case "Root":
{
settings.PlaceProductsInRoot = true;
break;
}
default:
{
settings.PlaceProductsInRoot = false;
break;
}
}
return settings;
}
private static ICollection<Group> GetTopLevelGroups(Shop shop, Language language)
{
return shop.GetTopLevelGroups(language.LanguageId);
}
private Language GetLanguage(UrlDataContext dataContext)
{
string languageId = null;
if (string.Equals(IncludeLanguage, "ContextLanguage", StringComparison.OrdinalIgnoreCase))
{
languageId = dataContext.EcomLanguageId;
}
else if (string.Equals(IncludeLanguage, "SelectedLanguage", StringComparison.OrdinalIgnoreCase))
{
languageId = SelectedLanguageId;
}
Language language = null;
if (!string.IsNullOrEmpty(languageId))
{
language = Services.Languages.GetLanguage(languageId);
}
return language;
}
private Shop GetShop(UrlDataContext dataContext)
{
string shopId = null;
if (string.Equals(IncludeFrom, "ContextShop", StringComparison.OrdinalIgnoreCase))
{
shopId = dataContext.EcomShopId;
}
else if (string.Equals(IncludeFrom, "SelectedShop", StringComparison.OrdinalIgnoreCase))
{
shopId = SelectedShopId;
}
Shop shop = null;
if (!string.IsNullOrEmpty(shopId))
{
shop = Services.Shops.GetShop(shopId);
}
return shop;
}
private void AddNodesRecursive(IList<UrlDataNode> entries, UrlDataNode parent, Group group, UrlDataSettings settings)
{
var groupUrlData = CreateGroupUrlData(parent, group);
if (settings.PlaceGroupsInRoot)
{
groupUrlData.IgnoreParentPath = true;
}
entries.Add(groupUrlData);
IEnumerable<Group> subGroups = GetSubGroups(group);
foreach (Group subGroup in subGroups)
AddNodesRecursive(entries, groupUrlData, subGroup, settings);
if (settings.IncludeProducts)
{
List<string> productIds = null;
if (GroupProductRelationIndex.TryGetValue(group.Id, out productIds))
{
Dictionary<string, UrlDataNode> indexByProductId = null;
if (ProductUrlDataIndex.TryGetValue(group.LanguageId, out indexByProductId))
{
foreach (string productId in productIds)
{
UrlDataNode indexedProductUrlData = null;
if (indexByProductId.TryGetValue(productId, out indexedProductUrlData))
{
// clone and set unique id
var productUrlData = CloneUrlData(indexedProductUrlData);
productUrlData.Id = $"{indexedProductUrlData.Id}_{groupUrlData.Id}";
productUrlData.ParentId = groupUrlData.Id;
if (settings.PlaceProductsInRoot)
{
productUrlData.IgnoreParentPath = true;
}
entries.Add(productUrlData);
if (settings.IncludeVariants)
{
var variantCombinations = Services.VariantCombinations.GetVariantCombinations(productId);
foreach (var variantCombination in variantCombinations)
{
var variantUrlData = CreateProductVariantUrlData(productUrlData, group.LanguageId, variantCombination);
if (settings.PlaceProductsInRoot)
{
variantUrlData.PathName = productUrlData.PathName + "-" + variantUrlData.PathName;
variantUrlData.IgnoreParentPath = true;
}
entries.Add(variantUrlData);
}
}
}
}
}
}
}
}
private static UrlDataNode CloneUrlData(UrlDataNode urlData)
{
var urlDataClone = new UrlDataNode()
{
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;
}
private static UrlDataNode CreateGroupUrlData(UrlDataNode parent, Group group)
{
var nodeData = new UrlDataNode()
{
Id = $"PRODUCTGROUP_{group.Id}_{parent.Id}",
ParentId = parent.Id,
PathName = string.IsNullOrEmpty(Converter.ToString(group.Meta.Url)) ? Converter.ToString(group.Name) : Converter.ToString(group.Meta.Url),
IgnoreInChildPath = false,
IgnoreParentPath = group.Meta.UrlIgnoreParent,
PathExact = string.Empty,
QueryStringParameter = "GroupID",
QueryStringValue = group.Id,
QueryStringExact = string.Empty,
IgnoreParentQuerystring = false
};
return nodeData;
}
private static UrlDataNode CreateProductVariantUrlData(UrlDataNode parent, string languageId, VariantCombination variantCombination)
{
var nodeData = new UrlDataNode()
{
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;
}
private static ICollection<Group> GetSubGroups(Group group)
{
var childRelations = GroupRelation.GetGroupRelationsByParentId(group.Id).OrderBy(relation => relation.Sorting);
var children = new List<Group>();
foreach (GroupRelation childRelation in childRelations)
{
var theGroup = Services.ProductGroups.GetGroup(childRelation.Id, group.LanguageId);
if (theGroup is object)
{
children.Add(theGroup);
}
}
return children;
}
private static ConcurrentDictionary<string, List<string>> InitializeGroupProductRelationIndex()
{
var result = new ConcurrentDictionary<string, List<string>>();
using (var reader = Database.CreateDataReader("SELECT DISTINCT [GroupProductRelationGroupId],[GroupProductRelationProductId] FROM [EcomGroupProductRelation] WITH (NOLOCK)"))
{
while (reader.Read())
{
string groupId = reader.GetString(0);
string productId = reader.GetString(1);
List<string> list = null;
if (!result.TryGetValue(groupId, out list))
{
list = new List<string>();
result.TryAdd(groupId, list);
}
list.Add(productId);
}
}
return result;
}
/// <summary>
/// UrlData by languageid, productid
/// </summary>
private static ConcurrentDictionary<string, Dictionary<string, UrlDataNode>> InitializeProductUrlDataIndex()
{
var indexByLanguageId = new ConcurrentDictionary<string, Dictionary<string, UrlDataNode>>();
using (var reader = Database.CreateDataReader("SELECT [ProductID], [ProductLanguageID], [ProductName], [ProductMetaUrl] FROM [EcomProducts] WITH (NOLOCK) WHERE ([ProductVariantID] IS NULL OR [ProductVariantID] = '') AND [EcomProducts].[ProductExcludeFromCustomizedUrls] = 0 ORDER BY ProductCreated ASC"))
{
while (reader.Read())
{
string productId = Convert.ToString(reader.GetString(0));
string productLanguageId = Convert.ToString(reader.GetString(1));
string productName = Convert.ToString(reader[2]);
string productMetaUrl = Convert.ToString(reader[3]);
Dictionary<string, UrlDataNode> indexByProductId = null;
if (!indexByLanguageId.TryGetValue(productLanguageId, out indexByProductId))
{
indexByProductId = new Dictionary<string, UrlDataNode>();
indexByLanguageId.TryAdd(productLanguageId, indexByProductId);
}
var nodeData = new UrlDataNode()
{
Id = $"PRODUCT_{productId}",
PathName = string.IsNullOrEmpty(Converter.ToString(productMetaUrl)) ? Converter.ToString(productName) : Converter.ToString(productMetaUrl),
IgnoreInChildPath = false,
IgnoreParentPath = false,
PathExact = string.Empty,
QueryStringParameter = "ProductID",
QueryStringValue = productId,
QueryStringExact = string.Empty,
IgnoreParentQuerystring = false
};
indexByProductId.Add(productId, nodeData);
}
}
return indexByLanguageId;
}
#region IDropDownOptionActions
public Hashtable GetOptions(string dropdownName)
{
var options = new Hashtable();
switch (dropdownName ?? "")
{
case nameof(Include):
{
options.Add("GroupsProductsVariants", Translator.Translate("Groups, products, variants"));
options.Add("GroupsProducts", Translator.Translate("Groups, products"));
options.Add("Groups", Translator.Translate("Groups"));
break;
}
case nameof(IncludeFrom):
{
options.Add("ContextShop", Translator.Translate("Current website shop"));
options.Add("SelectedShop", Translator.Translate("Selected shop"));
break;
}
case nameof(SelectedShopId):
{
foreach (Shop shop in Services.Shops.GetShops())
options.Add(shop.Id, shop.Name);
break;
}
case nameof(IncludeLanguage):
{
options.Add("ContextLanguage", Translator.Translate("Current website language"));
options.Add("SelectedLanguage", Translator.Translate("Selected language"));
break;
}
case nameof(SelectedLanguageId):
{
foreach (Language language in Services.Languages.GetLanguages())
options.Add(language.LanguageId, language.Name);
break;
}
case nameof(GroupPath):
{
options.Add("Tree", Translator.Translate("Tree path"));
options.Add("Root", Translator.Translate("Root path"));
break;
}
case nameof(ProductPath):
{
options.Add("Tree", Translator.Translate("Tree path"));
options.Add("Root", Translator.Translate("Root path"));
break;
}
}
return options;
}
/// <inheritdoc/>
public List<string> GetParametersToHide(string dropdownName, string optionKey)
{
var parameters = new List<string>();
switch (dropdownName)
{
case nameof(IncludeFrom):
if (!"SelectedShop".Equals(optionKey, StringComparison.OrdinalIgnoreCase))
{
parameters.Add(nameof(SelectedShopId));
}
break;
case nameof(IncludeLanguage):
if (!"SelectedLanguage".Equals(optionKey, StringComparison.OrdinalIgnoreCase))
{
parameters.Add(nameof(SelectedLanguageId));
}
break;
}
return parameters;
}
public List<string> GetSectionsToHide(string dropdownName, string optionKey) => new List<string>();
#endregion
public override void RenderAdditionalContent(TextWriter output)
{
base.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>");
}
private class UrlDataSettings
{
public bool IncludeProducts { get; set; }
public bool PlaceProductsInRoot { get; set; }
public bool IncludeVariants { get; set; }
public bool PlaceGroupsInRoot { get; set; }
}
[Subscribe(Notifications.Ecommerce.Product.ProductGroupRelationAfterSave)]
[Subscribe(Notifications.Ecommerce.Product.ProductGroupRelationDeleted)]
public class ProductGroupRelationChangedObserver : NotificationSubscriber
{
public override void OnNotify(string notification, NotificationArgs args)
{
lazyGroupProductRelationIndex = new Lazy<ConcurrentDictionary<string, List<string>>>(InitializeGroupProductRelationIndex);
UrlHelper.SetResetNeeded();
}
}
[Subscribe(Notifications.Ecommerce.Product.AfterSave)]
[Subscribe(Notifications.Ecommerce.Product.AfterDelete)]
public class ProductChangedObserver : NotificationSubscriber
{
public override void OnNotify(string notification, NotificationArgs args)
{
var changeAffectUrlIndex = false;
if (args is Dynamicweb.Ecommerce.Notifications.Ecommerce.Product.AfterSaveArgs)
{
var saveArgs = args as Notifications.Ecommerce.Product.AfterSaveArgs;
var beforeSaveProduct = saveArgs.ProductBeforeChanges;
var afterSaveProduct = saveArgs.Product;
if (beforeSaveProduct != null)
{
changeAffectUrlIndex |= !beforeSaveProduct.Id.Equals(afterSaveProduct.Id);
changeAffectUrlIndex |= !beforeSaveProduct.VariantId.Equals(afterSaveProduct.VariantId);
changeAffectUrlIndex |= !beforeSaveProduct.LanguageId.Equals(afterSaveProduct.LanguageId);
changeAffectUrlIndex |= !beforeSaveProduct.Name.Equals(afterSaveProduct.Name);
changeAffectUrlIndex |= !beforeSaveProduct.Meta.Url.Equals(afterSaveProduct.Meta.Url);
}
else
{
changeAffectUrlIndex = true;
}
}
if (changeAffectUrlIndex)
{
lazyProductUrlDataIndex = new Lazy<ConcurrentDictionary<string, Dictionary<string, UrlDataNode>>>(InitializeProductUrlDataIndex);
UrlHelper.SetResetNeeded();
}
}
}
[Subscribe(Notifications.Ecommerce.Group.AfterSave)]
[Subscribe(Notifications.Ecommerce.Group.Deleted)]
public class GroupChangedObserver : NotificationSubscriber
{
public override void OnNotify(string notification, NotificationArgs args)
{
var changeAffectUrlIndex = false;
if (args is Dynamicweb.Ecommerce.Notifications.Ecommerce.Group.AfterSaveArgs)
{
var saveArgs = args as Notifications.Ecommerce.Group.AfterSaveArgs;
var beforeSaveGroup = saveArgs.GroupBeforeChanges;
var afterSaveGroup = saveArgs.Group;
if (beforeSaveGroup != null)
{
changeAffectUrlIndex |= !beforeSaveGroup.Id.Equals(afterSaveGroup.Id);
changeAffectUrlIndex |= !beforeSaveGroup.LanguageId.Equals(afterSaveGroup.LanguageId);
changeAffectUrlIndex |= !beforeSaveGroup.Name.Equals(afterSaveGroup.Name);
changeAffectUrlIndex |= !beforeSaveGroup.Meta.Url.Equals(afterSaveGroup.Meta.Url);
}
else
{
changeAffectUrlIndex = true;
}
}
if (changeAffectUrlIndex)
{
lazyProductUrlDataIndex = new Lazy<ConcurrentDictionary<string, Dictionary<string, UrlDataNode>>>(InitializeProductUrlDataIndex);
UrlHelper.SetResetNeeded();
}
}
}
[Subscribe(Notifications.Ecommerce.Group.RelationUpdated)]
[Subscribe(Notifications.Ecommerce.Group.RelationDeleted)]
public class GroupRelationChangedObserver : NotificationSubscriber
{
public override void OnNotify(string notification, NotificationArgs args)
{
UrlHelper.SetResetNeeded();
}
}
[Subscribe(Dynamicweb.Notifications.Standard.Area.OnAreaSaved)]
[Subscribe(Dynamicweb.Notifications.Standard.Area.OnAfterAreaDeleted)]
public class AreaChangedObserver : NotificationSubscriber
{
public override void OnNotify(string notification, NotificationArgs args)
{
UrlHelper.SetResetNeeded();
}
}
}
}