OrchardCMS / OrchardCore

Orchard Core is an open-source modular and multi-tenant application framework built with ASP.NET Core, and a content management system (CMS) built on top of that framework.
https://orchardcore.net
BSD 3-Clause "New" or "Revised" License
7.42k stars 2.39k forks source link

The TaxonomyFieldDisplayDriver.Edit method ignores the field.TaxonomyContentItemId parameter value. #14148

Open jwrye opened 1 year ago

jwrye commented 1 year ago

Describe the bug

The TaxonomyFieldDisplayDriver.Edit method ignores the TaxonomyContentItemId value for TaxonomyField properties.

To Reproduce

Steps to reproduce the behavior:

  1. Create a ContentPart with a TaxonomyField property named Category.
public class MyContentItem : ContentPart
{
    public TaxonomyField Category { get; set; }
}
  1. Create an instance of the ContentType and alter it to use a different TaxonomyContentItemId value than the TaxonomyFieldSettings value and call the BuildEditorAsync method like the following:
var categoryTaxonomyField = new TaxonomyField() { TaxonomyContentItemId = "NotTheIDFromTheSettingsInstance" };

contentItem.Alter<MyContentItem>("MyContentItem", response =>
{
    response.Category = categoryTaxonomyField;
});

var model = await _contentItemDisplayManager.BuildEditorAsync(contentItem, _updateModelAccessor.ModelUpdater, true);
  1. The TaxonomyFieldDisplayDriver doesn't respect the value I've set prior to calling BuildEditorAsync.

Expected behavior

The BuildEditorAsync call should use the TaxonomyContentItmeId value I've set prior to using the settings value.

As a workaround I've created a new Editor Option named "Settable" and updated my ContentType to use this Editor with the following services configuration, but this also doesn't work and displays two editors for the field.

services.AddContentField<TaxonomyField>()
                .RemoveDisplayDriver<TaxonomyFieldDisplayDriver>()
                .UseDisplayDriver<TaxonomyFieldDisplayDriver>(d => string.IsNullOrEmpty(d))
                .UseDisplayDriver<TaxonomyFieldTagsDisplayDriver>(d => String.Equals(d, "Tags", StringComparison.OrdinalIgnoreCase))
                .UseDisplayDriver<CategtoryDisplayDriver>(d => String.Equals(d, "Settable", StringComparison.OrdinalIgnoreCase))
                .AddHandler<TaxonomyFieldHandler>();

Following is the Custom DisplayDriver that will satisfy my needs.

 public class CategtoryDisplayDriver : ContentFieldDisplayDriver<TaxonomyField>
    {
        private readonly IContentManager _contentManager;
        private readonly IStringLocalizer S;

        public CategtoryDisplayDriver(
            IContentManager contentManager,
            IStringLocalizer<TaxonomyFieldDisplayDriver> localizer)
        {
            _contentManager = contentManager;
            S = localizer;
        }

        public override IDisplayResult Display(TaxonomyField field, BuildFieldDisplayContext context)
        {
            return Initialize<DisplayTaxonomyFieldViewModel>(GetDisplayShapeType(context), model =>
            {
                model.Field = field;
                model.Part = context.ContentPart;
                model.PartFieldDefinition = context.PartFieldDefinition;
            })
            .Location("Detail", "Content")
            .Location("Summary", "Content");
        }

        public override IDisplayResult Edit(TaxonomyField field, BuildFieldEditorContext context)
        {
            return Initialize<EditTaxonomyFieldViewModel>(GetEditorShapeType(context), async model =>
            {
                var settings = context.PartFieldDefinition.GetSettings<TaxonomyFieldSettings>();

                if (string.IsNullOrWhiteSpace(field.TaxonomyContentItemId)) 
                { 
                    model.Taxonomy = await _contentManager.GetAsync(settings.TaxonomyContentItemId, VersionOptions.Latest);
                }
                else
                {
                    model.Taxonomy = await _contentManager.GetAsync(field.TaxonomyContentItemId, VersionOptions.Latest);
                }

                if (model.Taxonomy != null)
                {
                    var termEntries = new List<TermEntry>();
                    TaxonomyFieldDriverHelper.PopulateTermEntries(termEntries, field, model.Taxonomy.As<TaxonomyPart>().Terms, 0);

                    model.TermEntries = termEntries;
                    model.UniqueValue = termEntries.FirstOrDefault(x => x.Selected)?.ContentItemId;
                }

                model.Field = field;
                model.Part = context.ContentPart;
                model.PartFieldDefinition = context.PartFieldDefinition;
            });
        }

        public override async Task<IDisplayResult> UpdateAsync(TaxonomyField field, IUpdateModel updater, UpdateFieldEditorContext context)
        {
            var model = new EditTaxonomyFieldViewModel();

            if (await updater.TryUpdateModelAsync(model, Prefix))
            {
                var settings = context.PartFieldDefinition.GetSettings<TaxonomyFieldSettings>();

                field.TaxonomyContentItemId = settings.TaxonomyContentItemId;
                field.TermContentItemIds = model.TermEntries.Where(x => x.Selected).Select(x => x.ContentItemId).ToArray();

                if (settings.Unique && !String.IsNullOrEmpty(model.UniqueValue))
                {
                    field.TermContentItemIds = new[] { model.UniqueValue };
                }

                if (settings.Required && field.TermContentItemIds.Length == 0)
                {
                    updater.ModelState.AddModelError(
                        nameof(EditTaxonomyFieldViewModel.TermEntries),
                        S["A value is required for '{0}'", context.PartFieldDefinition.DisplayName()]);
                }
            }

            return Edit(field, context);
        }
    }
jwrye commented 1 year ago

The workaround wasn't working because "OrchardCore.Taxonomies" wasn't included in my manifest as a dependency. However, I think the original bug I reported should be reviewed.