Sitecore Dynamic Field Editor Custom Experience Button

19 April 2016 marrrcin sitecore , pipelines , experience , author , dynamic

Recently, I've worked on the Sitecore project which required author-friendly editing of item fields. Although Sitecore CMS has Field Editor feature, but I wanted something more. In this post I will show you how to rapidly implement custom experience button which will allow to dynamically choose fields depending on any business logic required.

I've taken the rapid development approach (which is often required when the deadline is approaching :-) and I took advantage of existing Sitecore's Field Editor to extend it a little bit. Of course, the built-in Field Editor have the ability to display various fields, but it's based only on the static list of fields stored in the Field Editor Button Item. It does not meet my requirements - truly dynamic field list. So... I've sat with the laptop and did the work.

Here is a list of features of my Dynamic Field Editor:

  • execute any business logic to provide dynamic field list
  • dynamically show or hide sections
  • high reusability - can be used with any rendering/sublayout without adding any new items/commands

If you are TL;DR person, take a look at this screencap and see if this functionality suits your needs:

How this will work?

My idea to implement this was to create new custom experience button and then replacing the webedit:fieldeditor command by my own. To make this highly reusable and extensible, the replacement command will invoke custom pipeline. The pipeline will have processors for every rendering/sublayout with the business logic. In this implementation, an assumption is made that the pipeline processors will modify Field Editor options only if the command was invoked from specific rendering/sublayout.

Prerequsites

  • Sitecore 8.X (I did this on 8.1-update 2, but it should work on any other version as well)
  • Visual Studio project set up for Sitecore (blank web app with references Sitecore DLLs)

Creating replacement command

First, we will implement custom web edit command which will invoke the pipeline. In the project, create a class which inherits from the Sitecore class:

Sitecore.Shell.Applications.WebEdit.Commands.FieldEditor

I always want to be as flexible as possible inside of my code and do not end up with "aww... I don't know XYZ property at this point", so my implementation takes as much as possible from the command context and passes it to custom pipeline. Here's what it looks like:

public class DynamicFieldEditor : FieldEditor
{
    protected CommandContext CommandContext;

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

    protected override PageEditFieldEditorOptions GetOptions(ClientPipelineArgs args, NameValueCollection form)
    {
        var options = base.GetOptions(args, form);
        var pipelineArgs = new GetDynamicFieldEditorPipelineArgs(args, form, CommandContext, options);
        CorePipeline.Run("getDynamicFieldEditor", pipelineArgs, false);
        return options;
    }
}

Now, we need to edit configuration file to replace original command:

 <commands>
    <command name="webedit:fieldeditor" type="Zablo.Net.Sitecore.Commands.DynamicFieldEditor, Zablo.Net.Sitecore" patch:instead="*[@name='webedit:fieldeditor']"></command>
</commands>

Custom pipeline - abstraction

The pipelines need two things: Pipeline Args and Pipeline Interface. As you could see above, my pipeline will be called getDynamicFieldEditor and it operates on the custom arguments class. Arguments class have as much context-related properties as possible, so the processors will be able to modify virtually anything.

public class GetDynamicFieldEditorPipelineArgs : PipelineArgs
{
    public GetDynamicFieldEditorPipelineArgs(ClientPipelineArgs args, NameValueCollection form, CommandContext commandContext, PageEditFieldEditorOptions options)
    {
        ClientPipelineArgs = args;
        Form = form;
        CommandContext = commandContext;
        FieldEditorOptions = options;
    }

    public ClientPipelineArgs ClientPipelineArgs { protected set; get; }

    public NameValueCollection Form { protected set; get; }

    public CommandContext CommandContext { protected set; get; }

    public PageEditFieldEditorOptions FieldEditorOptions { protected set; get; }
}

The interface is pretty simple too:

public interface IGetDynamicFieldEditorProcessor
{
    void Process(GetDynamicFieldEditorPipelineArgs args);
}

Custom pipeline - base implementation

I hope that you didn't stopped at this point and started implementing your own processor. There is more - I've implemented base processor, which extracts even more data - like the rendering/sublayout datasource item - to make development of this pipeline's processors even faster. Here you go:

public abstract class BaseDynamicFieldEditorProcessor : IGetDynamicFieldEditorProcessor
{
    protected abstract ID RenderingId { get; }
    public virtual void Process(GetDynamicFieldEditorPipelineArgs args)
    {
        var parameters = args.ClientPipelineArgs.Parameters;
        var renderingId = parameters.Get("renderingId");
        var options = args.FieldEditorOptions;

        if (string.IsNullOrEmpty(renderingId) || ID.Parse(renderingId) != RenderingId)
        {
            return;
        }
        var datasourceUri = parameters["uri"];
        Item datasource = null;
        if (datasourceUri != null)
        {
            datasource = Database.GetItem(ItemUri.Parse(datasourceUri));
        }
        ProvideDynamicFields(args, options, parameters, datasource);
    }

    /// <summary>
    /// Provide custom logic for displaying fields here
    /// </summary>
    /// <param name="args">
    /// <param name="options">
    /// <param name="parameters">
    /// <param name="datasource">
    protected abstract void ProvideDynamicFields(GetDynamicFieldEditorPipelineArgs args,
        PageEditFieldEditorOptions options, NameValueCollection parameters, Item datasource);
}

The first condition in Process method is really important. It matches my assumption, that specific processor can only proceed execution only if it is invoked from it's related rendering/sublayout.

Custom pipeline - processor for rendering/sublayout

Like I said before, you can apply my Dynamic Field Editor to any rendering/sublayout, so I will show you how to implement processor for Sitecore's built-in Sample Rendering.

First, create class which inherits from BaseDynamicFieldEditorProcessor, then open the configuration file from your solution, and add first processor to the getDynamicFieldEditor pipeline:

<pipelines>
    <getdynamicfieldeditor>
    <processor type="Zablo.Net.Sitecore.Pipelines.GetDynamicFieldEditor.SampleRenderingFieldProcessor, Zablo.Net.Sitecore"></processor>
    </getdynamicfieldeditor>
</pipelines>

There are two things to implement:

  1. RenderingId property getter
  2. ProvideDynamicFields method

The property can be hard-coded (like mine here), but there is no obstacle in calling the configuration, to get your renderingId from config XML or any other place. The method focuses only on adding fields to the Field Editor. Here is a full implementation:

public class SampleRenderingFieldProcessor : BaseDynamicFieldEditorProcessor
{
    protected override ID RenderingId { get { return ID.Parse("{493B3A83-0FA7-4484-8FC9-4680991CF743}"); } }

    protected override void ProvideDynamicFields(GetDynamicFieldEditorPipelineArgs args, PageEditFieldEditorOptions options,
        NameValueCollection parameters, Item datasource)
    {
        options.Fields.Clear();
        if (datasource.TemplateID == ID.Parse("{76036F5E-CBCE-46D1-AF0A-4143F9B557AA}"))
        {
            options.ShowSections = false;
            options.PreserveSections = false;
            options.Fields.Add(new FieldDescriptor(datasource,"Title"));
        }
        else
        {
            options.ShowSections = true;
            options.PreserveSections = true;
            options.Fields.Add(new FieldDescriptor(datasource, "SomeOtherField"));
        }
    }
}

There's nothing fancy it the logic above - just template checking. I didn't wanted to obfuscate the code, just wanted to show the idea of implementation.

Adding Custom Experience Button

Last thing to do is to add the Custom Experience Button. Head to the CORE database and duplicate the item from path:

/sitecore/content/Applications/WebEdit/Edit Frame Buttons/Default/Edit

and move it to:

/sitecore/content/Applications/WebEdit/Custom Experience Buttons

I've also renamed it to Dynamic Field Editor and changed the icon - which was obviously the most important task ;) Make sure that Field List field of duplicated item is not empty, because Sitecore will throw exception otherwise. Finally, switch back to MASTER database, find your rendering and add the Custom Experience Button to it (the field is located under Editor Options section):

Summary

That's it! I hope that Dynamic Field Editor and it's extensibility will give you an opportunity to improve Sitecore author experience without lot of custom development. I will appreciate your feedback, please comment and share if you like it!

Full source code can be found on my github:

https://github.com/marrrcin/sitecore-dynamic-field-editor

Comments