Know your Placeholders!

Posted 10/17/2013 by asura

I have been meaning to write this piece of functionality for over a year and I finally got around to it. I think this should be inbuilt functionality in Sitecore.

When using content editor, if you try setting the presentation on an item, it’s a pain to remember the placeholder name, especially if you are dealing with a huge project with multiple layouts and sublayouts. What if I don’t need to remember the exact name of my placeholder?

Here is the solution. As you can see from the screenshot below, its able to load all placeholders that are already in use along with any placeholders defined inside the layout and sublayouts on the device selected.

Select Placeholders

We accomplished this using two ways, one by reading the placeholder keys already in use in the layout definition and two, by parsing the html in the layouts and sublayouts using HTML Agility pack. I haven’t seen any performance issues and it’s very convenient.

To implement this we need to override the functionality in SelectRendering.xml and DeviceEditor.xml. To do this, create the following folders under \Website\sitecore\shell\Override folder:

  1. Applications
    • Dialogs
      • SelectRendering
  2. Layouts
    • DeviceEditor

Copy SelectRendering.xml from \Website\sitecore\shell\Applications\Dialogs\SelectRendering 
To 
\Website\sitecore\shell\Override\Applications\Dialogs\SelectRendering

Copy DeviceEditor.xml from
\Website\sitecore\shell\Applications\Layouts\DeviceEditor
To
\Website\sitecore\shell\Override\Applications\Layouts\DeviceEditor

Now for some code!!

Use a refactor tool like the .NET Reflector or JetBrains dotPeek to decompile Sitecore.Client and grab code for Sitecore.Shell.Applications.Dialogs.SelectRendering. SelectRenderingForm and Sitecore.Shell.Applications.Layouts.DeviceEditor.DeviceEditorForm.

Add two new classes in your BL or Sitecore Extensions project to mirror the two classes we want to override. First, we want to tackle the DeviceEditorForm since we want to pass in the ID of the item and the Device ID we are currently on. The reason we need to do this is that, when a user clicks on the Add button to add a new sublayout/rendering, these are not passed in and we need this to add our functionality.

In the DeviceEditorForm class, locate the Add method. In here we want to replace the SheerResponse.ShowModalDialog line in the else clause with the highlighted lines as shown below:

        [UsedImplicitly]
        [HandleMessage("device:add", true)]
        protected void Add(ClientPipelineArgs args)
        {
            Assert.ArgumentNotNull((object)args, "args");
            if (args.IsPostBack)
            {
                if (!args.HasResult)
                    return;
                string[] strArray = args.Result.Split(new char[1]
                {
                  ','
                });
                string str1 = strArray[0];
                string str2 = strArray[1].Replace("-c-", ",");
                bool flag = strArray[2] == "1";
                LayoutDefinition layoutDefinition = DeviceEditorForm.GetLayoutDefinition();
                DeviceDefinition device = layoutDefinition.GetDevice(this.DeviceID);
                RenderingDefinition renderingDefinition = new RenderingDefinition()
                {
                    ItemID = str1,
                    Placeholder = str2
                };
                device.AddRendering(renderingDefinition);
                DeviceEditorForm.SetDefinition(layoutDefinition);
                this.Refresh();
                if (flag)
                {
                    ArrayList renderings = device.Renderings;
                    if (renderings != null)
                    {
                        this.SelectedIndex = renderings.Count - 1;
                        Context.ClientPage.SendMessage((object)this, "device:edit");
                    }
                }
                Registry.SetString("/Current_User/SelectRendering/Selected", str1);
            }
            else
            {
                Item itm = UIUtil.GetItemFromQueryString(Sitecore.Client.ContentDatabase);
                SelectRenderingOptions renderingOptions = new SelectRenderingOptions()
                {
                    ShowOpenProperties = true,
                    ShowPlaceholderName = true,
                    PlaceholderName = string.Empty
                };
                string @string = Registry.GetString("/Current_User/SelectRendering/Selected");
                if (!string.IsNullOrEmpty(@string))
                    renderingOptions.SelectedItem = Sitecore.Client.ContentDatabase.GetItem(@string);

                //Modify the following line to include id and deviceid parameters
                SheerResponse.ShowModalDialog(renderingOptions.ToUrlString(Sitecore.Client.ContentDatabase).ToString() + "&id=" + System.Web.HttpUtility.UrlEncode(itm.ID.ToString()) + "&deviceid=" + System.Web.HttpUtility.UrlEncode(this.DeviceID), true);
                args.WaitForPostBack();
            }
        }

I added three asterisk's on the Add button for demo purposes.

Device Editor Form

That finishes our changes in the DeviceEditorForm.cs. Now in the SelectRenderingForm class, we need to add a Combobox control to house the placeholder keys. Add the following line near the property definitions:

    /// 
    /// Gets or sets the Combobox.
    /// 
    /// 
    /// 
    /// 
    /// The combobox with placeholder keys.
    /// 
    [UsedImplicitly]
    protected Combobox Placeholders  { get; set; }

I copied two methods from DeviceEditorForm into SelectRenderingForm class called GetLayoutDefinition and GetSessionHandle.

In the OnLoad method add the following highlighted lines above the this.SetOpenPropertiesState:

    /// 
    /// Raises the load event.
    /// 
    /// The  instance containing the event data.
    protected override void OnLoad(EventArgs e)
    {
        Assert.ArgumentNotNull((object) e, "e");
        base.OnLoad(e);
        if (Context.ClientPage.IsEvent)
            return;
        this.IsOpenPropertiesChecked = Registry.GetBool("/Current_User/SelectRendering/IsOpenPropertiesChecked");
        SelectRenderingOptions renderingOptions = SelectItemOptions.Parse();

        if (renderingOptions.ShowOpenProperties)
        {
            this.OpenPropertiesBorder.Visible = true;
            this.OpenProperties.Checked = this.IsOpenPropertiesChecked;
        }
        if (renderingOptions.ShowPlaceholderName)
        {
            this.PlaceholderNameBorder.Visible = true;
            this.PlaceholderName.Value = renderingOptions.PlaceholderName;
        }
        if (!renderingOptions.ShowTree)
        {
            this.TreeviewContainer.Class = string.Empty;
            this.TreeviewContainer.Visible = false;
            this.TreeSplitter.Visible = false;
            GridPanel gridPanel = this.TreeviewContainer.Parent as GridPanel;
            if (gridPanel != null)
                gridPanel.SetExtensibleProperty((System.Web.UI.Control) this.TreeviewContainer, "class", "scDisplayNone");
            this.Renderings.InnerHtml = this.RenderPreviews((IEnumerable) renderingOptions.Items);
        }

        //get the device definition based on the device id passed in and the layout definition 
        DeviceDefinition device = GetLayoutDefinition().GetDevice(WebUtil.GetQueryString("deviceid"));
        //load placeholders into the combobox
        this.LoadPlaceholders(device);

        this.SetOpenPropertiesState(renderingOptions.SelectedItem);
    }

Here is the definition for LoadPlaceholders and GetPlaceholdersFromFile methods:

    
    private void LoadPlaceholders(DeviceDefinition deviceDefinition)
    {
        Assert.ArgumentNotNull((object)deviceDefinition, "deviceDefinition");
        List<string> placeholderKeys = new List<string>();

        //get an array of the renderings
        ArrayList renderings = deviceDefinition.Renderings;
        if (renderings != null)
        {
            foreach (RenderingDefinition renderingDefinition in renderings)
            {
                //for every rendering, check if we have a valid ItemID
                if (renderingDefinition.ItemID != null)
                {
                    //if we didnt already add the placeholder to the list add it
                    if (!string.IsNullOrEmpty(renderingDefinition.Placeholder) && !placeholderKeys.Contains(renderingDefinition.Placeholder))
                        placeholderKeys.Add(renderingDefinition.Placeholder);

                    //get the sublayout item in reference
                    Item sublayout = Context.ContentDatabase.GetItem(new ID(renderingDefinition.ItemID));
                    if (sublayout != null)
                    {
                        //extract the placeholders from the file. Get the Sublayout Path field ID from Constants
                        List<string> sublayoutPlaceholders = GetPlaceholdersFromFile(Sitecore.IO.FileUtil.MapPath(sublayout[Constants.ItemIDs.SublayoutPath]));
                        //if any placeholders were extracted, add them to the main list of placeholders
                        if (sublayoutPlaceholders.Count > 0)
                            placeholderKeys.AddRange(sublayoutPlaceholders);
                    }
                }
            }
        }

        //get the layout item from Devicedefinition
        Item layout = Context.ContentDatabase.GetItem(new ID(deviceDefinition.Layout));
        if (layout != null)
        {
            //extract the placeholders from the aspx file. Get the Layout Path field ID from Constants
            List<string> layoutPlaceholders = GetPlaceholdersFromFile(Sitecore.IO.FileUtil.MapPath(layout[Constants.ItemIDs.LayoutPath]));
            //if any placeholders were extracted, add them to the main list of placeholders
            if (layoutPlaceholders.Count > 0)
                placeholderKeys.AddRange(layoutPlaceholders);
        }

        //if we have any placeholders to display in the combobox
        if (placeholderKeys.Count > 0)
        {
            //get distinct placeholder keys
            List<string> distinctPlaceholderKeys = placeholderKeys.Distinct().ToList();
            //sort the list
            distinctPlaceholderKeys.Sort();

            //add the default list item
            ListItem listItem = new ListItem();
            this.Placeholders.Controls.Add(listItem);
            listItem.ID = Sitecore.Web.UI.HtmlControls.Control.GetUniqueID("ListItem");
            listItem.Header = "Select a Placeholder";
            listItem.Value = "";

            //for every placeholder key we found, add a list item in the combobox
            foreach (string placeholderKey in distinctPlaceholderKeys)
            {
                listItem = new ListItem();
                this.Placeholders.Controls.Add(listItem);
                listItem.ID = Sitecore.Web.UI.HtmlControls.Control.GetUniqueID("ListItem");
                listItem.Header = placeholderKey;
                listItem.Value = placeholderKey;        
            }
        }
    }

    private List<string> GetPlaceholdersFromFile(string path)
    {
        List<string> placeholderKeys = new List<string>();
        HtmlDocument htmlDoc = htmlDoc = new HtmlDocument();

        //load file
        htmlDoc.Load(path);
        if (htmlDoc.DocumentNode != null)
        {
            //get all nodes which match a sitecore placeholder
            var descendants = htmlDoc.DocumentNode.Descendants("sc:placeholder");
            foreach (HtmlNode hn in descendants)
            {
                //if a key is defined add it to the List
                if (hn.Attributes.Contains("key"))
                    placeholderKeys.Add(hn.Attributes["key"].Value);
            }
        }
        return placeholderKeys;
    }

Select a Rendering

Once all of this is done, compile the code.

Modify the SelectRendering.xml file in the override folder and replace the <CodeBeside tag with the one below:


Modify the DeviceEditor.xml file in the override folder and replace the <CodeBeside tag with the one below:


If you have any questions or need the source, email me at Akshay dot sura at nttdata dot com.




Share:

Archive

Syndication