Home » Commands » Take the Field By Replicating Sitecore Field Values

Take the Field By Replicating Sitecore Field Values

Sitecore Technology MVP 2016
Sitecore MVP 2015
Sitecore MVP 2014

Enter your email address to follow this blog and receive notifications of new posts by email.

I pondered the other day whether anyone had ever erroneously put content into fields on the wrong Sitecore item, only to discover they had erred after laboring away for an extended period of time — imagine the ensuing frustration after realizing such a blunder.

You might think that this isn’t a big deal — why not just rename the item to be the name of the item you were supposed to be putting content into in the first place?

Well, things might not be that simple.

What if the item already had content in it before? What do you do?

This hypothetical — or fictitious — scenario got the creative juices flowing. Why not create new item context menu options — check out part 1 and part 2 of my post discussing how one would go about augmenting the item context menu, and also my last post showing how one can delete sitecore items using a deletion basket which is serves as another example of adding to the item context menu — that give copy and paste functionality for field values?

The cornerstone of my idea comes from functionality that comes with Sitecore “out of the box”. You have the option to cut, copy and paste items:

cut-copy-paste-context-menu

I find these three menu options to be indispensable. I frequently use them throughout the day when developing new features in Sitecore, and have also seen content authors use these to do their work.

The only problem with these is they don’t work at the field level, ergo the reason for this post: to showcase my efforts in building copy and paste utilities that work at the field level.

I first had to come up with a way to save field values. The following interface serves as the definition of objects that save information associated with a key:

namespace Sitecore.Sandbox.Utilities.Storage.Base
{
    public interface IRepository<TKey, TValue>
    {
        bool Contains(TKey key);

        TValue this[TKey key] { get; set; }

        void Put(TKey key, TValue value);

        void Remove(TKey key);

        void Clear();

        TValue Get(TKey key);
    }
}

For my copy and paste utilities, I decided I would store them in session. That steered me into building the following session repository class:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;
using System.Web;
using System.Web.SessionState;

using Sitecore.Diagnostics;

using Sitecore.Sandbox.Utilities.Storage.Base;

namespace Sitecore.Sandbox.Utilities.Storage
{
    public class SessionRepository : IRepository<string, object>
    {
        private HttpSessionStateBase Session { get; set; }

        public object this[string key]
        {
            get
            {
                return Get(key);
            }
            set
            {
                Put(key, value);
            }
        }

        private SessionRepository(HttpSessionState session)
            : this(CreateNewHttpSessionStateWrapper(session))
        {
        }

        private SessionRepository(HttpSessionStateBase session)
        {
            SetSession(session);
        }

        private void SetSession(HttpSessionStateBase session)
        {
            Assert.ArgumentNotNull(session, "session");
            Session = session;
        }

        public bool Contains(string key)
        {
            return Session[key] != null;
        }

        public void Put(string key, object value)
        {
            Assert.ArgumentNotNullOrEmpty(key, "key");
            Assert.ArgumentCondition(IsSerializable(value), "value", "value must be serializable!");
            Session[key] = value;
        }

        private static bool IsSerializable(object instance)
        {
            Assert.ArgumentNotNull(instance, "instance");
            return instance.GetType().IsSerializable;
        }

        public void Remove(string key)
        {
            Session.Remove(key);
        }

        public void Clear()
        {
            Session.Clear();
        }

        public object Get(string key)
        {
            return Session[key];
        }

        private static HttpSessionStateWrapper CreateNewHttpSessionStateWrapper(HttpSessionState session)
        {
            Assert.ArgumentNotNull(session, "session");
            return new HttpSessionStateWrapper(session);
        }

        public static IRepository<string, object> CreateNewSessionRepository(HttpSessionState session)
        {
            return new SessionRepository(session);
        }

        public static IRepository<string, object> CreateNewSessionRepository(HttpSessionStateBase session)
        {
            return new SessionRepository(session);
        }
    }
}

If you’ve read some of my previous posts, you must have ascertained how I favor composition over inheritance — check out this article that discusses this subject — and created another utility object for storing string values — instances of this class delegate to other repository objects that save generic objects (an instance of the session repository class above is an example of such an object):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;
using System.Web;
using System.Web.SessionState;

using Sitecore.Diagnostics;

using Sitecore.Sandbox.Utilities.Storage.Base;

namespace Sitecore.Sandbox.Utilities.Storage
{
    public class StringRepository : IRepository<string, string>
    {
        private IRepository<string, object> InnerRepository { get; set; }

        public string this[string key]
        {
            get
            {
                return Get(key);
            }
            set
            {
                Put(key, value);
            }
        }

        private StringRepository(IRepository<string, object> innerRepository)
        {
            SetInnerRepository(innerRepository);
        }

        private void SetInnerRepository(IRepository<string, object> innerRepository)
        {
            Assert.ArgumentNotNull(innerRepository, "innerRepository");
            InnerRepository = innerRepository;
        }

        public bool Contains(string key)
        {
            return InnerRepository.Contains(key);
        }

        public void Put(string key, string value)
        {
            Assert.ArgumentNotNullOrEmpty(key, "key");
            InnerRepository.Put(key, value);
        }

        public void Remove(string key)
        {
            InnerRepository.Remove(key);
        }

        public void Clear()
        {
            InnerRepository.Clear();
        }

        public string Get(string key)
        {
            return InnerRepository.Get(key) as string;
        }

        public static IRepository<string, string> CreateNewStringRepository(IRepository<string, object> innerRepository)
        {
            return new StringRepository(innerRepository);
        }
    }
}

I then built another — yes one more — repository class that uses instances of Sitecore.Data.ID as keys:

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

using Sitecore.Data;
using Sitecore.Data.Fields;
using Sitecore.Diagnostics;

using Sitecore.Sandbox.Utilities.Storage.Base;

namespace Sitecore.Sandbox.Utilities.Storage
{
    class IDValueRepository : IRepository<ID, string>
    {
        private IRepository<string, string> InnerRepository { get; set; }
        
        public string this[ID key] 
        {
            get
            {
                return Get(key);
            }
            set
            {
                Put(key, value);
            }
        }

        private IDValueRepository(IRepository<string, string> innerRepository)
        {
            SetInnerRepository(innerRepository);
        }

        private void SetInnerRepository(IRepository<string, string> innerRepository)
        {
            Assert.ArgumentNotNull(innerRepository, "innerRepository");
            InnerRepository = innerRepository;
        }

        public bool Contains(ID key)
        {
            return InnerRepository.Contains(GetInnerRepositoryKey(key));
        }

        public void Put(ID key, string value)
        {
            InnerRepository.Put(GetInnerRepositoryKey(key), value);
        }

        public void Remove(ID key)
        {
            InnerRepository.Remove(GetInnerRepositoryKey(key));
        }

        public void Clear()
        {
            InnerRepository.Clear();
        }

        public string Get(ID key)
        {
            return InnerRepository.Get(GetInnerRepositoryKey(key));
        }

        private static string GetInnerRepositoryKey(ID key)
        {
            AssertKey(key);
            return key.ToString();
        }

        private static void AssertKey(ID key)
        {
            Assert.ArgumentNotNull(key, "key");
            Assert.ArgumentCondition(!ID.IsNullOrEmpty(key), "key", "key must be set!");
        }

        public static IRepository<ID, string> CreateNewIDValueRepository(IRepository<string, string> innerRepository)
        {
            return new IDValueRepository(innerRepository);
        }
    }
}

Instances of the above class delegate down to repository objects that save strings using strings as keys.

Now that I have an unwieldy arsenal of repository utility classes — I went a little bananas on creating the utility classes above — I figured having a factory class as a central place to instantiate these repository objects would aid in keeping things organized:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Sitecore.Data.Fields;
using System.Web;
using System.Web.SessionState;
using Sitecore.Data;

namespace Sitecore.Sandbox.Utilities.Storage.Base
{
    public interface IStorageFactory
    {
        IRepository<string, object> CreateNewSessionRepository(HttpSessionState session);

        IRepository<string, object> CreateNewSessionRepository(HttpSessionStateBase session);

        IRepository<string, string> CreateNewStringRepository(IRepository<string, object> innerRepository);

        IRepository<ID, string> CreateNewIDValueRepository(HttpSessionState session);

        IRepository<ID, string> CreateNewIDValueRepository(IRepository<string, string> innerRepository);
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using System.Web.SessionState;

using Sitecore.Data;
using Sitecore.Data.Fields;

using Sitecore.Sandbox.Utilities.Storage.Base;

namespace Sitecore.Sandbox.Utilities.Storage
{
    public class StorageFactory : IStorageFactory
    {
        private StorageFactory()
        {
        }

        public IRepository<string, object> CreateNewSessionRepository(HttpSessionState session)
        {
            return SessionRepository.CreateNewSessionRepository(session);
        }

        public IRepository<string, object> CreateNewSessionRepository(HttpSessionStateBase session)
        {
            return SessionRepository.CreateNewSessionRepository(session);
        }

        public IRepository<string, string> CreateNewStringRepository(IRepository<string, object> innerRepository)
        {
            return StringRepository.CreateNewStringRepository(innerRepository);
        }

        public IRepository<ID, string> CreateNewIDValueRepository(HttpSessionState session)
        {
            return CreateNewIDValueRepository(CreateNewStringRepository(CreateNewSessionRepository(session)));
        }

        public IRepository<ID, string> CreateNewIDValueRepository(IRepository<string, string> innerRepository)
        {
            return IDValueRepository.CreateNewIDValueRepository(innerRepository);
        }

        public static IStorageFactory CreateNewStorageFactory()
        {
            return new StorageFactory();
        }
    }
}

Let’s make these utility repository classes earn their keep. It’s time to build a copy command:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;

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

using Sitecore.Sandbox.Utilities.Storage;
using Sitecore.Sandbox.Utilities.Storage.Base;

namespace Sitecore.Sandbox.Commands
{
    public class CopyFieldValues : Command
    {
        private static readonly IStorageFactory Factory = StorageFactory.CreateNewStorageFactory();
        private static readonly IRepository<ID, string> FieldValueRepository = CreateNewIDValueRepository();

        public override void Execute(CommandContext commandContext)
        {
            Assert.ArgumentNotNull(commandContext, "commandContext");
            StoreFieldValues(GetItem(commandContext));
        }

        private static void StoreFieldValues(Item item)
        {
            if (item != null)
            {
                item.Fields.ReadAll();
                StoreFieldValues(item.Fields);
            }
        }

        private static void StoreFieldValues(IEnumerable<Field> fields)
        {
            Assert.ArgumentNotNull(fields, "fields");
            foreach (Field field in fields)
            {
                FieldValueRepository.Put(field.ID, field.Value);
            }
        }

        public override CommandState QueryState(CommandContext commandContext)
        {
            Assert.ArgumentNotNull(commandContext, "commandContext");
            if (GetItem(commandContext).Appearance.ReadOnly)
            {
                return CommandState.Disabled;
            }

            return base.QueryState(commandContext);
        }

        private static Item GetItem(CommandContext commandContext)
        {
            Assert.ArgumentNotNull(commandContext, "commandContext");
            Assert.ArgumentNotNull(commandContext.Items, "commandContext.Items");
            Assert.ArgumentCondition(commandContext.Items.Count() > 0, "commandContext.Items", "There must be at least one item in the array!");
            return commandContext.Items.FirstOrDefault();
        }

        private static IRepository<ID, string> CreateNewIDValueRepository()
        {
            return Factory.CreateNewIDValueRepository(HttpContext.Current.Session);
        }
    }
}

The above command iterates over all fields on the currently select item in the content tree, and saves their values using an instance of the IDValueRepository class.

What good is a copy command without a paste? The following paste command complements the copy command defined above:

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Text;
using System.Web;

using Sitecore.Collections;
using Sitecore.Data;
using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Shell.Framework.Commands;
using Sitecore.Web.UI.Sheer;

using Sitecore.Sandbox.Utilities.Storage;
using Sitecore.Sandbox.Utilities.Storage.Base;

namespace Sitecore.Sandbox.Commands
{
    public class PasteFieldValues : Command
    {
        private static readonly IStorageFactory Factory = StorageFactory.CreateNewStorageFactory();
        private static readonly IRepository<ID, string> FieldValueRepository = CreateNewIDValueRepository();

        public override void Execute(CommandContext commandContext)
        {
            Assert.ArgumentNotNull(commandContext, "commandContext");
            PasteValuesIfApplicable(commandContext);
        }

        private void PasteValuesIfApplicable(CommandContext commandContext)
        {
            Item item = GetItem(commandContext);
            if (item == null)
            {
                return;
            }

            PasteValuesIfApplicable(item);
        }

        private void PasteValuesIfApplicable(Item item)
        {
            Assert.ArgumentNotNull(item, "item");

            if (DoesFieldsHaveValues(item))
            {
                ConfirmThenPaste(item);
            }
            else
            {
                PasteValues(item);
            }
        }

        private static bool DoesFieldsHaveValues(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            Assert.ArgumentNotNull(item.Fields, "item.Fields");

            foreach (Field field in item.Fields)
            {
                if (!string.IsNullOrEmpty(field.Value))
                {
                    return true;
                }
            }

            return false;
        }

        private void ConfirmThenPaste(Item item)
        {
            NameValueCollection parameters = new NameValueCollection();
            parameters["items"] = SerializeItems(new Item[] { item });
            Context.ClientPage.Start(this, "ConfirmAndPaste", new ClientPipelineArgs { Parameters = parameters });
        }

        private void ConfirmAndPaste(ClientPipelineArgs args)
        {
            ShowConfirmationDialogIfApplicable(args);
            PasteValuesIfConfirmed(args);
        }

        private void ShowConfirmationDialogIfApplicable(ClientPipelineArgs args)
        {
            if (!args.IsPostBack)
            {
                Context.ClientPage.ClientResponse.YesNoCancel("Some fields are not empty! Are you sure you want to paste field values into this item?", "200", "200");
                args.WaitForPostBack();
            }
        }

        private void PasteValuesIfConfirmed(ClientPipelineArgs args)
        {
            bool canPaste = args.IsPostBack && args.Result == "yes";
            if (canPaste)
            {
                Item item = DeserializeItems(args.Parameters["items"]).FirstOrDefault();
                PasteValues(item);
            }
        }

        private static void PasteValues(Item item)
        {
            if (item != null)
            {
                item.Editing.BeginEdit();
                item.Fields.ReadAll();
                PasteValues(item.Fields);
                item.Editing.EndEdit();
            }
        }

        private static void PasteValues(IEnumerable<Field> fields)
        {
            Assert.ArgumentNotNull(fields, "fields");
            foreach (Field field in fields)
            {
                string value = FieldValueRepository.Get(field.ID);
                if (!string.IsNullOrEmpty(value))
                {
                    field.Value = value;
                }

                FieldValueRepository.Remove(field.ID);
            }
        }

        public override CommandState QueryState(CommandContext commandContext)
        {
            Assert.ArgumentNotNull(commandContext, "commandContext");
            if (GetItem(commandContext).Appearance.ReadOnly)
            {
                return CommandState.Disabled;
            }

            return base.QueryState(commandContext);
        }

        private static Item GetItem(CommandContext commandContext)
        {
            Assert.ArgumentNotNull(commandContext, "commandContext");
            Assert.ArgumentNotNull(commandContext.Items, "commandContext.Items");
            Assert.ArgumentCondition(commandContext.Items.Count() > 0, "commandContext.Items", "There must be at least one item in the array!");
            return commandContext.Items.FirstOrDefault();
        }

        private static IRepository<ID, string> CreateNewIDValueRepository()
        {
            return Factory.CreateNewIDValueRepository(HttpContext.Current.Session);
        }
    }
}

The paste command determines if the target item has any fields with content in them — a confirmation dialog box is displayed if any of the fields are not empty — and pastes values into fields if they are present on the item.

Plus, once a field value is retrieved from the IDValueRepository instance, the above command removes it. I couldn’t think of a good reason why these should linger in session after they are pasted. If you can of a reason why they should persist in session, please leave a comment.

I registered the copy and paste commands above into a patch include file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <sitecore>
    <commands>
      <command name="item:copyfieldvalues" type="Sitecore.Sandbox.Commands.CopyFieldValues,Sitecore.Sandbox"/>
      <command name="item:pastefieldvalues" type="Sitecore.Sandbox.Commands.PasteFieldValues,Sitecore.Sandbox"/>
    </commands>
  </sitecore>
</configuration>

I created context menu options for these in the Core database:

copy-field-values-context-menu-option

paste-field-values-context-menu-option

Let’s take all of the above for a spin.

I created an item with some content:

just-some-item-with-content

I then created another item with less content:

just-some-other-item-sparse-content

I right-clicked to launch the item context menu, and clicked the ‘Copy Field Values’ option:

clicked-copy-field-values

I then navigated to the second item I created in the content tree, right-clicked, and selected the ‘Paste Field Values’ option:

clicked-paste-field-values

By now, I had put my feet up on my desk thinking it was smooth sailing from this point on, only to be impeded by an intrusive confirmation box ;):

fields-not-empty-confirm

I clicked ‘Yes’, and saw the following thereafter:

field-values-copied

In retrospect, it probably would have made more sense to omit standard fields from being copied, albeit I will leave that for another day.

Until next time, have a Sitecoretastic day! 🙂

Advertisement

1 Comment

  1. […] This solution reuses an instance of a storage class I had used in a previous post. […]

Comment

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: