This blog has moved to http://jermdavis.wordpress.com/

Showing posts with label User Interface. Show all posts
Showing posts with label User Interface. Show all posts

Friday, 21 March 2014

Multiple data source locations in the “Select Associated Content” dialog box

Working on some components for a client’s site recently, it struck me that there were circumstances where it would be helpful to editors to allow the “Select Associated Content” dialog to have multiple options without just showing the whole content tree. Scenarios like having both a global shared content folder and a sub-site specific shared content folder that editors could choose between, for example. Something that looked like this:

image

More than one root item displayed in the selection list allows editors to choose an appropriate location without the option to put their data in places you didn’t want them to…

When I started looking into whether this was possible or not, I initially assumed it would end up being quite a complicated modification – involving modifying this dialog, and the pipelines and commands which are used for this bit of UI. But after a bit of digging through the code, it turns out that it’s actually much simpler than this, since the dialog already supports the idea of multiple roots. What we need to provide is the right user interface to configure the roots and a bit of code to ensure this config is passed through to the dialog. Here’s what I came up with, after a bit of hacking:

So what happens when you click the Associated Content button in the UI? Well looking at the commands configuration for the Page Editor, we find that the UI triggers the command Sitecore.Shell.Applications.WebEdit.Commands.SetDatasource. Digging through that class with Reflector, it seems that the Execute() method gets the client page to call the Run() method. This method does all sorts of stuff, but the interesting bit is that it calls a static method called CreatePipelineArgs() to set up the data for the rest of the pipeline. And this is where the interesting stuff happens:

private static GetRenderingDatasourceArgs CreatePipelineArgs(
ClientPipelineArgs args, Item renderingItem)
{
Item clientContentItem =
WebEditUtil.GetClientContentItem(Client.ContentDatabase);
GetRenderingDatasourceArgs getRenderingDatasourceArgs =
new GetRenderingDatasourceArgs(renderingItem)
{
FallbackDatasourceRoots = new List<item>
{
Client.ContentDatabase.GetRootItem()
},
ContentLanguage = (clientContentItem != null) ?
clientContentItem.Language : null,
ContextItemPath = (clientContentItem != null) ?
clientContentItem.Paths.FullPath : string.Empty,
ShowDialogIfDatasourceSetOnRenderingItem = true
};
LayoutDefinition currentLayoutDefinition =
SetDatasource.GetCurrentLayoutDefinition();
ID clientDeviceId = WebEditUtil.GetClientDeviceId();
string uniqueId = args.Parameters["uniqueId"];
if (currentLayoutDefinition != null && !ID.IsNullOrEmpty(clientDeviceId))
{
RenderingDefinition renderingByUniqueId = currentLayoutDefinition
.GetDevice(clientDeviceId.ToString()).GetRenderingByUniqueId(uniqueId);
if (renderingByUniqueId != null)
{
getRenderingDatasourceArgs.CurrentDatasource =
renderingByUniqueId.Datasource;
}
}
return getRenderingDatasourceArgs;
}

The critical bit for what we’re trying to achieve is where the FallbackDatasourceRoots property is assigned a list of items. If we follow this through the code for the rest of the display of the dialog, this property is used to set the contents of the tree view. So this is the bit of code we need to modify to deal with the Datasource Location field of a sublayout or rendering having multiple items assigned.

Slightly annoyingly, the class defining this method doesn’t make life easy for modifying it. In fact due to the use of static methods, we’re pretty much stuck with the idea that we need to decompile the whole class and copy it into our own codebase in order to modify it. Once we’ve done that, we need some code that can process the Datasource Location field and generate a List<Item> that contains whatever items it finds.

If we assume that the data is going to be formatted using the standards for a multi-select field, then we could write a method something like this:

private static List<item> fetchDatasourceRoots(Item renderingItem)
{
List<item> roots = new List<item>();

string itemIDs = renderingItem.Fields["Datasource Location"].Value;

if (string.IsNullOrWhiteSpace(itemIDs))
{
roots.Add(Client.ContentDatabase.GetRootItem());
return roots;
}

string[] ids = itemIDs.Split('|');

if (ids.Length > 0)
{
foreach (string id in ids)
{
roots.Add(Client.ContentDatabase.GetItem(id));
}
}
else
{
roots.Add(Client.ContentDatabase.GetRootItem());
}

return roots;
}

We take the item that represents the Rendering or Sublayout and we extract the value for the Datasource Location field. If it’s empty then we return the default data – the same data the original code used. If it’s not empty then we split it into individual IDs and add each of the items these represent to the collection we return.

And with that, we can modify the code of the CreatePipelineArgs() to initialise the FallbackDatasourceRoots property using our new method instead of the original code.

Two more things need we need to do for this to work. First, we need Sitecore to use our replacement custom SetDatasource class. That just needs a quick config patch, along the lines of:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
<sitecore>
<commands>
<command name="webedit:setdatasource">
<patch:attribute name="type">
Testing.CustomSetDataSource, Testing
</patch:attribute>
</command>
</commands>
</sitecore>
</configuration>

The “patch:attribute” element here tells the configuration system to replace the value of the specified attribute of the context element with the new value we provide.

The second thing is that the Datasource Location field of the Rendering and Sublayout templates needs to become a multi-select type of field. The TreeListEx control seems appropriate. That field lives at /sitecore/templates/System/Layout/Sections/Rendering Options/Editor Options/Datasource Location in the content tree, so we can change the type:

image

That will change the type of this field for all the renderings and sublayouts. And with that, you can now set multiple items as the Datasource Location:

image

And now both of those items will show up as roots in the Set Associated Content dialog.

Now, there are a few caveats with this modification as it stands. One is that it’s not compatible with the relative data source locations code from a previous post, because it stores GUIDs in the Datasource Location field rather than a string. It also means that any pre-existing data in the Datasource Location field is now of the wrong type – so you will have to reset any values set before this change. And thirdly, because we’ve had to copy and replace an entire command class any future modifications to this in the Sitecore codebase won’t make it into our solution without us doing it manually. So borrow this with a bit of caution...

Monday, 17 March 2014

Delete aliases along with their items

Last week I looked at how to visualise aliases in the Content Editor, based on a requirement from one of my clients. The second part of the work I was considering here was how you can automatically remove any aliases related to an item when you remove that item. What I wanted to achieve was having the system prompt you to ask if you want the aliases removed whenever you delete an item that has aliases attached. Something like:

image

I bit of digging through docs and Reflector shows that when you delete an item via the UI, Sitecore runs the “uiDeleteItems” pipeline. This performs the series of steps required to verity and then perform the deletion:

<uiDeleteItems>
<processor mode="on" method="CheckPermissions" type="Sitecore.Shell.Framework.Pipelines.DeleteItems,Sitecore.Kernel"/>
<processor mode="on" method="Confirm" type="Sitecore.Shell.Framework.Pipelines.DeleteItems,Sitecore.Kernel"/>
<processor mode="on" method="CheckTemplateLinks" type="Sitecore.Shell.Framework.Pipelines.DeleteItems,Sitecore.Kernel"/>
<processor mode="on" method="CheckCloneLinks" type="Sitecore.Shell.Framework.Pipelines.DeleteItems,Sitecore.Kernel"/>
<processor mode="on" method="CheckLinks" type="Sitecore.Shell.Framework.Pipelines.DeleteItems,Sitecore.Kernel"/>
<processor mode="on" method="CheckLanguage" type="Sitecore.Shell.Framework.Pipelines.DeleteItems,Sitecore.Kernel"/>
<processor mode="on" method="UncloneItems" type="Sitecore.Shell.Framework.Pipelines.DeleteItems,Sitecore.Kernel"/>
<processor mode="on" method="Execute" type="Sitecore.Shell.Framework.Pipelines.DeleteItems,Sitecore.Kernel"/>
<processor mode="on" method="PostAction" type="Sitecore.Shell.Framework.Pipelines.DeleteItems,Sitecore.Kernel"/>
</uiDeleteItems>

Looking at the code behind these components, it seems sensible that if we’re going to extend this pipeline, the “are you sure you want to delete aliases” component needs to go between the “Confirm” step (which prompts to confirm whether you really want to delete the item) and the “CheckTemplateLinks” step. That would allow us to prompt before Sitecore starts the process of deletion. Now as you can probably tell from the config above, the original Sitecore code breaks this sort of “confirm” and “perform” behaviour into multiple pipeline steps. But for the purposes of this example we’ll stick to just the one for simplicity.

So, as we’ve seen with other customisations, we need to start with a basic extension class which will provide our new pipeline step. For this particular pipeline, that class looks like:

public class DeleteExtensions : ItemOperation
{
public void CheckAndDeleteAliases(ClientPipelineArgs args)
{
}
}

and it gets configured with a configuration patch like so:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
<sitecore>
<processors>
<uiDeleteItems>
<processor patch:after="*[@method='Confirm']" mode="on"
type="Testing.DeleteExtensions ,Testing"
method="CheckAndDeleteAliases"/>
</uiDeleteItems>
</processors>
</sitecore>
</configuration>

Confirming an operation and then optionally performing it in a pipeline requires  pattern of code that we saw in the previous post about confirming commands in the Sitecore UI – the code needs a two step approach where initially we present the UI and the the second time the code is run we detect the results from the UI dialog and act on them. So we need a clause to detect the user saying “no” to alias removal, a clause to detect the user saying “yes” and the code to generate the confirmation dialog. Now interestingly when we have multiple dialogs in a pipeline it turns out we can’t make use of args.IsPostBack directly because it will already be true due to the previous dialog. But the args.Result property is correct when our code runs. Hence we can work around this issue by structuring the code a bit differently here:

    public class DeleteItems : ItemOperation
{
private static Sitecore.Data.ID aliasID =
new Sitecore.Data.ID("{54BCFFB7-8F46-4948-AE74-DA5B6B5AFA86}");

public void CheckAndDeleteAliases(ClientPipelineArgs args)
{
Assert.ArgumentNotNull(args, "args");

//
// Check for postback data from our dialog
//

// User clicked no - abort the whole deletion
if (args.Result == "no")
{
args.AbortPipeline();
return;
}

// Both further steps require these bits of context data
ListString items = new ListString(args.Parameters["items"], '|');
Database db = getDatabase(args);

// User clicked yes - ok to delete
if (args.Result == "yes")
{
args.Result = string.Empty;
removeAliases(items, db);
return;
}

//
// If we're not handling a postback, check for aliases and present UI
//

// Count the aliases for any items we have as parameters
int aliases = countAliases(items, db);

// If we got any aliases, ask the UI to show a confirm dialog for us
if (aliases > 0)
{
string message;

if(items.Count == 1)
{
Item item = db.GetItem(items[0]);

message = string.Format(
"The item \"{0}\" has {1} alias{2} which will also be {3}." +
" Are you sure?",
item.DisplayName,
aliases,
aliases != 1 ? "es" : string.Empty,
Settings.RecycleBinActive ? "recycled" : "deleted"
);
}
else
{
message = string.Format(
"These {0} items have {1} alias{2} which will also be {3}." +
" Are you sure?",
items.Count,
aliases,
aliases != 1 ? "es" : string.Empty,
Settings.RecycleBinActive ? "recycled" : "deleted"
);
}

Context.ClientPage.ClientResponse.Confirm(message);
args.WaitForPostBack();
}
}

private Database getDatabase(ClientPipelineArgs args)
{
Assert.ArgumentNotNull(args, "args");
Database database = Factory.GetDatabase(args.Parameters["database"]);
Assert.IsNotNull(database, typeof(Database), "Name: {0}", new object[]
{
args.Parameters["database"]
});
return Assert.ResultNotNull<database>(database);
}

private int countAliases(ListString items, Database db)
{
int aliases = 0;

try
{
using (new TaskContext("DeleteItems pipeline - count aliases"))
{
using (new SecurityDisabler())
{
foreach (string item in items)
{
Item itm = db.GetItem(item);
if (itm != null)
{
// Count this one
aliases += countAliases(itm);

// And process any descendent items too
foreach(var descendantItm in itm.Axes.GetDescendants())
{
aliases += countAliases(descendantItm);
}
}
}
}
}
}
catch (Exception ex)
{
Log.Error("Error while counting aliases for items", ex, this);
HttpUnhandledException ex2 =
new HttpUnhandledException(ex.Message, ex);
string htmlErrorMessage = ex2.GetHtmlErrorMessage();
UrlString urlString =
new UrlString("/sitecore/shell/controls/error.htm");
Context.ClientPage.ClientResponse.ShowModalDialog(urlString.ToString(),
htmlErrorMessage);
}

return aliases;
}

private int countAliases(Item item)
{
int aliases = 0;

try
{
aliases = Sitecore.Globals.LinkDatabase.GetReferrers(item)
.Select(l => l.GetSourceItem())
.Where(s => s.TemplateID == aliasID)
.Count();
}
catch (Exception)
{
// this should always succeed - exceptions seem to come
// if link database is out of date, so they can be discarded
}

return aliases;
}
}

So the first time the code runs we count the aliases that might need deletion. This uses a two step process because the parameters for the pipeline give us a list of strings representing the items. So the code goes through those strings, loads each item in turn and counts up the aliases for each one (using a similar query to the one we used in the Alias gutter rendering). One interesting question here is what security context to do the counting with. Ideally we’d do it as the current user – but what happens if the user didn’t create the Alias? For the purposes of this demo code I’m just making use of SecurityDisabler here – but in production code it would be sensible to work out the correct security context to do this under. Though the right answer to that question may well depend on the business rules for your particular site.

If we get more than one alias across all the items we render a confirmation dialog. There’s a bit of code to format the message correctly – depending on whether we have one or more aliases to delete and whether Sitecore is configured to use the Recycle Bin or not. Once we’ve shown the message box we call args.WaitForPostback() to tell the UI that we need to wait for a response.

When the code gets called a second time we check the results. If the result says the user doesn’t want to continue the deletion then we abort the pipeline with a call to args.AbortPipeline(). If the result says that the user says yes to the deletion then we remove the aliases. This code follows a similar pattern to the alias counting code:

    private void removeAliases(ListString items, Database db)
{
try
{
using (new TaskContext("DeleteItems pipeline - remove aliases"))
{
using (new SecurityDisabler())
{
foreach (string item in items)
{
Item itm = db.GetItem(item);
if (itm != null)
{
// process this item
removeAliases(itm);

// And process any descendent items too
foreach (var descendantItm in itm.Axes.GetDescendants())
{
removeAliases(descendantItm);
}
}
}
}
}
}
catch (Exception ex)
{
Log.Error("Error while removing aliases for items", ex, this);
HttpUnhandledException ex2 =
new HttpUnhandledException(ex.Message, ex);
string htmlErrorMessage = ex2.GetHtmlErrorMessage();
UrlString urlString =
new UrlString("/sitecore/shell/controls/error.htm");
Context.ClientPage.ClientResponse.ShowModalDialog(urlString.ToString(),
htmlErrorMessage);
}
}

private void removeAliases(Item item)
{
var aliases = Sitecore.Globals.LinkDatabase.GetReferrers(item)
.Select(l => l.GetSourceItem())
.Where(s => s.TemplateID == aliasID);

foreach (var alias in aliases)
{
if (Settings.RecycleBinActive)
{
Log.Audit(this, "Recycle alias {0} of item {1}",
AuditFormatter.FormatItem(alias),
AuditFormatter.FormatItem(item));
alias.Recycle();
}
else
{
Log.Audit(this, "Delete alias {0} of item: {1}",
AuditFormatter.FormatItem(alias),
AuditFormatter.FormatItem(item));
alias.Delete();
}
}
}

However here, instead of counting the aliases, this code removes them. As mentioned above, this code pays attention to the configuration for whether Sitecore uses the Recycle Bin approach to deletion or not, in the same way the original item deletion code in Sitecore does.

Testing this gives us the dialog box shown in the image at the top of the page, and if we approve the deletion, removes the appropriate aliases. Job’s a good ‘un.

Now whenever I come across blocks of code as similar as the removeAliases(ListString items, Database db) and countAliases(ListString items, Database db) methods I start thinking about how to reduce that down to one method. But I’ll save that for a future discussion...

Monday, 10 March 2014

Visualising Aliases

Recently a client I work for came to me with an interesting question. Their Sitecore-based website makes heavy use of Aliases to set up shortened URLs for publicity purposes, but they were finding it difficult to manage the large number of aliases they were creating. Their key issues were remembering which pages had aliases defined, and remembering to delete aliases when they removed the pages they were related to.

Sitecore provides a helpful dialog for managing aliases, in the Presentation ribbon tab:

image

However you still have to remember to click on it – and that’s one of the things the client was hoping for help with.

Having had a bit of a think about what could be done here, I came up with two ideas – visualising which items have aliases defined, and automating that deletion. Lets have a look at visualisation first.

Having done a bit of reading around, it turns out that you are able to easily customise the “gutter” in the Sitecore Content Editor. This is the left hand column of the content tree. Out of the box, right-clicking the gutter gives you a series of optional information icons to enable:

image

When you select one of these options, the system will render extra icons in the gutter for any item which meets the criteria defined by this custom gutter control – so lets see what’s involved in adding a gutter rendering to show when content items have aliases defined.

Custom gutter renderings are classes based on the Sitecore.Shell.Applications.ContentEditor.Gutters.GutterRenderer class. All you have to provide is an implementation for the GetIconDescriptor() method:

public class AliasGutterRenderer : GutterRenderer
{
protected override GutterIconDescriptor
GetIconDescriptor(Sitecore.Data.Items.Item item)
{
return null;
}
}

If we want to visualise Aliases, we need to write some sort of query to detect if the current item has any Aliases attached to it. There are a couple of ways you can go about this. One way would be to write a query for children of the /sitecore/system/Aliases item, and look for references to the current item’s ID in the Linked Item field. But Sitecore can make our life easier than this – since it maintains something called the Link Database for us. Inside the Sitecore database, it maintains references between items. Whenever you fill in a field which stores a link between two or more items this relationship is stored in the Link Database to make it easy to query these relationships later. You can see this data yourself when you click the Links button in the Navigate tab of the ribbon:

image

You’ll notice that this example page has lots of links in the database table – it references things like workflow, templates, sublayouts etc. But it also shows that it is referred to by an Alias item. And that shows us that we can use the Links Database to do the query we need.

So we can write a bit of code to count the number of Aliases related to the current item with a simple Linq query:

public class AliasGutterRenderer : GutterRenderer
{
private static Sitecore.Data.ID aliasID =
new Sitecore.Data.ID("{54BCFFB7-8F46-4948-AE74-DA5B6B5AFA86}");

protected override GutterIconDescriptor
GetIconDescriptor(Sitecore.Data.Items.Item item)
{
int aliases = Sitecore.Globals.LinkDatabase.GetReferrers(item)
.Select(l => l.GetSourceItem())
.Where(s => s.TemplateID == aliasID)
.Count();

return null;
}
}

Every time Sitecore processes an item in the content tree it passes that item into our method – and we count the Aliases. The GetReferrers() method here fetches the list of items which refer to the item in question. We then use a select clause to change the data we get back from IDs into items. And then we filter those items by Template ID to ensure we have a list of only referring Aliases. Finally calling Count() tells us how many items matched our criteria.

Having done a bit of testing, it turns out there are some scenarios where the data in the Link Database can be outdated. And that can cause this query to fail in certain circumstances. The most common being after the deployment of a package which affects the items and their aliases. This issue is resolved with the “Rebuild Link Database” option in the Control Panel – but to prevent issues, for demo purposes we'll catch any exceptions thrown by this query. In real-world code we should do somehting like log errors or generate a custom icon that can trigger the link database rebuild if necessary – but for the purposes of this example we’ll just discard the error.

The next job is generating the custom icon to display – and this is where the return type of the GetIconDescriptor() method comes  in. The return type of the method is the GutterIconDescriptor class – and we fill in and return an instance of this to tell the UI what to render. It’s simple enough to fill in – you can provide a path to an icon, a tooltip string and an on-click event to trigger. And when you return this data, the UI renders it in the gutter.

Putting all that together gives us:

public class AliasGutterRenderer : GutterRenderer
{
private static Sitecore.Data.ID aliasID =
new Sitecore.Data.ID("{54BCFFB7-8F46-4948-AE74-DA5B6B5AFA86}");

protected override GutterIconDescriptor
GetIconDescriptor(Sitecore.Data.Items.Item item)
{
GutterIconDescriptor gid = null;
int aliases = 0;

try
{
aliases = Sitecore.Globals.LinkDatabase.GetReferrers(item)
.Select(l => l.GetSourceItem())
.Where(s => s.TemplateID == aliasID)
.Count();
}
catch (Exception)
{
// this should always succeed - exceptions seem to come
// if link database is out of date
// In production code we'd do something cleverer than
// this and try to prevent rather than catch the exception
}

if (aliases > 0)
{
gid = new GutterIconDescriptor();
gid.Icon = "Network/32x32/spy.png";
gid.Tooltip = string.Format("This item has {0} alias{1}",
aliases, aliases > 1 ? "es" : "");
gid.Click = "item:setaliases(id=" + item.ID + ")";
}

return gid;
}
}

Here we’ve set the gutter icon to be the same as the icon used by the Aliases button in the ribbon for consistency. And we’ve set the tooltip to show how many aliases the current item has. Finally, the Click event has been set to the same command used by the Aliases button in the ribbon – a call to the “item:setaliases” command, passing the ID of the current item.

So there’s the code – what do we need to do to make this custom gutter rendering available for users? The data for configuring the Content Editor UI lives in the Core database, under /sitecore/content/Applications/Content Editor/Gutters and configuring a new Gutter Renderer is as simple as adding a new item here based on the “Gutter Renderer” template:

image

The “Header” field here contains the text displayed in the right-click context menu we saw back at the beginning of this post. The “Type” field contains the fully qualified .Net type descriptor for the custom rendering class we defined. As usual for configuring extension types, this is formatted as “<fully qualified type name>, <assembly name>”.

And once you have configured that for your custom class, you can go back to the Master database and enable the new renderer by right-clicking the gutter:

image

Our new custom gutter render is now visible, and its selected state will be remembered between user sessions. Once it’s selected, we start to see the alias icon in the gutter for any item which has Aliases defined:

image

And clicking the icon will launch the Aliases dialog.

Next week we’ll have a look at how we might remove Aliases automatically when the item they refer to is deleted.

Monday, 17 February 2014

Confirming Sitecore Commands

Be honest people: how many of you have wasted a few hours of your life by accidentally clicking “Revert Database” when you meant to click “Update Database” after a long evening at the development coal face? I certainly have. And if you’re like me and tend to serialise only the things you’ve changed in your project, then that means you generally end up with a dead instance of Sitecore.

Once is enough for this sort of mistake, and it lead me to thinking about how you can customise the Sitecore UI to prevent these issues in a generic way. What I decided to try was some code to allow you to configure a confirmation dialog before the action of any command on the ribbon. And it turns out it’s not too hard to do.
The starting point for anything triggered by clicking a button in the ribbon is a class which inherits from Sitecore.Shell.Framework.Commands.Command, and they require an Execute() method:

namespace Blog.Examples
{
    public class ConfirmCommand : Command
    {
        public override void Execute(CommandContext context)
        {
        }
    }
}

Whenever you want to make use of a class derived from Command in your ribbon configuration, you need to register it in the Sitecore configuration. And that means a quick configuration patch. Each entry in the commands collection in configuration needs two bits of data. The “type” attribute is a normal .Net type descriptor for the class you’ve implemented, and the “name” attribute is the textual name you will use later when you specify what gets called when a ribbon button is clicked. So we can use example:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <commands>
      <command name="extensions:confirm" 
 type="Blog.Examples.ConfirmCommand,Blog.Examples" />
    </commands>
  </sitecore>
</configuration>

With that done, we need to make the command present some UI. Having done a bit of spelunking through the commands that ship with Sitecore, they share a common pattern where the command launches a pipeline in order to allow the UI to display UI and then process its results:

public class ConfirmCommand : Command
{
    public override void Execute(CommandContext context)
    {
        Sitecore.Context.ClientPage.Start(this, "Confirm", 
context.Parameters);
    }

    protected void Confirm(ClientPipelineArgs args)
    {
    }
}

The next thing to consider is how the code will find out what it is supposed to be confirming. Looking at some of the commands that ship with Sitecore, they get parameters passed from their item in the ribbon in the definition of their click event:

image

Examining the code for these commands shows that the parameters inside the brackets are parsed into the Parameters collection of the commands context object as a name/value collection. A quick test shows that this means you can just wrap the command you want to confirm as a parameter of the confirm command itself. So, thinking this through the confirm command will also need to know what text to put in the confirmation dialog, so we need two parameters:

extensions:confirm(title=revert the database,
         cmd=itemsync:loaddatabase(revert=1))

The “title” parameter optionally provides the text to append to the dialog message, and “cmd” provides the command to be executed. And the code to process this looks like this: (Error handling and validation omitted for clarity)

protected void Run(ClientPipelineArgs args)
{
    string msg;
    if (!string.IsNullOrWhiteSpace(args.Parameters["title"]))
    {
        msg = "Are you sure you want to " + 
args.Parameters["title"] + "?";
    }
    else
    {
        msg = "Are you sure you want to do that?";
    }

    SheerResponse.Confirm(msg);
    args.WaitForPostBack();
}

The call to args.WaitForPostBack() signals to the internals of Sitecore that this code wants to know the response from the dialog box that we’ve displayed. What happens as a result of that is when the user makes their click our code gets run again, passing in a new parameter to say what button got clicked. So we need to refactor this method slightly to detect that we’ve had a “postback” from the dialog box, and process it appropriately. That makes our Run() method look like this:

protected void Run(ClientPipelineArgs args)
{
    if (args.IsPostBack)
    {
        if (args.Result == "yes")
        {
            string cmd = args.Parameters["cmd"];
            Sitecore.Context.ClientPage.SendMessage(this, cmd);
            return;
        }

        if (args.Result == "no")
        {
            return;
        }
    }
    else
    {
        string msg;
        if (!string.IsNullOrWhiteSpace(args.Parameters["title"]))
        {
            msg = "Are you sure you want to " + 
args.Parameters["title"] + "?";
        }
        else
        {
            msg = "Are you sure you want to do that?";
        }

        SheerResponse.Confirm(msg);
        args.WaitForPostBack();
    }
}

Simple enough change. When the code is run for the first time, we display the dialog. But on the second pass through we check the args.Result value. If the user does not confirm the action then we just return and do nothing else. But if they confirm that they do want the command to run, we use the SendMessage() API call to tell Sitecore to start the original command.

So all that’s left to do is to configure a ribbon button with the confirm command and try it out. The buttons in the Content Editor ribbon are defined in the Core database, at:

/sitecore/content/Applications/Content Editor/Ribbons/Chunks

So you can find your required ribbon button in there and adjust its configuration by wrapping the existing command with the new confirm command. To avoid messing things up while I’m testing, I’ve created a new ribbon button that wraps up the Revert Item command for testing purposes:

image

And with that saved, the ribbon updates and you can test out the command:

image

Success – and we can now wrap any command in the ribbon with a confirmation.