Home » Search results for 'Command' (Page 3)

Search Results for: Command

Bucket and Unbucket Items via Custom Item Context Menu Options Using Sitecore PowerShell Extensions

Last Wednesday I was honored to present Sitecore PowerShell Extensions (SPE) at the Milwaukee Sitecore Meetup. My presentation was all about how easy it is to add, execute and reuse PowerShell scripts in SPE, and I showcased this using version 3.0 of SPE on Sitecore XP 8.

During the presentation, I demonstrated how one can go about adding custom Item Context Menu options using SPE, and did so with the following scripts which bucket and unbucket Sitecore Items:

 <#
    .NAME 
        Convert To Item Bucket

    .SYNOPSIS
        Converts the context item to an Item Bucket
     
    .NOTES
        Mike Reynolds
#>
 
$item = Get-Item .

if($item."__Is Bucket" -eq "1") {
   return 
}

Get-ChildItem . -Recurse | %{ $_.__Bucketable = "1" }
Invoke-ShellCommand -Name "item:bucket" -Item $item
Close-Window

The above PowerShell script basically checks to see if an Item is an Item Bucket and does nothing if it is. If the Item is not an Item Bucket, we make sure all sub-items are bucketable — we just tick the “__Bucketable” Checkbox field on them — and invoke the Sheer UI command for converting an Item into an Item Bucket — this is done via the Invoke-ShellCommand commandlet that ships with SPE — and then close the script execution dialog users are presented with when SPE executes a script in the Item Context Menu.

The following script does the exact opposite of the script above — it converts an Item Bucket back to a regular Sitecore Item using the Sheer UI command for unbucketing:

<#
    .NAME 
        Convert Item Bucket To A Regular Item

    .SYNOPSIS
        Converts an Item Bucket to a regular Item
     
    .NOTES
        Mike Reynolds
#>

$item = Get-Item .

if($item."__Is Bucket" -ne "1") {
   return 
}

Invoke-ShellCommand -Name "item:unbucket" -Item $item
Close-Window
 

I then saved the above scripts to a Context Menu integration point in a SPE module I created during the presentation using the SPE Integrated Scripting Environment (ISE) (to get to the ISE in SPE, go to Sitecore ==> Development Tools ==> PowerShell ISE in the Sitecore Start menu of the Sitecore Desktop):

Saved-context-menu-script

I’ve omitted screenshots on saving the “Convert Item Bucket To A Regular Item” script for brevity.

I then set rules in the “Show if rules are met or not defined” field on both Context Menu script Items — we only want the “Convert To Item Bucket” Context Menu option to show when the Item isn’t an Item Bucket, and the “Convert Item Bucket To A Regular Item” Context Menu option to show when the Item it is an Item Bucket:

The “Convert To Item Bucket” Context Menu item (this Item was saved to /sitecore/system/Modules/PowerShell/Script Library/SitecoreUG Module/Content Editor/Context Menu/Convert To Item Bucket in my Sitecore instance):

set-rules-convert-to-bucket

The “Convert Item Bucket To A Regular Item” Context Menu item:

set-rules-convert-to-unbucket

After saving the above, I navigated to an Item in my content tree that has sub-items, right-clicked on it, and clicked on the “Convert To Item Bucket” Context Menu option:

convert-to-bucket-right-click

I was then presented with a confirmation dialog:

convert-to-bucket-confirm

As you can see the Item is now an Item Bucket:

item-is-a-bucket

I right-clicked on the Item again, and clicked on the “Convert Item Bucket To A Regular Item” Context Menu option:

convert-to-unbucket-right-click

I was presented with another confirmation dialog:

convert-to-ubucket-confirm

As you can see the Item is no longer an Item Bucket:

no-longer-a-bucket

If you have any thoughts on this or ideas for other Context Menu PowerShell scripts for SPE, please drop a comment.

If you would like to watch the Milwaukee Sitecore Meetup presentation where I showed the above — you’ll also get to see some cool Sitecore PowerShell Extensions stuff from Adam Brauer, Senior Product Engineer at Active Commerce, in this presentation as well — have a look below:

Until next time, have a Sitecoretastic day!

Augment Configuration-defined Sitecore Functionality using the Composite Design Pattern

In a couple of my past posts — Synchronize IDTable Entries Across Multiple Sitecore Databases Using a Composite IDTableProvider and
Chain Together Sitecore Client Commands using a Composite Command — I used the Composite design pattern to chain together functionality in two or more classes with the same interface — by interface here, I don’t strictly mean a C# interface but any class that servers as a base class for others where all instances share the same methods — and had a thought: how could I go about making a generic solution to chain together any class type defined in Sitecore configuration?

As a “proof of concept” I came up with the following solution while taking a break from my judgely duties reviewing 2015 Sitecore Hackathon modules.

I first defined an interface for classes that will construct objects using the Sitecore Configuration Factory:

using System.Collections.Generic;
using System.Xml;

namespace Sitecore.Sandbox.Shared
{
    public interface IConfigurationFactoryInstances<TInstance>
    {
        void AddInstance(XmlNode source);

        IEnumerable<TInstance> GetInstances();
    }
}

The following class implements the methods defined in the interface above:

using System.Collections.Generic;
using System.Xml;

using Sitecore.Configuration;

namespace Sitecore.Sandbox.Shared
{
    public class ConfigurationFactoryInstances<TInstance> : IConfigurationFactoryInstances<TInstance> where TInstance : class
    {
        private IList<TInstance> Instances { get; set; }

        public ConfigurationFactoryInstances()
        {
            Instances = new List<TInstance>();
        }

        public virtual IEnumerable<TInstance> GetInstances()
        {
            return Instances;
        }

        public void AddInstance(XmlNode configNode)
        {
            TInstance instance = CreateInstance(configNode);
            if (instance == null)
            {
                return;
            }

            Instances.Add(instance);
        }

        protected virtual TInstance CreateInstance(XmlNode configNode)
        {
            if (configNode == null)
            {
                return null;
            }

            TInstance instance = Factory.CreateObject(configNode, true) as TInstance;
            if (instance == null)
            {
                return null;
            }

            return instance;
        }
    }
}

The AddInstance() method in the class above — along with the help of the CreateInstance() method — takes in a System.Xml.XmlNode instance and attempts to create an instance of the type denoted by TInstance using the Sitecore Configuration Factory. If the instance was successfully created (i.e. it’s not null), it is added to a list.

The GetInstances() method in the class above just returns the list of the instances that were added by the AddInstance() method.

Since I’ve been posting a lot of meme images on Twitter lately — you can see the evidence here — I’ve decided to have a little fun tonight with this “proof of concept”, and created the following composite MediaProvider:

using System.Xml;

using Sitecore.Data.Items;
using Sitecore.Resources.Media;

using Sitecore.Sandbox.Shared;

namespace Sitecore.Sandbox.Resources.Media
{
    public class CompositeMediaProvider : MediaProvider
    {
        private static IConfigurationFactoryInstances<MediaProvider> Instances { get; set; }

        static CompositeMediaProvider()
        {
            Instances = new ConfigurationFactoryInstances<MediaProvider>();
        }

        public override string GetMediaUrl(MediaItem item, MediaUrlOptions options)
        {
            foreach (MediaProvider mediaProvider in Instances.GetInstances())
            {
                string url = mediaProvider.GetMediaUrl(item, options);
                if(!string.IsNullOrWhiteSpace(url))
                {
                    return url;
                }
            }

            return base.GetMediaUrl(item, options);
        }

        protected virtual void AddMediaProvider(XmlNode configNode)
        {
            Instances.AddInstance(configNode);
        }
    }
}

The AddMediaProvider() method in the class above adds new instances of Sitecore.Resources.Media.MediaProvider through delegation to an instance of the ConfigurationFactoryInstances class. The Sitecore Configuration Factory will call the AddMediaProvider() method since it’s defined in the patch include configuration file shown later in this post

The GetMediaUrl() method iterates over all instances of Sitecore.Resources.Media.MediaProvider that were created and stored by the ConfigurationFactoryInstances instance, and calls each of their GetMediaUrl() methods. The first non-null or empty URL from one of these “inner” instances is returned to the caller. If none of the instances return a URL, then the class above returns the value given by its base class’ GetMediaUrl() method.

I then spun up three MediaProvider classes to serve up specific image URLs of John West — I found these somewhere on the internet 😉 — when they encounter media Items with specific names (I am not advocating that anyone hard-codes anything like this — these classes are only here to serve as examples):

using System;

using Sitecore.Data.Items;
using Sitecore.Resources.Media;

namespace Sitecore.Sandbox.Resources.Media
{
    public class JohnWestOneMediaProvider : MediaProvider
    {
        public override string GetMediaUrl(MediaItem item, MediaUrlOptions options)
        {
            if(item.Name.Equals("john-west-1", StringComparison.CurrentCultureIgnoreCase))
            {
                return "http://cdn.meme.am/instances/500x/43030540.jpg";
            }

            return string.Empty;
        }
    }
}

using System;

using Sitecore.Data.Items;
using Sitecore.Resources.Media;

namespace Sitecore.Sandbox.Resources.Media
{
    public class JohnWestTwoMediaProvider : MediaProvider
    {
        public override string GetMediaUrl(MediaItem item, MediaUrlOptions options)
        {
            if (item.Name.Equals("john-west-2", StringComparison.CurrentCultureIgnoreCase))
            {
                return "http://cdn.meme.am/instances/500x/43044627.jpg";
            }

            return string.Empty;
        }
    }
}
using System;

using Sitecore.Data.Items;
using Sitecore.Resources.Media;

namespace Sitecore.Sandbox.Resources.Media
{
    public class JohnWestThreeMediaProvider : MediaProvider
    {
        public override string GetMediaUrl(MediaItem item, MediaUrlOptions options)
        {
            if (item.Name.Equals("john-west-3", StringComparison.CurrentCultureIgnoreCase))
            {
                return "http://cdn.meme.am/instances/500x/43030625.jpg";
            }

            return string.Empty;
        }
    }
}

I then registered all of the above in Sitecore using a patch configuration file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <mediaLibrary>
      <mediaProvider>
        <patch:attribute name="type">Sitecore.Sandbox.Resources.Media.CompositeMediaProvider, Sitecore.Sandbox</patch:attribute>
        <mediaProviders hint="raw:AddMediaProvider">
          <mediaProvider type="Sitecore.Sandbox.Resources.Media.JohnWestOneMediaProvider, Sitecore.Sandbox" />
          <mediaProvider type="Sitecore.Sandbox.Resources.Media.JohnWestTwoMediaProvider, Sitecore.Sandbox" />
          <mediaProvider type="Sitecore.Sandbox.Resources.Media.JohnWestThreeMediaProvider, Sitecore.Sandbox" />
          <mediaProvider type="Sitecore.Resources.Media.MediaProvider, Sitecore.Kernel" />
        </mediaProviders>
      </mediaProvider>
    </mediaLibrary>
  </sitecore>
</configuration>

Let’s see this in action!

To test, I uploaded four identical photos of John West to the Media Library:

four-jw-media-library

I then inserted these into a Rich Text field on my home Item:

rte-jw-times-four

I saved my home Item and published everything. Once the publish was finished, I navigated to my home page and saw the following:

four-jw-home-page

As you can see, the three custom John West MediaProvider class instances served up their URLs, and the “out of the box” Sitecore.Resources.Media.MediaProvider instance served up its URL on the last image.

If you have any thoughts on this, please share in a comment.

Until next time, have a Sitecoretastic day!

Chain Source and Clone Items Together in Sitecore Workflow

Two months ago, I worked on a project where I had to find a solution to chain source Items and their clones together in Sitecore workflow — don’t worry, the clone Items were “locked down” by being protected so content authors cannot make changes to content on the clones — the clones serve as content copies of their source Items for a multi-site solution in a single Sitecore instance.

After some research, a few mistakes — well, maybe more than a few 😉 — and massive help from Oleg Burov, Escalation Engineer at Sitecore USA, I put together a subclass of Sitecore.Workflows.Simple.Workflow — this lives in Sitecore.Kernel.dll — similar to the following:

using Sitecore.Data.Items;
using Sitecore.Workflows;
using Sitecore.Workflows.Simple;

namespace Sitecore.Sandbox.Workflows.Simple
{
    public class ChainSourceClonesWorkflow : Workflow 
    {
        public ChainSourceClonesWorkflow(string workflowID, WorkflowProvider owner)
            : base(workflowID, owner)
        {

        }
        public override WorkflowResult Execute(string commandID, Item item, string comments, bool allowUI, params object[] parameters)
        {
            WorkflowResult result = base.Execute(commandID, item, comments, allowUI, parameters);
            foreach (Item clone in item.GetClones())
            {
                base.Execute(commandID, clone, comments, allowUI, parameters);
            }

            return result;
        }
    }
}

The Execute() method above basically moves the passed Item through to the next workflow state by calling the base class’ Execute() method, and grabs all clones for the passed Item — each are also pushed through to the next workflow state via the base class’ Execute() method.

Workflow instances are created by Sitecore.Workflows.Simple.WorkflowProvider. I created the following class to return an instance of the ChainSourceClonesWorkflow class above:

using Sitecore.Workflows;
using Sitecore.Workflows.Simple;

namespace Sitecore.Sandbox.Workflows.Simple
{
    public class ChainSourceClonesWorkflowProvider : WorkflowProvider
    {
        public ChainSourceClonesWorkflowProvider(string databaseName, HistoryStore historyStore)
            : base(databaseName, historyStore)
        {
        }

        protected override IWorkflow InstantiateWorkflow(string workflowId, WorkflowProvider owner)
        {
            return new ChainSourceClonesWorkflow(workflowId, owner);
        }
    }
}

I then replaced the “out of the box” WorkflowProvider with the one defined above using the following configuration file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <databases>
      <database id="master">
        <workflowProvider type="Sitecore.Workflows.Simple.WorkflowProvider, Sitecore.Kernel">
          <patch:attribute name="type">Sitecore.Sandbox.Workflows.Simple.ChainSourceClonesWorkflowProvider, Sitecore.Sandbox</patch:attribute>
        </workflowProvider>
      </database>
    </databases>
  </sitecore>
</configuration>

Let’s take this for a spin!

I first started with a source and clone in a “Draft” workflow state:

source-clone-draft

Let’s push the source — and hopefully clone 😉 — through to the next workflow state by submitting it:

source-clone-submit

As you can see, both are “Awaiting Approval”:

source-clone-awaiting-approval

Let’s approve them:

source-clone-approve

As you can see, both are approved:

source-clone-approved

If you have any thoughts or comments on this, or know of ways to improve the code above, please drop a comment.

Also, keep in mind the paradigm above is not ideal when content authors are able to make content changes to clones which differ from their source Items. In that scenario, it would be best to let source and clone Items’ workflow be independent.

Make Bulk Item Updates using Sitecore PowerShell Extensions

In my Sitecore PowerShell Extensions presentation at the Sitecore User Group Conference 2014, I demonstrated how simple it is to make bulk Item updates — perform the same update to multiple Sitecore items — using a simple PowerShell script, and thought I would write down what I had shown.

Sadly, I do not remember which script I had shared with the audience — the scratchpad text file I referenced during my presentation contains multiple scripts for making bulk updates to Items (if you attended my talk, and remember exactly what I had shown, please drop a comment).

Since I cannot recall which script I had shown — please forgive me 😉 — let’s look at the following PowerShell script (this might be the script I had shown):

@(Get-Item .) + (Get-ChildItem -r .) | ForEach-Object { Expand-Token $_ }

This script grabs the context Item — this is denoted by a period — within the PowerShell ISE via the Get-Item command, and puts it into an array so that we can concatenate it with an array of all of its descendants — this is returned by the Get-ChildItem command with the -r parameter (r stands for recursive). The script then iterates over all Items in the resulting array, passes each to the Expand-Token command — this command is offered “out of the box” in Sitecore PowerShell Extensions — which expands tokens in every field on the Item.

Let’s see this in action!

My home Item has some tokens in its Title field:

home-tokens

One of its descendants also has tokens in its Title field:

descendant-tokens

I opened up the PowerShell ISE, wrote my script, and executed:

powershell-ise-tokens

As you can see, the tokens on the home Item were expanded:

home-tokens-expanded

They were also expanded on the home Item’s descendant:

descendant-tokens-expanded

If you have any thoughts or questions on this, please share in a comment.

Launch PowerShell Scripts in the Item Context Menu using Sitecore PowerShell Extensions

Last week during my Sitecore PowerShell Extensions presentation at the Sitecore User Group Conference 2014 — a conference held in Utrecht, Netherlands — I demonstrated how to invoke PowerShell scripts from the Item context menu in Sitecore, and felt I should capture what I had shown in a blog post — yes, this is indeed that blog post. 😉

During that piece of my presentation, I shared the following PowerShell script to expands tokens in fields of a Sitecore item (if you want to learn more about tokens in Sitecore, please take a look at John West’s post about them, and also be aware that one can also invoke the Expand-Token PowerShell command that comes with Sitecore PowerShell Extensions to expand tokens on Sitecore items — this makes things a whole lot easier 😉 ):

$item = Get-Item .
$tokenReplacer = [Sitecore.Configuration.Factory]::GetMasterVariablesReplacer()
$item.Editing.BeginEdit()
$tokenReplacer.ReplaceItem($item)
$item.Editing.EndEdit()
Close-Window

The script above calls Sitecore.Configuration.Factory.GetMasterVariablesReplacer() for an instance of the MasterVariablesReplacer class — which is defined and can be overridden in the “MasterVariablesReplacer” setting in your Sitecore instance’s Web.config — and passes the context item — this is denote by a period — to the MasterVariablesReplacer instance’s ReplaceItem() method after the item has been put into editing mode.

Once the Item has been processed, it is taken out of editing mode.

So how do we save this script so that we can use it in the Item context menu? The following screenshot walks you through the steps to do just that:

item-context-menu-powershell-ise

The script is saved to an Item created by the dialog above:

expand-tokens-item

Let’s test this out!

I selected an Item with unexpanded tokens:

home-tokens-to-expand

I then launched its Item context menu, and clicked the option we created to ‘Expand Tokens’:

home-item-context-menu-expand-tokens

As you can see the tokens were expanded:

home-tokens-expanded

If you have any questions or thoughts on this, please drop a comment.

Until next time, have a scriptolicious day 😉

Export to CSV in the Form Reports of Sitecore’s Web Forms for Marketers

The other day I was poking around Sitecore.Forms.Core.dll — this is one of the assemblies that comes with Web Forms for Marketers (what, you don’t randomly look at code in the Sitecore assemblies? 😉 ) — and decided to check out how the export functionality of the Form Reports work.

Once I felt I understood how the export code functions, I decided to take a stab at building my own custom export: functionality to export to CSV, and built the following class to serve as a pipeline processor to wedge Form Reports data into CSV format:

using System;
using System.Collections.Generic;
using System.Linq;

using Sitecore.Diagnostics;
using Sitecore.Form.Core.Configuration;
using Sitecore.Form.Core.Pipelines.Export;
using Sitecore.Forms.Data;
using Sitecore.Jobs;

namespace Sitecore.Sandbox.Form.Core.Pipelines.Export.Csv
{
    public class ExportToCsv
    {
        public void Process(ExportArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            LogInfo();
            args.Result = GenerateCsv(args.Packet.Entries);
        }

        protected virtual void LogInfo()
        {
            Job job = Context.Job;
            if (job != null)
            {
                job.Status.LogInfo(ResourceManager.Localize("EXPORTING_DATA"));
            }
        }

        private string GenerateCsv(IEnumerable<IForm> forms)
        {
            return string.Join(Environment.NewLine, GenerateAllCsvRows(forms));
        }

        protected virtual IEnumerable<string> GenerateAllCsvRows(IEnumerable<IForm> forms)
        {
            Assert.ArgumentNotNull(forms, "forms");
            IList<string> rows = new List<string>();
            rows.Add(GenerateCsvHeader(forms.FirstOrDefault()));
            foreach (IForm form in forms)
            {
                string row = GenerateCsvRow(form);
                if (!string.IsNullOrWhiteSpace(row))
                {
                    rows.Add(row);
                }
            }

            return rows;
        }

        protected virtual string GenerateCsvHeader(IForm form)
        {
            Assert.ArgumentNotNull(form, "form");
            return string.Join(",", form.Field.Select(field => field.FieldName));
        }

        protected virtual string GenerateCsvRow(IForm form)
        {
            Assert.ArgumentNotNull(form, "form");
            return string.Join(",", form.Field.Select(field => field.Value));
        }
    }
}

There really isn’t anything magical happening in the code above. The code creates a string of comma-separated values for each row of entries in args.Packet.Entries, and puts these plus a CSV header into a collection of strings.

Once all rows have been placed into a collection of strings, they are munged together on the newline character ultimately creating a multi-row CSV string. This CSV string is then set on the Result property of the ExportArgs instance.

Now we need a way to invoke a pipeline that contains the above class as a processor, and the following command does just that:

using System.Collections.Specialized;

using Sitecore.Diagnostics;
using Sitecore.Forms.Core.Commands.Export;
using Sitecore.Form.Core.Configuration;
using Sitecore.Shell.Framework.Commands;

namespace Sitecore.Sandbox.Forms.Core.Commands.Export
{
    public class Export : ExportToXml
    {
        protected override void AddParameters(NameValueCollection parameters)
        {
            parameters["filename"] = FileName;
            parameters["contentType"] = MimeType;
        }

        public override void Execute(CommandContext context)
        {
            SetProperties(context);
            base.Execute(context);
        }

        private void SetProperties(CommandContext context)
        {
            Assert.ArgumentNotNull(context, "context");
            Assert.ArgumentNotNull(context.Parameters, "context.Parameters");
            Assert.ArgumentNotNullOrEmpty(context.Parameters["fileName"], "context.Parameters[\"fileName\"]");
            Assert.ArgumentNotNullOrEmpty(context.Parameters["mimeType"], "context.Parameters[\"mimeType\"]");
            Assert.ArgumentNotNullOrEmpty(context.Parameters["exportPipeline"], "context.Parameters[\"exportPipeline\"]");
            Assert.ArgumentNotNullOrEmpty(context.Parameters["progressDialogTitle"], "context.Parameters[\"progressDialogTitle\"]");
            FileName = context.Parameters["fileName"];
            MimeType = context.Parameters["mimeType"];
            ExportPipeline = context.Parameters["exportPipeline"];
            ProgressDialogTitle = context.Parameters["progressDialogTitle"];
        }

        protected override string GetName()
        {
            return ProgressDialogTitle;
        }

        protected override string GetProcessorName()
        {
            return ExportPipeline;
        }

        private string FileName { get; set; }

        private string MimeType { get; set; }

        private string ExportPipeline { get; set; }

        private string ProgressDialogTitle { get; set; }
    }
}

I modeled the above command after Sitecore.Forms.Core.Commands.Export.ExportToExcel in Sitecore.Forms.Core.dll: this command inherits some useful logic of Sitecore.Forms.Core.Commands.Export.ExportToXml but differs along the pipeline being invoked, the name of the export file, and content type of the file being created.

I decided to make the above command be generic: the name of the file, pipeline, progress dialog title — this is a heading that is displayed in a modal dialog that is launched when the data is being exported from the Form Reports — and content type of the file are passed to it from Sitecore via Sheer UI buttons (see below).

I then registered all of the above in Sitecore via the following patch configuration file:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <commands>
      <command name="forms:export" type="Sitecore.Sandbox.Forms.Core.Commands.Export.Export, Sitecore.Sandbox" />
    </commands>
    <pipelines>
      <exportToCsv>
        <processor type="Sitecore.Sandbox.Form.Core.Pipelines.Export.Csv.ExportToCsv, Sitecore.Sandbox" />
        <processor type="Sitecore.Form.Core.Pipelines.Export.SaveContent, Sitecore.Forms.Core" />
      </exportToCsv>
    </pipelines>
  </sitecore>
</configuration>

Now we must wire the command to Sheer UI buttons. This is how I wired up the export ‘All’ button (this button is available in a dropdown of the main export button in the Form Reports):

to-csv-all-core-db

I then created another export button which is used when exporting selected rows in the Form Reports:

to-csv-core-db

Let’s see this in action!

I opened up the Form Reports for a test form I had built for a previous blog post, and selected some rows (notice the ‘To CSV’ button in the ribbon):

form-reports-selected-export-csv

I clicked the ‘To CSV’ button — doing this launched a progress dialog (I wasn’t fast enough to grab a screenshot of it) — and was prompted to download the following file:

export-csv-txt

As you can see, the file looks beautiful in Excel 😉 :

export-csv-excel

If you have any thoughts on this, or ideas for other export data formats that could be incorporated into the Form Reports of Web Forms for Marketers, please share in a comment.

Until next time, have a Sitecoretastic day!

Delete All But This: Delete Sibling Items Using a Custom Item Context Menu Option in Sitecore

Every so often I find myself having to delete all Sitecore items in a folder except for one — the reason for this eludes me at the moment, but it does make an appearance once in a while (if this also happens to you, and you remember the context for why it happens, please share in a comment) — and it feels like it takes ages to delete all of these items: I have to step through all of these items in the Sitecore content tree, and delete each individually .

When this happens I usually say to myself “wouldn’t it be cool to have something like the ‘Close All But This’ feature found in Visual Studio?”:

close-all-but-this-vs

I always forget to write this idea down, but did remember it a couple of days ago, and decided to build something to save time when deleting all items in a folder except for one.

To delete all items in a folder, we need a way to get all sibling items, and exclude the item we don’t want to delete. I decided to create a custom pipeline to do this, and defined the following parameter object for it:

using System.Collections.Generic;
using System.Linq;

using Sitecore.Data.Items;
using Sitecore.Pipelines;

namespace Sitecore.Sandbox.Pipelines.GetSiblings
{
    public class GetSiblingsArgs : PipelineArgs
    {
        public Item Item { get; set; }

        public IEnumerable<Item> Siblings { get; set; } 
    }
}

Now that we have the parameter object defined, we need a class with methods that will compose our custom pipeline for grabbing sibling items in Sitecore. The following class does the trick:

using System.Linq;

using Sitecore.Diagnostics;

namespace Sitecore.Sandbox.Pipelines.GetSiblings
{
    public class GetSiblingsOperations
    {
        public void EnsureItem(GetSiblingsArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            if (args.Item == null)
            {
                args.AbortPipeline();
            }
        }

        public void GetSiblings(GetSiblingsArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.Item, "args.Item");
            args.Siblings = (from sibling in args.Item.Parent.GetChildren()
                             where sibling.ID != args.Item.ID
                             select sibling).ToList();
        }
    }
}

The EnsureItem() method above just makes sure the item instance passed to it isn’t null, and aborts the pipeline if it is.

The GetSiblings() method gets all siblings items of the item — it just grabs all children of its parent, and excludes the item in question from the resulting collection using LINQ.

Now that we have a way to get sibling items, we need a way to delete them. I decided to build another custom pipeline to get sibling items for an item — by leveraging the pipeline created above — and delete them, and created the following parameter object for it:

using System.Collections.Generic;

using Sitecore.Data.Items;
using Sitecore.Web.UI.Sheer;

namespace Sitecore.Sandbox.Shell.Framework.Pipelines.DeleteSiblings
{
    public class DeleteSiblingsArgs : ClientPipelineArgs
    {
        public Item Item { get; set; }

        public bool ShouldDelete { get; set; }

        public IEnumerable<Item> Siblings { get; set; } 
    }
}

The following class contains methods that will be used it our custom client pipeline to delete sibling items:

using System;
using System.Collections.Generic;

using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Pipelines;
using Sitecore.Web.UI.Sheer;

using Sitecore.Sandbox.Pipelines.GetSiblings;

namespace Sitecore.Sandbox.Shell.Framework.Pipelines.DeleteSiblings
{
    public class DeleteSiblingsOperations
    {
        private string DeleteConfirmationMessage { get; set; }
        private string DeleteConfirmationWindowWidth { get; set; }
        private string DeleteConfirmationWindowHeight { get; set; }

        public void ConfirmDeleteAction(DeleteSiblingsArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNullOrEmpty(DeleteConfirmationMessage, "DeleteConfirmationMessage");
            Assert.ArgumentNotNullOrEmpty(DeleteConfirmationWindowWidth, "DeleteConfirmationWindowWidth");
            Assert.ArgumentNotNullOrEmpty(DeleteConfirmationWindowHeight, "DeleteConfirmationWindowHeight");
            if (!args.IsPostBack)
            {
                SheerResponse.YesNoCancel(DeleteConfirmationMessage, DeleteConfirmationWindowWidth, DeleteConfirmationWindowHeight);
                args.WaitForPostBack();
            }
            else if (args.HasResult)
            {
                args.ShouldDelete = AreEqualIgnoreCase(args.Result, "yes");
                args.IsPostBack = false;
            }
        }

        public void GetSiblingsIfConfirmed(DeleteSiblingsArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            if (!args.ShouldDelete)
            {
                args.AbortPipeline();
                return;
            }
            
            args.Siblings = GetSiblings(args.Item);
        }

        protected virtual IEnumerable<Item> GetSiblings(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            GetSiblingsArgs getSiblingsArgs = new GetSiblingsArgs { Item = item };
            CorePipeline.Run("getSiblings", getSiblingsArgs);
            return getSiblingsArgs.Siblings;
        }

        public void DeleteSiblings(DeleteSiblingsArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.Siblings, "args.Siblings");
            DeleteItems(args.Siblings);
        }

        protected virtual void DeleteItems(IEnumerable<Item> items)
        {
            Assert.ArgumentNotNull(items, "items");
            foreach (Item item in items)
            {
                item.Recycle();
            }
        }

        private static bool AreEqualIgnoreCase(string one, string two)
        {
            return string.Equals(one, two, StringComparison.CurrentCultureIgnoreCase);
        }
    }
}

The ConfirmDeleteAction() method — which is invoked first in the custom client pipeline — asks the user if he/she would like to delete all sibling items for the item in question.

The GetSiblingsIfConfirmed() method is then processed next in the pipeline sequence, and ascertains whether the user had clicked the ‘Yes’ button — this is a button in the YesNoCancel dialog that was presented to the user via the ConfirmDeleteAction() method.

If the user had clicked the ‘Yes’ button, sibling items are grabbed from Sitecore — this is done using the custom pipeline built above — and is set on Siblings property of the DeleteSiblingsArgs instance.

The DeleteSiblings() method is then processed next, and basically does as named: it deletes all items in the Siblings property of the DeleteSiblingsArgs instance.

Now that we are armed with our pipelines above, we need a way to call them from the Sitecore UI. The following command was built for that purpose:

using System.Collections.Generic;
using System.Linq;

using Sitecore;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Pipelines;
using Sitecore.Shell.Framework.Commands;

using Sitecore.Sandbox.Pipelines.GetSiblings;
using Sitecore.Sandbox.Shell.Framework.Pipelines.DeleteSiblings;

namespace Sitecore.Sandbox.Shell.Framework.Commands
{
    public class DeleteAllButThis : Command
    {
        public override void Execute(CommandContext context)
        {
            Assert.ArgumentNotNull(context, "context");
            DeleteSiblings(context);
        }

        private static void DeleteSiblings(CommandContext context)
        {
            Context.ClientPage.Start("uiDeleteSiblings", new DeleteSiblingsArgs { Item = GetItem(context) });
        }

        public override CommandState QueryState(CommandContext context)
        {
            Assert.ArgumentNotNull(context, "context");
            if (!HasSiblings(GetItem(context)))
            {
                return CommandState.Hidden;
            }

            return CommandState.Enabled;
        }

        private static bool HasSiblings(Item item)
        {
            return GetSiblings(item).Any();
        }

        private static IEnumerable<Item> GetSiblings(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            GetSiblingsArgs getSiblingsArgs = new GetSiblingsArgs { Item = item };
            CorePipeline.Run("getSiblings", getSiblingsArgs);
            return getSiblingsArgs.Siblings;
        }

        private static Item GetItem(CommandContext context)
        {
            Assert.ArgumentNotNull(context, "context");
            Assert.ArgumentNotNull(context.Items, "context.Items");
            return context.Items.FirstOrDefault();
        }
    }
}

The command above will only display if the context item has siblings — we invoke the pipeline defined towards the beginning of this post to get sibling items. If none are returned, the command is hidden.

When the command executes, we basically just pass the context item to our custom pipeline that deletes sibling items, and sit back and wait for them to be deleted (I just lean back, and put my feet up on my desk).

I then strung everything together using the following configuration include file:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <commands>
      <command name="item:DeleteAllButThis" type="Sitecore.Sandbox.Shell.Framework.Commands.DeleteAllButThis, Sitecore.Sandbox"/>
    </commands>
    <pipelines>
      <getSiblings>
        <processor type="Sitecore.Sandbox.Pipelines.GetSiblings.GetSiblingsOperations, Sitecore.Sandbox" method="EnsureItem" />
        <processor type="Sitecore.Sandbox.Pipelines.GetSiblings.GetSiblingsOperations, Sitecore.Sandbox" method="GetSiblings" />
      </getSiblings>
    </pipelines>
    <processors>
      <uiDeleteSiblings>
        <processor type="Sitecore.Sandbox.Shell.Framework.Pipelines.DeleteSiblings.DeleteSiblingsOperations, Sitecore.Sandbox" method="ConfirmDeleteAction">
          <DeleteConfirmationMessage>Are you sure you want to delete all sibling items and their descendants?</DeleteConfirmationMessage>
          <DeleteConfirmationWindowWidth>200</DeleteConfirmationWindowWidth>
          <DeleteConfirmationWindowHeight>200</DeleteConfirmationWindowHeight>
        </processor>
        <processor type="Sitecore.Sandbox.Shell.Framework.Pipelines.DeleteSiblings.DeleteSiblingsOperations, Sitecore.Sandbox" method="GetSiblingsIfConfirmed"/>
        <processor type="Sitecore.Sandbox.Shell.Framework.Pipelines.DeleteSiblings.DeleteSiblingsOperations, Sitecore.Sandbox" method="DeleteSiblings"/>
	    </uiDeleteSiblings>
    </processors>
  </sitecore>
</configuration>

I also had to wire the command above to the Sitecore UI by defining a context menu item in the core database. I’ve omitted how I’ve done this. If you would like to learn how to do this, check out my first and second posts on adding to the item context menu.

Let’s see this in action.

I first created some items to delete, and one item that I don’t want to delete:

stuff-to-delete

I then right-clicked on the item I don’t want to delete, and was presented with the new item context menu option:

delete-all-but-this-context-menu

I was then prompted with a confirmation dialog:

delete-all-but-this-confirmation-dialog

I clicked ‘Yes’, and then saw that all sibling items were deleted:

items-deleted

If you have any suggestions on making this better, please drop a comment.

Until next time, have a Sitecoretastic day!

Restart the Sitecore Client and Server Using Custom Pipelines

Last week Michael West asked me about creating shortcuts to restart the Sitecore client and server via this tweet, and I was immediately curious myself on what was needed to accomplish this.

If you are unfamiliar with restarting the Sitecore client and server, these are options that are presented to Sitecore users — typically developers — after installing a package into Sitecore:

install-package-end-wizard

Until last week, I never understood how either of these checkboxes worked, and uncovered the following code during my research:

restart-code-in-InstallPackageForm

Michael West had conveyed how he wished these two lines of code lived in pipelines, and that prompted me to write this article — basically encapsulate the logic above into two custom pipelines: one to restart the Sitecore client, and the other to restart the Sitecore server.

I decided to define the concept of a ‘Restarter’, an object that restarts something — this could be anything — and defined the following interface for such objects:

namespace Sitecore.Sandbox.Utilities.Restarters
{
    public interface IRestarter
    {
        void Restart();
    }
}

I then created the following IRestarter for the Sitecore client:

using Sitecore;
using Sitecore.Diagnostics;
using Sitecore.Web.UI.Sheer;

namespace Sitecore.Sandbox.Utilities.Restarters
{
    public class SitecoreClientRestarter : IRestarter
    {
        private ClientResponse ClientResponse { get; set; }

        public SitecoreClientRestarter()
            : this(Context.ClientPage)
        {
        }

        public SitecoreClientRestarter(ClientPage clientPage)
        {
            SetClientResponse(clientPage);
        }

        public SitecoreClientRestarter(ClientResponse clientResponse)
        {
            SetClientResponse(clientResponse);
        }

        private void SetClientResponse(ClientPage clientPage)
        {
            Assert.ArgumentNotNull(clientPage, "clientPage");
            SetClientResponse(clientPage.ClientResponse);
        }

        private void SetClientResponse(ClientResponse clientResponse)
        {
            Assert.ArgumentNotNull(clientResponse, "clientResponse");
            ClientResponse = clientResponse;
        }

        public void Restart()
        {
            ClientResponse.Broadcast(ClientResponse.SetLocation(string.Empty), "Shell");
        }
    }
}

The class above has three constructors. One constructor takes an instance of Sitecore.Web.UI.Sheer.ClientResponse — this lives in Sitecore.Kernel.dll — and another constructor takes in an instance of
Sitecore.Web.UI.Sheer.ClientPage — this also lives in Sitecore.Kernel.dll — which contains a property instance of Sitecore.Web.UI.Sheer.ClientResponse, and this instance is set on the ClientResponse property of the SitecoreClientRestarter class.

The third constructor — which is parameterless — calls the constructor that takes in a Sitecore.Web.UI.Sheer.ClientPage instance, and passes the ClientResponse instance set in Sitecore.Context.

I followed building the above class with another IRestarter — one that restarts the Sitecore server:

using Sitecore.Install;

namespace Sitecore.Sandbox.Utilities.Restarters
{
    public class SitecoreServerRestarter : IRestarter
    {
        public SitecoreServerRestarter()
        {
        }

        public void Restart()
        {
            Installer.RestartServer();
        }
    }
}

There really isn’t much happening in the class above. It just calls the static method RestartServer() — this method changes the timestamp on the Sitecore instance’s Web.config to trigger a web application restart — on Sitecore.Install.Installer in Sitecore.Kernel.dll.

Now we need to a way to use the IRestarter classes above. I built the following class to serve as a processor of custom pipelines I define later on in this post:

using Sitecore.Diagnostics;
using Sitecore.Pipelines;

using Sitecore.Sandbox.Utilities.Restarters;

namespace Sitecore.Sandbox.Pipelines.RestartRestarter
{
    public class RestartRestarterOperations
    {
        private IRestarter Restarter { get; set; }

        public void Process(PipelineArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(Restarter, "Restarter");
            Restarter.Restart();
        }
    }
}

Through a pipeline processor configuration setting, we define the type of IRestarter — it’s magically created by Sitecore when the pipeline processor instance is created.

After some null checks, the Process() method invokes Restart() on the IRestarter instance, ultimately restarting whatever the IRestarter is set to restart.

I then needed a way to test the pipelines I define later on in this post. I built the following class to serve as commands that I added into the Sitecore ribbon:

using Sitecore.Diagnostics;
using Sitecore.Pipelines;
using Sitecore.Shell.Framework.Commands;

namespace Sitecore.Sandbox.Commands.Admin
{
    public class Restart : Command
    {
        public override void Execute(CommandContext context)
        {
            Assert.ArgumentNotNull(context, "context");
            Assert.ArgumentNotNull(context.Parameters, "context.Parameters");
            Assert.ArgumentNotNullOrEmpty(context.Parameters["pipeline"], "context.Parameters[\"pipeline\"]");
            CorePipeline.Run(context.Parameters["pipeline"], new PipelineArgs());
        }
    }
}

The command above expects a pipeline name to be supplied via the Parameters NameValueCollection instance set on the CommandContext instance passed to the Execute method() — I show later on in this post how I pass the name of the pipeline to the command.

If the pipeline name is given, we invoke the pipeline.

I then glued everything together using the following configuration file:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <commands>
      <command name="admin:Restart" type="Sitecore.Sandbox.Commands.Admin.Restart, Sitecore.Sandbox"/>
    </commands>
    <pipelines>
      <restartClient>
        <processor type="Sitecore.Sandbox.Pipelines.RestartRestarter.RestartRestarterOperations, Sitecore.Sandbox">
          <Restarter type="Sitecore.Sandbox.Utilities.Restarters.SitecoreClientRestarter, Sitecore.Sandbox"/>
        </processor>  
      </restartClient>
      <restartServer>
        <processor type="Sitecore.Sandbox.Pipelines.RestartRestarter.RestartRestarterOperations, Sitecore.Sandbox">
          <Restarter type="Sitecore.Sandbox.Utilities.Restarters.SitecoreServerRestarter, Sitecore.Sandbox"/>
        </processor>
      </restartServer>
    </pipelines>
  </sitecore>
</configuration>

I’m omitting how I created custom buttons in a custom ribbon in Sitecore to test this. If you want to learn about adding buttons to the Sitecore Ribbon, please read John West’s blog post on doing so.

However, I did do the following to pass the name of the pipeline we want to invoke in the custom command class defined above:

restart-client-button

I wish I could show you some screenshots on how this works. However, there really isn’t much visual to see here.

If you have any suggestions on how I could show this in action, or improve the code above, please share in a comment.

Perform a Virus Scan on Files Uploaded into Sitecore

Last week I took notice of a SDN forum post asking whether there are any best practices for checking files uploaded into Sitecore for viruses.

Although I am unaware of there being any best practices for this, adding a processor into the <uiUpload> pipeline to perform virus scans has been on my plate for the past month. Not being able to find an open source .NET antivirus library has been my major blocker for building one.

However, after many hours of searching the web — yes, I do admit some of this time is sprinkled with idle surfing — over multiple weeks, I finally discovered nClam — a .NET client library for scanning files using a Clam Antivirus server (I setup one using instructions enumerated here).

Before I continue, I would like to caution you on using this or any antivirus solution before doing a substantial amount of research — I do not know how robust the solution I am sharing actually is, and I am by no means an antivirus expert. The purpose of this post is to show how one might go about adding antivirus capabilities into Sitecore, and I am not advocating or recommending any particular antivirus software package. Please consult with an antivirus expert before using any antivirus software/.NET client library .

The solution I came up with uses the adapter design pattern — it basically wraps and makes calls to the nClam library, and is accessible through the following antivirus scanner interface:

using System.IO;

namespace Sitecore.Sandbox.Security.Antivirus
{
    public interface IScanner
    {
        ScanResult Scan(Stream sourceStream);

        ScanResult Scan(string filePath);
    }
}

This interface defines the bare minimum methods our antivirus classes should have. Instances of these classes should offer the ability to scan a file through the file’s Stream, or scan a file located on the server via the supplied file path.

The two scan methods defined by the interface must return an instance of the following data transfer object:

namespace Sitecore.Sandbox.Security.Antivirus
{
    public class ScanResult
    {
        public string Message { get; set; }

        public ScanResultType ResultType { get; set; }
    }
}

Instances of the above class contain a detailed message coupled with a ScanResultType enumeration value which conveys whether the scanned file is clean, contains a virus, or something else went wrong during the scanning process:

namespace Sitecore.Sandbox.Security.Antivirus
{
    public enum ScanResultType
    {
        Clean,
        VirusDetected,
        Error,
        Unknown
    }
}

I used the ClamScanResults enumeration as a model for the above.

I created and used the ScanResultType enumeration instead of the ClamScanResults enumeration so that this solution can be extended for other antivirus libraries — or calls could be made to other antivirus software through the command-line — and these shouldn’t be tightly coupled to the nClam library.

I then wrapped the nClam library calls in the following ClamScanner class:

using System;
using System.IO;
using System.Linq;

using Sitecore.Diagnostics;

using nClam;

namespace Sitecore.Sandbox.Security.Antivirus
{
    public class ClamScanner : IScanner
    {
        private ClamClient Client { get; set; }

        public ClamScanner(string server, string port)
            : this(server, int.Parse(port))
        {
        }

        public ClamScanner(string server, int port)
            : this(new ClamClient(server, port))
        {
        }

        public ClamScanner(ClamClient client)
        {
            SetClient(client);
        }

        private void SetClient(ClamClient client)
        {
            Assert.ArgumentNotNull(client, "client");
            Client = client;
        }

        public ScanResult Scan(Stream sourceStream)
        {
            ScanResult result;
            try
            {
                result = CreateNewScanResult(Client.SendAndScanFile(sourceStream));
            }
            catch (Exception ex)
            {
                result = CreateNewExceptionScanResult(ex);
            }

            return result;
        }

        public ScanResult Scan(string filePath)
        {
            ScanResult result;
            try
            {
                result = CreateNewScanResult(Client.SendAndScanFile(filePath));
            }
            catch (Exception ex)
            {
                result = CreateNewExceptionScanResult(ex);
            }

            return result;
        }

        private static ScanResult CreateNewScanResult(ClamScanResult result)
        {
            Assert.ArgumentNotNull(result, "result");
            if (result.Result == ClamScanResults.Clean)
            {
                return CreateNewScanResult("Yay! No Virus found!", ScanResultType.Clean);
            }

            if (result.Result == ClamScanResults.VirusDetected)
            {
                string message = string.Format("Oh no! The {0} virus was found!", result.InfectedFiles.First().VirusName);
                return CreateNewScanResult(message, ScanResultType.VirusDetected);
            }

            if (result.Result == ClamScanResults.Error)
            {
                string message = string.Format("Something went terribly wrong somewhere. Details: {0}", result.RawResult);
                return CreateNewScanResult(message, ScanResultType.Error);
            }

            return CreateNewScanResult("I have no clue about what just happened.", ScanResultType.Unknown);
        }

        private static ScanResult CreateNewExceptionScanResult(Exception ex)
        {
            string message = string.Format("Something went terribly wrong somewhere. Details:\n{0}", ex.ToString());
            return CreateNewScanResult(ex.ToString(), ScanResultType.Error);
        }

        private static ScanResult CreateNewScanResult(string message, ScanResultType type)
        {
            return new ScanResult
            {
                Message = message,
                ResultType = type
            };
        }
    }
}

Methods in the above class make calls to the nClam library, and return a ScanResult intance containing detailed information about the file scan.

Next I developed the following <uiUpload> pipeline processor to use instances of classes that define the IScanner interface above, and these instances are set via Sitecore when instantiating this pipeline processor — the ClamScanner type is defined in the patch configuration file shown later in this post:

using System.IO;
using System.Web;

using Sitecore.Diagnostics;
using Sitecore.Globalization;
using Sitecore.Pipelines.Upload;

using Sitecore.Sandbox.Security.Antivirus;

namespace Sitecore.Sandbox.Pipelines.Upload
{
    public class ScanForViruses
    {
        public void Process(UploadArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            foreach (string fileKey in args.Files)
            {
                HttpPostedFile file = args.Files[fileKey];
                ScanResult result = ScanFile(file.InputStream);
                if(result.ResultType != ScanResultType.Clean)
                {
                    args.ErrorText = Translate.Text(string.Format("The file \"{0}\" cannot be uploaded. Reason: {1}", file.FileName, result.Message));
                    Log.Warn(args.ErrorText, this);
                    args.AbortPipeline();
                }
            }
        }

        private ScanResult ScanFile(Stream fileStream)
        {
            Assert.ArgumentNotNull(fileStream, "fileStream");
            return Scanner.Scan(fileStream);
        }

        private IScanner Scanner { get; set; }
    }
}

Uploaded files are passed to the IScanner instance, and the pipeline is aborted if something isn’t quite right — this occurs when a virus is detected, or an error is reported by the IScanner instance. If a virus is discovered, or an error occurs, the message contained within the ScanResult instance is captured in the Sitecore log.

I then glued everything together using the following patch configuration file:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <processors>
      <uiUpload>
        <processor mode="on" patch:before="processor[@type='Sitecore.Pipelines.Upload.CheckSize, Sitecore.Kernel']" 
                   type="Sitecore.Sandbox.Pipelines.Upload.ScanForViruses, Sitecore.Sandbox">
          <Scanner type="Sitecore.Sandbox.Security.Antivirus.ClamScanner">
            <param desc="server">localhost</param>
            <param desc="port">3310</param>
          </Scanner>
        </processor>
      </uiUpload>
    </processors>
  </sitecore>
</configuration>

I wish I could show you this in action when a virus is discovered in an uploaded file. However, I cannot put my system at risk by testing with an infected file.

But, I can show you what happens when an error is detected. I will do this by shutting down the Clam Antivirus server on my machine:

scanner-turned-off

On upload, I see the following:

tried-to-upload-file

When I opened up the Sitecore log, I see what the problem is:

log-error

Let’s turn it back on:

scanner-turned-on

I can now upload files again:

upload-no-error

If you have any suggestions on making this better, or know of a way to test it with a virus, please share in a comment below.

Choose Template Fields to Display in the Sitecore Content Editor

The other day I was going through search terms people had used to get to my blog, and discovered a few people made their way to my blog by searching for ‘sitecore hide sections in data template’.

I had built something like this in the past, but no longer remember how I had implemented that particular solution — not that I could show you that solution since it’s owned by a previous employer — and decided I would build another solution to accomplish this.

Before I move forward, I would like to point out that Sitecore MVP Andy Uzick wrote a blog post recently showing how to hide fields and sections in the content editor, though I did not have much luck with hiding sections in the way that he had done it — sections with no fields were still displaying for me in the content editor — and I decided to build a different solution altogether to make this work.

I thought it would be a good idea to let users turn this functionality on and off via a checkbox in the ribbon, and used some code from a previous post — in this post I had build an object to keep track of the state of a checkbox in the ribbon — as a model. In the spirit of that object, I defined the following interface:

namespace Sitecore.Sandbox.Utilities.ClientSettings
{
    public interface IRegistrySettingToggle
    {
        bool IsOn();

        void TurnOn();

        void TurnOff();
    }
}

I then created the following abstract class which implements the interface above, and stores the state of the setting defined by the given key — the key of the setting and the “on” value are passed to it via a subclass — in the Sitecore registry.

using Sitecore.Diagnostics;

using Sitecore.Web.UI.HtmlControls;

namespace Sitecore.Sandbox.Utilities.ClientSettings
{
    public abstract class RegistrySettingToggle : IRegistrySettingToggle
    {
        private string RegistrySettingKey { get; set; }

        private string RegistrySettingOnValue { get; set; }

        protected RegistrySettingToggle(string registrySettingKey, string registrySettingOnValue)
        {
            SetRegistrySettingKey(registrySettingKey);
            SetRegistrySettingOnValue(registrySettingOnValue);
        }

        private void SetRegistrySettingKey(string registrySettingKey)
        {
            Assert.ArgumentNotNullOrEmpty(registrySettingKey, "registrySettingKey");
            RegistrySettingKey = registrySettingKey;
        }

        private void SetRegistrySettingOnValue(string registrySettingOnValue)
        {
            Assert.ArgumentNotNullOrEmpty(registrySettingOnValue, "registrySettingOnValue");
            RegistrySettingOnValue = registrySettingOnValue;
        }

        public bool IsOn()
        {
            return Registry.GetString(RegistrySettingKey) == RegistrySettingOnValue;
        }

        public void TurnOn()
        {
            Registry.SetString(RegistrySettingKey, RegistrySettingOnValue);
        }

        public void TurnOff()
        {
            Registry.SetString(RegistrySettingKey, string.Empty);
        }
    }
}

I then built the following class to toggle the display settings for our displayable fields:

using System;

namespace Sitecore.Sandbox.Utilities.ClientSettings
{
    public class ShowDisplayableFieldsOnly : RegistrySettingToggle
    {
        private const string RegistrySettingKey = "/Current_User/Content Editor/Show Displayable Fields Only";
        private const string RegistrySettingOnValue = "on";

        private static volatile IRegistrySettingToggle current;
        private static object lockObject = new Object();

        public static IRegistrySettingToggle Current
        {
            get
            {
                if (current == null)
                {
                    lock (lockObject)
                    {
                        if (current == null)
                        {
                            current = new ShowDisplayableFieldsOnly();
                        }
                    }
                }

                return current;
            }
        }

        private ShowDisplayableFieldsOnly()
            : base(RegistrySettingKey, RegistrySettingOnValue)
        {
        }
    }
}

It passes its Sitecore registry key and “on” state value to the RegistrySettingToggle base class, and employs the Singleton pattern — I saw no reason for there to be multiple instances of this object floating around.

In order to use a checkbox in the ribbon, we have to create a new command for it:

using System.Linq;

using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Shell.Framework.Commands;

using Sitecore.Sandbox.Utilities.ClientSettings;

namespace Sitecore.Sandbox.Commands
{
    public class ToggleDisplayableFieldsVisibility : Command
    {
        public override void Execute(CommandContext context)
        {
            Assert.ArgumentNotNull(context, "context");
            ToggleDisplayableFields();
            Refresh(context);
        }

        private static void ToggleDisplayableFields()
        {
            IRegistrySettingToggle showDisplayableFieldsOnly = ShowDisplayableFieldsOnly.Current;
            if (!showDisplayableFieldsOnly.IsOn())
            {
                showDisplayableFieldsOnly.TurnOn();
            }
            else
            {
                showDisplayableFieldsOnly.TurnOff();
            }
        }

        public override CommandState QueryState(CommandContext context)
        {
            if (!ShowDisplayableFieldsOnly.Current.IsOn())
            {
                return CommandState.Enabled;
            }

            return CommandState.Down;
        }

        private static void Refresh(CommandContext context)
        {
            Refresh(GetItem(context));
        }

        private static void Refresh(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            Context.ClientPage.ClientResponse.Timer(string.Format("item:load(id={0})", item.ID), 1);
        }

        private static Item GetItem(CommandContext context)
        {
            Assert.ArgumentNotNull(context, "context");
            return context.Items.FirstOrDefault();
        }
    }
}

The command above leverages the instance of the ShowDisplayableFieldsOnly class defined above to turn the displayable fields feature on and off.

I followed the creation of the command above with the definition of the ribbon checkbox in the core database:

show-displayable-fields-checkbox-defined

The command name above — which is set in the Click field — is defined in the patch configuration file towards the end of this post.

I then created the following data template with a TreelistEx field to store the displayable fields:

displayable-fields-template

The TreelistEx field above will pull in all sections and their fields into the TreelistEx dialog, but only allow the selection of template fields, as is dictated by the following parameters that I have mapped in its Source field:

DataSource=/sitecore/templates/Sample/Sample Item&IncludeTemplatesForSelection=Template field&IncludeTemplatesForDisplay=Template section,Template field&AllowMultipleSelection=no

I then set this as a base template in my sandbox solution’s Sample Item template:

set-displayable-fields-template-as-base

In order to remove fields, we need a <getContentEditorFields> pipeline processor. I built the following class for to serve as one:

using System;

using Sitecore.Configuration;
using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using Sitecore.Data.Managers;
using Sitecore.Data.Templates;
using Sitecore.Diagnostics;
using Sitecore.Shell.Applications.ContentManager;
using Sitecore.Shell.Applications.ContentEditor.Pipelines.GetContentEditorFields;

using Sitecore.Sandbox.Utilities.ClientSettings;

namespace Sitecore.Sandbox.Shell.Applications.ContentEditor.Pipelines.GetContentEditorFields
{
    public class RemoveUndisplayableFields
    {
        public void Process(GetContentEditorFieldsArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.Item, "args.Item");
            Assert.ArgumentCondition(!string.IsNullOrWhiteSpace(DisplayableFieldsFieldName), "DisplayableFieldsFieldName", "DisplayableFieldsFieldName must be set in the configuration file!");
            if (!ShowDisplayableFieldsOnly.Current.IsOn())
            {
                return;
            }

            foreach (Editor.Section section in args.Sections)
            {
                AddDisplayableFields(args.Item[DisplayableFieldsFieldName], section);
            }
        }

        private void AddDisplayableFields(string displayableFieldIds, Editor.Section section)
        {
            Editor.Fields displayableFields = new Editor.Fields();
            foreach (Editor.Field field in section.Fields)
            {
                if (IsDisplayableField(displayableFieldIds, field))
                {
                    displayableFields.Add(field);
                }
            }

            section.Fields.Clear();
            section.Fields.AddRange(displayableFields);
        }

        private bool IsDisplayableField(string displayableFieldIds, Editor.Field field)
        {
            if (IsStandardValues(field.ItemField.Item))
            {
                return true;
            }

            if (IsDisplayableFieldsField(field.ItemField))
            {
                return false;
            }

            return IsStandardTemplateField(field.ItemField)
                    || string.IsNullOrWhiteSpace(displayableFieldIds)
                    || displayableFieldIds.Contains(field.ItemField.ID.ToString());
        }
        
        private bool IsDisplayableFieldsField(Field field)
        {
            return string.Equals(field.Name, DisplayableFieldsFieldName, StringComparison.CurrentCultureIgnoreCase);
        }

        private static bool IsStandardValues(Item item)
        {
            if (item.Template.StandardValues != null)
            {
                return item.Template.StandardValues.ID == item.ID;
            }

            return false;
        }

        private bool IsStandardTemplateField(Field field)
        {
            Assert.IsNotNull(StandardTemplate, "The Stardard Template could not be found.");
            return StandardTemplate.ContainsField(field.ID);
        }

        private static Template GetStandardTemplate()
        {
            return TemplateManager.GetTemplate(Settings.DefaultBaseTemplate, Context.ContentDatabase);
        }

        private Template _StandardTemplate;
        private Template StandardTemplate
        {
            get
            {
                if (_StandardTemplate == null)
                {
                    _StandardTemplate = GetStandardTemplate();
                }

                return _StandardTemplate;
            }
        }

        private string DisplayableFieldsFieldName { get; set; }
    }
}

The class above iterates over all fields for the supplied item, and adds only those that were selected in the Displayable Fields TreelistEx field, and also Standard Fields — we don’t want to remove these since they are shown/hidden by the Standard Fields feature in Sitecore — to a new Editor.Fields collection. This new collection is then set on the GetContentEditorFieldsArgs instance.

Plus, we don’t want to show the Displayable Fields TreelistEx field when the feature is turned on, and we are on an item. This field should only display when we are on the standard values item when the feature is turned on — this is how we will choose our displayable fields.

Now we have to handle sections without fields — especially after ripping them out via the pipeline processor above.

I built the following class to serve as a <renderContentEditor> pipeline processor to do this:

using System.Linq;

using Sitecore.Diagnostics;
using Sitecore.Shell.Applications.ContentManager;
using Sitecore.Shell.Applications.ContentEditor.Pipelines.RenderContentEditor;

namespace Sitecore.Sandbox.Shell.Applications.ContentEditor.Pipelines.RenderContentEditor
{
    public class FilterSectionsWithFields
    {
        public void Process(RenderContentEditorArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            args.Sections = GetSectionsWithFields(args.Sections);
        }

        private static Editor.Sections GetSectionsWithFields(Editor.Sections sections)
        {
            Assert.ArgumentNotNull(sections, "sections");
            Editor.Sections sectionsWithFields = new Editor.Sections();
            foreach (Editor.Section section in sections)
            {
                AddIfContainsFields(sectionsWithFields, section);
            }

            return sectionsWithFields;
        }

        private static void AddIfContainsFields(Editor.Sections sections, Editor.Section section)
        {
            Assert.ArgumentNotNull(sections, "sections");
            Assert.ArgumentNotNull(section, "section");
            if (!ContainsFields(section))
            {
                return;
            }

            sections.Add(section);
        }

        private static bool ContainsFields(Editor.Section section)
        {
            Assert.ArgumentNotNull(section, "section");
            return section.Fields != null && section.Fields.Any();
        }
    }
}

It basically builds a new collection of sections that contain at least one field, and sets it on the RenderContentEditorArgs instance being passed through the <renderContentEditor> pipeline.

In order for this to work, we must make this pipeline processor run before all other processors.

I tied all of the above together with the following patch configuration file:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <commands>
      <command name="contenteditor:ToggleDisplayableFieldsVisibility" type="Sitecore.Sandbox.Commands.ToggleDisplayableFieldsVisibility, Sitecore.Sandbox"/>
    </commands>
    <pipelines>
      <getContentEditorFields>
        <processor type="Sitecore.Sandbox.Shell.Applications.ContentEditor.Pipelines.GetContentEditorFields.RemoveUndisplayableFields, Sitecore.Sandbox">
          <DisplayableFieldsFieldName>Displayable Fields</DisplayableFieldsFieldName>
        </processor>  
      </getContentEditorFields>
      <renderContentEditor>
        <processor patch:before="processor[@type='Sitecore.Shell.Applications.ContentEditor.Pipelines.RenderContentEditor.RenderSkinedContentEditor, Sitecore.Client']"
                   type="Sitecore.Sandbox.Shell.Applications.ContentEditor.Pipelines.RenderContentEditor.FilterSectionsWithFields, Sitecore.Sandbox"/>
      </renderContentEditor>
    </pipelines>
  </sitecore>
</configuration>

Let’s try this out.

I navigated to the standard values item for my Sample Item data template, and selected the fields I want to display in the content editor:

selected-some-fields

I then went to an item that uses this data template, and turned on the displayable fields feature:

displayable-fields-on

As you can see, only the fields we had chosen display — along with standard fields since the Standard Fields checkbox is checked.

I then turned off the displayable fields feature:

displayable-fields-off

Now all fields for the item display.

I then turned the displayable fields feature back on, and turned off Standard Fields:

standard-fields-off-displayable-fields-on

Now only our selected fields display.

If you have any thoughts on this, or ideas around making this better, please share in a comment.