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:
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...