This blog has moved to http://jermdavis.wordpress.com/

Showing posts with label DataSource. Show all posts
Showing posts with label DataSource. Show all posts

Friday, 21 March 2014

Multiple data source locations in the “Select Associated Content” dialog box

Working on some components for a client’s site recently, it struck me that there were circumstances where it would be helpful to editors to allow the “Select Associated Content” dialog to have multiple options without just showing the whole content tree. Scenarios like having both a global shared content folder and a sub-site specific shared content folder that editors could choose between, for example. Something that looked like this:

image

More than one root item displayed in the selection list allows editors to choose an appropriate location without the option to put their data in places you didn’t want them to…

When I started looking into whether this was possible or not, I initially assumed it would end up being quite a complicated modification – involving modifying this dialog, and the pipelines and commands which are used for this bit of UI. But after a bit of digging through the code, it turns out that it’s actually much simpler than this, since the dialog already supports the idea of multiple roots. What we need to provide is the right user interface to configure the roots and a bit of code to ensure this config is passed through to the dialog. Here’s what I came up with, after a bit of hacking:

So what happens when you click the Associated Content button in the UI? Well looking at the commands configuration for the Page Editor, we find that the UI triggers the command Sitecore.Shell.Applications.WebEdit.Commands.SetDatasource. Digging through that class with Reflector, it seems that the Execute() method gets the client page to call the Run() method. This method does all sorts of stuff, but the interesting bit is that it calls a static method called CreatePipelineArgs() to set up the data for the rest of the pipeline. And this is where the interesting stuff happens:

private static GetRenderingDatasourceArgs CreatePipelineArgs(
ClientPipelineArgs args, Item renderingItem)
{
Item clientContentItem =
WebEditUtil.GetClientContentItem(Client.ContentDatabase);
GetRenderingDatasourceArgs getRenderingDatasourceArgs =
new GetRenderingDatasourceArgs(renderingItem)
{
FallbackDatasourceRoots = new List<item>
{
Client.ContentDatabase.GetRootItem()
},
ContentLanguage = (clientContentItem != null) ?
clientContentItem.Language : null,
ContextItemPath = (clientContentItem != null) ?
clientContentItem.Paths.FullPath : string.Empty,
ShowDialogIfDatasourceSetOnRenderingItem = true
};
LayoutDefinition currentLayoutDefinition =
SetDatasource.GetCurrentLayoutDefinition();
ID clientDeviceId = WebEditUtil.GetClientDeviceId();
string uniqueId = args.Parameters["uniqueId"];
if (currentLayoutDefinition != null && !ID.IsNullOrEmpty(clientDeviceId))
{
RenderingDefinition renderingByUniqueId = currentLayoutDefinition
.GetDevice(clientDeviceId.ToString()).GetRenderingByUniqueId(uniqueId);
if (renderingByUniqueId != null)
{
getRenderingDatasourceArgs.CurrentDatasource =
renderingByUniqueId.Datasource;
}
}
return getRenderingDatasourceArgs;
}

The critical bit for what we’re trying to achieve is where the FallbackDatasourceRoots property is assigned a list of items. If we follow this through the code for the rest of the display of the dialog, this property is used to set the contents of the tree view. So this is the bit of code we need to modify to deal with the Datasource Location field of a sublayout or rendering having multiple items assigned.

Slightly annoyingly, the class defining this method doesn’t make life easy for modifying it. In fact due to the use of static methods, we’re pretty much stuck with the idea that we need to decompile the whole class and copy it into our own codebase in order to modify it. Once we’ve done that, we need some code that can process the Datasource Location field and generate a List<Item> that contains whatever items it finds.

If we assume that the data is going to be formatted using the standards for a multi-select field, then we could write a method something like this:

private static List<item> fetchDatasourceRoots(Item renderingItem)
{
List<item> roots = new List<item>();

string itemIDs = renderingItem.Fields["Datasource Location"].Value;

if (string.IsNullOrWhiteSpace(itemIDs))
{
roots.Add(Client.ContentDatabase.GetRootItem());
return roots;
}

string[] ids = itemIDs.Split('|');

if (ids.Length > 0)
{
foreach (string id in ids)
{
roots.Add(Client.ContentDatabase.GetItem(id));
}
}
else
{
roots.Add(Client.ContentDatabase.GetRootItem());
}

return roots;
}

We take the item that represents the Rendering or Sublayout and we extract the value for the Datasource Location field. If it’s empty then we return the default data – the same data the original code used. If it’s not empty then we split it into individual IDs and add each of the items these represent to the collection we return.

And with that, we can modify the code of the CreatePipelineArgs() to initialise the FallbackDatasourceRoots property using our new method instead of the original code.

Two more things need we need to do for this to work. First, we need Sitecore to use our replacement custom SetDatasource class. That just needs a quick config patch, along the lines of:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
<sitecore>
<commands>
<command name="webedit:setdatasource">
<patch:attribute name="type">
Testing.CustomSetDataSource, Testing
</patch:attribute>
</command>
</commands>
</sitecore>
</configuration>

The “patch:attribute” element here tells the configuration system to replace the value of the specified attribute of the context element with the new value we provide.

The second thing is that the Datasource Location field of the Rendering and Sublayout templates needs to become a multi-select type of field. The TreeListEx control seems appropriate. That field lives at /sitecore/templates/System/Layout/Sections/Rendering Options/Editor Options/Datasource Location in the content tree, so we can change the type:

image

That will change the type of this field for all the renderings and sublayouts. And with that, you can now set multiple items as the Datasource Location:

image

And now both of those items will show up as roots in the Set Associated Content dialog.

Now, there are a few caveats with this modification as it stands. One is that it’s not compatible with the relative data source locations code from a previous post, because it stores GUIDs in the Datasource Location field rather than a string. It also means that any pre-existing data in the Datasource Location field is now of the wrong type – so you will have to reset any values set before this change. And thirdly, because we’ve had to copy and replace an entire command class any future modifications to this in the Sitecore codebase won’t make it into our solution without us doing it manually. So borrow this with a bit of caution...

Monday, 3 March 2014

Next step: Relative data sources…

Having spent a bit of time thinking about relative Data Source Locations last week, it struck me that the logical extension of this is to allow the data sources of components themselves to be relative to the context item. This is particularly useful when you need a branch template, that will include some child items of a page and you want to pre-configure the page’s presentation to display these children via the data sources of UI components.

And happily this is a pretty trivial change for Sublayouts.

It’s a common pattern to develop a base class for sublayouts in order to share code for common tasks like fetching data source values, so it makes perfect sense to extend a bases class like that to manage this behaviour. The first thing it needs to do is find the Sublayout component that exposes the specified data source – which is always the parent object of your UI components. It also makes sense that some components will not need this behaviour, so we’ll create a property that only does any work if we actually use it:

public class BaseSublayout : System.Web.UI.UserControl
{
private Sublayout _sublayout = null;

public Sublayout Sublayout
{
get
{
if (_sublayout == null)
{
_sublayout = this.Parent as Sublayout;
}

return _sublayout;
}
}
}

With that in place, we can now write a similar property for fetching the data source. If the value of the SubLayout object’s DataSource property starts with “./” then it’s relative, and we can substitute the “.” for the Sitecore path of the context item. And we can wrap that behaviour into the same sort of “only calculate it if it’s used” property as we used above:

public class BaseSublayout : System.Web.UI.UserControl
{
private Sublayout _sublayout = null;
private string _dataSource = null;

public Sublayout Sublayout
{
get
{
if (_sublayout == null)
{
_sublayout = this.Parent as Sublayout;
}

return _sublayout;
}
}

public string DataSource
{
get
{
if (_dataSource == null)
{
_dataSource = Sublayout.DataSource;
if (_dataSource.StartsWith("./"))
{
_dataSource = Sitecore.Context.Item.Paths.FullPath
+ _dataSource.Substring(1);
}
}
return _dataSource;
}
}
}

So now if we ask for the DataSource property when the current item is “/sitecore/Content/Home” and the data source for the component’s binding is set to “./Headlines” then the result will be “/sitecore/Content/Home/Headlines”.

You can then use this base class in a simple user control:

public partial class ExampleControl : BaseSublayout
{
protected void Page_Load(object sender, EventArgs e)
{
if (!string.IsNullOrWhiteSpace(base.DataSource))
{
Item itm = Sitecore.Context.Database
.GetItem(base.DataSource);
//
// do something with the item
//
}
else
{
//
// component has no data source configured
// possible error state?
//
}
}
}

Interestingly, it turns out that you cannot do the same thing with Renderings easily – code inside Sitecore for displaying renderings detects that the relative data source path “does not exist” and hence hides the rendering. You can see this if you put Page Editor into Debug mode and look through the output that generates.

And it sounds like that could be something to investigate for the future…

Friday, 21 February 2014

Improving your Sitecore IA with relative DataSource Locations

As someone famous** once said, with great power comes great responsibility – and the power of Sitecore’s component-based page model puts a lot of responsibility on us developers to create a structure for component data sources that makes sense to content editors. The two most common patterns I find myself using are that of having a “shared content” folder somewhere in the content tree which reusable DataSource items live in, and having items as children of the component’s page. When using the “shared content” folder you can easily set the DataSource Location field for your UI component to point to location where all the relevant data gets filed, but you can’t easily do that if you want to have your DataSource items as children of the page. So you tend to end up leaving the DataSource Location field blank to allow the user to pick the current page as the place to create the new item.

Experience shows that doesn’t work too well in practice. When you don’t control where the DataSource items get stored, they tend to end up getting spread around the content tree and making a bit of a mess of the IA. That lead to some thinking about how we might be able to improve on this situation – is it possible to force the DataSource Location to be relative to the current item?

Well, a bit of digging through the code and some experimentation says it’s not too hard to provide relative DataSource Locations. You can set up a UI component with a relative path for the DataSource Location, and then manually create a folder under your page:

One

This just works! When you click the “Set Associated Content” button, the tree view shows the right folder:

Two

But sadly this doesn’t work in the situation where the folder called “Items” doesn’t exist. In that case, Sitecore tries to deal with the error condition of “that folder doesn’t exist” by setting the root of the tree above to the root of the content tree…. Not so good…

We could deal with this by using a Branch Template – when you create your page that could automatically create the Items folder too. But the maintenance of that approach is a bit tedious because you probably end up with one Branch Template for every one of your Page Templates, and you have to remember to change all your Insert Options to match. What would be much better is if we could magically create the “Items” folder whenever it was needed.

After a bit of research, I discover that when Sitecore puts up the “Select the Associated Content” dialog box, in the background it runs the “getRenderingDatasource” pipeline in order to work out what to show in the tree view. So extending this pipeline should enable us to ensure that the location exists. We can create an extension class by providing a method called “Process” which accepts the correct arguments object:

namespace Testing
{
  public class CreateRelativeDataSourceFolder
  {
    public void Process(GetRenderingDatasourceArgs args)
    {
    }
  }
}

And then we can add it to the pipeline with a quick configuration patch:

<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <getRenderingDatasource>
        <processor patch:before="processor[@type='Sitecore.Pipelines.GetRenderingDatasource.GetDatasourceLocation, Sitecore.Kernel']"
            type="Testing.CreateRelativeDataSourceFolder, Testing"/>
        </getRenderingDatasource>
      </pipelines>
    </sitecore>
  </configuration>

The “patch:before” attribute here tells Sitecore to insert this new item at the start of the pipeline, before Sitecore attempts to load the appropriate item – thus giving us the chance to create it first if necessary.


The first thing we need to do is get the value of the DataSource Location field for the component we’re setting the data source for. The GetRenderingDatasourceArgs parameter that i passed in to our pipeline processor includes a reference to this data – the args.RenderingItem property gives us access to the Sitecore Item for the UI component. So with that item we can grab the value of the field that stores the DataSource Location. We can get the ID of this field from the Sitecore UI, and write a quick bit of code to get the value and check it’s valid.

public class CreateRelativeDataSourceFolder
{
  private static ID DataSourceLocationField = 
             new ID("{B5B27AF1-25EF-405C-87CE-369B3A004016}");
  private static string RelativePath = "./";

  public void Process(GetRenderingDatasourceArgs args)
  {
    string dataSourceLocation = args.RenderingItem
             .Fields[DataSourceLocationField].Value;

    if (string.IsNullOrWhiteSpace(dataSourceLocation))
    {
      return;
    }

    if (!dataSourceLocation.StartsWith(RelativePath))
    {
      return;
    }
  }
}

Once we’ve got the value of the field we check that it’s not empty and that it starts with a “./” relative path. If either of these isn’t true then this pipeline component has nothing to do and we can bail out and let the rest of the pipeline sort things out for us.


With that done, the next step is to work out what the full Sitecore path of our relative item would be, and then check if this item exists in the database. If it does exist then we have nothing to do – we can just return control to the rest of the pipeline.  But if the item doesn’t exist, we can create it. And that means adding a few more lines of code. To create an item you need to have a name for it and to have the Template ID for the sort of item to create. In this case, the name is just the path specified for the DataSourceLocation without the preceding “./” on the front. And the ID for the “Folder” template is easy to find from the Sitecore UI. So that extends our basic code to this:

public class CreateRelativeDataSourceFolder
{
  private static ID DataSourceLocationField = 
      new ID("{B5B27AF1-25EF-405C-87CE-369B3A004016}");
  private static ID FolderTemplateID = 
      new ID("{A87A00B1-E6DB-45AB-8B54-636FEC3B5523}");
  private static TemplateID FolderTemplate =  
      new TemplateID(FolderTemplateID);
  private static string RelativePath = "./";

  public void Process(GetRenderingDatasourceArgs args)
  {
    string dataSourceLocation = args.RenderingItem
      .Fields[DataSourceLocationField].Value;

    if (string.IsNullOrWhiteSpace(dataSourceLocation))
    {
      return;
    }

    if (!dataSourceLocation.StartsWith(RelativePath))
    {
      return;
    }

    if (string.IsNullOrWhiteSpace(args.ContextItemPath))
    {
      return;
    }  

    string subFolderPath = args.ContextItemPath +
       dataSourceLocation.Substring(1);

    if (args.ContentDatabase.GetItem(subFolderPath) != null)
    {
      return;
    }

    Item currentItem = args.ContentDatabase
      .GetItem(args.ContextItemPath);

    if (currentItem == null)
    {
      return;
    }

    string newItemName = dataSourceLocation.Substring(2);

    using (new SecurityDisabler())
    {
      currentItem.Add(newItemName, FolderTemplate);
    }
  }
}

Recompile that, give it a test and now the child folder will be automatically created if it does not exist – success!


** The internet isn’t entirely sure if that was Stan Lee writing Uncle Ben from Spiderman, or Voltaire. Pick whichever you prefer…