Home » Search results for 'Sitecore Configuration Factory' (Page 2)

Search Results for: Sitecore Configuration Factory

Advertisements

Prevent Duplicate Names of Bucketed Sitecore Items

In my previous post, I gave a solution that removes names of Bucket Folder Items from URLs in Sitecore, and also resolves those same URLs when they are called up in a browser.

However, that solution wasn’t complete — code is needed to ensure Bucketed Item names are unique given the solution assumes Bucketed Item names are unique (code in that solution uses the Sitecore.ContentSearch API to find an Item by name within an Item Bucket, and if there are two or more Items with the same name, only one will be returned — this will prevent the resolution of URLs for those other Bucketed page Items).

I decided to take up the challenge on continuing that solution over this previous weekend, and share what I built.

challenge

One thing to note: the solution that follows is not a complete solution for preventing duplicate Bucketed Item names as such a post could go on for ages — actually, I probably would still be writing the code for it. I leave the rest for you guys to do as a homework assignment. πŸ˜‰

Cat-ate-my-homework-GIF

I first defined the following interface to centralize common methods I have been using in my Sitecore Item Buckets code (I will be reusing this same interface and its implementation in future blog posts):

using Sitecore.Data.Items;

namespace Sitecore.Sandbox.Buckets.Util.Methods
{
    public interface IItemBucketsFeatureMethods
    {
        bool IsItemBucketFolder(Item item);
        
        bool IsItemContainedWithinBucket(Item item);
        
        bool IsItemBucketable(Item item);
        
        Item GetItemBucket(Item item);

        bool IsItemBucket(Item item);

        bool HasBucketedItemWithName(Item itemBucket, string itemName);
    }
}

The following class implements the interface above:

using Sitecore.Buckets.Extensions;
using Sitecore.Buckets.Managers;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;

using Sitecore.Sandbox.Buckets.Providers.Items;

namespace Sitecore.Sandbox.Buckets.Util.Methods
{
    public class ItemBucketsFeatureMethods : IItemBucketsFeatureMethods
    {
        private IFindBucketedItemProvider FindBucketedItemProvider { get; set; }

        public virtual bool IsItemBucketFolder(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            return item.IsABucketFolder();
        }

        public virtual bool IsItemContainedWithinBucket(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            return BucketManager.IsItemContainedWithinBucket(item);
        }

        public virtual bool IsItemBucketable(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            return item.IsItemBucketable();
        }

        public virtual Item GetItemBucket(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            Item ancestor = item.GetParentBucketItemOrParent();
            if (!IsItemBucket(ancestor))
            {
                return null;
            }

            return ancestor;
        }

        public virtual bool IsItemBucket(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            return item.IsABucket();
        }

        public virtual bool HasBucketedItemWithName(Item itemBucket, string itemName)
        {
            EnsureFindBucketedItemProvider();
            Assert.ArgumentNotNull(itemBucket, "itemBucket");
            Assert.ArgumentNotNullOrEmpty(itemName, "itemName");
            Item item = FindBucketedItemProvider.FindBucketedItemByName(itemBucket, itemName);
            if(item == null)
            {
                return false;
            }

            return true;
        }

        protected virtual void EnsureFindBucketedItemProvider()
        {
            Assert.IsNotNull(FindBucketedItemProvider, "FindBucketedItemProvider must be set in configuration!");
        }
    }
}

I’m not going to go into details of the code above as it is self-explanatory.

However, I do want to call out that I am reusing the IFindBucketedItemProvider code from my previous post. I advise having a look at that code before moving forward.

I then defined the following class whose Process() method will serve as a processor of the <uiDragItemTo> and
<uiMoveItems> pipelines of the Sitecore Client:

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

using Sitecore.Configuration;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Text;
using Sitecore.Web.UI.Sheer;

using Sitecore.Sandbox.Buckets.Util.Methods;

namespace Sitecore.Sandbox.Buckets.Shell.Framework.Pipelines.MoveItems
{
    public class HandleDuplicateBucketedItemName
    {
        protected string ItemIdsParameterName { get; set; }

        protected IItemBucketsFeatureMethods ItemBucketsFeatureMethods { get; set; }

        protected string RenameItemMessage { get; set; }

        public void Process(ClientPipelineArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            IEnumerable<string> itemIds = GetItemIds(args);
            if (itemIds == null || !itemIds.Any() || itemIds.Count() > 1)
            {
                return;
            }
            
            string targetId = GetTargetId(args);
            if (string.IsNullOrWhiteSpace(targetId))
            {
                return;
            }

            Database database = GetDatabase(args);
            if (database == null)
            {
                return;
            }

            Item targetItem = GetItem(database, targetId);
            if (targetItem == null)
            {
                return;
            }

            Item item = GetItem(database, itemIds.First());
            if (item == null)
            {
                return;
            }

            Item itemBucket = GetItemBucket(targetItem);
            if (itemBucket == null || !HasBucketedItemWithName(itemBucket, item.Name))
            {
                return;
            }

            PromptRenameItem(args, item);
        }

        protected virtual IEnumerable<string> GetItemIds(ClientPipelineArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.Parameters, "args.Parameters");
            string itemIdsParameterName = GetItemIdsParameterName(args);
            Assert.IsNotNullOrEmpty(itemIdsParameterName, "GetItemIdParameterName() cannot return null or the empty string!");
            return new ListString(itemIdsParameterName, '|');
        }

        protected virtual string GetItemIdsParameterName(ClientPipelineArgs args)
        {
            Assert.IsNotNullOrEmpty(ItemIdsParameterName, "ItemIdParameterName must be set in configuration!");
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.Parameters, "args.Parameters");
            return args.Parameters[ItemIdsParameterName];
        }

        protected virtual string GetTargetId(ClientPipelineArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.Parameters, "args.Parameters");
            return args.Parameters["target"];
        }

        protected virtual Database GetDatabase(ClientPipelineArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.Parameters, "args.Parameters");
            return Factory.GetDatabase(args.Parameters["database"]);
        }

        protected virtual Item GetItem(Database database, string itemId)
        {
            Assert.ArgumentNotNull(database, "database");
            Assert.ArgumentNotNullOrEmpty(itemId, "itemId");
            try
            {
                return database.GetItem(itemId);

            }
            catch(Exception ex)
            {
                Log.Error(ToString(), ex, this);
            }

            return null;
        }

        protected virtual Item GetItemBucket(Item item)
        {
            EnsureItemBucketsFeatureMethods();
            Assert.ArgumentNotNull(item, "item");
            if(!ItemBucketsFeatureMethods.IsItemBucket(item))
            {
                return ItemBucketsFeatureMethods.GetItemBucket(item);
            }

            return item;
        }

        protected virtual bool HasBucketedItemWithName(Item itemBucket, string bucketedItemName)
        {
            EnsureItemBucketsFeatureMethods();
            Assert.ArgumentNotNull(itemBucket, "itemBucket");
            Assert.ArgumentNotNullOrEmpty(bucketedItemName, "bucketedItemName");
            return ItemBucketsFeatureMethods.HasBucketedItemWithName(itemBucket, bucketedItemName);
        }

        protected virtual void PromptRenameItem(ClientPipelineArgs args, Item item)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(item, "item");
            if (args.IsPostBack)
            {
                if(!args.HasResult)
                {
                    args.AbortPipeline();
                    return;
                }

                GetProposedValidItemName(args.Result);
                RenameItem(item, GetProposedValidItemName(args.Result));
                ClearResult(args);
            }
            else
            {
                SheerResponse.Input(GetRenameItemMessage(), string.Empty);
                args.WaitForPostBack();    
            }   
        }

        protected virtual void ClearResult(ClientPipelineArgs args)
        {
            args.Result = string.Empty;
            args.IsPostBack = false;
        }

        protected virtual string GetProposedValidItemName(string itemName)
        {
            Assert.ArgumentNotNull(itemName, "itemName");
            return ItemUtil.ProposeValidItemName(itemName);
        }

        protected virtual void RenameItem(Item item, string name)
        {
            Assert.ArgumentNotNull(item, "item");
            Assert.ArgumentNotNullOrEmpty(name, "name");
            using (new EditContext(item, false, true))
            {
                item.Name = name;
            }
        }

        protected virtual string GetRenameItemMessage()
        {
            Assert.ArgumentNotNull(RenameItemMessage, "RenameItemMessage must be set in configuration!");
            return RenameItemMessage;
        }

        protected virtual void EnsureItemBucketsFeatureMethods()
        {
            Assert.IsNotNull(ItemBucketsFeatureMethods, "ItemBucketsFeatureMethods must be set in configuration!");
        }
    }
}

The Process() method above basically gets the Item IDs of the Items that are moving from the Parameters collection on the arguments object passed to it — these are stored under different keys in the Parameters collection by the <uiDragItemTo> and <uiMoveItems> pipelines, so I’m letting the Sitecore Configuration Factory pass in the name of that parameter for each (see the patch configuration file below).

If more than one Item ID is returned, the code exits.

The Process() method then gets the target Item’s ID; the Database instance; the target Item; and the instance of the Item we are moving. If any of these are null, it exits.

The code then attempts to get the Item Bucket for the target Item — this is done via the GetItemBucket() method which just delegates to the GetItemBucket() method on the IItemBucketsFeatureMethods instance, or returns the passed Item if it is an Item Bucket. If the returned Item is null, the code exits.

The Process() method then calls the HasBucketedItemWithName() — this method just makes a call to a method with the same name on the ItemBucketsFeatureMethods instance — to see if there is another Bucketed Item within the Item Bucket with the same name. If one is not found, we prompt the user for a new Item name, and rename the Item if one is supplied. If no Item name is supplied, we abort the pipeline completely to prevent the user from moving forward.

I do want to highlight one more thing. the RenameItem() method uses a Sitecore.Data.Items.EditContext instance when changing the Item name. I decided to use an instance of this class to have this change be silent and not log any update statistics as this was causing the Item to become un-bucketable after it was moved (no clue as to why this would happen).

I then plugged-in all of the code above via the following patch configuration file:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <buckets>
      <methods>
        <itemBucketsFeatureMethods type="Sitecore.Sandbox.Buckets.Util.Methods.ItemBucketsFeatureMethods, Sitecore.Sandbox">
          <FindBucketedItemProvider ref="buckets/providers/items/findBucketedItemProvider" />
        </itemBucketsFeatureMethods>
      </methods>
    </buckets>
    <processors>
      <uiDragItemTo>
        <processor patch:before="processor[@type='Sitecore.Buckets.Pipelines.UI.ItemDrag, Sitecore.Buckets' and @method='Execute']"
                   type="Sitecore.Sandbox.Buckets.Shell.Framework.Pipelines.MoveItems.HandleDuplicateBucketedItemName, Sitecore.Sandbox" mode="on">
          <ItemIdsParameterName>id</ItemIdsParameterName>
          <ItemBucketsFeatureMethods ref="buckets/methods/itemBucketsFeatureMethods" />
          <RenameItemMessage>Duplicate bucketed Item names are not allowed.  Please enter in a new name for the item:</RenameItemMessage>
        </processor>
      </uiDragItemTo>
      <uiMoveItems>
        <processor patch:before="processor[@type='Sitecore.Buckets.Pipelines.UI.ItemMove, Sitecore.Buckets' and @method='Execute']"
                     type="Sitecore.Sandbox.Buckets.Shell.Framework.Pipelines.MoveItems.HandleDuplicateBucketedItemName, Sitecore.Sandbox" mode="on">
          <ItemIdsParameterName>items</ItemIdsParameterName>
          <ItemBucketsFeatureMethods ref="buckets/methods/itemBucketsFeatureMethods" />
          <RenameItemMessage>Duplicate bucketed Item names are not allowed.  Please enter in a new name for the item:</RenameItemMessage>
        </processor>
      </uiMoveItems>
    </processors>
  </sitecore>
</configuration>

plugged-in

Let’s take this for a spin.

Let’s test this out by dragging an Item to an Item Bucket that has a bucketed Item with the same name:

unique-name-move-1

Of course, I do:

unique-name-move-2

I gave it a unique name:

unique-name-move-3

As you can see, the Item was renamed and then moved:

unique-name-move-4

One thing to note: the Item will not be moved if the user clicks the ‘Cancel’ button in the dialog that asks for a new name.

Moreover, the above code only works when moving Items in the Sitecore Client. These pipeline processors will not run when moving Items via Sitecore API code (you’ll have to tap into one of the “move” related events, instead).

I’m going to omit sharing my testing of the “Move To” piece of the code above as it is using the same code. Trust me, it works. πŸ˜‰

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

Advertisements

A Sitecore Item Buckets GutterRenderer to Convey Which Algorithm Was Used for Creating Bucket Folders

In my previous post, I gave a solution which I leverages the Sitecore Rules Engine to create a custom Item Buckets folder structure for storing bucketable Items.

Last night, I had a thought: what if you needed to know which “algorithm” created a given bucket folder structure for an Item Bucket? How could we go about conveying this type of information?

Immediately, the Sitecore Gutter came to mind — it’s a great way to communicate this type of information visually.

mind-outta-gutter

Before I move forward on the solution I started last night and completed today, let me explain what the Sitecore Gutter is, just in case you are unfamiliar with this feature.

The Sitecore Gutter lives here in the Content Editor:

smart-bucket-gutter-sitecore-gutter

If you right-click in this area, you get a context menu to turn on/off Gutter indicators:

smart-gutter-sitecore-gutter-context-menu

I turned on the Item Buckets Gutter indicator, and now can see which Items are Buckets:

smart-gutter-turn-on-buckets-gutter

There is a huge body of blog posts out on the interwebs which give examples on adding to the Sitecore Gutter. Here are a few posts from some fellow Sitecore MVPs which I highly recommend reading (in order of publish date):

I also wrote a post on how to add to the Sitecore Gutter using the Sitecore PowerShell Extensions module. I recommend having a look at that as well. πŸ˜‰

Now that we are well-versed — or “wicked smaht” as we Bostonians would alternatively say — on what the Sitecore Gutter is, let’s move on to the solution I came up with.

three-stooges-ejoomicated

Just a “heads up”: there is a lot of code in this solution so don’t freak out and/or get too overwhelmed. Stay the course. πŸ˜‰

curly-bug-out

I first explored Sitecore.Buckets.Gutters.BucketGutter in Sitecore.Buckets.dll to see if I should take note of anything special I need to know about when creating custom Sitecore.Shell.Applications.ContentEditor.Gutters.GutterRenderer — this lives in Sitecore.Kernel.dll and needs to be subclassed when adding to the Sitecore Gutter — subclasses for Item Buckets.

I noticed there is code in there which ascertains whether the Item Buckets feature is turned on/off, and obviously returns a null instance of Sitecore.Shell.Applications.ContentEditor.Gutters.GutterIconDescriptor — this lives in Sitecore.Kernel.dll — via its GetIconDescriptor() method.

I decided I needed to a way to also ascertain this. I came up with the following interface for classes that determined whether a feature is enabled or not:

namespace Sitecore.Sandbox.Determiners.Features
{
    public interface IFeatureDeterminer
    {
        bool IsEnabled();
    }
}

I then implemented the above interface with the following class:

using Sitecore.ContentSearch;
using Sitecore.ContentSearch.Utilities;

using Sitecore.Sandbox.Determiners.Features;

namespace Sitecore.Sandbox.Buckets.Determiners.Features
{
    public class ItemBucketsFeatureDeterminer : IFeatureDeterminer
    {
        public virtual bool IsEnabled()
        {
            return ContentSearchManager.Locator.GetInstance<IContentSearchConfigurationSettings>().ItemBucketsEnabled();
        }
    }
}

The code in the IsEnabled() method basically returns a boolean indicating whether the Item Buckets feature is turned on/off.

We now need classes whose instances can ascertain whether a bucketed Item’s path matches the paths generated by the bucketing algorithms they represent. I created the following class whose instances would serve as a parameters object to these objects:

using System;

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

namespace Sitecore.Sandbox.Buckets.Ascertainers
{
    public class BucketFolderPathAscertainerParameters
    {
        public Item BucketItem { get; set; }

        public Item BucketedItem { get; set; }

        public DateTime CreationDateOfNewItem { get; set; }
    }
}

We’re just going to pass the Item Bucket, the bucketed Item and the creation date of the bucketed Item.

Next, we need those objects that ascertain whether a given Item Bucket uses a particular bucketing algorithm for its folder structure. I created the following interface for classes whose instances do just that:


namespace Sitecore.Sandbox.Buckets.Ascertainers
{
    public interface IBucketFolderPathAscertainer
    {
        string GetIcon();

        string GetToolTip();

        bool IsFolderPathMatch(BucketFolderPathAscertainerParameters parameters);
    }
}

After implementing two classes which implemented the interface above, I noticed some code similarities between them, and decided to employ Martin Fowler‘s refactoring technique Pull Up Method to move up these code similarities into a base class — I highly recommend reading his book Refactoring: Improving the Design of Existing Code which discusses this refactoring technique as well as a host of others — to make it easier for creating future subclasses and to hopefully abate the chances of code duplication. That exercise gave birth to the following abstract class:

using System;

using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;

namespace Sitecore.Sandbox.Buckets.Ascertainers
{
    public abstract class BucketFolderPathAscertainer : IBucketFolderPathAscertainer
    {
        private string icon;
        protected string Icon
        {
            get
            {
                Assert.IsNotNullOrEmpty(icon, "Icon must be set in configuration!");
                return icon;
            }
            set
            {
                Assert.IsNotNullOrEmpty(value, "Icon must be set in configuration!");
                icon = value;
            }
        }

        private string toolTip;
        protected string ToolTip
        {
            get
            {
                Assert.IsNotNullOrEmpty(toolTip, "ToolTip must be set in configuration!");
                return toolTip;
            }
            set
            {
                Assert.IsNotNullOrEmpty(value, "ToolTip must be set in configuration!");
                toolTip = value;
            }
        }
        
        public virtual string GetIcon()
        {
            return Icon;
        }

        public virtual string GetToolTip()
        {
            return ToolTip;
        }

        public bool IsFolderPathMatch(BucketFolderPathAscertainerParameters parameters)
        {
            EnsureParameters(parameters);
            ID settingsItemID = GetSettingsItemID();
            Assert.IsTrue(!ID.IsNullOrEmpty(settingsItemID), "GetSettingsItemID() cannot return null or empty!");
            Item settingsItem = parameters.BucketItem.Database.GetItem(settingsItemID);
            Assert.IsNotNull(settingsItem, string.Format("Setting Item does not exist! Make sure it exists! Item ID: {0}", settingsItemID.ToString()));

            string resolvedPath = GetResolvedPath(parameters, settingsItem);
            if (string.IsNullOrWhiteSpace(resolvedPath))
            {
                return false;
            }

            return IsPathMatch(parameters, resolvedPath);
        }

        protected virtual void EnsureParameters(BucketFolderPathAscertainerParameters parameters)
        {
            Assert.ArgumentNotNull(parameters, "parameters");
            Assert.ArgumentNotNull(parameters.BucketItem, "parameters.BucketItem");
            Assert.ArgumentNotNull(parameters.BucketedItem, "parameters.BucketedItem");
            Assert.ArgumentCondition(parameters.BucketItem.Database == parameters.BucketedItem.Database, "parameters.BucketItem.Database", "parameters.BucketItem.Database and parameters.BucketedItem.Database must be the same database");
            Assert.ArgumentCondition(parameters.BucketItem.Axes.IsAncestorOf(parameters.BucketedItem), "parameters.BucketItem", string.Format("parameters.BucketItem", "Bucket Item: {0} must be an ancestor of Bucketed Item: {1}", parameters.BucketItem.ID.ToString(), parameters.BucketedItem.ID.ToString()));
            Assert.ArgumentNotNull(parameters.BucketedItem, "parameters.BucketedItem");
        }

        protected virtual ID GetSettingsItemID()
        {
            return Sitecore.Buckets.Util.Constants.SettingsItemId;
        }

        protected abstract string GetResolvedPath(BucketFolderPathAscertainerParameters parameters, Item settingsItem);

        protected virtual bool IsPathMatch(BucketFolderPathAscertainerParameters parameters, string resolvedPath)
        {
            if (string.IsNullOrWhiteSpace(resolvedPath))
            {
                return false;
            }

            string bucketedFolderPath = parameters.BucketedItem.Paths.ParentPath.Replace(parameters.BucketItem.Paths.FullPath, string.Empty);
            if(bucketedFolderPath.StartsWith("/"))
            {
                bucketedFolderPath = bucketedFolderPath.Substring(1);
            }

            return string.Equals(bucketedFolderPath, resolvedPath, StringComparison.OrdinalIgnoreCase);
        }
    }
}

The Icon and ToolTip property values in the above class live in the patch configuration file further down in this post. The Sitecore Configuration Factory will inject those values into these properties, and the GetIcon() and GetToolTip() will return the values housed in the Icon and ToolTip properties, respectively.

The IsFolderPathMatch() gets the settingsItem Item instance — this Item lives in /sitecore/system/Settings/Buckets/Item Buckets Settings in Sitecore — which is needed by the GetResolvedPath() method — this method is declared abstract and must be implemented by subclasses — whose job it is to get the bucketed Item’s folder path via the algorithm which the subclass implementation represents.

When the algorithm path is return, it is then passed to the IsPathMatch() method which determines if there is a match. If there is a match, true is returned to the caller of the IsFolderPathMatch() method; false is returned otherwise.

The class above combined with its subclasses would be an example of the Template method design pattern in action.

Now, we need a subclass of the above to determine if a bucketed Item’s path was generated by the Sitecore Rules Engine. Since we don’t want the Rules Engine to evaluate the rules defined on /sitecore/system/Settings/Buckets/Item Buckets Settings for the bucketed Item given the Item Bucket’s current state — its “when” Condition will most likely evaluate to false — we need a way to trick the Rules Engine. I came up with the following Condition class that always evaluates to true:

using Sitecore.Rules;
using Sitecore.Rules.Conditions;

namespace Sitecore.Sandbox.Rules
{
    public class AlwaysTrueWhenCondition<TRuleContext> : WhenCondition<TRuleContext> where TRuleContext : RuleContext
    {
        protected override bool Execute(TRuleContext ruleContext)
        {
            return true;
        }
    }
}

There isn’t much going on in the above class. Its Execute() method always returns true.

The following subclass of the BucketFolderPathAscertainer class determines if a bucketed Item’s path was generated by the Sitecore Rules Engine:

using System;
using System.Collections.Generic;

using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Buckets.Rules.Bucketing;
using Sitecore.Rules;
using Sitecore.Sandbox.Rules;

namespace Sitecore.Sandbox.Buckets.Ascertainers
{
    public class RulesDefinedBucketFolderPathAscertainer : BucketFolderPathAscertainer
    {
        protected override string GetResolvedPath(BucketFolderPathAscertainerParameters parameters, Item settingsItem)
        {
            string bucketRulesFieldId = GetBucketRulesFieldId();
            Assert.IsNotNullOrEmpty(bucketRulesFieldId, "GetBucketRulesFieldId() cannot return null or empty!");
            BucketingRuleContext ruleContext = CreateNewBucketingRuleContext(parameters);
            RuleList<BucketingRuleContext> rules = GetRuleList<BucketingRuleContext>(settingsItem, bucketRulesFieldId);
            SetAlwaysTrueWhenConditions(rules.Rules);
            if (rules == null)
            {
                return string.Empty;
            }

            try
            {
                rules.Run(ruleContext);
            }
            catch (Exception ex)
            {
                Log.Error(ToString(), ex, this);
            }

            return ruleContext.ResolvedPath;
        }

        protected virtual string GetBucketRulesFieldId()
        {
            return Sitecore.Buckets.Util.Constants.BucketRulesFieldId;
        }

        protected virtual BucketingRuleContext CreateNewBucketingRuleContext(BucketFolderPathAscertainerParameters parameters)
        {
            return new BucketingRuleContext(parameters.BucketedItem.Database, parameters.BucketItem.ID, parameters.BucketedItem.ID, parameters.BucketedItem.Name,
                            parameters.BucketedItem.TemplateID, parameters.CreationDateOfNewItem)
            {
                NewItemId = parameters.BucketedItem.ID,
                CreationDate = parameters.CreationDateOfNewItem
            };
        }

        protected virtual RuleList<T> GetRuleList<T>(Item settingsItem, string bucketRulesFieldId) where T : BucketingRuleContext
        {
            Assert.ArgumentNotNull(settingsItem, "settingsItem");
            Assert.ArgumentNotNullOrEmpty(bucketRulesFieldId, "bucketRulesFieldId");
            return RuleFactory.GetRules<T>(new[] { settingsItem }, bucketRulesFieldId);
        }

        protected virtual void SetAlwaysTrueWhenConditions<TRuleContext>(IEnumerable<Rule<TRuleContext>> rules) where TRuleContext : RuleContext
        {
            foreach(Rule<TRuleContext> rule in rules)
            {
                rule.Condition = CreateNewAlwaysTrueWhenCondition<TRuleContext>();
            }
        }

        protected virtual AlwaysTrueWhenCondition<TRuleContext> CreateNewAlwaysTrueWhenCondition<TRuleContext>() where TRuleContext : RuleContext
        {
            return new AlwaysTrueWhenCondition<TRuleContext>();
    }
    }
}

I’m not going to go too much into all the methods of this class given that most of the magic happens in the GetResolvedPath() method. It basically replaces all Conditions in the rules Sitecore.Rules.RulesList instance with an instance of the Condition class above; calls the Rules Engine API to evaluate the rules defined on the settingsItem instance though with the Conditions always returning true; and returns the resolved path to the caller.

The following class which also subclasses the BucketFolderPathAscertainer class — if I only had a dollar for every instance of the word “class” or “subclass” in this post — basically wraps an Sitecore.Buckets.Util.IDynamicBucketFolderPath instance — ahem, I mean employs the Adapter design pattern — where its GetResolvedPath() method delegates a call to the IDynamicBucketFolderPath instances GetFolderPath() method:

using Sitecore.Buckets.Util;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;

namespace Sitecore.Sandbox.Buckets.Ascertainers
{
    public class DynamicBucketFolderPathPathAscertainer : BucketFolderPathAscertainer
    {
        private IDynamicBucketFolderPath PathResolver { get; set; }

        protected override string GetResolvedPath(BucketFolderPathAscertainerParameters parameters, Item settingsItem)
        {
            Assert.IsNotNull(PathResolver, "The IDynamicBucketFolderPath instance named PathResolver must be set in configuration!");
            return PathResolver.GetFolderPath(parameters.BucketItem.Database, parameters.BucketedItem.Name, parameters.BucketedItem.TemplateID,
                                                                parameters.BucketedItem.ID, parameters.BucketItem.ID, parameters.CreationDateOfNewItem);
        }
    }
}

You might be asking “what are we using the above class for?” Well, we are going to inject an instance of Sitecore.Buckets.Util.DateBasedFolderPath into the PathResolver property via the Sitecore Configuration Factory (please see the patch configuration file further down in this post).

I thought it might be cumbersome for a class to make calls to every single IBucketFolderPathAscertainer instance — sure, we only have two above but just think about how messy things will quickly progress if more are added. I decided I would utilize the Composite design pattern via the following class:

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

using Sitecore.Configuration;
using Sitecore.Diagnostics;

namespace Sitecore.Sandbox.Buckets.Ascertainers
{
    
    public class CompositeBucketFolderPathAscertainer : IBucketFolderPathAscertainer
    {
        private string Icon { get; set; }

        private string ToolTip { get; set; }

        private List<IBucketFolderPathAscertainer> FolderPathAscertainers { get; set; }

        public CompositeBucketFolderPathAscertainer()
        {
            FolderPathAscertainers = new List<IBucketFolderPathAscertainer>();
        }

        public string GetIcon()
        {
            return Icon;
        }

        protected virtual void SetIcon(string icon)
        {
            Assert.ArgumentNotNullOrEmpty(icon, "icon");
            Icon = icon;
        }

        public string GetToolTip()
        {
            return ToolTip;
        }

        protected virtual void SetToolTip(string toolTip)
        {
            Assert.ArgumentNotNullOrEmpty(toolTip, "toolTip");
            ToolTip = toolTip;
        }

        public bool IsFolderPathMatch(BucketFolderPathAscertainerParameters parameters)
        {
            if(FolderPathAscertainers == null || !FolderPathAscertainers.Any())
            {
                return false;
            }

            foreach(IBucketFolderPathAscertainer ascertainer in FolderPathAscertainers)
            {
                if(ascertainer.IsFolderPathMatch(parameters))
                {
                    SetIcon(ascertainer.GetIcon());
                    SetToolTip(ascertainer.GetToolTip());
                    return true;
                }
            }

            return false;
        }

        protected virtual void AddFolderPathAscertainer(XmlNode configNode)
        {
            if(configNode == null)
            {
                return;
            }

            IBucketFolderPathAscertainer ascertainer = Factory.CreateObject(configNode, false) as IBucketFolderPathAscertainer;
            Assert.IsNotNull(ascertainer, "An IBucketFolderPathAscertainer was not defined correctly in configuration!");
            FolderPathAscertainers.Add(ascertainer);
        }
    }
}

All configuration-defined IBucketFolderPathAscertainer instances will be added to the FolderPathAscertainers List property via the AddFolderPathAscertainer() method (have a look at the configuration file below to see the AddFolderPathAscertainer() method being there for the Sitecore Configuration Factory to use).

The IsFolderPathMatch() method will then iterate over all IBucketFolderPathAscertainer instances and try to find a match. If a match is found, the instance of the class above will grab and save local copies of that IBucketFolderPathAscertainer instance’s Icon and Tooltip values, and then return true. If no match was found, it returns false.

Now, we need a way to grab one bucketed Item for an Item Bucket. I defined the following interface for a class whose instance will return one Item given another Item:

using Sitecore.Data.Items;

namespace Sitecore.Sandbox.Providers.Items
{
    public interface IItemProvider
    {
        Item GetItem(Item item);
    }
}

The following class implements the interface above:

using System;
using System.Linq;
using System.Linq.Expressions;

using Sitecore.ContentSearch;
using Sitecore.ContentSearch.Linq;
using Sitecore.ContentSearch.Linq.Utilities;
using Sitecore.ContentSearch.SearchTypes;
using Sitecore.ContentSearch.Utilities;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;

using Sitecore.Sandbox.Providers.Items;

namespace Sitecore.Sandbox.Buckets.Providers.Items
{
    public class BucketedItemProvider : IItemProvider
    {
        private string BucketFolderTemplateId { get; set; }

        private string SearchIndexName { get; set; }

        private ISearchIndex searchIndex;
        private ISearchIndex SearchIndex
        {
            get
            {
                if (searchIndex == null && !string.IsNullOrWhiteSpace(SearchIndexName))
                {
                    searchIndex = GetSearchIndex(SearchIndexName);
                }

                return searchIndex;
            }
        }

        public virtual Item GetItem(Item bucketItem)
        {
            ID bucketFolderTemplateId;
            Assert.ArgumentCondition(ID.TryParse(BucketFolderTemplateId, out bucketFolderTemplateId), "BucketFolderTemplateId", "BucketFolderTemplateId cannot be empty and must be a Sitecore.Data.ID! Check its configuration setting!");
            Assert.ArgumentNotNull(bucketItem, "bucketItem");
            Assert.IsNotNullOrEmpty(SearchIndexName, "SearchIndexName is empty. Double-check its configuration setting!");
            Assert.IsNotNull(SearchIndex, "SearchIndex is null. Double-check the SearchIndexName configuration setting!");

            using (IProviderSearchContext searchContext = SearchIndex.CreateSearchContext())
            {
                var predicate = GetSearchPredicate<SearchResultItem>(bucketItem.ID, bucketFolderTemplateId);
                IQueryable<SearchResultItem> query = searchContext.GetQueryable<SearchResultItem>().Filter(predicate);
                SearchResults<SearchResultItem> results = query.GetResults();
                if (results.Count() < 1)
                {
                    return null;
                }

                SearchHit<SearchResultItem> hit = results.Hits.First();
                return hit.Document.GetItem();
            }
        }

        protected virtual ISearchIndex GetSearchIndex(string searchIndexName)
        {
            Assert.ArgumentNotNullOrEmpty(searchIndexName, "searchIndexName");
            return ContentSearchManager.GetIndex(searchIndexName);
        }

        protected virtual Expression<Func<T, bool>> GetSearchPredicate<T>(ID bucketItemId, ID bucketFolderTemplateId) where T : SearchResultItem
        {
            var predicate = PredicateBuilder.True<T>();
            predicate = predicate.And(item => item.Paths.Contains(bucketItemId));
            predicate = predicate.And(item => item.TemplateId != bucketFolderTemplateId);
            predicate = predicate.And(item => item.Parent != bucketItemId);
            predicate = predicate.And(item => item.ItemId != bucketItemId);
            return predicate;
        }
    }
}

The class above is leveraging the Sitecore.ContentSearch API to get this bucketed Item.

Why am I using the Sitecore.ContentSearch API? Well, imagine if there are thousands if not tens of thousands of bucketed Items under the Item Bucket. A Sitecore query would be slow as molasses, and we need keep performance on our minds at all times for all of our solutions. Don’t lose sight of that on anything you build.

The GetSearchPredicate() method builds up and returns an Expression<Func> instance — let’s call this instance the “predicate”. The predicate basically says we want an Item who is a descendant of the Item Bucket; isn’t a Bucket Folder Item; lives under a Bucket Folder; and isn’t the Item Bucket Item.

The GetItem() method then uses that predicate and the Sitecore.ContentSearch API to gather those SearchResultItem instances, and then returns the Item instance on the first one in the result set if any were returned. If none were found, it returns null.

Since we have a lot of moving parts in this solution — just look at all of the classes you’ve just gone through — I need a way to piece all of this together for a GutterRenderer.

Unfortunately, we can’t magically inject instances into a GutterRenderer via the Sitecore Configuration Factory — well, you can call it but imagine all the calls I would need for this — so I decided to define the following interface whose classes would be used by a GutterRenderer, and these classes would be defined in Sitecore configuration:

using Sitecore.Data.Items;
using Sitecore.Shell.Applications.ContentEditor.Gutters;

namespace Sitecore.Sandbox.Shell.Applications.ContentEditor.Gutters
{
    public interface IGutter
    {
        GutterIconDescriptor GetIconDescriptor(Item item);

        bool IsVisible();
    }
}

The following class implements the interface above:

using System;

using Sitecore.Buckets.Extensions;
using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Globalization;
using Sitecore.Shell.Applications.ContentEditor.Gutters;

using Sitecore.Sandbox.Buckets.Ascertainers;
using Sitecore.Sandbox.Determiners.Features;
using Sitecore.Sandbox.Providers.Items;
using Sitecore.Sandbox.Shell.Applications.ContentEditor.Gutters;

namespace Sitecore.Sandbox.Buckets.Gutters
{
    public class FolderPathBucketGutter : IGutter
    {
        private string DefaultIcon { get; set; }

        private string DefaultToolTip { get; set; }

        private string CreatedDatetimeFieldName { get; set; }

        private IFeatureDeterminer ItemBucketsFeatureDeterminer { get; set; }
        
        private IItemProvider BucketedItemProvider { get; set; }

        private IBucketFolderPathAscertainer FolderPathAscertainer { get; set; }

        public virtual GutterIconDescriptor GetIconDescriptor(Item item)
        {
            EnsureRequiredProperties();
            Assert.ArgumentNotNull(item, "item");

            if(!AreItemBucketsEnabled() || !item.IsABucket())
            {
                return null;
            }

            Item bucketedItem = GetBucketedItem(item);
            if(bucketedItem == null)
            {
                return CreateNewGutterIconDescriptor(DefaultIcon, DefaultToolTip);
            }

            BucketFolderPathAscertainerParameters parameters = new BucketFolderPathAscertainerParameters
            {
                BucketItem = item,
                BucketedItem = bucketedItem,
                CreationDateOfNewItem = GetItemCreatedDateTime(bucketedItem)
            };

            if(!FolderPathAscertainer.IsFolderPathMatch(parameters))
            {
                return CreateNewGutterIconDescriptor(DefaultIcon, DefaultToolTip);
            }

            return CreateNewGutterIconDescriptor(FolderPathAscertainer.GetIcon(), FolderPathAscertainer.GetToolTip());
        }

        protected virtual void EnsureRequiredProperties()
        {
            Assert.IsNotNull(FolderPathAscertainer, "FolderPathAscertainer must be defined in configuration!");
            Assert.IsNotNullOrEmpty(DefaultIcon, "DefaultIcon must be defined in configuration!");
            Assert.IsNotNullOrEmpty(DefaultToolTip, "DefaultToolTip must be defined in configuration!");
            Assert.IsNotNullOrEmpty(CreatedDatetimeFieldName, "CreatedDatetimeFieldName must be defined in configuration!");
        }

        public virtual bool IsVisible()
        {
            return AreItemBucketsEnabled();
        }

        protected virtual bool AreItemBucketsEnabled()
        {
            Assert.IsNotNull(ItemBucketsFeatureDeterminer, "ItemBucketsFeatureDeterminer must be set in configuration!");
            return ItemBucketsFeatureDeterminer.IsEnabled();
        }

        protected virtual Item GetBucketedItem(Item bucketItem)
        {
            Assert.IsNotNull(BucketedItemProvider, "BucketedItemProvider must be set in configuration!");
            return BucketedItemProvider.GetItem(bucketItem);
        }

        protected virtual GutterIconDescriptor CreateNewGutterIconDescriptor(string icon, string toolTip)
        {
            Assert.ArgumentNotNullOrEmpty(icon, "icon");
            Assert.ArgumentNotNullOrEmpty(toolTip, "toolTip");
            return new GutterIconDescriptor
            {
                Icon = icon,
                Tooltip = TranslateText(toolTip)
            };
        }

        protected virtual string TranslateText(string text)
        {
            Assert.ArgumentNotNullOrEmpty(text, "text");
            return Translate.Text(text);
        }

        protected virtual DateTime GetItemCreatedDateTime(Item item)
        {
            Assert.IsNotNullOrEmpty(CreatedDatetimeFieldName, "CreatedDatetimeFieldName must be defined in configuration!");
            Assert.ArgumentNotNull(item, "item");
            DateField created = item.Fields[CreatedDatetimeFieldName];
            if(created == null)
            {
                return DateTime.MinValue;
            }

            return created.DateTime;
        }
    }
}

The AreItemBucketsEnabled() method in the above classes determines if the Item Bucket feature is enabled via the injected IBucketFolderPathAscertainer instance. This method is then used by the IsVisible() method which represents the method by the same name on GutterRenderer instances, and is also called by the GetIconDescriptor() method.

If the Item Buckets feature is not enabled, the GetIconDescriptor() method will return null as well as when the passed Item is not an Item Bucket.

If the passed Item is an Item Bucket, the GetIconDescriptor() gets one bucketed Item via the GetBucketedItem() method — this method just delegates to the IItemProvider instance injected into the class instance — and puts this bucketed Item as well as the Item Bucket into a BucketFolderPathAscertainerParameters parameters object instance. The creation date of the bucketed is also set on this parameters object since it is required when ascertaining whether the folder structure was constructed based on its creation date.

The BucketFolderPathAscertainerParameters instance is then passed to the IsFolderPathMatch() method on the injected IBucketFolderPathAscertainer property which determines if there is a folder path match.

If there is a match, a new GutterIconDescriptor instance is returned which contains the appropriate Icon and Tooltip.

If there is no match, then a new GutterIconDescriptor is returned with default values for the Icon and Tooltip.

This next class subclasses the GutterRenderer class:

using System;

using Sitecore.Configuration;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Shell.Applications.ContentEditor.Gutters;

namespace Sitecore.Sandbox.Shell.Applications.ContentEditor.Gutters
{
    public class ConfigDefinedGutterRenderer : GutterRenderer
    {
        private IGutter gutter;
        private IGutter Gutter
        {
            get
            {
                if (gutter == null)
                {
                    gutter = GetInnerGutterRenderer();
                }

                return gutter;
            }
        }

        protected override GutterIconDescriptor GetIconDescriptor(Item item)
        {
            Assert.IsNotNull(Gutter, "Gutter wasn't set properly. Double-check it!");
            return Gutter.GetIconDescriptor(item);
        }

        public override bool IsVisible()
        {
            Assert.IsNotNull(Gutter, "Gutter wasn't set properly. Double-check it!");
            return Gutter.IsVisible();
        }

        protected virtual IGutter GetInnerGutterRenderer()
        {
            string configPath = GetConfigPath();
            if (string.IsNullOrWhiteSpace(configPath))
            {
                Log.Error("ConfigDefinedGutterRenderer: configPath must be set as a parameter!", this);
                return null;
            }

            try
            {
                IGutter gutter = Factory.CreateObject(configPath, false) as IGutter;
                if (gutter == null)
                {
                    Log.Error(string.Format("ConfigDefinedGutterRenderer: the IGutter defined in {0} isn't correctly defined. Double-check it!", configPath), this);
                    return null;
                }

                return gutter;

            }
            catch (Exception ex)
            {
                Log.Error(ToString(), ex, this);
            }

            return null;
        }

        protected virtual string GetConfigPath()
        {
            string key = "configPath";
            if (Parameters.ContainsKey(key))
            {
                return Parameters[key];
            }

            return string.Empty;
        }
    }
}

The GetInnerGutterRenderer() method above calls the Sitecore Configuration Factory to grab an IGutter instance from a configuration path which is set on the Parameters field of the definition Item for this GutterRenderer in the Core database — see the screenshot further down in this post — when the Gutter property is called for the first time, and sets this instance on a private member on the class instance.

Both the GetIconDescriptor() and IsVisible() methods delegate to the methods on the IGutter instance with the same names (quiz time: what design pattern is this class using? πŸ˜‰ ).

I then duct-taped everything together via the following patch configuration file:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <gutters>
      <folderPathBucketGutter type="Sitecore.Sandbox.Buckets.Gutters.FolderPathBucketGutter, Sitecore.Sandbox" singleInstance="true">
        <DefaultIcon>business/32x32/chest_add.png</DefaultIcon>
        <DefaultToolTip>This item is a bucket. You can use this as a content repository.</DefaultToolTip>
        <CreatedDatetimeFieldName>__Created</CreatedDatetimeFieldName>
        <ItemBucketsFeatureDeterminer ref="determiners/features/itemBucketsFeatureDeterminer" />
        <BucketedItemProvider ref="providers/items/bucketedItemProvider" />
        <FolderPathAscertainer ref="ascertainers/buckets/compositeBucketFolderPathAscertainer" />
      </folderPathBucketGutter>
    </gutters>
    <ascertainers>
      <buckets>
        <compositeBucketFolderPathAscertainer type="Sitecore.Sandbox.Buckets.Ascertainers.CompositeBucketFolderPathAscertainer, Sitecore.Sandbox" singleInstance="true">
          <ascertainers hint="raw:AddFolderPathAscertainer">
            <ascertainer ref="ascertainers/buckets/rulesDefinedBucketFolderPathAscertainer" />
            <ascertainer ref="ascertainers/buckets/dateBasedFolderPathAscertainer" />
          </ascertainers>
        </compositeBucketFolderPathAscertainer>
        <dateBasedFolderPathAscertainer type="Sitecore.Sandbox.Buckets.Ascertainers.DynamicBucketFolderPathPathAscertainer, Sitecore.Sandbox" singleInstance="true">
          <Icon>Business/32x32/calendar_down.png</Icon>
          <ToolTip>This item is a bucket. Its bucket folders were generated based on the creation date of the bucketed items. You can use this as a content repository.</ToolTip>
          <PathResolver type="Sitecore.Buckets.Util.DateBasedFolderPath, Sitecore.Buckets" />
        </dateBasedFolderPathAscertainer>
        <rulesDefinedBucketFolderPathAscertainer type="Sitecore.Sandbox.Buckets.Ascertainers.RulesDefinedBucketFolderPathAscertainer, Sitecore.Sandbox"
        singleInstance="true">
          <Icon>Business/32x32/briefcase_add.png</Icon>
          <ToolTip>This item is a bucket. Its bucket folders were generated by the rules engine. You can use this as a content repository.</ToolTip>
        </rulesDefinedBucketFolderPathAscertainer>
      </buckets>
    </ascertainers>
    <determiners>
      <features>
        <itemBucketsFeatureDeterminer type="Sitecore.Sandbox.Buckets.Determiners.Features.ItemBucketsFeatureDeterminer" singleInstance="true" />
      </features>
    </determiners>
    <providers>
      <items>
        <bucketedItemProvider type="Sitecore.Sandbox.Buckets.Providers.Items.BucketedItemProvider" singleInstance="true">
          <BucketFolderTemplateId>{ADB6CA4F-03EF-4F47-B9AC-9CE2BA53FF97}</BucketFolderTemplateId>
          <SearchIndexName>sitecore_master_index</SearchIndexName>
        </bucketedItemProvider>
      </items>
    </providers>
  </sitecore>
</configuration>

We need to let Sitecore know about the new Gutter addition. I did this in the Core database:

smart-bucket-gutter-core-db

One thing to keep in mind is that the Sitecore Rules Engine folder path match will only work when we have an algorithm that will return the same path consistently for a bucketed Item. This unfortunately means I could not use the same Action from my previous post given that it generates random folder paths.

To over come this hurdle, I built the following Action which just reverses the bucketed Item’s ID (oh no, more code πŸ˜‰ ):

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

using Sitecore.Buckets.Rules.Bucketing;
using Sitecore.Buckets.Rules.Bucketing.Actions;
using Sitecore.Diagnostics;

namespace Sitecore.Sandbox.Buckets.Rules.Actions
{
    public class CreateReversedIDBasedPath<TContext> : CreateIDBasedPath<TContext> where TContext : BucketingRuleContext
    {
        public override void Apply(TContext ruleContext)
        {
            Assert.ArgumentNotNull(ruleContext, "ruleContext");
            base.Apply(ruleContext);
            if (string.IsNullOrWhiteSpace(ruleContext.ResolvedPath))
            {
                return;
            }

            ruleContext.ResolvedPath = ReversePath(ruleContext.ResolvedPath);
        }

        protected virtual string ReversePath(string path)
        {
            if(string.IsNullOrWhiteSpace(path))
            {
                return string.Empty;
            }

            List<string> pieces = path.Split('/').ToList();
            pieces.Reverse();
            return string.Join("/", pieces);
        }
    }
}

I’m not going to go into details of how I set this in the rules on /sitecore/system/Settings/Buckets/Item Buckets Settings — you can see an example of how this is done from my previous post.

Now that everything is set, we can see that the new Gutter option is available:

smart-bucket-gutter-right-click-lets-turn-on

I then turned it on:

smart-gutter-new-gutter-turned-on

As you can see, we have different Gutter icons for different folder structures.

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

Oh, by the way, if you made it all the way to the end of this post, then you deserve a treat. Go get yourself a cookie. You deserve it. πŸ˜‰

cookie

Until next time, keep up the good fight, one piece of code at time. πŸ˜€

Abstract Out Sitecore FileWatcher Logic Which Monitors Rendering Files on the File System

While digging through code of Sitecore.IO.FileWatcher subclasses this weekend, I noticed a lot of code similarities between the LayoutWatcher and XslWatcher classes, and thought it might be a good idea to abstract this logic out into a new base abstract class so that future FileWatchers which monitor other renderings on the file system can easily be added without having to write much logic.

Before I move forward on how I did this, let me explain what the LayoutWatcher FileWatcher does. The LayoutWatcher FileWatcher clears the html cache of all websites defined in Sitecore when it determines that a layout file (a.k.a .aspx) or sublayout file (a.k.a .ascx) has been changed, deleted, renamed or added.

Likewise, the XslWatcher FileWatcher does the same thing for XSLT renderings but also clears the XSL cache along with the html cache.

Ok, now back to the abstraction. I came up with the following class to serve as the base class for any FileWatcher that will monitor renderings that live on the file system:

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

using Sitecore.Configuration;
using Sitecore.Diagnostics;
using Sitecore.IO;
using Sitecore.Web;

namespace Sitecore.Sandbox.IO.Watchers
{
    public abstract class RenderingFileWatcher : FileWatcher
    {
        private string RenderingFileModifiedMessage { get; set; }

        private string RenderingFileDeletedMessage { get; set; }

        private string RenderingFileRenamedMessage { get; set; }

        private string FileWatcherErrorMessage { get; set; }

        private object Owner { get; set; }

        public RenderingFileWatcher(string configPath)
            : base(configPath)
        {
            SetMessages();
        }

        private void SetMessages()
        {
            string modifiedMessage = GetRenderingFileModifiedMessage();
            AssertNotNullOrWhiteSpace(modifiedMessage, "modifiedMessage", "GetRenderingFileModifiedMessage() cannot return null, empty or whitespace!");
            RenderingFileModifiedMessage = modifiedMessage;

            string deletedMessage = GetRenderingFileDeletedMessage();
            AssertNotNullOrWhiteSpace(deletedMessage, "deletedMessage", "GetRenderingFileDeletedMessage() cannot return null, empty or whitespace!");
            RenderingFileDeletedMessage = deletedMessage;

            string renamedMessage = GetRenderingFileRenamedMessage();
            AssertNotNullOrWhiteSpace(renamedMessage, "renamedMessage", "GetRenderingFileRenamedMessage() cannot return null, empty or whitespace!");
            RenderingFileRenamedMessage = renamedMessage;

            string errorMessage = GetFileWatcherErrorMessage();
            AssertNotNullOrWhiteSpace(errorMessage, "errorMessage", "GetFileWatcherErrorMessage() cannot return null, empty or whitespace!");
            FileWatcherErrorMessage = errorMessage;

            object owner = GetOwner();
            Assert.IsNotNull(owner, "GetOwner() cannot return null!");
            Owner = owner;
        }

        protected abstract string GetRenderingFileModifiedMessage();

        protected abstract string GetRenderingFileDeletedMessage();

        protected abstract string GetRenderingFileRenamedMessage();

        protected abstract string GetFileWatcherErrorMessage();

        protected abstract object GetOwner();

        private void AssertNotNullOrWhiteSpace(string argument, string argumentName, string errorMessage)
        {
            Assert.ArgumentCondition(!string.IsNullOrWhiteSpace(argument), argumentName, errorMessage);
        }

        protected override void Created(string fullPath)
        {
            try
            {
                Log.Info(string.Format("{0}: {1}", RenderingFileModifiedMessage, fullPath), Owner);
                ClearCaches();
            }
            catch (Exception ex)
            {
                Log.Error(FileWatcherErrorMessage, ex, Owner);
            }
        }

        protected override void Deleted(string filePath)
        {
            try
            {
                Log.Info(string.Format("{0}: {1}", RenderingFileDeletedMessage, filePath), Owner);
                ClearCaches();
            }
            catch (Exception ex)
            {
                Log.Error(FileWatcherErrorMessage, ex, Owner);
            }
        }

        protected override void Renamed(string filePath, string oldFilePath)
        {
            try
            {
                Log.Info(string.Format("{0}: {1}. Old path: {2}", RenderingFileRenamedMessage, filePath, oldFilePath), Owner);
                ClearCaches();
            }
            catch (Exception ex)
            {
                Log.Error(FileWatcherErrorMessage, ex, this);
            }
        }

        protected virtual void ClearCaches()
        {
            ClearHtmlCaches();
        }

        protected virtual void ClearHtmlCaches()
        {
            IEnumerable<SiteInfo> siteInfos = GetSiteInfos();
            if (IsNullOrEmpty(siteInfos))
            {
                return;
            }

            foreach(SiteInfo siteInfo in siteInfos)
            {
                if (siteInfo.HtmlCache != null)
                {
                    Log.Info(string.Format("Clearing Html Cache for site: {0}", siteInfo.Name), Owner);
                    siteInfo.HtmlCache.Clear();
                }
            }
        }

        protected virtual IEnumerable<SiteInfo> GetSiteInfos()
        {
            IEnumerable<string> siteNames = GetSiteNames();
            if (IsNullOrEmpty(siteNames))
            {
                return Enumerable.Empty<SiteInfo>();
            }

            IList<SiteInfo> siteInfos = new List<SiteInfo>();
            foreach(string siteName in siteNames)
            {
                SiteInfo siteInfo = Factory.GetSiteInfo(siteName);
                if(siteInfo != null)
                {
                    siteInfos.Add(siteInfo);
                }
            }

            return siteInfos;
        }

        protected virtual IEnumerable<string> GetSiteNames()
        {
            IEnumerable<string> siteNames = Factory.GetSiteNames();
            if(IsNullOrEmpty(siteNames))
            {
                return Enumerable.Empty<string>();
            }

            return siteNames;
        }

        protected virtual bool IsNullOrEmpty<T>(IEnumerable<T> collection)
        {
            if (collection == null || !collection.Any())
            {
                return true;
            }

            return false;
        }
    }
}

The above class defines five abstract methods which subclasses must implement. Data returned by these methods are used when logging information or errors in the Sitecore log. The SetMessages() method vets whether subclass returned these objects correctly, and sets them in private properties which are used in the Created(), Deleted() and Renamed() methods.

The Created(), Deleted() and Renamed() methods aren’t really doing anything different from each other — they are all clearing the html cache for each Sitecore.Web.SiteInfo instance returned by the GetSiteInfos() method, though I do want to point out that each of these methods are defined on the Sitecore.IO.FileWatcher base class and serve as event handlers for file system file actions:

  • Created() is invoked when a new file is dropped in a directory or subdirectory being monitored, or when a targeted file is changed.
  • Deleted() is invoked when a targeted file is deleted.
  • Renamed() is invoked when a targeted file is renamed.

In order to ascertain whether the code above works, I need a subclass whose instance will serve as the actual FileWatcher. I decided to build the following subclass which will monitor Razor files under the /Views directory of my Sitecore website root:

namespace Sitecore.Sandbox.IO.Watchers
{
    public class RazorViewFileWatcher : RenderingFileWatcher
    {
        public RazorViewFileWatcher()
            : base("watchers/view")
        {
        }
        
        protected override string GetRenderingFileModifiedMessage()
        {
            return "Razor View modified";
        }

        protected override string GetRenderingFileDeletedMessage()
        {
            return "Razor View deleted";
        }

        protected override string GetRenderingFileRenamedMessage()
        {
            return "Razor View renamed";
        }

        protected override string GetFileWatcherErrorMessage()
        {
            return "Error in RazorViewFileWatcher";
        }

        protected override object GetOwner()
        {
            return this;
        }
    }
}

I’m sure Sitecore has another way of clearing/replenishing the html cache for Sitecore MVC View and Controller renderings — if you know how this works in Sitecore, please share in a comment, or better yet: please share in a blog post — but I went with this just for testing.

There’s not much going on in the above class. It’s just defining the needed methods for its base class to work its magic.

I then had to add the configuration needed by the RazorViewFileWatcher above in the following patch include configuration file:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <settings>
      <setting name="ViewsFolder" value="/Views" />
    </settings>
    <watchers>
      <view>
        <folder ref="settings/setting[@name='ViewsFolder']/@value"/>
        <filter>*.cshtml</filter>
      </view>
    </watchers>
  </sitecore>
</configuration>

As I’ve discussed in this post, FileWatchers in Sitecore are Http Modules — these must be registered under <modules> of <system.webServer> of your Web.config:


<!-- lots of stuff up here -->

<system.webServer>
    <modules runAllManagedModulesForAllRequests="true">

		<!-- stuff here -->
      
		<add type="Sitecore.Sandbox.IO.Watchers.RazorViewFileWatcher, Sitecore.Sandbox" name="SitecoreRazorViewFileWatcher" />

		<!-- stuff here as well -->

	</modules>

	<!-- more stuff down here -->

</system.webServer>

<!-- even more stuff down here -->

Let’s see if this works.

I decided to choose the following Razor file which comes with Web Forms For Marketers 8.1 Update-2:

razor-view-file-watcher-cshtml-1

I first performed a copy and paste of the Razor file into a new file:

razor-view-file-watcher-cshtml-2

I then deleted the new Razor file:

razor-view-file-watcher-cshtml-3

Next, I renamed the Razor file:

razor-view-file-watcher-cshtml-4

When I looked in my Sitecore log file, I saw that all operations were executed:

razor-view-file-watcher-log

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

Upload Files via the Sitecore UploadWatcher to a Configuration Specified Media Library Folder

In my previous to last post, I discussed the Sitecore UploadWatcher — a Sitecore.IO.FileWatcher which uploads files to the Media Library when files are dropped into the /upload directory of your Sitecore website root.

Unfortunately, in the “out of the box” solution, files are uploaded directly under the Media Library root (/sitecore/media library). Imagine having to sift through all kinds of Media Library Items and folders just to find the image that you are looking for. Such would be an arduous task at best.

I decided to dig through Sitecore.Kernel.dll to see why this is the case, and discovered why: the FileCreated() method on the Sitecore.Resources.Media.MediaCreator class uses an empty Sitecore.Resources.Media.MediaCreatorOptions instance. In order for the file to be uploaded to a specified location in the Media Library, the Destination property on the Sitecore.Resources.Media.MediaCreatorOptions instance must be set, or it will be uploaded directly to the Media Library root.

Here’s the good news: the FileCreated() method is declared virtual, so why not subclass it and then override this method to include some custom logic to set the Destination property on the MediaCreatorOptions instance?

I did just that in the following class:

using System.IO;

using Sitecore.Configuration;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.IO;
using Sitecore.Pipelines.GetMediaCreatorOptions;
using Sitecore.Resources.Media;

namespace Sitecore.Sandbox.Resources.Media
{
    public class MediaCreator : Sitecore.Resources.Media.MediaCreator
    {
        private string UploadLocation { get; set; }

        public override void FileCreated(string filePath)
        {
            Assert.ArgumentNotNullOrEmpty(filePath, "filePath");
            if (string.IsNullOrWhiteSpace(UploadLocation))
            {
                base.FileCreated(filePath);
                return;
            }

            SetContext();
            lock (FileUtil.GetFileLock(filePath))
            {
                string destination = GetMediaItemDestination(filePath);
                if (FileUtil.IsFolder(filePath))
                {
                    MediaCreatorOptions options = MediaCreatorOptions.Empty;
                    options.Destination = destination;
                    options.Build(GetMediaCreatorOptionsArgs.FileBasedContext);
                    this.CreateFromFolder(filePath, options);
                }
                else
                {
                    MediaCreatorOptions options = MediaCreatorOptions.Empty;
                    options.Destination = destination;
                    long length = new FileInfo(filePath).Length;
                    options.FileBased = (length > Settings.Media.MaxSizeInDatabase) || Settings.Media.UploadAsFiles;
                    options.Build(GetMediaCreatorOptionsArgs.FileBasedContext);
                    this.CreateFromFile(filePath, options);
                }
            }
        }

        protected virtual void SetContext()
        {
            if (Context.Site == null)
            {
                Context.SetActiveSite("shell");
            }
        }

        protected virtual string GetMediaItemDestination(string filePath)
        {
            if(string.IsNullOrWhiteSpace(UploadLocation))
            {
                return null;
            }

            string fileNameNoExtension = Path.GetFileNameWithoutExtension(filePath);
            string itemName = ItemUtil.ProposeValidItemName(fileNameNoExtension);
            return string.Format("{0}/{1}", UploadLocation, itemName);
        }
    }
}

The UploadLocation property in the class above is to be defined in Sitecore Configuration — see the patch include configuration file below — and then populated via the Sitecore Configuration Factory when the class is instantiated (yes, I’m defining this class in Sitecore Configuration as well).

Most of the logic in the FileCreated() method above comes from its base class Sitecore.Resources.Media.MediaCreator. I had to copy and paste most of this code from its base class’ FileCreated() method as I couldn’t just delegate to the base class’ FileCreated() method — I needed to set the Destination property on the MediaCreatorOptions instance.

The Destination property on the MediaCreatorOptions instance is being set to be the UploadLocation plus the Media Library Item name — I determine this full path in the GetMediaItemDestination() method.

Unfortunately, I also had to bring in the SetContext() method from the base class since it’s declared private — this method is needed in the FileCreated() method to ensure we have a context site defined.

Now, we need a way to set an instance of the above in the Creator property on the Sitecore.Sandbox.Resources.Media.MediaProvider instance. Unfortunately, there was no easy way to do this without having to subclass the Sitecore.Sandbox.Resources.Media.MediaProvider class, and then set the Creator property via its constructor:

using Sitecore.Configuration;

namespace Sitecore.Sandbox.Resources.Media
{
    public class MediaProvider : Sitecore.Resources.Media.MediaProvider
    {
        private MediaCreator MediaCreator { get; set; }

        public MediaProvider()
        {
            OverrideMediaCreator();
        }

        protected virtual void OverrideMediaCreator()
        {
            Sitecore.Resources.Media.MediaCreator mediaCreator = GetMediaCreator();
            if (mediaCreator == null)
            {
                return;
            }

            Creator = mediaCreator;
        }

        protected virtual Sitecore.Resources.Media.MediaCreator GetMediaCreator()
        {
            return Factory.CreateObject("mediaLibrary/mediaCreator", false) as MediaCreator;
        }
    }
}

The OverrideMediaCreator() method above tries to get an instance of a Sitecore.Resources.Media.MediaCreator using the Sitecore Configuration Factory — it delegates to the GetMediaCreator() method to get this instance — and then set it on the Creator property of its base class if the MediaCreator obtained from the GetMediaCreator() method isn’t null.

If it is null, it just exits out — there is a default instance created in the Sitecore.Resources.Media.MediaCreator base class, so that one would be used instead.

I then replaced the “out of the box” Sitecore.Resources.Media.MediaProvider with the new one above, and also defined the MediaCreator above in the following patch include configuration file:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <mediaLibrary>
      <mediaProvider patch:instead="mediaProvider[@type='Sitecore.Resources.Media.MediaProvider, Sitecore.Kernel']"
                     type="Sitecore.Sandbox.Resources.Media.MediaProvider, Sitecore.Sandbox" />
      <mediaCreator type="Sitecore.Sandbox.Resources.Media.MediaCreator, Sitecore.Sandbox">
        <UploadLocation>/sitecore/media library/uploaded</UploadLocation>
      </mediaCreator>
  </mediaLibrary>
  </sitecore>
</configuration>

Let’s see how we did.

As you can see, I have an empty uploaded Media Library folder:

upload-watcher-not-uploaded-to-folder

Let’s move an image into the /upload folder of my Sitecore instance:

upload-watcher-upload-to-folder

After reloading the “uploaded” Media Library folder, I see that the image was uploaded to it:

upload-watcher-uploaded-to-folder

I would also to like to mention that this solution will also work if there is no uploaded folder in the Media Library — it will be created during upload process.

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

Augment the Sitecore UploadWatcher to Delete Files from the Upload Directory After Uploading to the Media Library

In my previous post I discussed the Sitecore UploadWatcher — a Sitecore.IO.FileWatcher which monitors the /upload directory of your Sitecore instance and uploads files dropped into that directory into the Media Library.

One thing I could never find in the UploadWatcher is functionality to delete files in the /upload directory after they are uploaded into the Media Library — if you know of an “out of the box” way of doing this, please drop a comment.

After peeking into Sitecore.Resources.Media.UploadWatcher using .NET Reflector, I discovered I could add this functionality quite easily since its Created() method — this method handles the uploading of the files into the Media Library by delegating to Sitecore.Resources.Media.MediaManager.Creator.FileCreated() — is overridable. In theory, all I would need to do would be to subclass the UploadWatcher class; override the Created() method; delegate to the base class’ Created() method to handle the upload of the file to the Media Library; then delete the file once the base class’ Create() method was done executing.

After coding, experimenting and refactoring, I built the following class that subclasses Sitecore.Resources.Media.UploadWatcher:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml;

using Sitecore.Configuration;
using Sitecore.Diagnostics;
using Sitecore.IO;
using Sitecore.Resources.Media;
using Sitecore.Xml;

namespace Sitecore.Sandbox.Resources.Media
{
    public class CleanupUploadWatcher : UploadWatcher
    {
        private IEnumerable<string> PathsToIgnore { get; set; }

        private IEnumerable<string> FileNamePartsToIgnore { get; set; }

        private bool ShouldDeleteAfterUpload { get; set; }

        public CleanupUploadWatcher()
            : base()
        {
            Initialize();
        }

        private void Initialize()
        {
            IEnumerable<XmlNode> configNodes = GetIgnoreConfigNodes();
            PathsToIgnore = GetPathsToIgnore(configNodes);
            FileNamePartsToIgnore = GetFileNamePartsToIgnore(configNodes);
            ShouldDeleteAfterUpload = GetShouldDeleteAfterUpload();
        }

        protected virtual IEnumerable<XmlNode> GetIgnoreConfigNodes()
        {
            string configNodeRoot = GetConfigNodeRoot();
            if (string.IsNullOrWhiteSpace(configNodeRoot))
            {
                return Enumerable.Empty<XmlNode>();
            }

            XmlNode rootNode = Factory.GetConfigNode(configNodeRoot);
            if (rootNode == null)
            {
                return Enumerable.Empty<XmlNode>();
            }

            return XmlUtil.GetChildNodes(rootNode, true);
        }

        protected virtual string GetConfigNodeRoot()
        {
            return "mediaLibrary/watcher/ignoreList";
        }

        protected virtual IEnumerable<string> GetPathsToIgnore(IEnumerable<XmlNode> nodes)
        {
            if (IsEmpty(nodes))
            {
                return Enumerable.Empty<string>();
            }

            HashSet<string> pathsToIgnore = new HashSet<string>();
            foreach (XmlNode node in nodes)
            {
                string containsValue = XmlUtil.GetAttribute("contains", node);
                if (ShouldAddContainsValue(containsValue, node, "ignorepath"))
                {
                    pathsToIgnore.Add(containsValue);
                }
            }

            return pathsToIgnore;
        }

        protected virtual IEnumerable<string> GetFileNamePartsToIgnore(IEnumerable<XmlNode> nodes)
        {
            if (IsEmpty(nodes))
            {
                return Enumerable.Empty<string>();
            }

            HashSet<string> partsToIgnore = new HashSet<string>();
            foreach (XmlNode node in nodes)
            {
                string containsValue = XmlUtil.GetAttribute("contains", node);
                if (ShouldAddContainsValue(containsValue, node, "ignore"))
                {
                    partsToIgnore.Add(containsValue);
                }
            }

            return partsToIgnore;
        }

        protected virtual bool ShouldAddContainsValue(string containsValue, XmlNode node, string targetNodeName)
        {
            return !string.IsNullOrWhiteSpace(containsValue)
                    && node != null
                    && string.Equals(node.Name, targetNodeName, StringComparison.OrdinalIgnoreCase);
        }

        protected static bool IsEmpty<T>(IEnumerable<T> collection)
        {
            return collection == null || !collection.Any();
        }

        protected override void Created(string filePath)
        {
            Assert.ArgumentNotNullOrEmpty(filePath, "filePath");
            if (!ShouldIgnoreFile(filePath))
            {
                base.Created(filePath);
                DeleteFile(filePath);
            }
        }

        protected virtual bool GetShouldDeleteAfterUpload()
        {
            XmlNode node = Factory.GetConfigNode("watchers/media/additionalSettings");
            if (node == null)
            {
                return false;
            }

            string deleteAfterUploadValue = XmlUtil.GetAttribute("deleteAfterUpload", node);
            if(string.IsNullOrWhiteSpace(deleteAfterUploadValue))
            {
                return false;
            }
            
            bool shouldDelete;
            bool.TryParse(deleteAfterUploadValue, out shouldDelete);
            return shouldDelete;
        }

        protected virtual bool ShouldIgnoreFile(string filePath)
        {
            Assert.ArgumentNotNullOrEmpty(filePath, "filePath");
            foreach (string path in PathsToIgnore)
            {
                if (ContainsSubstring(filePath, path))
                {
                    return true;
                }
            }

            string fileName = Path.GetFileName(filePath);
            foreach (string part in FileNamePartsToIgnore)
            {
                if(ContainsSubstring(fileName, part))
                {
                    return true;
                }
            }

            return false;
        }

        protected virtual bool ContainsSubstring(string value, string substring)
        {
            if(string.IsNullOrWhiteSpace(value) || string.IsNullOrWhiteSpace(substring))
            {
                return false;
            }

            return value.IndexOf(substring, StringComparison.OrdinalIgnoreCase) > -1;
        }

        protected virtual void DeleteFile(string filePath)
        {
            Assert.ArgumentNotNullOrEmpty(filePath, "filePath");
            if(!ShouldDeleteAfterUpload)
            {
                return;
            }

            try
            {
                FileUtil.Delete(filePath);
            }
            catch(Exception ex)
            {
                Log.Error(ToString(), ex, this);
            }
        }
    }
}

You might thinking “Mike, there is more going on in here than just delegating to the base class’ Created() method and deleting the file. Well, you are correct — I had to do a few things further than what I thought I needed to do, and I’ll explain why.

I had to duplicate the logic — actually I wrote my own logic — to parse the Sitecore configuration which defines the substrings of file names and paths to ignore since these collections on the base UploadWatcher class are private — subclasses cannot access these collections — in order to prevent the code from deleting a file that should be ignored.

I also wedged in configuration with code to turn off the delete functionality if needed.

I then created the following patch include configuration file to hold the configuration setting to turn the delete functionality on/off:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <watchers>
      <media>
        <additionalSettings deleteAfterUpload="true" />
      </media>
    </watchers>
  </sitecore>
</configuration>

I then registered the new CleanupUploadWatcher in the Web.config — please see my previous post which explains why this is needed:

<system.webServer>
    <modules runAllManagedModulesForAllRequests="true">

      <!-- stuff here -->

      <!-- <add type="Sitecore.Resources.Media.UploadWatcher, Sitecore.Kernel" name="SitecoreUploadWatcher" /> -->
      <add type="Sitecore.Sandbox.Resources.Media.CleanupUploadWatcher, Sitecore.Sandbox" name="SitecoreUploadWatcher" />

      <!-- more stuff down here -->

    </modules>

    <!-- and more stuff down here -->
    
<system.webServer>

Let’s see this in action!

As you can see, there is no Media Library Item in the root of my Media Library (/sitecore/media library) — this is where the UploadWatcher uploads the file to:

cleanup-uploadwatcher-media-library-no-file

I then copied an image from a folder on my Desktop to the /upload directory of my Sitecore instance:

cleanup-uploadwatcher-move-file

As you can see above, the image is deleted after it is dropped.

I then went back to my Media Library and refreshed. As you can see here, the file was uploaded:

cleanup-uploadwatcher-media-library-file

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

Download Random Giphy Images and Save to the Media Library Via a Custom Content Editor Image Field in Sitecore

In my previous post I created a custom Content Editor image field in the Sitecore Experience Platform. This custom image field gives content authors the ability to download an image from outside of their Sitecore instance; save the image to the Media Library; and then map that resulting Media Library Item to the custom Image field on an Item in the content tree.

Building that solution was a great way to spend a Friday night (and even the following Saturday morning) — though I bet some people would argue watching cat videos on YouTube might be better way to spend a Friday night — and even gave me the opportunity to share that solution with you guys.

After sharing this post on Twitter, Sitecore MVP Kam Figy replied to that tweet with the following:

This gave me an idea: why not modify the solution from my previous post to give the ability to download a random image from Giphy via their the API?

You might be asking yourself “what is this Giphy thing?” Giphy is basically a site that allows users to upload images — more specifically animated GIFs — and then associate those uploaded images with tags. These tags are used for finding images on their site and also through their API.

You might be now asking “what’s the point of Giphy?” The point is to have fun and share a laugh; animated GIFs can be a great way of achieving these.

Some smart folks out there have built integrations into other software platforms which give users the ability pull images from the Giphy API. An example of this can be seen in Slack messaging application.

As a side note, if you aren’t on the Sitecore Community Slack, you probably should be. This is the fastest way to get help, share ideas and even have some good laughs from close to 1000 Sitecore developers, architects and marketers from around the world in real-time. If you would like to join the Sitecore Community Slack, please let me know and I will send you an invite though please don’t ask for an invite in comments section below on this post. Instead reach out to me on Twitter: @mike_i_reynolds. You can also reach out to Sitecore MVP Akshay Sura: @akshaysura13.

Here’s an example of me calling up an image using some tags in one of the channels on the Sitecore Community Slack using the Giphy integration for Slack:

giphy-image-slack

There really isn’t anything magical about the Giphy API — all you have to do is send an HTTP request with some query string parameters. Giphy’s API will then give you a response in JSON:

giphy-image-json

Before I dig into the solution below, I do want to let you know I will not be talking about all of the code in the solution. Most of the code was repurposed from my previous post. If you have not read my previous post, please read it before moving forward so you have a full understanding of how this works.

Moreover, do note there is probably no business value in using the following solution as is — it was built for fun on another Friday night and Saturday morning. πŸ˜‰

To get data out of this JSON response, I decided to use Newtonsoft.Json. Why did I choose this? It was an easy decision: Newtonsoft.Json comes with Sitecore “out of the box” so it was convenient for me to choose this as a way to parse the JSON coming from the Giphy API.

I created the following model classes with JSON to C# property mappings:

using Newtonsoft.Json;

namespace Sitecore.Sandbox.Providers
{
    public class GiphyData
    {
        [JsonProperty("type")]
        public string Type { get; set; }

        [JsonProperty("id")]
        public string Id { get; set; }

        [JsonProperty("url")]
        public string Url { get; set; }

        [JsonProperty("image_original_url")]
        public string ImageOriginalUrl { get; set; }

        [JsonProperty("image_url")]
        public string ImageUrl { get; set; }

        [JsonProperty("image_mp4_url")]
        public string ImageMp4Url { get; set; }

        [JsonProperty("image_frames")]
        public string ImageFrames { get; set; }

        [JsonProperty("image_width")]
        public string ImageWidth { get; set; }

        [JsonProperty("image_height")]
        public string ImageHeight { get; set; }

        [JsonProperty("fixed_height_downsampled_url")]
        public string FixedHeightDownsampledUrl { get; set; }

        [JsonProperty("fixed_height_downsampled_width")]
        public string FixedHeightDownsampledWidth { get; set; }

        [JsonProperty("fixed_height_downsampled_height")]
        public string FixedHeightDownsampledHeight { get; set; }

        [JsonProperty("fixed_width_downsampled_url")]
        public string FixedWidthDownsampledUrl { get; set; }

        [JsonProperty("fixed_width_downsampled_width")]
        public string FixedWidthDownsampledWidth { get; set; }

        [JsonProperty("fixed_width_downsampled_height")]
        public string FixedWidthDownsampledHeight { get; set; }

        [JsonProperty("fixed_height_small_url")]
        public string FixedHeightSmallUrl { get; set; }

        [JsonProperty("fixed_height_small_still_url")]
        public string FixedHeightSmallStillUrl { get; set; }

        [JsonProperty("fixed_height_small_width")]
        public string FixedHeightSmallWidth { get; set; }

        [JsonProperty("fixed_height_small_height")]
        public string FixedHeightSmallHeight { get; set; }

        [JsonProperty("fixed_width_small_url")]
        public string FixedWidthSmallUrl { get; set; }

        [JsonProperty("fixed_width_small_still_url")]
        public string FixedWidthSmallStillUrl { get; set; }

        [JsonProperty("fixed_width_small_width")]
        public string FixedWidthSmallWidth { get; set; }

        [JsonProperty("fixed_width_small_height")]
        public string FixedWidthSmallHeight { get; set; }

        [JsonProperty("username")]
        public string Username { get; set; }

        [JsonProperty("caption")]
        public string Caption { get; set; }
    }
}
using Newtonsoft.Json;

namespace Sitecore.Sandbox.Providers
{
    public class GiphyMeta
    {
        [JsonProperty("status")]
        public int Status { get; set; }

        [JsonProperty("msg")]
        public string Message { get; set; }
    }
}
using Newtonsoft.Json;

namespace Sitecore.Sandbox.Providers
{
    public class GiphyResponse
    {
        [JsonProperty("data")]
        public GiphyData Data { get; set; }

        [JsonProperty("meta")]
        public GiphyMeta Meta { get; set; }
    }
}

Every property above in every class represents a JSON property/object in the response coming back from the Giphy API.

Now, we need a way to make a request to the Giphy API. I built the following interface whose instances will do just that:

namespace Sitecore.Sandbox.Providers
{
    public interface IGiphyImageProvider
    {
        GiphyData GetRandomGigphyImageData(string tags);
    }
}

The following class implements the interface above:

using System;
using System.Net;

using Sitecore.Diagnostics;

using Newtonsoft.Json;
using System.IO;

namespace Sitecore.Sandbox.Providers
{
    public class GiphyImageProvider : IGiphyImageProvider
    {
        private string RequestUrlFormat { get; set; }

        private string ApiKey { get; set; }

        public GiphyData GetRandomGigphyImageData(string tags)
        {
            Assert.IsNotNullOrEmpty(RequestUrlFormat, "RequestUrlFormat");
            Assert.IsNotNullOrEmpty(ApiKey, "ApiKey");
            Assert.ArgumentNotNullOrEmpty(tags, "tags");
            string response = GetJsonResponse(GetRequestUrl(tags));
            if(string.IsNullOrWhiteSpace(response))
            {
                return new GiphyData();
            }

            try
            {
                GiphyResponse giphyResponse = JsonConvert.DeserializeObject<GiphyResponse>(response);
                if(giphyResponse != null && giphyResponse.Meta != null && giphyResponse.Meta.Status == 200 && giphyResponse.Data != null)
                {
                    return giphyResponse.Data;
                }
            }
            catch(Exception ex)
            {
                Log.Error(ToString(), ex, this);
            }

            return new GiphyData();
        }

        protected virtual string GetRequestUrl(string tags)
        {
            Assert.ArgumentNotNullOrEmpty(tags, "tags");
            return string.Format(RequestUrlFormat, ApiKey, Uri.EscapeDataString(tags));
        }

        protected virtual string GetJsonResponse(string requestUrl)
        {
            Assert.ArgumentNotNullOrEmpty(requestUrl, "requestUrl");
            try
            {
                WebRequest request = HttpWebRequest.Create(requestUrl);
                request.Method = "GET";
                string json;
                using (WebResponse response = request.GetResponse())
                {
                    using (Stream responseStream = response.GetResponseStream())
                    {
                        using (StreamReader sr = new StreamReader(responseStream))
                        {
                            return sr.ReadToEnd();
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                Log.Error(ToString(), ex, this);
            }

            return string.Empty;
        }
    }
}

Code in the methods above basically take in tags for the type of random image we want from Giphy; build up the request URL — the template of the request URL and API key (I’m using the public key which is open for developers to experiment with) are populated via the Sitecore Configuration Factory (have a look at the patch include configuration file further down in this post to get an idea of how the properties of this class are populated); make the request to the Giphy API; get back the response; hand the response over to some Newtonsoft.Json API code to parse JSON into model instances of the classes shown further above in this post; and then return the nested model instances.

I then created the following Sitecore.Shell.Applications.ContentEditor.Image subclass which represents the custom Content Editor Image field:

using System;

using Sitecore.Configuration;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Pipelines;
using Sitecore.Shell.Framework;
using Sitecore.Web.UI.Sheer;

using Sitecore.Sandbox.Pipelines.DownloadImageToMediaLibrary;
using Sitecore.Sandbox.Providers;

namespace Sitecore.Sandbox.Shell.Applications.ContentEditor
{
    public class GiphyImage : Sitecore.Shell.Applications.ContentEditor.Image
    {
        private IGiphyImageProvider GiphyImageProvider { get; set; }

        public GiphyImage()
            : base()
        {
            GiphyImageProvider = GetGiphyImageProvider();
        }

        protected virtual IGiphyImageProvider GetGiphyImageProvider()
        {
            IGiphyImageProvider giphyImageProvider = Factory.CreateObject("imageProviders/giphyImageProvider", false) as IGiphyImageProvider;
            Assert.IsNotNull(giphyImageProvider, "The giphyImageProvider was not properly defined in configuration");
            return giphyImageProvider;
        }

        public override void HandleMessage(Message message)
        {
            Assert.ArgumentNotNull(message, "message");
            if (string.Equals(message.Name, "contentimage:downloadGiphy", StringComparison.CurrentCultureIgnoreCase))
            {
                GetInputFromUser();
                return;
            }

            base.HandleMessage(message);
        }

        protected void GetInputFromUser()
        {
            RunProcessor("GetGiphyTags", new ClientPipelineArgs());
        }

        protected virtual void GetGiphyTags(ClientPipelineArgs args)
        {
            if (!args.IsPostBack)
            {
                SheerResponse.Input("Enter giphy tags:", string.Empty);
                args.WaitForPostBack();
            }
            else if (args.HasResult)
            {
                args.Parameters["tags"] = args.Result;
                args.IsPostBack = false;
                RunProcessor("GetGiphyImageUrl", args);
            }
            else
            {
                CancelOperation(args);
            }
        }

        protected virtual void GetGiphyImageUrl(ClientPipelineArgs args)
        {
            GiphyData giphyData = GiphyImageProvider.GetRandomGigphyImageData(args.Parameters["tags"]);
            if (giphyData == null || string.IsNullOrWhiteSpace(giphyData.ImageUrl))
            {
                SheerResponse.Alert("Unfortunately, no image matched the tags you specified. Please try again.");
                CancelOperation(args);
                return;
            }

            args.Parameters["imageUrl"] = giphyData.ImageUrl;
            args.IsPostBack = false;
            RunProcessor("ChooseMediaLibraryFolder", args);
        }

        protected virtual void RunProcessor(string processor, ClientPipelineArgs args)
        {
            Assert.ArgumentNotNullOrEmpty(processor, "processor");
            Sitecore.Context.ClientPage.Start(this, processor, args);
        }

        public void ChooseMediaLibraryFolder(ClientPipelineArgs args)
        {
            if (!args.IsPostBack)
            {
                Dialogs.BrowseItem
                (
                    "Select A Media Library Folder",
                    "Please select a media library folder to store the Giphy image.",
                    "Applications/32x32/folder_into.png",
                    "OK",
                    "/sitecore/media library", 
                    string.Empty
                );

                args.WaitForPostBack();
            }
            else if (args.HasResult)
            {
                Item folder = Client.ContentDatabase.Items[args.Result];
                args.Parameters["mediaLibaryFolderPath"] = folder.Paths.FullPath;
                args.IsPostBack = false;
                RunProcessor("DownloadImage", args);
            }
            else
            {
                CancelOperation(args);
            }
        }

        protected virtual void DownloadImage(ClientPipelineArgs args)
        {
            DownloadImageToMediaLibraryArgs downloadArgs = new DownloadImageToMediaLibraryArgs
            {
                Database = Client.ContentDatabase,
                ImageUrl = args.Parameters["imageUrl"],
                MediaLibaryFolderPath = args.Parameters["mediaLibaryFolderPath"]
            };

            CorePipeline.Run("downloadImageToMediaLibrary", downloadArgs);
            SetMediaItemInField(downloadArgs);
        }

        protected virtual void SetMediaItemInField(DownloadImageToMediaLibraryArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            if(string.IsNullOrWhiteSpace(args.MediaId) || string.IsNullOrWhiteSpace(args.MediaPath))
            {
                return;
            }

            XmlValue.SetAttribute("mediaid", args.MediaId);
            Value = args.MediaPath;
            Update();
            SetModified();
        }

        protected virtual void CancelOperation(ClientPipelineArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            args.AbortPipeline();
        }
    }
}

The class above does not differ much from the Image class I shared in my previous post. The only differences are in the instantiation of an IGiphyImageProvider object using the Sitecore Configuration Factory — this object is used for getting the Giphy image URL from the Giphy API; the GetGiphyTags() method prompts the user for tags used in calling up a random image from Giphy; and in the GetGiphyImageUrl() method which uses the IGiphyImageProvider instance to get the image URL. The rest of the code in this class is unmodified from the Image class shared in my previous post.

I then defined the IGiphyImageProvider code in the following patch include configuration file:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <imageProviders>
      <giphyImageProvider type="Sitecore.Sandbox.Providers.GiphyImageProvider, Sitecore.Sandbox" singleInstance="true">
        <RequestUrlFormat>http://api.giphy.com/v1/gifs/random?api_key={0}&amp;tag={1}</RequestUrlFormat>
        <ApiKey>dc6zaTOxFJmzC</ApiKey>
      </giphyImageProvider>
    </imageProviders>
  </sitecore>
</configuration>

Be sure to check out the patch include configuration file from my previous post as it contains the custom pipeline that downloads images from a URL.

You should also refer my previous post which shows you how to register a custom Content Editor field in the core database of Sitecore.

Let’s test this out.

We need to add this new field to a template. I’ve added it to the “out of the box” Sample Item template:
giphy-image-sample-item-new-field

My Home item uses the above template. Let’s download a random Giphy image on it:

giphy-image-home-1

I then supplied some tags for getting a random image:

giphy-image-home-2

Let’s choose a place to save the image in the Media Library:

giphy-image-home-3

As you can see, the image was downloaded and saved into the Media Library in the selected folder, and then saved in the custom field on the Home item:

giphy-image-home-4

If you are curious, this is the image that was returned by the Giphy API:

giphy-image-downloaded

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

Download Images and Save to the Media Library Via a Custom Content Editor Image Field in Sitecore

Yesterday evening — a Friday evening by the way (what, you don’t code on Friday evenings? πŸ˜‰ ) — I wanted to have a bit of fun by building some sort of customization in Sitecore but was struggling on what to build.

After about an hour of pondering, it dawned on me: I was determined to build a custom Content Editor Image field that gives content authors the ability to download images from a supplied URL; save the image to disk; upload the image into the Media Libary; and then set it on a custom Image field of an Item.

I’m sure someone has built something like this in the past and may have even uploaded a module that does this to the Sitecore Marketplace — I didn’t really look into whether this had already been done before since I wanted to have some fun by taking on the challenge. What follows is the fruit of that endeavor.

Before I move forward, I would like to caution you on using the code that follows — I have not rigorously tested this code at all so use at your own risk.

Before I began coding, I thought about how I wanted to approach this challenge. I decided I would build a custom Sitecore pipeline to handle this code. Why? Well, quite frankly, it gives you flexibility on customization, and also native Sitecore code is hugely composed of pipelines — why deviate from the framework?

First, I needed a class whose instances would serve as the custom pipeline’s arguments object. The following class was built for that:

using Sitecore.Data;
using Sitecore.Pipelines;

namespace Sitecore.Sandbox.Pipelines.DownloadImageToMediaLibrary
{
    public class DownloadImageToMediaLibraryArgs : PipelineArgs
    {
        public Database Database { get; set; }

        public string ImageFileName { get; set; }

        public string ImageFilePath { get; set; }

        public string ImageItemName { get; set; }

        public string ImageUrl { get; set; }

        public string MediaId { get; set; }

        public string MediaLibaryFolderPath { get; set; }

        public string MediaPath { get; set; }

        public bool FileBased { get; set; }

        public bool IncludeExtensionInItemName { get; set; }

        public bool OverwriteExisting { get; set; }

        public bool Versioned { get; set; }
    }
}

I didn’t just start off with all of the properties you see on this class — it was an iterative process where I had to go back, add more and even remove some that were no longer needed. You will see why I have these on it from the code below.

I decided to employ the Template method pattern in this code, and defined the following abstract base class which all processors of my custom pipeline will sub-class:

using Sitecore.Diagnostics;

namespace Sitecore.Sandbox.Pipelines.DownloadImageToMediaLibrary
{
    public abstract class DownloadImageToMediaLibraryProcessor
    {
        public void Process(DownloadImageToMediaLibraryArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            if(!CanProcess(args))
            {
                AbortPipeline(args);
                return;
            }

            Execute(args);
        }

        protected abstract bool CanProcess(DownloadImageToMediaLibraryArgs args);

        protected virtual void AbortPipeline(DownloadImageToMediaLibraryArgs args)
        {
            args.AbortPipeline();
        }

        protected abstract void Execute(DownloadImageToMediaLibraryArgs args);
    }
}

All processors of the custom pipeline will have to implement the CanProcess and Execute methods above, and also have the ability to redefine the AbortPipeline method if needed.

The main magic for all processors happen in the Process method above — if the processor can process the data supplied via the arguments object, then it will do so using the Execute method. Otherwise, the pipeline will be aborted via the AbortPipeline method.

The following class serves as the first processor of the custom pipeline.

using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.IO;

namespace Sitecore.Sandbox.Pipelines.DownloadImageToMediaLibrary
{
    public class SetProperties : DownloadImageToMediaLibraryProcessor
    {
        private string UploadDirectory { get; set; }

        protected override bool CanProcess(DownloadImageToMediaLibraryArgs args)
        {
            Assert.IsNotNullOrEmpty(UploadDirectory, "UploadDirectory must be set in configuration!");
            Assert.IsNotNull(args.Database, "args.Database must be supplied!");
            return !string.IsNullOrWhiteSpace(args.ImageUrl)
                && !string.IsNullOrWhiteSpace(args.MediaLibaryFolderPath);
        }

        protected override void Execute(DownloadImageToMediaLibraryArgs args)
        {
            args.ImageFileName = GetFileName(args.ImageUrl);
            args.ImageItemName = GetImageItemName(args.ImageUrl);
            args.ImageFilePath = GetFilePath(args.ImageFileName);
        }

        protected virtual string GetFileName(string url)
        {
            Assert.ArgumentNotNullOrEmpty(url, "url");
            return FileUtil.GetFileName(url);
        }

        protected virtual string GetImageItemName(string url)
        {
            Assert.ArgumentNotNullOrEmpty(url, "url");
            string fileNameNoExtension = GetFileNameNoExtension(url);
            if(string.IsNullOrWhiteSpace(fileNameNoExtension))
            {
                return string.Empty;
            }

            return ItemUtil.ProposeValidItemName(fileNameNoExtension);
        }

        protected virtual string GetFileNameNoExtension(string url)
        {
            Assert.ArgumentNotNullOrEmpty(url, "url");
            return FileUtil.GetFileNameWithoutExtension(url);
        }

        protected virtual string GetFilePath(string fileName)
        {
            Assert.ArgumentNotNullOrEmpty(fileName, "fileName");
            return string.Format("{0}/{1}", FileUtil.MapPath(UploadDirectory), fileName);
        }
    }
}

Instances of the above class will only run if an upload directory is supplied via configuration (see the patch include configuration file down below); a Sitecore Database is supplied (we have to upload this image somewhere); an image URL is supplied (can’t download an image without this); and a Media Library folder is supplied (where are we storing this image?).

The Execute method then sets additional properties on the arguments object that the next processors will need in order to complete their tasks.

The following class serves as the second processor of the custom pipeline. This processor will download the image from the supplied URL:

using System.Net;

namespace Sitecore.Sandbox.Pipelines.DownloadImageToMediaLibrary
{
    public class DownloadImage : DownloadImageToMediaLibraryProcessor
    {
        protected override bool CanProcess(DownloadImageToMediaLibraryArgs args)
        {
            return !string.IsNullOrWhiteSpace(args.ImageUrl)
                && !string.IsNullOrWhiteSpace(args.ImageFilePath);
        }

        protected override void Execute(DownloadImageToMediaLibraryArgs args)
        {
            using (WebClient client = new WebClient())
            {
                client.DownloadFile(args.ImageUrl, args.ImageFilePath);
            }
        }
    }
}

The processor instance of the above class will only execute when an image URL is supplied and a location on the file system is given — this is the location on the file system where the image will live before being uploaded into the Media Library.

If all checks out, the image is downloaded from the given URL into the specified location on the file system.

The next class serves as the third processor of the custom pipeline. This processor will upload the image on disk to the Media Library:

using System.IO;

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

namespace Sitecore.Sandbox.Pipelines.DownloadImageToMediaLibrary
{
    public class UploadImageToMediaLibrary : DownloadImageToMediaLibraryProcessor
    {
        private string Site { get; set; }

        protected override bool CanProcess(DownloadImageToMediaLibraryArgs args)
        {
            Assert.IsNotNullOrEmpty(Site, "Site must be set in configuration!");
            return !string.IsNullOrWhiteSpace(args.MediaLibaryFolderPath)
                && !string.IsNullOrWhiteSpace(args.ImageItemName)
                && !string.IsNullOrWhiteSpace(args.ImageFilePath)
                && args.Database != null;
        }

        protected override void Execute(DownloadImageToMediaLibraryArgs args)
        {
            MediaCreatorOptions options = new MediaCreatorOptions
            {
                Destination = GetMediaLibraryDestinationPath(args),
                FileBased = args.FileBased,
                IncludeExtensionInItemName = args.IncludeExtensionInItemName,
                OverwriteExisting = args.OverwriteExisting,
                Versioned = args.Versioned,
                Database = args.Database
            };
            
            MediaCreator creator = new MediaCreator();
            MediaItem mediaItem;
            using (SiteContextSwitcher switcher = new SiteContextSwitcher(GetSiteContext()))
            {
                using (FileStream fileStream = File.OpenRead(args.ImageFilePath))
                {
                    mediaItem = creator.CreateFromStream(fileStream, args.ImageFilePath, options);
                }
            }
            
            if (mediaItem == null)
            {
                AbortPipeline(args);
                return;
            }
            
            args.MediaId = mediaItem.ID.ToString();
            args.MediaPath = mediaItem.MediaPath;
        }

        protected virtual SiteContext GetSiteContext()
        {
            SiteContext siteContext = SiteContextFactory.GetSiteContext(Site);
            Assert.IsNotNull(siteContext, string.Format("The site: {0} does not exist!", Site));
            return siteContext;
        }

        protected virtual string GetMediaLibraryDestinationPath(DownloadImageToMediaLibraryArgs args)
        {
            return string.Format("{0}/{1}", args.MediaLibaryFolderPath, args.ImageItemName);
        }
    }
}

The processor instance of the class above will only run when we have a Media Library folder location; an Item name for the image; a file system path for the image; and a Database to upload the image to. I also ensure a “site” is supplied via configuration so that I can switch the site context — when using the default of “shell”, I was being brought to the image Item in the Media Library after it was uploaded which was causing the image not to be set on the custom Image field on the Item.

If everything checks out, we upload the image to the Media Library in the specified location.

The next class serves as the last processor of the custom pipeline. This processor just deletes the image from the file system (why keep it around since we are done with it?):

using Sitecore.IO;

namespace Sitecore.Sandbox.Pipelines.DownloadImageToMediaLibrary
{
    public class DeleteImageFromFileSystem : DownloadImageToMediaLibraryProcessor
    {
        protected override bool CanProcess(DownloadImageToMediaLibraryArgs args)
        {
            return !string.IsNullOrWhiteSpace(args.ImageFilePath)
                && FileUtil.FileExists(args.ImageFilePath);
        }

        protected override void Execute(DownloadImageToMediaLibraryArgs args)
        {
            FileUtil.Delete(args.ImageFilePath);
        }
    }
}

The processor instance of the class above can only delete the image if its path is supplied and the file exists.

If all checks out, the image is deleted.

The next class is the class that serves as the custom Image field:

using System;
using System.Net;

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

using Sitecore.Sandbox.Pipelines.DownloadImageToMediaLibrary;

namespace Sitecore.Sandbox.Shell.Applications.ContentEditor
{
    public class Image : Sitecore.Shell.Applications.ContentEditor.Image
    {
        public Image()
            : base()
        {
        }

        public override void HandleMessage(Message message)
        {
            Assert.ArgumentNotNull(message, "message");
            if (string.Equals(message.Name, "contentimage:download", StringComparison.CurrentCultureIgnoreCase))
            {
                GetInputFromUser();
                return;
            }

            base.HandleMessage(message);
        }

        protected void GetInputFromUser()
        {
            RunProcessor("GetImageUrl", new ClientPipelineArgs());
        }

        protected virtual void GetImageUrl(ClientPipelineArgs args)
        {
            if (!args.IsPostBack)
            {
                SheerResponse.Input("Enter the url of the image to download:", string.Empty);
                args.WaitForPostBack();
            }
            else if (args.HasResult && IsValidUrl(args.Result))
            {
                args.Parameters["imageUrl"] = args.Result;
                args.IsPostBack = false;
                RunProcessor("ChooseMediaLibraryFolder", args);
            }
            else
            {
                CancelOperation(args);
            }
        }

        protected virtual bool IsValidUrl(string url)
        {
            if (string.IsNullOrWhiteSpace(url))
            {
                return false;
            }

            try
            {
                HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(url);
                request.Method = "HEAD";
                request.GetResponse();
            }
            catch (Exception ex)
            {
                SheerResponse.Alert("The specified url is not valid. Please try again.");
                return false;
            }

            return true;
        }

        protected virtual void RunProcessor(string processor, ClientPipelineArgs args)
        {
            Assert.ArgumentNotNullOrEmpty(processor, "processor");
            Sitecore.Context.ClientPage.Start(this, processor, args);
        }

        public void ChooseMediaLibraryFolder(ClientPipelineArgs args)
        {
            if (!args.IsPostBack)
            {
                Dialogs.BrowseItem
                (
                    "Select A Media Library Folder",
                    "Please select a media library folder to store this image.",
                    "Applications/32x32/folder_into.png",
                    "OK",
                    "/sitecore/media library", 
                    string.Empty
                );

                args.WaitForPostBack();
            }
            else if (args.HasResult)
            {
                Item folder = Client.ContentDatabase.Items[args.Result];
                args.Parameters["mediaLibaryFolderPath"] = folder.Paths.FullPath;
                RunProcessor("DownloadImage", args);
            }
            else
            {
                CancelOperation(args);
            }
        }

        protected virtual void DownloadImage(ClientPipelineArgs args)
        {
            DownloadImageToMediaLibraryArgs downloadArgs = new DownloadImageToMediaLibraryArgs
            {
                Database = Client.ContentDatabase,
                ImageUrl = args.Parameters["imageUrl"],
                MediaLibaryFolderPath = args.Parameters["mediaLibaryFolderPath"]
            };

            CorePipeline.Run("downloadImageToMediaLibrary", downloadArgs);
            SetMediaItemInField(downloadArgs);
        }

        protected virtual void SetMediaItemInField(DownloadImageToMediaLibraryArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            if(string.IsNullOrWhiteSpace(args.MediaId) || string.IsNullOrWhiteSpace(args.MediaPath))
            {
                return;
            }

            XmlValue.SetAttribute("mediaid", args.MediaId);
            Value = args.MediaPath;
            Update();
            SetModified();
        }

        protected virtual void CancelOperation(ClientPipelineArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            args.AbortPipeline();
        }
    }
}

The class above subclasses the Sitecore.Shell.Applications.ContentEditor.Image class — this lives in Sitecore.Kernel.dll — which is the “out of the box” Content Editor Image field. The Sitecore.Shell.Applications.ContentEditor.Image class provides hooks that we can override in order to augment functionality which I am doing above.

The magic of this class starts in the HandleMessage method — I intercept the message for a Menu item option that I define below for downloading an image from a URL.

If we are to download an image from a URL, we first prompt the user for a URL via the GetImageUrl method using a Sheer UI api call (note: I am running these methods as one-off client pipeline processors as this is the only way you can get Sheer UI to run properly).

If we have a valid URL, we then prompt the user for a Media Library location via another Sheer UI dialog (this is seen in the ChooseMediaLibraryFolder method).

If the user chooses a location in the Media Library, we then call the DownloadImage method as a client pipeline processor — I had to do this since I was seeing some weird behavior on when the image was being saved into the Media Library — which invokes the custom pipeline for downloading the image to the file system; uploading it into the Media Library; and then removing it from disk.

I then duct-taped everything together using the following patch include configuration file:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <controlSources>
      <source mode="on" namespace="Sitecore.Sandbox.Shell.Applications.ContentEditor" assembly="Sitecore.Sandbox" prefix="sandbox-content"/>
    </controlSources>
    <overrideDialogs>
      <override dialogUrl="/sitecore/shell/Applications/Item%20browser.aspx" with="/sitecore/client/applications/dialogs/InsertSitecoreItemViaTreeDialog">
        <patch:delete/>
      </override>
    </overrideDialogs>
    <pipelines>
      <downloadImageToMediaLibrary>
        <processor type="Sitecore.Sandbox.Pipelines.DownloadImageToMediaLibrary.SetProperties, Sitecore.Sandbox">
          <UploadDirectory>/upload</UploadDirectory>
        </processor>  
        <processor type="Sitecore.Sandbox.Pipelines.DownloadImageToMediaLibrary.DownloadImage, Sitecore.Sandbox" />
        <processor type="Sitecore.Sandbox.Pipelines.DownloadImageToMediaLibrary.UploadImageToMediaLibrary, Sitecore.Sandbox">
          <Site>website</Site>
        </processor>
        <processor type="Sitecore.Sandbox.Pipelines.DownloadImageToMediaLibrary.DeleteImageFromFileSystem, Sitecore.Sandbox" />
      </downloadImageToMediaLibrary>
    </pipelines>
  </sitecore>
</configuration>

One thing to note in the above file: I’ve disabled the SPEAK dialog for the Item Browser — you can see this in the <overrideDialogs> xml element — as I wasn’t able to set messaging text on it but could do so using the older Sheer UI dialog.

Now that all code is in place, we need to tell Sitecore that we have a new Image field. I do so by defining it in the Core database:

external-image-core-1

We also need a new Menu item for the “Download Image” link:

external-image-core-2

Let’s take this for a spin!

I added a new field using the custom Image field type to my Sample item template:

template-new-field-external-image

As you can see, we have this new Image field on my “out of the box” home item. Let’s click the “Download Image” link:

home-external-image-1

I was then prompted with a dialog to supply an image URL. I pasted one I found on the internet:

home-external-image-2

After clicking “OK”, I was prompted with another dialog to choose a Media Library location for storing the image. I chose some random folder:

home-external-image-3

After clicking “OK” on that dialog, the image was magically downloaded from the internet; uploaded into the Media Library; and set in the custom Image field on my home item:

home-external-image-4

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

Addendum:
It’s not a good idea to use the /upload directory for temporarily storing download images — see this post for more details.

If you decide to use this solution — by the way, use this solution at your own risk πŸ˜‰ — you will have to change the /upload directory in the patch include configuration file above.

Inject Dependencies Into Sitecore MVC Razor Views Using a SitecoreHelper Extension Method

Last week before the Christmas holiday break, I came up with a solution similar to what I am going to show here except that I had used the Simple Injector Dependency Injection framework in that solution (if you would like me to add a blog post on that approach, please let me know in a comment).

I wanted a “quick and dirty” solution — well, I guess this is really in the eye of the beholder on whether the following is a good idea or not, but I’m throwing it out there just in case it helps out someone — where I could inject a dependency into an MVC Razor view via an HtmlHelper extension method, and in that solution, I used this approach for injecting an object instance which grabs values from a Sitecore Dictionary Domain. I am going to recreate this solution here though using the Sitecore Configuration Factory (we’re all not using Simple Injector in our solutions but the Sitecore Configuration Factory is available to all of us via the Sitecore API).

I first defined the following interface for class instances which return a value for a given dictionary entry key:

using Sitecore.Globalization;

namespace Sitecore.Sandbox.Dictionaries
{
    public interface ITranslator
    {
        string Text(string key, params object[] parameters);

        string Text(TranslateOptions options, string key, params object[] parameters);
    }
}

The following class implements the interface above:

using Sitecore.Diagnostics;
using Sitecore.Globalization;

namespace Sitecore.Sandbox.Dictionaries
{
    public class DomainDictionaryTranslator : ITranslator
    {
        public string Domain { get; set; }

        public virtual string Text(string key, params object[] parameters)
        {
            AssertDomainDictionary();
            Assert.ArgumentNotNullOrEmpty(key, "key");
            return Translate.TextByDomain(Domain, key, parameters);
        }

        public virtual string Text(TranslateOptions options, string key, params object[] parameters)
        {
            AssertDomainDictionary();
            Assert.ArgumentNotNullOrEmpty(key, "key");
            return Translate.TextByDomain(Domain, options, key, parameters);
        }

        protected virtual void AssertDomainDictionary()
        {
            Assert.IsNotNullOrEmpty(Domain, "The Domain must be set!");
        }
    }
}

Client code of the class above must supply the Domain name for the Sitecore Dictionary via the Domain property on the class.

Client code can then use either Text method by supplying a key for the lookup — both methods just delegate to the corresponding static methods defined on the Sitecore.Globalization.Translate class.

We now need classes that serve as Factories. I created the following interface for such classes:

namespace Sitecore.Sandbox.Helpers.Sitecore
{
    public interface IFactory
    {
        T CreateObject<T>(string configPath, bool assert) where T : class;
    }
}

The following class implements the above interface, and basically has one method that delegates to the static CreateObject() method on the Sitecore.Configuration.Factory class:

using Sitecore.Configuration;

namespace Sitecore.Sandbox.Helpers.Sitecore
{
    public class ConfigurationFactory : IFactory
    {
        public T CreateObject<T>(string configPath, bool assert) where T : class
        {
            return Factory.CreateObject(configPath, assert) as T;
        }
    }
}

Now, we need an extension method on the Sitecore.Mvc.Helpers.SitecoreHelper class — this lives in Sitecore.Mvc.dll — so that we can leverage the code defined above (check out Sitecore MVP Kevin BrechbΓΌhl’s blog post where he talks about this approach as well as another):

using Sitecore.Configuration;
using Sitecore.Diagnostics;
using Sitecore.Mvc.Helpers;

namespace Sitecore.Sandbox.Helpers.Sitecore
{
    public static class ConfigurationFactoryHelper
    {
        private static IFactory ConfigurationFactory { get; set; }

        static ConfigurationFactoryHelper()
        {
            ConfigurationFactory = CreateFactory();
        }

        public static T CreateObject<T>(this SitecoreHelper sitecoreHelper, string configPath, bool assert) where T : class
        {
            return ConfigurationFactory.CreateObject<T>(configPath, assert);
        }

        private static IFactory CreateFactory()
        {
            IFactory factory = Factory.CreateObject("configurationFactory", true) as IFactory;
            Assert.IsNotNull(factory, "The configurationFactory must be defined in configuration!");
            return factory;
        }
    }
}

We are instantiating an instance of the ConfigurationFactory class defined above via the Sitecore Configuration Factory, and then setting it on a static property in this class — all members and methods on this class must be static since it contains extension methods.

The CreateObject() method just delegates to the ConfigurationFactory class instance’s CreateObject() method and returns its value.

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

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <configurationFactory type="Sitecore.Sandbox.Helpers.Sitecore.ConfigurationFactory, Sitecore.Sandbox" singleInstance="true" />
    <domainDictionaryTranslator type="Sitecore.Sandbox.Dictionaries.DomainDictionaryTranslator, Sitecore.Sandbox" singleInstance="true">
      <Domain>MyCoolDictionary</Domain>
    </domainDictionaryTranslator>
  </sitecore>
</configuration>

Let’s see this in action!

First, we need a Dictionary Domain. I created one with the following folder with entries:

label-one-dictionary

label-two-dictionary

Now, we new a Razor view to make this work. I created the following, and mapped it to my home page’s presentation details:

@using Sitecore.Mvc;
@using Sitecore.Sandbox.Helpers.Sitecore
@using Sitecore.Sandbox.Dictionaries

@{
    var translator = Html.Sitecore().CreateObject<ITranslator>("domainDictionaryTranslator", true);
    if (translator == null)
    {
        return;
    }

    var labelOne = translator.Text("mycooldictionary.somelabels.labelone");
    var labelTwo = translator.Text("mycooldictionary.somelabels.labeltwo");
    if (string.IsNullOrWhiteSpace(labelOne) && string.IsNullOrWhiteSpace(labelTwo))
    {
        return;
    }
}

<div>
    @if (!string.IsNullOrWhiteSpace(labelOne))
    {
        <h2>@labelOne</h2>
    }

    @if (!string.IsNullOrWhiteSpace(labelTwo))
    {
        <h2>@labelTwo</h2>
    }
</div>

After building and deploying, I navigated to my home page. As you can see, the dictionary entry values display:

dictionary-values-displayed

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

Render a Custom General Link Field Attribute using a Custom Glass.Mapper Controller in Sitecore

In my previous posts — please be sure to read this post followed by this post and then this post before moving forward since you’ll need some context, and some of the code below is dependent on code in these previous posts — I showed two approaches for rendering a custom attribute — I called this attribute Tag in these posts and will continue to do so here — on a link set in a General Link field which is rendered using the Sitecore ORM Glass.Mapper.

In this post, I am going to share an approach on achieving the same but using a custom GlassController coupled with a class that implements the IGlassHtml interface — the code for this class can be found in this post.

First, we need a model to experiment with. I built the following class to serve as an example for this post:

using Sitecore.Data;

using Glass.Mapper.Sc.Configuration.Attributes;

using Sitecore.Sandbox.Glass.Mapper.Sc.Fields;

namespace Sitecore.Sandbox.Models
{
    public class SampleItemModel
    {
        [SitecoreId]
        ID ItemID { get; set; }

        public string Title { get; set; }

        public string Text { get; set; }

        [SitecoreField("Link One")]
        public TagLink LinkOne { get; set; }

        [SitecoreField("Link Two")]
        public TagLink LinkTwo { get; set; }
    }
}

Next, we need a ViewModel. I built the following as well to serve as an example for this post:

using System.Web.Mvc;

using Sitecore.Data;

using Glass.Mapper.Sc.Configuration.Attributes;

namespace Sitecore.Sandbox.Models.ViewModels
{
    public class SampleItemViewModel
    {
        public MvcHtmlString Title { get; set; }

        public MvcHtmlString Text { get; set; }

        public MvcHtmlString LinkOne { get; set; }

        public MvcHtmlString LinkTwo { get; set; }
    }
}

I then built the following subclass of Glass.Mapper.Sc.Web.Mvc.GlassController:

using System;
using System.Linq.Expressions;
using System.Web;
using System.Web.Mvc;

using Sitecore.Configuration;
using Sitecore.Diagnostics;

using Glass.Mapper.Sc;
using Glass.Mapper.Sc.Web.Mvc;
using Glass.Mapper.Sc.Web;
using GlassMapperSc = Glass.Mapper.Sc;

namespace Sitecore.Sandbox.Glass.Mapper.Sc.Web.Mvc.Controllers
{
    public abstract class SandboxGlassController : GlassController
    {
        private static IGlassHtmlFactory GlassHtmlFactory { get; set; }

        static SandboxGlassController()
        {
            GlassHtmlFactory = CreateGlassHtmlFactory();
        }

        public SandboxGlassController() 
            : this(GetContextFromHttp())
        {
        }

        protected SandboxGlassController(ISitecoreContext sitecoreContext) 
            : this(sitecoreContext, GlassHtmlFactory.CreateGlassHtml(sitecoreContext), new RenderingContextMvcWrapper(), null)
        {
        }

        public SandboxGlassController(ISitecoreContext sitecoreContext, IGlassHtml glassHtml, IRenderingContext renderingContextWrapper, HttpContextBase httpContext)
            : base(sitecoreContext, glassHtml, renderingContextWrapper, httpContext)
        {
        }

        protected static IGlassHtmlFactory CreateGlassHtmlFactory()
        {
            IGlassHtmlFactory factory = Factory.CreateObject("sandbox.Glass.Mvc/glassHtmlFactory", true) as IGlassHtmlFactory;
            Assert.IsNotNull(factory, "Be sure the configuration is correct in utilities/customAttributesAdder of your Sitecore configuration!");
            return factory;
        }

        protected static ISitecoreContext GetContextFromHttp()
        {
            try
            {
                return GlassMapperSc.SitecoreContext.GetFromHttpContext(null);
            }
            catch (Exception exception)
            {
                Log.Error("Failed to create SitecoreContext", exception, typeof(SandboxGlassController));
            }

            return null;
        }

        protected virtual MvcHtmlString Editable<T>(T model, Expression<Func<T, object>> field, object parameters = null)
        {
            return ConvertToMvcHtmlString(GlassHtml.Editable(model, field, parameters));
        }

        protected virtual MvcHtmlString RenderLink<T>(T model, Expression<Func<T, object>> link, object attributes = null, bool IsEditable = false)
        {
            return ConvertToMvcHtmlString(GlassHtml.RenderLink(model, link, attributes, IsEditable));
        }

        protected virtual MvcHtmlString ConvertToMvcHtmlString(string value)
        {
            return new MvcHtmlString(value);
        }
    }
}

I declared the above class as abstract since I don’t see how it could live on its own — none of its methods return ViewResult or ActionResult instances.

When an instance of the above class is created, an instance of a IGlassHtmlFactory — this is defined in my last post — is used to create an instance of a IGlassHtml which adds a Tag attribute and value to the link attributes when applicable. The Editable and RenderLink methods delegate to methods with the same signature on the IGlassHtml instance, and then transform the returned strings into MvcHtmlString instances via the ConvertToMvcHtmlString method so that the fields work in the Sitecore Experience Editor

I then built the following subclass of the above class for testing:

using System.Web.Mvc;

using Sitecore.Sandbox.Glass.Mapper.Sc.Web.Mvc.Controllers;
using Sitecore.Sandbox.Models;
using Sitecore.Sandbox.Models.ViewModels;

namespace Sitecore.Sandbox.Web.Mvc.Controllers
{
    public class SampleItemController : SandboxGlassController
    {
        public ViewResult MainContent()
        {
            SampleItemModel model = SitecoreContext.GetCurrentItem<SampleItemModel>();
            if(model == null)
            {
                return View();
            }

            SampleItemViewModel viewModel = new SampleItemViewModel
            {
                Title = Editable(model, x => x.Title),
                Text = Editable(model, x => x.Text),
                LinkOne = RenderLink(model, x => x.LinkOne, null, true),
                LinkTwo = RenderLink(model, x => x.LinkTwo, null, true)
            };

            return View(viewModel);
        }
    }
}

The MainContent method gets an instance of a SampleItemModel from the current Item — in this example these are fields on the home Item — and transforms this into a SampleItemViewModel instance using the protected methods defined on its base class. The SampleItemViewModel instance is then sent to the View.

I then whipped up the following Razor View so we can see some data on the front-end:

@model Sitecore.Sandbox.Models.ViewModels.SampleItemViewModel

@if(Model == null)
{
    return;
}

<div id="Content">
    <div id="LeftContent">
    </div>
    <div id="CenterColumn">
        <div id="Header">
            <img src="/~/media/Default Website/sc_logo.png" id="scLogo" />
        </div>
        <h1 class="contentTitle">
            @Model.Title
        </h1>
        <div class="contentDescription">
            @Model.Text
            <div>
                @Model.LinkOne
            </div>
            <div>
                @Model.LinkTwo
            </div>
        </div>
    </div>
</div>

There isn’t much going on in the above Razor View — it’s just displaying data from the ViewModel.

I am going to omit how I wired-up the Controller above with a Controller rendering in Sitecore. If you don’t know how to wire-up Controllers with Controller renderings in Sitecore, please watch this video by Martina Welander.

Let’s see if this works (works on my machine πŸ˜‰ ).

First, I ensured I had Tag atrributes set on my two General Link fields on my home Item in Sitecore:

tag-attributes-are-set-controller

Once I built and deployed everything, I navigated to my homepage Item, and saw the following rendered HTML:

tag-attribute-rendered-controller

As you can see, it worked. πŸ˜€

If you have any questions/comments/thoughts, please leave a comment.

A 2nd Approach to Render a Custom General Link Field Attribute in a Sitecore MVC View Rendering via Glass.Mapper

In my previous post, I shared an approach for customizing the Glass.Mapper Sitecore ORM to render a custom attribute on a link defined in a General Link field (I called this attribute Tag and will continue to do so in this post).

In this post, I will share a second approach — an approach that extends the “out of the box” Html Helper in Glass.

Note: be sure to read this post first followed by my last post before reading the current post — I am omitting code from both of these which is used here.

I first created a class that implements the Glass.Mapper.Sc.IGlassHtml interface:

using System;
using System.Collections.Specialized;
using System.ComponentModel.Composition;
using System.IO;
using System.Linq.Expressions;

using Sitecore.Collections;
using Sitecore.Configuration;
using Sitecore.Data;
using Sitecore.Diagnostics;

using Glass.Mapper.Sc;
using Glass.Mapper.Sc.Fields;
using Glass.Mapper.Sc.Web.Ui;
using Utilities = Glass.Mapper.Utilities;

using Sitecore.Sandbox.Glass.Mapper.Sc.Attributes;
using Sitecore.Sandbox.Glass.Mapper.Sc.Fields;

namespace Sitecore.Sandbox.Glass.Mapper.Sc.Web.Mvc
{
    public class SandboxGlassHtml : IGlassHtml
    {
        private ICustomAttributesAdder attributesAdder;
        private ICustomAttributesAdder AttributesAdder
        {
            get
            {
                if (attributesAdder == null)
                {
                    attributesAdder = GetCustomAttributesAdder();
                }

                return attributesAdder;
            }
        }

        private IGlassHtml InnerGlassHtml { get; set; }

        public ISitecoreContext SitecoreContext
        {
            get
            {
                return InnerGlassHtml.SitecoreContext;
            }
        }

        public SandboxGlassHtml(ISitecoreContext sitecoreContext)
            : this(new GlassHtml(sitecoreContext))
        {
        }

        protected SandboxGlassHtml(IGlassHtml innerGlassHtml)
        {
            SetInnerGlassHtml(innerGlassHtml);
        }

        private void SetInnerGlassHtml(IGlassHtml innerGlassHtml)
        {
            Assert.ArgumentNotNull(innerGlassHtml, "innerGlassHtml");
            InnerGlassHtml = innerGlassHtml;
        }

        public virtual RenderingResult BeginRenderLink<T>(T model, Expression<Func<T, object>> field, TextWriter writer, object attributes = null, bool isEditable = false)
        {
            object attributesModified = AttributesAdder.AddTagAttribute(model, field, attributes);
            return InnerGlassHtml.BeginRenderLink(model, field, writer, attributesModified, isEditable);
        }

        public virtual string Editable<T>(T target, Expression<Func<T, object>> field, object parameters = null)
        {
            return InnerGlassHtml.Editable(target, field, parameters);
        }

        public virtual string Editable<T>(T target, Expression<Func<T, object>> field, Expression<Func<T, string>> standardOutput, object parameters = null)
        {
            return InnerGlassHtml.Editable(target, field, standardOutput, parameters);
        }

        public virtual GlassEditFrame EditFrame(string buttons, string path = null, TextWriter output = null)
        {
            return InnerGlassHtml.EditFrame(buttons, path, output);
        }

        public virtual GlassEditFrame EditFrame<T>(T model, string title = null, TextWriter output = null, params Expression<Func<T, object>>[] fields) where T : class
        {
            return InnerGlassHtml.EditFrame(model, title, output, fields);
        }

        public virtual T GetRenderingParameters<T>(NameValueCollection parameters) where T : class
        {
            return InnerGlassHtml.GetRenderingParameters<T>(parameters);
        }

        public virtual T GetRenderingParameters<T>(string parameters) where T : class
        {
            return InnerGlassHtml.GetRenderingParameters<T>(parameters);
        }

        public virtual T GetRenderingParameters<T>(NameValueCollection parameters, ID renderParametersTemplateId) where T : class
        {
            return InnerGlassHtml.GetRenderingParameters<T>(parameters, renderParametersTemplateId);
        }

        public virtual T GetRenderingParameters<T>(string parameters, ID renderParametersTemplateId) where T : class
        {
            return InnerGlassHtml.GetRenderingParameters<T>(parameters, renderParametersTemplateId);
        }

        public virtual string RenderImage<T>(T model, Expression<Func<T, object>> field, object parameters = null, bool isEditable = false, bool outputHeightWidth = false)
        {
            return InnerGlassHtml.RenderImage(model, field, parameters, isEditable, outputHeightWidth);
        }

        public virtual string RenderLink<T>(T model, Expression<Func<T, object>> field, object attributes = null, bool isEditable = false, string contents = null)
        {
            object attributesModified = AttributesAdder.AddTagAttribute(model, field, attributes);
            return InnerGlassHtml.RenderLink(model, field, attributesModified, isEditable, contents);
        }

        public virtual string ProtectMediaUrl(string url)
        {
            return InnerGlassHtml.ProtectMediaUrl(url);
        }

        protected virtual ICustomAttributesAdder GetCustomAttributesAdder()
        {
            return CustomAttributesAdder.Current;
        }
    }
}

In the above class, I’m using the Decorator Pattern — another Glass.Mapper.Sc.IGlassHtml instance (this is set to an instance of Glass.Mapper.Sc.GlassHtml by default — have a look at the public constructor above) is passed to the class instance and stored in a private property. Every interface-defined method implemented in this class delegates to the inner-IGlassHtml instance.

Since I’m only targeting links in this solution, I utilize a CustomAttributesAdder instance — this is a Singleton which I shared in my last post which is defined in the Sitecore configuration file further down in this post — in both RenderLink methods. The CustomAttributesAdder instance adds the Tag attribute name and value to the attributes collection when applicable. The modified/unmodified attributes collection is then passed to the RenderLink method with the same signature on the inner Glass.Mapper.Sc.IGlassHtml instance.

Now, we need a way to instantiate the above class. I decided to create the following interface for classes that create instances of classes that implement the Glass.Mapper.Sc.IGlassHtml interface:

using Glass.Mapper.Sc;

namespace Sitecore.Sandbox.Glass.Mapper.Sc.Web.Mvc
{
    public interface IGlassHtmlFactory
    {
        IGlassHtml CreateGlassHtml(ISitecoreContext sitecoreContext);
    }
}

I then built the following class which creates an instance of the SandboxGlassHtml class defined above:

using Sitecore.Diagnostics;

using Glass.Mapper.Sc;

namespace Sitecore.Sandbox.Glass.Mapper.Sc.Web.Mvc
{
    public class SandboxGlassHtmlFactory : IGlassHtmlFactory
    {
        public IGlassHtml CreateGlassHtml(ISitecoreContext sitecoreContext)
        {
            Assert.ArgumentNotNull(sitecoreContext, "sitecoreContext");
            return new SandboxGlassHtml(sitecoreContext);
        }
    }
}

There isn’t much going on in the the class above exception object instantiation — the above is an example of the Factory method pattern for those who are curious.

Now, we need an extension method on the ASP.NET MVC HtmlHelper instance used in our Razor views in order to leverage the custom Glass.Mapper.Sc.IGlassHtml class defined above:

using System.Web.Mvc;

using Sitecore.Configuration;
using Sitecore.Diagnostics;

using Glass.Mapper.Sc;
using Glass.Mapper.Sc.Web.Mvc;

using Sitecore.Sandbox.Glass.Mapper.Sc.Attributes;

namespace Sitecore.Sandbox.Glass.Mapper.Sc.Web.Mvc
{
    public static class SandboxHtmlHelperExtensions
    {
        private static IGlassHtmlFactory GlassHtmlFactory { get; set; }

        static SandboxHtmlHelperExtensions()
        {
            GlassHtmlFactory = CreateGlassHtmlFactory();
        }

        public static GlassHtmlMvc<T> SandboxGlass<T>(this HtmlHelper<T> htmlHelper)
        {
            IGlassHtml glassHtml = GlassHtmlFactory.CreateGlassHtml(SitecoreContext.GetFromHttpContext(null));
            Assert.IsNotNull(glassHtml, "glassHtml cannot be null!");
            return new GlassHtmlMvc<T>(glassHtml, htmlHelper.ViewContext.Writer, htmlHelper.ViewData.Model);
        }

        private static IGlassHtmlFactory CreateGlassHtmlFactory()
        {
            IGlassHtmlFactory factory = Factory.CreateObject("sandbox.Glass.Mvc/glassHtmlFactory", true) as IGlassHtmlFactory;
            Assert.IsNotNull(factory, "Be sure the configuration is correct in utilities/customAttributesAdder of your Sitecore configuration!");
            return factory;
        }
    }
}

In the SandboxGlass method above, we instantiate an instance of the IGlassHtmlFactory which is defined in Sitecore configuration (see the patch configuration file below) and use it to create an instance of whatever Glass.Mapper.Sc.IGlassHtml it is tasked to create (in our case here it’s an instance of the SandboxGlassHtml class defined above). This is then passed to a newly created instance of Glass.Mapper.Sc.Web.Mvc.GlassHtmlMvc.

I then glued all the pieces together using the following Sitecore patch configuration file:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <controlSources>
      <source mode="on" namespace="Sitecore.Sandbox.Shell.Applications.ContentEditor" assembly="Sitecore.Sandbox" prefix="sandbox-content"/>
    </controlSources>
    <fieldTypes>
      <fieldType name="General Link">
        <patch:attribute name="type">Sitecore.Sandbox.Data.Fields.TagLinkField, Sitecore.Sandbox</patch:attribute>
      </fieldType>
      <fieldType name="General Link with Search">
        <patch:attribute name="type">Sitecore.Sandbox.Data.Fields.TagLinkField, Sitecore.Sandbox</patch:attribute>
      </fieldType>
      <fieldType name="link">
        <patch:attribute name="type">Sitecore.Sandbox.Data.Fields.TagLinkField, Sitecore.Sandbox</patch:attribute>
        </fieldType>
    </fieldTypes>
    <pipelines>
      <dialogInfo>
        <processor type="Sitecore.Sandbox.Pipelines.DialogInfo.SetDialogInfo, Sitecore.Sandbox">
          <ParameterNameAttributeName>name</ParameterNameAttributeName>
          <ParameterValueAttributeName>value</ParameterValueAttributeName>
          <Message>contentlink:externallink</Message>
          <Url>/sitecore/shell/Applications/Dialogs/External link.aspx</Url>
          <parameters hint="raw:AddParameter">
            <parameter name="height" value="300" />
          </parameters>
        </processor>
      </dialogInfo>
      <renderField>
        <processor patch:after="processor[@type='Sitecore.Pipelines.RenderField.GetInternalLinkFieldValue, Sitecore.Kernel']" 
                   type="Sitecore.Sandbox.Pipelines.RenderField.SetTagAttributeOnLink, Sitecore.Sandbox">
          <TagXmlAttributeName>tag</TagXmlAttributeName>
          <TagAttributeName>tag</TagAttributeName>
          <BeginningHtml>&lt;a </BeginningHtml>
        </processor>  
      </renderField>
    </pipelines>
    <sandbox.Glass.Mvc>
      <customAttributesAdder type="Sitecore.Sandbox.Glass.Mapper.Sc.Attributes.CustomAttributesAdder, Sitecore.Sandbox" singleInstance="true" />
      <glassHtmlFactory type="Sitecore.Sandbox.Glass.Mapper.Sc.Web.Mvc.SandboxGlassHtmlFactory, Sitecore.Sandbox" singleInstance="true" />
    </sandbox.Glass.Mvc>
  </sitecore>
</configuration>

Let’s see if this works.

For testing, I created the following Razor view — notice how I’m using the Html Helper instead of using the methods on the class the Razor view inherits from:

@inherits Glass.Mapper.Sc.Web.Mvc.GlassView<Sitecore.Sandbox.Models.ViewModels.ISampleItem>
@using Glass.Mapper.Sc.Web.Mvc
@using Sitecore.Sandbox.Glass.Mapper.Sc.Web.Mvc

<div id="Content">
    <div id="LeftContent">
    </div>
    <div id="CenterColumn">
        <div id="Header">
            <img src="/~/media/Default Website/sc_logo.png" id="scLogo" />
        </div>
        <h1 class="contentTitle">
            @Html.SandboxGlass().Editable(x => x.Title)
        </h1>
        <div class="contentDescription">
            @Html.SandboxGlass().Editable(x => x.Text)
            <div>
                @Html.SandboxGlass().RenderLink(x => x.LinkOne)
            </div>
            <div>
                @Html.SandboxGlass().RenderLink(x => x.LinkTwo)
            </div>
        </div>
    </div>
</div>

After building and deploying everything above, I made sure I had some tags defined on some General Link fields on my home Item in Sitecore:

tag-attributes-raw-values

I then navigated to my homepage; looked at the rendered HTML; and saw the following:

tag-attributes-rendered-SandboxGlassHtml

As you can see it worked. πŸ™‚

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

Until next time, be sure to:

sitecore-all-the-things

πŸ˜€