Mar 26, 2013

How to Deploy Display Templates via SharePoint feature

[As to the question(s) "What are Display Templates, what have they eaten and how do I create my own?", see answer in the post The Anatomy of SharePoint 2013 Display Templates.]

Instead of adding my own Display Templates to a customer environment through Design Manager, I'd rather use a bit more controlled and less manual way of doing it, namely the same way as a I prefer when provisioning any branding files: Feature. A custom solution with a feature. As for how to create the branding solution base, please see Notes on Creating a SharePoint 2013 Branding Solution. This post will focus on the special issues of provisioning Display Templates.

While you still can create and provision MasterPages as .master files and PageLayouts as .aspx files, the Display Templates have been built for SharePoint 2013 solely using this HTML file -> conversion to *smtg* (in this case, .js file) technology. So even when using VisualStudio 2012 and features and such, we are working with HTML files that will be automatically converted once deployed to the _catalogs/masterpage Gallery.

Keeping that in mind, issue #1: adding the files to the solution

To add Display Template HTML files into your solution
  1. add a new module to your solution
  2. download a copy/copies of existing Display Template HTML files from the _catalogs/masterpage/Display Templated/Content Web Parts or Search directory, depending on which ones you need to create
  3. add the copies to the module and rename them
Issue #2: Display Template Properties

The properties for Display Templates are mostly set in the <head> section of the actual HTML file:


so don't define e.g. Title, ContentType, MasterPageDescription... in the Elements.xml. What you should add to the file in Elements.xml are the attributes Level and ReplaceContent. Level should be set to "Draft" in order to enable the .js conversion (it won't happen if the HTML file is published). ReplaceContent should be set to "TRUE" in order to enable the re-conversion when the file is modified and reprovisioned. And the usual Type="GhostableInLibrary" is needed too.

So this is how the basic Elements.xml would look for one Display Template only:
<Module Name="DisplayTemplates" Url="_catalogs/masterpage/Display Templates/Content Web Parts" RootWebOnly="TRUE">

<File Path="DisplayTemplates\Item_MyTemplate.html" Url="Item_MyTemplate.html" Type="GhostableInLibrary" Level="Draft" ReplaceContent="TRUE"/>

</Module>

Issue #3: the .js file after the conversion?

I found quite many references in the interwebs stating that it would be good to provision the (converted) JavaScript file together with the HTML file. If you do this, add the property
<Property Name="ContentType" Value="Display Template Code" />
to the file. 

Issue #4: publishing the Display Templates on feature activation

On activation, the feature provisions the files, and Master Page Gallery receiver code performs the conversions, but the files are still drafts.For guaranteed usability, the HTML files should be published. This can be done manually, but it can also be dealt with in a feature event receiver. The basis for this is that the HTML file be stamped to the feature by issueing it the Feature ID as a property in Elements.xml:

<File Path="DisplayTemplates\Item_MyTemplate.html" Url="Item_MyTemplate.html" Type="GhostableInLibrary" Level="Draft" ReplaceContent="TRUE">
<Property Name="FeatureId" Value="$SharePoint.Feature.Id$" Type="string"/>
</File>

Then, in the feature event receiver this can be used as an identifier by which to select the files to publish. My event receiver code is based on code provided by Waldek Mastykarz in his article Automatically publishing files provisioned with Sandboxed Solutions and looks like this:

 private string[] folderUrls = { "_catalogs/masterpage/Display Templates/Content Web Parts" };

        public override void FeatureActivated(SPFeatureReceiverProperties properties)
        {
            SPSite site = properties.Feature.Parent as SPSite;
            if (site != null)
            {
                SPWeb rootWeb = site.RootWeb;

                SPList gallery = site.GetCatalog(SPListTemplateType.MasterPageCatalog);

                if (gallery != null)
                {
                    SPListItemCollection folders = gallery.Folders;
                    string featureId = properties.Feature.Definition.Id.ToString();

                    foreach (string folderUrl in folderUrls)
                    {
                        SPFolder folder = GetFolderByUrl(folders, folderUrl);
                        if (folder != null)
                        {
                            PublishFiles(folder, featureId);
                        }
                    }
                }
            }
        }
        private static SPFolder GetFolderByUrl(SPListItemCollection folders, string folderUrl)
        {
            if (folders == null)
            {
                throw new ArgumentNullException("folders");
            }

            if (String.IsNullOrEmpty(folderUrl))
            {
                throw new ArgumentNullException("folderUrl");
            }

            SPFolder folder = null;

            SPListItem item = (from SPListItem i
                               in folders
                               where i.Url.Equals(folderUrl, StringComparison.InvariantCultureIgnoreCase)
                               select i).FirstOrDefault();

            if (item != null)
            {
                folder = item.Folder;
            }

            return folder;
        }
        private static void PublishFiles(SPFolder folder, string featureId)
        {
            if (folder == null)
            {
                throw new ArgumentNullException("folder");
            }

            if (String.IsNullOrEmpty(featureId))
            {
                throw new ArgumentNullException("featureId");
            }

            SPFileCollection files = folder.Files;
            var drafts = from SPFile f
                                  in files
                                  where String.Equals(f.Properties["FeatureId"] as string, featureId, StringComparison.InvariantCultureIgnoreCase) &&
                                  f.Level == SPFileLevel.Draft
                                  select f;

            foreach (SPFile f in drafts)
            {
                f.Publish("");
                f.Update();
            }
        }

Issue #5: Ah, this should pretty much cover it - get to work!

10 comments:

Anonymous said...

Would writing a solution work on SharePoint Foundation as Design Manager is not available?

Anonymous said...

Very useful post. Thank you.

kaviya Balasubramanian said...
This comment has been removed by the author.
kaviya Balasubramanian said...

Hi ,

Thanks for the post,, It is very useful for me.

I've added the event receiver, still templates are in drafts. Could you please give solution for this?

Alan said...

Thank you so much for taking the time to share your solution, Sanna. You helped me immensely and I appreciate it!

SM said...

@kaviya, I have noticed also, that in some environments this feature receiver has not done what it was supposed to. I have not had time to research the issue, but have simply published them manually then. If someone notices an error or figures out the problem, I'd be thankful if you posted the solution here :)

John Dudley said...

Acetech has many years of experience in custom software development. Find out more about custom software development at http://www.acetechindia.com

Unknown said...

Hi there,
In your file publishing loop, you need to add the following:
f.RevertContentStream();
In some cases, republishing the file and updating its database entry still doesn't update the ghosted file in the memory cache. Reverting the content stream forces it to re-ghost from the database.
Thanks for this article and the code!

SM said...

Thanks for that tip, Unknown!

Srinadh said...

A smallish campaign with a homemade list would not be likely to yield much of a result. To achieve anything worthwhile, a much more aggressive effort is needed. Then, the age-old value analysis applies: projected earnings = margin on total projected sales - cost of campaign.