Using multiple button roots on a Sitecore edit frame

Edit frames are a well-known part of the Sitecore page editor: they allow developers to show buttons around arbitrary parts of the page.  Edit frames will take a single button root and then display all the buttons in that folder on a toolbar.  From someone who uses edit frames quite a bit, I always used to find it quite annoying that I would have to duplicate buttons if I wanted to have the same button on different edit frame toolbars.  After a bit of digging, I found a way to extend edit frames that would allow me to specify multiple button roots on each toolbar, allowing me to reuse groups of buttons.

The GetChromeData pipeline is used by Sitecore to build up the page editor toolbars for the various different chrome types in Sitecore (fields, placeholders, renderings, etc.).  One of the processors (GetEditFrameChromeData) handles the generation of the chrome data for edit frames.  It will create the buttons to be displayed on the toolbar and set the tool bar’s title if one hasn’t already been provided.  Somewhat unfortunately, you have to override the whole Process method in order to supply your own button collection logic, which means that you ought to verify this doesn’t break anything when you upgrade.

The key thing is that you need to use the GetButtons method on each root item.  The final list of items then needs to be sent through to the  AddButtonsToChromeData method, which will do the hard work of generating the actual output. The below class shows an example of how you might do this.

public class MultiRootEditFrameChromeData : Sitecore.Pipelines.GetChromeData.GetEditFrameChromeData
{
    public override void Process(Sitecore.Pipelines.GetChromeData.GetChromeDataArgs args)
    {
        Assert.ArgumentNotNull(args, "args");
        Assert.IsNotNull(args.ChromeData, "Chrome Data");
 
        if (!"editFrame".Equals(args.ChromeType, StringComparison.OrdinalIgnoreCase))
        {
            // Don't do anything for non-editFrame chrome requests.
            return;
        }
 
        Database database = Factory.GetDatabase("core");
        Assert.IsNotNull(database, "core");
 
        // args.CustomData["buttonsPath] is populated by the EditFrame
        string buttonPaths = StringUtil.GetString(args.CustomData["buttonsPath"], Settings.WebEdit.DefaultButtonPath);
        var buttons = new List<WebEditButton>();
 
        foreach (var buttonPath in buttonPaths.Split(new []{'|'}, StringSplitOptions.RemoveEmptyEntries))
        {
            var path = buttonPath;
            if (!ID.IsID(path) && !path.StartsWith("/"))
            {
                // Allow button paths to be relative to the edit frame button root
                path = "/sitecore/content/Applications/WebEdit/Edit Frame Buttons/" + path;
            }
 
            Item item = database.GetItem(path);
            Assert.IsNotNull(item, "buttonRoot does not exist for edit frame");
 
            buttons.AddRange(GetButtons(item));
        }
 
        // Title can be set by the edit frame, so only do this if one hasn't been set
        args.ChromeData.DisplayName = StringUtil.GetString(new [] { args.ChromeData.DisplayName, "Page Area" });
        AddButtonsToChromeData(buttons, args);
    }
}

You would patch your custom processor in using an include file, and then you can go ahead and use it by having an edit frame somewhat like the the following, with pipe separated folders in the Buttons attribute:

<sc:EditFrame runat="server" Buttons="Folder1|Folder2">
  …
</sc:EditFrame>

Now you can go ahead and separate out your buttons according to function. You’ll certainly still have some buttons that will only ever be used in one place, but buttons you use over and over again can be placed in a single location, meaning less repetition and better maintainability.