Using a Document Library to store global Document Templates


With the use of Content Types, you can assigned a Document Template to the content type. That way when the user goes to create a new document of that content type, they will be given a Word/PowerPoint/Excel document that you want them to use. For example a company expense form, or a company report document.

Typically you would create a content type, and upload the document template you wish to use. Then you add the content type to your library and then every time you wish to create a new document the document template opens. However if you look under the covers although you have just uploaded one document template, the document template is being copied into each Document library the content type is being added to.

With the use of SharePoint Designer I can prove this point.

I have created a Content Type called CandC Document, and added a few extra site columns to it.

Then in the advance settings I have uploaded a Document Template that I wish my Content Type to use.

By opening my site in SharePoint Designer, click on All files, and in the _cts folder, there is now a folder named after my Content Type. (This is the first copy of my Document Template within SharePoint)

After creating a document library called Team Documents and adding my Content Type I can then use my document template.

However if I look at this document library in SharePoint Designer, I see that SharePoint has actually copied my Document Template to be local to the list. (This is the second copy of my Document Template within SharePoint)

Every document library I create and add the document template to will also create a copy. (This is the X copy of my Document Template within SharePoint).

Now you are right if you are thinking, when I have a new Document Template I just need to go to the Content Type, upload the new template and ensure that when I click OK, ensuring I’ve selected Yes for ‘Update all content types inheriting from this type’. This works fine in one site collection, therefore having multiple copies isn’t really an issue.

What if the Content Type being used in multiple site collections?

If the Content Type is being used in multiple site collections and you wish to update the Document template then the choices are to:

  1. Use the Content Hub.
  2. Define a document library in a different site collection as the global location.

Manually going to each site collection and update the Content Type.

This is a valid option, you might only have 4 site collections. Hopefully you have used code to create your content types in each site collection (otherwise the GUID’s) won’t match. If you have 100s of site collections this isn’t suitable. You will still have multiple copies of your Document Template.

Use the Content Type Hub.

The full explanation and setup of the Content Type hub is out of scope for this document, but basically it is a built in publishing of content type mechanism. You would create the Content Type in the Content Hub with the Document Template and then publish the content type. This will be pushed out to every site collection. Any updates/changes to the Content Type (including changing the Document Template) can be republished and pushed out to all site collections. Again this a valid option, however there can be caveats using the Content Type Hub, and I fully recommend you do your research online before making this decision. Not all site columns play nicely with the Content Hub. Also using this approach you are creating copies of your Document Template everywhere.

http://blogs.msdn.com/b/chaks/archive/2011/02/09/content-type-hub-limitations.aspx

Define a document library in a Site Collection as the Global Location.

This is the solution I’ve used recently and it ensures there is just one copy of the Document Template. The update is instantly, unlike using the Content Hub which relies on a timer job to push the changes everywhere, and there isn’t multiple copies of the template. Also you have the added bonus that because you are storing the Document Template is a Document Library, you can apply versioning, workflow approval on it too if you wish.

You will need to ensure your Content Types are created in code, as you need the GUID’s to match.

First create a document library in the Site Collection you are going to store your document templates in. Then upload all your document templates into this library. Please note, that if you wish to use your document template with multiple content types then you will need to have a copy for each content type still. (I know I said one copy, but there are limitations if using the same document template for multiple content types and you will understand why later, also having one document template for each content type even if they are the same is not a bad idea, as in the future the documents templates could different from each other.)

So I have create my Site Collection on my root site (can be any other site collection), and created my Document Library called Document Template. Inside my document library I have create 2 folders, one called CandC and once called Cann0nF0dder. Inside each folder is the same two templates I want to assign to my Content Types. Ensure you have also given everyone access to this library.

In your other site collections, using code, however you wish to do it, PowerShell, C# CSOM or JavaScript, create your Content Types in your Sites and ensure that the Document template URL is a relative path to your Document template stored in the global location. Below is sample PowerShell code I used for this demo.

$UserName = Read-Host -Prompt "UserName"
$Password = Read-Host -Prompt "Password" -AsSecureString
$Url = https://tenant.sharepoint.com/sites/Teams

#location is https://tenant.sharepoint.com/DocumentTemplates
$location = "/DocumentTemplates"
Add-Type -Path "C:\Program Files\Common Files\microsoft shared\Web Server Extensions\16\ISAPI\Microsoft.SharePoint.Client.dll"
Add-Type -Path "C:\Program Files\Common Files\microsoft shared\Web Server Extensions\16\ISAPI\Microsoft.SharePoint.Client.Runtime.dll"

$ctx = New-Object Microsoft.SharePoint.Client.ClientContext($Url)
$ctx.Credentials = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($UserName, $Password)
$ctx.ExecuteQuery();

#Create Content Type
$lci = New-Object Microsoft.SharePoint.Client.ContentTypeCreationInformation
$lci.Description = "CandC Document"
$lci.Name = "CandC Documents"
$lci.ID = "0x0101005F05888B19FD40C798741D9F1B5401E4"
$lci.Group = "CandC"

#Add Content Type
$contentType = $ctx.Web.ContentTypes.Add($lci)
$ctx.Load($contentType)
$ctx.ExecuteQuery()

#Update Document Template
$contentType.DocumentTemplate = $location + "/CandC/CCDocument Template.docx"
$contentType.Update($true)
$ctx.ExecuteQuery()

#Create Content Type
$lci = New-Object Microsoft.SharePoint.Client.ContentTypeCreationInformation
$lci.Description = "CandC Presentation"
$lci.Name = "CandC Presentation"
$lci.ID = "0x0101005F05888B19FD40C798741D9F1B5401E5"
$lci.Group = "CandC"

#Add Content Type
$contentType = $ctx.Web.ContentTypes.Add($lci)
$ctx.Load($contentType)
$ctx.ExecuteQuery();

#Update Document Template
$contentType.DocumentTemplate = $location + "/CandC/CCPresentation Template.pptx"
$contentType.Update($true)
$ctx.ExecuteQuery()

The above code only creates 2 content types (CandC Document and CandC Presentation), and it also doesn’t add the additional columns I wanted, you’ll have to work that out on your own for now. However I did create 4 content types, one for each document template.

By going into the Content Type, and viewing the Advanced Settings, you will see that it is pointing to the correct location in my Global Document Template library.

I’ve added these content types to my library (this can also be done in code) and now select whatever content type/template I wish to use.

However we are not finished yet. I discovered that when I select a Content Type, it opens up the relevant template, but when I save it back to the list, the Content Type for the document is the default content type for the library (in my case Document) not the Content Type I have picked. Also, any extra columns in my Content type were not showing up in the Document Information Panel at the top of the Microsoft Office application.

To ensure that it saves with the correct content type, you need to use your code to deploy the content types to the site collection that is holding the Global Templates. Then you need to add these Content Types to the document library that is holding the Document Templates.

Edit the property of each document and assign them to the correct content type.

Now when you create a new document from one of the content types, first of all you will notice that the Document Information panel displays with your columns.

After you have saved your document back to your library, you will also notice that it has been saved with the correct content type.

By using SharePoint designer I can also check that there is no template stored within the Site Collections, the only location you will find your template is in the Site Collection holding your Global Document Templates.

Advertisements

Office applications crashing with SharePoint Content Types and Document Templates


The scenario I had set up was, a few content types with different site columns assigned to the content type. (Single Line of Text, Ratings and Managed Metadata). Each content type also had a document template assigned to them.

The content types were then added to a document library list. When I click the New button on the list menu bar, I’m presented with a choice of documents I can create.

On my environment I was using Office 2013, and opening each one didn’t cause me a problem. The Document Information Panel (DIP) was there, my Presentation document template loaded correctly.

However when my clients tried to open any of the document templates it crashed. It appeared to crash just as the Document Information Panel attempted to load. They were using MS Office 2010, perhaps that was the problem?

It wasn’t. As I tried to open the same document on numerous of other PC’s that had either MS Office 2010 or MS Office 2013, none of them had a problem. The client had a few different people attempt to open the document templates and they all had MS Office crashing on them.

Going back to basics, I decided to create a basic content type with just single line of text columns and use the same document template. This loaded no problem in the clients MS Office 2010, the document information panel was there and the document saved back to the SharePoint Library. Therefore only difference then between the two content types were the columns.

Working with the client, I removed a column from the content type one at time and got them to open the Document Template. It turns out that after all Managed Metadata columns were removed the document open OK. This was great that I worked out the problem, but this wasn’t a solution, I needed the Managed Metadata columns to be there. I did a bit of searching on the internet, and many forums were pointing people in the direction that there are two version of Microsoft Office installed on the PC’s that are crashing.

The guys I were dealing with were working with SharePoint 2013 a lot, and turned out that they had SharePoint Designer 2013 installed. When SharePoint Designer 2013 is installed so is a bunch of Microsoft Office 2013 dlls. These Shared Dlls get registered and override some of the Office 2010 dlls.

Solution

The solution is a simple fix, (maybe not if you have to do it to everyone in your company), basically you need to either upgrade everyone to 2013 or get the PC’s with Office 2010 to repair the install. This will re-register the dlls for 2010 correctly and the Document Templates will open correctly. (SharePoint designer 2013 will continue to work)

To repair your Office 2010, on the PC go to Control Panel > Program and Features. Click on MS Office 2010, and then on the menu bar Click Change. Then select Repair. Once the repair is complete you will need to restart your PC, and afterwards you will be able to open Document Templates with Managed Metadata columns.

Final thought

Even though MS Office 2010 is very old now, and less people will probably encounter this issue, there is a chance that this issue could resurface for people when MS Office 2016 is released.

Setting default Content Types for List/Libraries using JavaScript


Long time ago, when using SharePoint 2010, I had to set the default Content types on a bunch of SharePoint Lists and Libraries. Of course I had the ease of doing this in C#. In a recent project I had to do the same thing, however being a SharePoint 2013 online project, I could only do this in JavaScript. It is amazing how something you know so well in C# that would take less than an hour to do, can take hours in JavaScript if you aren’t a JavaScript expert like myself.

How you use this code is down to you, my code originally was part of a huge JavaScript configuration file, using namespaces and callbacks. For simplicity sake, I have written this blog just with straight JavaScript code to perform setting the default content type, and show you how to set up your environment to test this. This isn’t production ready code, just a way to show you how you can do it.

Environment Setup

  • Create yourself a simple team site.
  • Create some Site Content Types, I’ve based mine on Document.

  • Create yourself a couple of Document Libraries.
    • Change the Advanced Settings to Allow management of content types
    • Click Add from existing site content types and add one or more content types to your library.

I have created a HR library which consists of CF Holiday content type, and an Accounts library which has CF Claim Form, CF Invoice and CF Purchase Order added to it.

Below HR Library

Below Accounts Library

This is the environment set up. Currently the Content Types I wish to use have already been added to the libraries, and Document is the default content type.

Code and Demo

As this is purely a demo, I’m going to use SharePoint Designer to create a basic page, that when you click a button, it updates the Default Content Types for lists and libraries. It’s not ideal using SPD as there is no intellisense, but you can quickly create a page and upload to SharePoint, which is great for testing like this.

  • Connect SharePoint Designer to your site.
  • Open the All Files section, then right click and add an aspx page. I called mine test.aspx
  • Open your aspx page, you will get a warning, Click Yes to open the page in advanced mode.
  • Delete everything in the page and paste the following. This will give you a basic page, for a Standard SharePoint Master Page.
<%@ Assembly Name="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c"%>
<%@ Import Namespace="Microsoft.SharePoint.WebPartPages" %> <%@ Register Tagprefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %> <%@ Register Tagprefix="Utilities" Namespace="Microsoft.SharePoint.Utilities" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Import Namespace="Microsoft.SharePoint" %> <%@ Assembly Name="Microsoft.Web.CommandUI, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register Tagprefix="WebPartPages" Namespace="Microsoft.SharePoint.WebPartPages" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Page Language="C#" MasterPageFile="~masterurl/default.master" Inherits="Microsoft.SharePoint.WebPartPages.WebPartPage,Microsoft.SharePoint,Version=15.0.0.0,Culture=neutral,PublicKeyToken=71e9bce111e9429c" meta:progid="SharePoint.WebPartPage.Document" meta:webpartpageexpansion="full" %>
<asp:Content ContentPlaceHolderID="PlaceHolderAdditionalPageHead" runat="server" >
</asp:Content>
<asp:Content ContentPlaceHolderId="PlaceHolderPageTitle" runat="server">
    <SharePoint:ProjectProperty Property="Title" runat="server"/>
</asp:Content>
<asp:Content ContentPlaceHolderId="PlaceHolderPageTitleInTitleArea" runat="server">
    <SharePoint:ProjectProperty Property="Title" runat="server"/>
</asp:Content>
<asp:Content ContentPlaceHolderID="PlaceHolderMain" runat="server">
</asp:Content>
  • If you save this page and then navigate to your site, you should get just a blank page, no errors. So your site URL would be similar to http://cf/sites/teams/test.aspx

First thing is to ensure I have all my references I require for the code to work. I’m using underscore.js. I have found underscore to be very useful for looping through arrays. I’ve downloaded underscore.js and using SharePoint designer I have created a folder called Assets/Scripts and placed the underscore.js file in there.

  • Add a reference inside your test.aspx. Put the reference inside your PlaceHolderAdditionalPageHead. I’ve also put stubs in place ready for our JavaScript in a later step.
<asp:Content ContentPlaceHolderID="PlaceHolderAdditionalPageHead" runat="server" >
    <script type="text/javascript" src="Assets/Scripts/underscore.js"></script>
    <script type="text/javascript">
        /*Our javascript code will go here */
    </script>
</asp:Content>
  • In PlaceHolderMain content placeholder, add a button that will call our JavaScript function to set Default Content Types.
<div>
    <button type="button" onclick="setDefaultContentType();return false">Update Default Content Types</button>
</div>
  • Within the PlaceHolderAdditionalPageHead after the underscore.js reference, add the following script.
//Collection of Lists and Default Content Types for given list.
 var defaultContentTypes = [
        { ListTitle: 'HR', ContentTypeId: '0x0101003293FA81A1DB8B47AC008BE39BCE695C0057F88502D28F9042A061813A2DA79AE9', ContentTypeName: 'CF Holiday' },
        { ListTitle: 'Accounts', ContentTypeId: '0x0101005C18A4FC55B2C84FB8DD5E5A71D1DA02000529316EC50BA14CB28D9E8D69BA43B4', ContentTypeName: 'CF Claim Form' }
];

 var context;

 function setDefaultContentType(){
    context = SP.ClientContext.get_current();
    _.each(defaultContentTypes, function(current){
var rootFolder = context.get_web().get_lists().getByTitle(current.ListTitle).get_rootFolder();
var contentTypes = context.get_web().get_lists().getByTitle(current.ListTitle).get_contentTypes();

context.load(rootFolder, 'ContentTypeOrder', 'UniqueContentTypeOrder');
context.load(contentTypes);
});

context.executeQueryAsync(loadContentTypesSuccess, onScriptFailure);
 };

function loadContentTypesSuccess(){
        _.each(defaultContentTypes, function (current) {
              var originalCTO;
              var rootFolder = context.get_web().get_lists().getByTitle(current.ListTitle).get_rootFolder();
              var contentTypes = context.get_web().get_lists().getByTitle(current.ListTitle).get_contentTypes();
              originalCTO = rootFolder.get_contentTypeOrder();

              var newCTO = new Array();
              var contentTypeEnum = contentTypes.getEnumerator();
                  while (contentTypeEnum.moveNext()) {
                        var currentCT = contentTypeEnum.get_current();
                        if (currentCT.get_name().toLowerCase() == 'folder')
                            continue;

                         if (currentCT.get_name().toLowerCase() == current.ContentTypeName.toLowerCase()) {
                            newCTO.splice(0, 0, currentCT.get_id());
                            continue;
                         }

                         for (i = 0; i < originalCTO.length; i++) {
                             if (originalCTO[i].toString() == currentCT.get_id().toString()) {
                                     newCTO.push(currentCT.get_id());
                                     break;
                             }
                         }
                      }

                      rootFolder.set_uniqueContentTypeOrder(newCTO);
                      rootFolder.update();
                });
                context.executeQueryAsync(onUpdateContentTypes, onScriptFailure);
};

function onUpdateContentTypes(){
    alert('Updated Default ContentTypes');
};

function onScriptFailure(sender, args){
      alert(args.get_message() + '\n' + args.get_stackTrace());
};

Let me explain each section to you. Right at the top of the script we have a global variable called defaultContentTypes. Here we have the List Name, the Content Type ID and Name which will be set as the default content type. If you have more than 2 lists to set the content type, just add another line to this collection for each list. You can add as many, or as little as you want, the code is written to be flexible here.

The first function setDefaultContentType (which is what the button will call when pressed), gets the SharePoint context, then using the underscore.js _.each function, loops through each list in our defaultContentTypes variable. Before we can update anything we need to grab the information from SharePoint. This is what this looping function does. It gets the List rootFolder, and current content types for that list.

The line below ensures that rootFolder contains the values for the properties ContentTypeOrder, and UniqueContentTypeOrder. The ContentTypeOrder is required to get the current order of the Content Types, and the UniqueContentTypeOrder is required because when we set the new order back, this is the property to use. Leaving them out of the following line would throw an error stating “The property or field ‘ContentTypeOrder’ has not been initialized”.

context.load(rootFolder, 'ContentTypeOrder', 'UniqueContentTypeOrder');

Once we have loaded all the lists and content types (loadContentTypesSuccess) we then loop through all in defaultContentTypes again. However because we have already loaded the rootFolder in the context, this time when we call it, we will have values. We grab the original order of the content types, and loop through them. Within the while statement there is 3 checks.

  1. If content type == “folder” then don’t add it.
  2. If content type == our default content type, then add first in the array.
  3. If it is still a content type to display, add it to the end of the content type order array.

The reason why the 3rd check is written as below, is if one of the Content types is not visible on the New button, it ensures that it continues to behave this way.

for (i = 0; i < originalCTO.length; i++) {
     if (originalCTO[i].toString() == currentCT.get_id().toString()) {
                newCTO.push(currentCT.get_id());
                break;
     }
}

Lastly, once we have obtained the correct Content Type Order for the list, setting the default Content type at number one, we need to write back to SharePoint and save this data. On the root folder, we set_uniqueContentTypeOrder passing in the new array order. Then call executeQueryAsync, which will either alert success or failure.

If we see this in action, by loading up the test.aspx page we will just have a simple button, saying “Update Default Content Type”. We click this button and hopefully receive a successful alert.

If we look at our Accounts library, we can see the CF Claims form is now the default content type.

And if we look at our HR library, we can see the CF Holiday is now the default content type.

If in your code, you didn’t want to make the SharePoint Document content type visible, in your code, just don’t add it to the newCTO array, like what we do with Folder.

if (currentCT.get_name().toLowerCase() == 'folder' || currentCT.get_name().toLowerCase() == 'document')
    continue;

Unable to delete content types


When you try to delete a content type in a farm you sometimes get an error message saying that the Content Type is still in use. You think it is lying, as you have deleted all the lists. Sometimes the reason is because the lists are still sitting in the recycle bin and are still being referenced.

However when you have exhausted a manual look of your site for the rogue list, or item that is still referencing the content type try this bit of code that I found directly on the Microsoft site http://msdn.microsoft.com/en-us/library/ms453791.aspx

Using a console application, the key class to use is SPContentTypeUsage. The below code can easily re-written to allow user prompts for Site, and Content Type, to make it more generic without having to re-compile each time.


namespace ContentTypeInUse
{
    class Program
    {
        static void Main(string[] args)
        {
            using (SPSite siteCollection = new SPSite("http://TestSite"))
            {
                using (SPWeb webSite = siteCollection.OpenWeb())
                {
                    // Get the obsolete content type.
                    SPContentType obsolete = webSite.ContentTypes["DemoCT"];

                    if (obsolete != null) // We have a content type.
                    {
                        IList<SPContentTypeUsage> usages = SPContentTypeUsage.GetUsages(obsolete);
                        if (usages.Count > 0) // It is in use.
                        {
                            Console.WriteLine("The content type is in use in the following locations:");
                            foreach (SPContentTypeUsage usage in usages)
                                Console.WriteLine(usage.Url);
                        }
                        else // The content type is not in use.
                        {
                            // Delete it.
                            Console.WriteLine("Deleting content type {0}...", obsolete.Name);
                            webSite.ContentTypes.Delete(obsolete.Id);
                        }
                    }
                    else // No content type found.
                    {
                        Console.WriteLine("The content type does not exist in this site collection.");
                    }
                }
            }
            Console.Write("\nPress ENTER to continue...");
            Console.ReadLine();
        }
    }
}