在Sitecore中实现多项创建功能

背景

在Sitecore内容管理中,经常会遇到需要批量创建多个相同模板项的情况。默认的Sitecore只支持一次创建一个项,这对于需要创建多个类似项(如卡片、图片等)的场景来说效率较低。本文将介绍如何扩展Sitecore的插入功能,实现一次性创建多个项的功能。

需求分析

基本需求

  • 在现有Insert选项旁添加批量创建选项
  • 允许用户指定基础名称和创建数量
  • 自动处理项目命名和序号
  • 处理名称冲突问题

技术要点

  • 扩展Sitecore命令系统
  • 创建符合Sitecore标准的对话框
  • 实现智能命名策略
  • 错误处理和用户反馈

实现方案

0. 在Core库中创建menu

  • /sitecore/content/Applications/Content Editor/Ribbons/Strips/Home/Multi New
    Reference : sitecore/content/Applications/Content Editor/Ribbons/Chunks/Multi Insert
  • /sitecore/content/Applications/Content Editor/Ribbons/Chunks/Multi Insert (Copy from Insert)
  • /sitecore/content/Applications/Content Editor/Ribbons/Chunks/Multi Insert/New
    Type : Sitecore.Foundation.SitecoreExtensions.Shell.Applications.ContentManager.Panels.CustomNewPanel,Sitecore.Foundation.SitecoreExtensions

1. 创建CustomNewPanel 类

首先创建继承自Sitecore.Shell.Web.UI.WebControls.RibbonPanel的命令类, 参考Sitecore.Shell.Applications.ContentManager.Panels.NewPanel

    public class CustomNewPanel : RibbonPanel
    {
       /// <summary>Renders the panel.</summary>
       /// <param name="output">The output.</param>
       /// <param name="ribbon">The ribbon.</param>
       /// <param name="button">The button.</param>
       /// <param name="context">The context.</param>
       public override void Render(
         HtmlTextWriter output,
         Ribbon ribbon,
         Item button,
         CommandContext context)
       {
           Sitecore.Diagnostics.Assert.ArgumentNotNull((object)output, nameof(output));
           Sitecore.Diagnostics.Assert.ArgumentNotNull((object)ribbon, nameof(ribbon));
           Sitecore.Diagnostics.Assert.ArgumentNotNull((object)button, nameof(button));
           Sitecore.Diagnostics.Assert.ArgumentNotNull((object)context, nameof(context));
           Item[] items = context.Items;
           if (items.Length != 1)
               return;
           Item obj1 = items[0];
           if (!obj1.Access.CanCreate() || !obj1.Access.CanWriteLanguage())
               return;
           List<Item> masters = Sitecore.Data.Masters.Masters.GetMasters(obj1);
           List<CustomNewPanel.InsertOption> options = new List<CustomNewPanel.InsertOption>();
           foreach (Item master in masters)
               options.Add(new CustomNewPanel.InsertOption(master.GetUIDisplayName(), master.Appearance.Icon, $"item:createmultiple(master={master.ID})"));
           //foreach (Item obj2 in ItemUtil.GetChildrenAt("/sitecore/content/Applications/Content Editor/Menues/New"))
           //    options.Add(new CustomNewPanel.InsertOption(obj2["display name"], obj2["icon"], obj2["message"].Replace("$Target", obj1.ID.ToString())));
           output.Write("<div id=\"CustomNewPanelHolder\" class=\"scRibbonGallery\">");
           output.Write("<div role=\"menu\" aria-orientation=\"vertical\" id=\"CustomNewPanelList\" class=\"scRibbonGalleryList\" onkeydown=\"scForm.onMenuKeyDown(this, event, false)\">");
           this.RenderOptions(output, options);
           output.Write("</div>");
           this.RenderPanelButtons(output, "CustomNewPanelList", this.GetClick(obj1));
           output.Write("</div>");
       }

       protected new void RenderPanelButtons(HtmlTextWriter output, string id, string click)
       {
           Assert.ArgumentNotNull(output, "output");
           Assert.ArgumentNotNull(id, "id");
           Assert.ArgumentNotNull(click, "click");
           output.Write("<div class=\"scRibbonPanelButtons\">");
           ImageBuilder imageBuilder = new ImageBuilder();
           imageBuilder.Src = "Images/Ribbon/ribbon_panel_up.png";
           imageBuilder.Class = "scRibbonPanelUp";
           imageBuilder.RollOver = true;
           imageBuilder.Disabled = !Enabled;
           imageBuilder.OnClick = "javascript:scContent.scrollPanel('" + id + "', true)";
           output.Write(imageBuilder.ToString());
           imageBuilder = new ImageBuilder();
           imageBuilder.Src = "Images/Ribbon/ribbon_panel_down.png";
           imageBuilder.Class = "scRibbonPanelDown";
           imageBuilder.RollOver = true;
           imageBuilder.Disabled = !Enabled;
           imageBuilder.OnClick = "javascript:scContent.scrollPanel('" + id + "', false)";
           output.Write(imageBuilder.ToString());
           output.Write("</div>");
       }

       /// <summary>Gets the click.</summary>
       /// <param name="item">The item.</param>
       /// <returns>The click.</returns>
       protected virtual string GetClick(Item item)
       {
           Sitecore.Diagnostics.Assert.ArgumentNotNull((object)item, nameof(item));
           UrlString url = CustomNewPanel.GetUrl(item);
           string empty = string.Empty;
           string height = "250";
           GalleryManager.GetGallerySize("NewGallery", ref empty, ref height);
           return "javascript:return scContent.showGallery(scForm.browser.getControl('CustomNewPanelHolder'), event, 'NewGallery','Gallery.New','" + (object)url + "','" + empty + "','" + height + "','expanding')";
       }

       /// <summary>Gets the URL.</summary>
       /// <param name="item">The item.</param>
       /// <returns>The URL.</returns>
       protected static UrlString GetUrl(Item item)
       {
           Sitecore.Diagnostics.Assert.ArgumentNotNull((object)item, nameof(item));
           UrlString url = new UrlString();
           url.Append("id", item.ID.ToString());
           url.Append("la", item.Language.ToString());
           url.Append("vs", item.Version.ToString());
           url.Append("db", item.Database.Name);
           return url;
       }

       /// <summary>Renders the masters.</summary>
       /// <param name="output">The output.</param>
       /// <param name="options">The masters.</param>
       private void RenderOptions(HtmlTextWriter output, List<CustomNewPanel.InsertOption> options)
       {
           Sitecore.Diagnostics.Assert.ArgumentNotNull((object)output, nameof(output));
           Sitecore.Diagnostics.Assert.ArgumentNotNull((object)options, "masters");
           if (options.Count <= 0)
           {
               output.Write("<div style=\"padding:4px; color:#333333;white-space:normal;width:200px\">" + Translate.Text("No Insert Options Available") + "</div>");
           }
           else
           {
               for (int index = 0; index < options.Count; ++index)
               {
                   CustomNewPanel.InsertOption option = options[index];
                   string clientEvent = Context.ClientPage.GetClientEvent(option.Click);
                   string str = string.Format("({0} {1} {2})", (object)(index + 1), (object)Translate.Text("of"), (object)options.Count);
                   if (this.Enabled)
                       output.Write("<a role=\"menuitem\" aria-label=\"" + option.DisplayName + " " + str + "\" href=\"#\" class=\"scRibbonToolbarSmallButton\" title=\"" + StringUtil.EscapeQuote(option.DisplayName) + "\" onclick=\"" + clientEvent + "\"" + (index > 0 ? " tabindex=\"-1\"" : "") + ">");
                   else
                       output.Write("<span class=\"scRibbonToolbarSmallButtonDisabled\">");
                   output.Write("<span class=\"scRibbonToolbarSmallButtonPrefix header\">");
                   output.Write(str);
                   output.Write("</span>");
                   output.Write("<span class=\"scRibbonToolbarSmallButtonLabel header \">{0}{1}</span>", (object)new ImageBuilder()
                   {
                       Src = Images.GetThemedImageSource(option.Icon, ImageDimension.id16x16),
                       Class = "scRibbonToolbarSmallButtonIcon",
                       Disabled = !this.Enabled,
                       Alt = option.DisplayName
                   }, (object)StringUtil.Clip(option.DisplayName, 50, true));
                   if (this.Enabled)
                       output.Write("</a>");
                   else
                       output.Write("</span>");
               }
           }
       }

       /// <summary>Insert option.</summary>
       [DebuggerDisplay("{DisplayName} {Click}")]
       private class InsertOption
       {
           /// <summary>
           /// Initializes a new instance of the <see cref="T:Sitecore.Shell.Applications.ContentManager.Panels.NewPanel.InsertOption" /> class.
           /// </summary>
           /// <param name="displayName">The display name.</param>
           /// <param name="icon">The icon.</param>
           /// <param name="click">The click.</param>
           public InsertOption(string displayName, string icon, string click)
           {
               this.Click = click;
               this.DisplayName = displayName;
               this.Icon = icon;
           }

           /// <summary>Gets the Click event.</summary>
           /// <value>The event to be executed.</value>
           public string Click { get; }

           /// <summary>Gets the display name.</summary>
           /// <value>The display name.</value>
           public string DisplayName { get; }

           /// <summary>Gets the icon.</summary>
           /// <value>The icon.</value>
           public string Icon { get; }
       }
   }

2. 创建命令类

创建继承自Sitecore.Shell.Framework.Commands.Command的命令类


    public class CreateMultipleItems : Command
    {

        /// <summary>Executes the command in the specified context.</summary>
        /// <param name="context">The context.</param>
        public override void Execute(CommandContext context)
        {
            if (context.Items.Length != 1 || !context.Items[0].Access.CanCreate())
                return;
            Item obj = context.Items[0];
            Context.ClientPage.Start((object)this, "Add", new NameValueCollection()
            {
                ["Master"] = context.Parameters["master"],
                ["ItemID"] = obj.ID.ToString(),
                ["Language"] = obj.Language.ToString(),
                ["Version"] = obj.Version.ToString()
            });
        }

        /// <summary>Queries the state of the command.</summary>
        /// <param name="context">The context.</param>
        /// <returns>The state of the command.</returns>
        public override CommandState QueryState(CommandContext context)
        {
            Error.AssertObject((object)context, nameof(context));
            if (context.Items.Length != 1)
                return CommandState.Hidden;
            return !context.Items[0].Access.CanCreate() ? CommandState.Disabled : base.QueryState(context);
        }

        /// <summary>Adds the specified args.</summary>
        /// <param name="args">The arguments.</param>
        protected void Add(ClientPipelineArgs args)
        {
            if (!SheerResponse.CheckModified())
                return;
            Item master = Context.ContentDatabase.GetItem(args.Parameters["Master"]);
            if (master == null)
                SheerResponse.Alert("Branch \"{0}\" not found.", args.Parameters["Master"]);
            if (args.IsPostBack)
            {
                if (!args.HasResult)
                    return;

                // 解析返回的结果
                var result = StringUtil.ParseNameValueCollection(args.Result, '&', '=');
                var baseName = result["basename"];
                var quantity = int.Parse(result["quantity"]);

                Item parent = Context.ContentDatabase.Items[StringUtil.GetString(args.Parameters["ItemID"]), Language.Parse(StringUtil.GetString(args.Parameters["Language"]))];
                if (parent == null)
                    SheerResponse.Alert("Parent item not found.");
                else if (!parent.Access.CanCreate())
                    Context.ClientPage.ClientResponse.Alert("You do not have permission to create items here.");
                else if (!Sitecore.Data.Masters.Masters.GetMasters(parent).Any<Item>((Func<Item, bool>)(x => x.ID == master.ID)))
                {
                    Context.ClientPage.ClientResponse.Alert("You do not have permission to create an item here.");
                }
                else
                {
                    // 创建项目
                    var creationResult = CreateItemsWithNameCheck(parent, master, baseName, quantity);

                    // 显示创建结果
                    if (creationResult.SkippedItems > 0)
                    {
                        Log.Info(
                            $"Created {creationResult.CreatedItems} items.\n" +
                            $"Skipped {creationResult.SkippedItems} items due to existing names.", this);
                        SheerResponse.Alert(
                            $"Created {creationResult.CreatedItems} items.\n" +
                            $"Skipped {creationResult.SkippedItems} items due to existing names.");
                    }
                }
            }
            else
            {
                SheerResponse.ShowModalDialog(
                    Sitecore.UIUtil.GetUri("control:CreateMultipleItems"),
                    "400", "300", string.Empty, true);
                args.WaitForPostBack();
            }
        }

        protected class CreationResult
        {
            public int CreatedItems { get; set; }
            public int SkippedItems { get; set; }
            public int StartIndex { get; set; }
            public int EndIndex { get; set; }
        }

        protected CreationResult CreateItemsWithNameCheck(Item parent, Item master, string baseName, int quantity)
        {
            var result = new CreationResult();

            // 获取已存在的子项名称
            var existingNames = new HashSet<string>(
                parent.Children
                    .Select(x => x.Name)
                    .Where(n => n.StartsWith(baseName, StringComparison.OrdinalIgnoreCase))
            );

            // 找出已使用的最大序号
            var maxNumber = existingNames
                .Select(name =>
                {
                    int num;
                    var match = Regex.Match(name, @"\d+$");
                    return match.Success && int.TryParse(match.Value, out num) ? num : 0;
                })
                .DefaultIfEmpty(0)
                .Max();

            // 从最大序号后开始创建
            int startIndex = maxNumber + 1;
            using (new SecurityDisabler())
            {
                for (int i = 0; i < quantity; i++)
                {
                    var currentIndex = startIndex + i;
                    var name = $"{baseName}{currentIndex}";

                    // 检查名称是否已存在
                    if (existingNames.Contains(name))
                    {
                        result.SkippedItems++;
                        continue;
                    }

                    try
                    {
                        Item obj = (Item)null;
                        if (master.TemplateID == TemplateIDs.BranchTemplate)
                        {
                            BranchItem branch = (BranchItem)master;
                            obj = Context.Workflow.AddItem(name, branch, parent);
                            Log.Audit(this, "Add item name: [{0}] from branch: {1}", name, AuditFormatter.FormatItem((Item)branch));
                        }
                        else
                        {
                            TemplateItem template = (TemplateItem)master;
                            obj = Context.Workflow.AddItem(name, template, parent);
                            Log.Audit(this, "Add item name: [{0}] from template: {1}", name, AuditFormatter.FormatItem((Item)template));
                        }
                        if(obj != null)
                            result.CreatedItems++;
                    }
                    catch (Exception ex)
                    {
                        Log.Error($"Failed to create item '{name}'", ex, this);
                        result.SkippedItems++;
                    }
                }
            }

            result.StartIndex = startIndex;
            result.EndIndex = startIndex + quantity - 1;

            return result;
        }
    }

3. 创建对话框

在/sitecore/shell/Applications/Dialogs/CreateMultiItems/下创建CreateMultipleItems.xml:

<?xml version="1.0" encoding="utf-8" ?> 
<control xmlns:def="Definition" xmlns="http://schemas.sitecore.net/Visual-Studio-Intellisense">
  <CreateMultipleItems>
    <CodeBeside Type="Sitecore.Foundation.SitecoreExtensions.sitecore.shell.Applications.Dialogs.CreateMultiItems.CreateMultipleItemsForm, Sitecore.Foundation.SitecoreExtensions"/>
    <FormDialog Icon="Applications/32x32/document_new.png" Header="Create Multiple Items" Text="Create multiple items from selected template.">
      <Border Class="scFlexColumnContainer">
        <Border Class="scFormRow">
          <Border Class="scFormLabel">Base name:</Border>
          <Edit ID="BaseName" Class="scContentControl"/>
        </Border>
        <Border Class="scFormRow">
          <Border Class="scFormLabel">Number of items:</Border>
          <Combobox ID="Quantity" Class="scContentControl"/>
        </Border>
      </Border>
    </FormDialog>
  </CreateMultipleItems>
</control>

对话框代码后置:

    public class CreateMultipleItemsForm : DialogForm
    {
        protected Edit BaseName;
        protected Combobox Quantity;

        protected override void OnLoad(EventArgs e)
        {
            base.OnLoad(e);
            if (!Context.ClientPage.IsEvent)
            {
                // 添加数量选项
                Quantity.Controls.Clear();
                for (int i = 1; i <= 10; i++)
                {
                    Quantity.Controls.Add(new ListItem() { Name = i.ToString(), Value = i.ToString(), Selected = i == 1 });
                }
            }
        }

        protected override void OnOK(object sender, EventArgs args)
        {
            // 验证输入
            if (string.IsNullOrEmpty(BaseName.Value))
            {
                SheerResponse.Alert("Please enter a base name.");
                return;
            }

            // 获取选择的数量
            var quantity = Quantity.SelectedItem.Value ?? "1";

            // 返回结果
            var result = $"basename={BaseName.Value}&quantity={quantity}";
            SheerResponse.SetDialogValue(result);

            base.OnOK(sender, args);
        }
    }

4. 配置

在配置文件中注册命令:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <commands>
      <command name="item:createmultiple" type="Sitecore.Foundation.SitecoreExtensions.Commands.CreateMultipleItems, Sitecore.Foundation.SitecoreExtensions"/>
    </commands>
  </sitecore>
</configuration>

使用效果

  1. 选择要创建的模板后,点击Multi Insert选项
  2. 在对话框中输入基础名称和选择数量
  3. 点击确定后系统会:
    • 检查现有项目名称
    • 从最大序号后开始创建
    • 自动跳过冲突名称
    • 显示创建结果

https://s21.ax1x.com/2024/12/18/pALrP8x.png

关键技术点

1. Sitecore命令集成

  • 继承Command类
  • 使用ClientPage.Start启动处理
  • 正确的命令注册

2. 对话框实现

  • 使用Sitecore标准对话框框架
  • XML定义UI
  • DialogForm处理交互

3. 命名策略

  • 获取现有名称
  • 智能序号处理
  • 冲突处理

4. 错误处理

  • 输入验证
  • 权限检查
  • 异常处理

注意事项

1. 命名规范

  • 基础名称不能为空
  • 序号自动递增
  • 跳过已存在名称

2. 性能考虑

  • 一次性获取现有名称
  • 批量创建使用事务
  • 错误不影响整体

3. 用户体验

  • 清晰的反馈
  • 简单的界面
  • 可预测的结果

总结

通过扩展Sitecore的命令系统,我们实现了一个实用的批量创建功能。该实现不仅提高了内容编辑效率,也展示了如何正确集成到Sitecore框架中。关键是遵循Sitecore的最佳实践,确保功能的可靠性和可维护性。

评论

还没有人评论,抢个沙发吧...

Viagle Blog

欢迎来到我的个人博客网站