Developer forum

Forum » Development » ItemType OptionList language inheritance

ItemType OptionList language inheritance

John Broers
Reply

We have an ItemType with a dropdown field that is populated dynamically with items from the current area context.

[DropdownList("Theme")]
[OptionItem("Theme", "Name", "Id", SourceType = FieldOptionItemSourceType.CurrentArea, IncludeChildItems = true, IncludeParagraphItems = true, ItemSystemName = "Theme")]
public virtual string ThemeID { get; set; }

When a paragraph using this ItemType is copied in the master language, corresponding paragraphs are automatically created on the language layers. The selected option for this dropdown field is copied directly from the master language to these language layers. However, the corresponding item on each language layer has of course a different ID. As a result, the copied value does not match any of the available options in the dropdown on the language layer resulting in 'Nothing selected'.

Is there a supported way to automatically map the selected item from the master language to its corresponding item on the language layer during this copy or save process?

If not, could someone point us in the right direction for implementing this ourselves? For example, are there any events or extension points available during the paragraph copy or creation process that would allow us to replace the copied Item ID with the corresponding language-specific Item ID?

We're using DW9

BR,

John


Replies

 
John Broers
Reply

Friendly bump. Does anyone have any suggestions or guidance on how this scenario should be handled?

 
Dmitriy Benyuk Dynamicweb Employee
Dmitriy Benyuk
Reply

Hi John,

There is no built-in mapping for this scenario. You can look on using the subscriber to: Standard.Paragraph.Saved.By the time your subscriber runs, all language paragraphs and their items are already persisted — you can load and fix them. The safest mapping strategy is existence-checking: for each language item, if the stored ThemeID doesn't match any item in the language area, it must be a master-area ID — find the matching item by Name and replace it. This is non-destructive for items that already have a valid language-area ID.

using Dynamicweb.Content;
using Dynamicweb.Content.Items;
using Dynamicweb.Content.Items.Metadata;
using Dynamicweb.Extensibility.Notifications;
using System;
using System.Linq;

[Subscribe(Dynamicweb.Notifications.Standard.Paragraph.Saved)]
public class OptionItemLanguageMappingSubscriber : NotificationSubscriber
{
    public override void OnNotify(string notification, NotificationArgs args)
    {
        var paragraphArgs = args as Dynamicweb.Notifications.Standard.Paragraph.ParagraphNotificationArgs;
        var master = paragraphArgs?.Target;

        if (master == null || !master.IsMaster || string.IsNullOrEmpty(master.ItemType))
            return;

        var itemTypeMeta = ItemManager.Metadata.GetItemType(master.ItemType);
        var fields = ItemManager.Metadata.GetItemFields(itemTypeMeta);

        // Find fields that are option-from-item-source using CurrentArea + Id as value
        var mappableFields = fields
            .Where(f =>
                f.Options?.SourceType == FieldOptionSourceType.ItemType &&
                ItemManager.Editors.IsListEditor(f.Editor) &&
                f.Options.Source is FieldOptionMetadataItemSource src &&
                src.ItemSourceType == FieldOptionItemSourceType.CurrentArea &&
                string.Equals(src.ValueField, "Id", StringComparison.OrdinalIgnoreCase))
            .ToList();

        if (!mappableFields.Any()) return;

        var masterItem = master.Item;
        if (masterItem == null) return;

        foreach (var langParagraph in master.Languages ?? Enumerable.Empty<Paragraph>())
        {
            if (string.IsNullOrEmpty(langParagraph.ItemId)) continue;

            var langItem = ItemManager.Storage.GetById(langParagraph.ItemType, langParagraph.ItemId);
            if (langItem == null) continue;

            var langAreaId = langParagraph.Page.AreaId;
            var updated = false;

            foreach (var field in mappableFields)
            {
                var storedValue = Convert.ToString(langItem[field.SystemName]);
                if (string.IsNullOrEmpty(storedValue)) continue;

                var optionSource = (FieldOptionMetadataItemSource)field.Options.Source;

                // Load all items available in the language area for this item type
                IEnumerable<Item> langAreaItems;
                using (var repo = ItemManager.Storage.Open(optionSource.ItemSystemName))
                    langAreaItems = repo.SelectByAreaId(langAreaId, null, optionSource.IncludeParagraphItems, false).ToList();

                // If the stored value already exists in the language area, nothing to do
                if (langAreaItems.Any(i => string.Equals(i.Id, storedValue, StringComparison.OrdinalIgnoreCase)))
                    continue;

                // Resolve the master item by ID to get its Name
                IEnumerable<Item> masterAreaItems;
                using (var repo = ItemManager.Storage.Open(optionSource.ItemSystemName))
                    masterAreaItems = repo.SelectByAreaId(master.Page.AreaId, null, optionSource.IncludeParagraphItems, false).ToList();

                var masterSourceItem = masterAreaItems.FirstOrDefault(i =>
                    string.Equals(i.Id, storedValue, StringComparison.OrdinalIgnoreCase));
                if (masterSourceItem == null) continue;

                var masterName = Convert.ToString(masterSourceItem[optionSource.NameField]);

                // Find counterpart in language area by name
                var langMatch = langAreaItems.FirstOrDefault(i =>
                    string.Equals(Convert.ToString(i[optionSource.NameField]), masterName, StringComparison.OrdinalIgnoreCase));
                if (langMatch == null) continue;

                langItem[field.SystemName] = langMatch.Id;
                updated = true;
            }

            if (updated)
            {
                var context = new ItemContext(langParagraph);
                langItem.Save(context);
            }
        }
    }
}

Key Notes

  • NameField as the stable key — the code uses whatever NameField is declared on the [OptionItem] attribute (in your case "Name"). If your Theme items don't have stable names across areas, you'd need a different stable identifier (e.g., a custom SystemName field).
  • Fires on every save, not just creation — the existence check (langAreaItems.Any(i => i.Id == storedValue)) makes it idempotent. It will silently skip language items that already have a valid area-local ID.
  • The AreaService pattern to reference — if you ever need to extend the built-in area copy (not just language creation), AreaService.UpdateItemLinks() at line 1064 follows the exact same NameField-based lookup pattern and is worth reading as a reference.

BR, Dmitrij 

 

You must be logged in to post in the forum