With release of SharePoint 2013, Microsoft has been promoting isolated space idea for the code written for SharePoint 2013. Take example of workflows, which are now hosted in a different workflow service rather than within the SharePoint server.
Similarly, Microsoft has been promoting creation of apps for SharePoint rather than deploying Farm or Sandbox solutions. This model for apps works fine for most of the times. But at times this model of running the code out of SharePoint can be a pain in the neck. E.g. There are lots of files/content that needs to be deployed within the SharePoint site itself (remember, your SharePoint team site/mysite etc. becomes the host web for the app).
Earlier we used to create Modules to deploy files into SharePoint or if we wanted to deploy any content type etc. But that model is no more available within SharePoint apps.
Now apps, can be SharePoint hosted, Auto-hosted (in Azure) or Provider hosted. Auto-hosted apps are now being deprecated by Microsoft and no longer you are able to create Auto-Hosted apps.
So the only options that remain are to create SharePoint hosted or Provider Hosted apps.
Let’s see how we can provision some branding files into SharePoint host web using a SharePoint app. I had created a provider hosted app for this but same logic can be utilized for a SharePoint hosted app (of-course, you would need to change from C# to JavaScript, as you cannot run C# code within SharePoint-hosted app… remember?)
We will be deploying the following files:
- customer.master -> This will be the master page that needs to be setup for the team sites
- search.master -> This will be the master page that needs to be applied to the search center site
- webpartlayout.aspx -> This will be our custom layout file of type “Welcome Page” which will be used for creating the home page
- Some more files like StyleSheets (.css), JavaScript (.js) and images (.png) would need to be deployed
These files will be first packaged and deployed to our app web (the website which will be hosting our app, in my case an Azure Website) and then copied over to SharePoint host web, when the app is installed in the host web.
For simplicity sake, I had added all the code in the default.aspx file of my app, so that whenever you hit the app icon in SharePoint, it will go ahead and deploy the files.
Now let’s get to the meat of it…
I will not go into details of how to create an app or get the client context, but will only concentrate on the file provisioning parts. If you are looking for something of how to create your first app, please click here.
We will create some methods (functions) which will help us in the deployment and code management.
As a best practice, I upload all the branding into a single folder in the master page gallery (_catalogs/masterpage). If the folder does not exist, we will create it:
[csharp]
private bool FolderExists(ClientContext clientContext)
{
//Get the site
Site site = clientContext.Site;
//Get master page gallery, where we want to create a folder
List gallery = site.RootWeb.Lists.GetByTitle("Master Page Gallery");
CamlQuery query = new CamlQuery();
// Checking if Folder Already Exist
Microsoft.SharePoint.Client.CamlQuery camlQuery = new CamlQuery();
camlQuery.ViewXml =
@"<View>
<Query>
<Where><Eq><FieldRef Name=’FileLeafRef’ /><Value Type=’File’>Customer_Branding</Value></Eq></Where>
</Query>
</View>";
Microsoft.SharePoint.Client.ListItemCollection listItems = gallery.GetItems(camlQuery);
clientContext.Load(listItems);
clientContext.ExecuteQuery();
return (listItems.Count == 0);
}
[/csharp]
Now if the folder does not exist, then we would need to create it. So I created a generic function that can create a folder in a given library.
[csharp]
private static bool CreateSPFolder(string libname, string correspondance, ClientContext clientContext)
{
bool bolReturn = false;
ClientContext ctx = clientContext;
Web web = clientContext.Web;
List documentLib = ctx.Web.Lists.GetByTitle(libname);
string targetFolderUrl = libname + "/" + correspondance;
Folder folder = web.GetFolderByServerRelativeUrl(targetFolderUrl);
ctx.Load(folder);
bool exists = false;
try
{
ctx.ExecuteQuery();
exists = true;
}
catch (Exception ex)
{
bolReturn = false;
}
if (!exists)
{
ContentTypeCollection listContentTypes = documentLib.ContentTypes;
ctx.Load(listContentTypes, types => types.Include
(type => type.Id, type => type.Name,
type => type.Parent));
var result = ctx.LoadQuery(listContentTypes.Where
(c => c.Name == "Folder"));
ctx.ExecuteQuery();
ContentType folderContentType = result.FirstOrDefault();
ListItemCreationInformation newItemInfo = new ListItemCreationInformation();
newItemInfo.UnderlyingObjectType = FileSystemObjectType.Folder;
newItemInfo.LeafName = correspondance;
Microsoft.SharePoint.Client.ListItem newListItem = documentLib.AddItem(newItemInfo);
newListItem["ContentTypeId"] = folderContentType.Id.ToString();
newListItem["Title"] = correspondance;
newListItem.Update();
ctx.Load(documentLib);
ctx.ExecuteQuery();
bolReturn = true;
}
return bolReturn;
}
[/csharp]
At this point in time, we have the folder where we want to upload the files. We need a method to upload the file to a given library.
[csharp]
private string UploadFile(string sourceFileURL, string targeFolder, string targetName, ClientContext clientContext)
{
string serverRelativeURL= string.Empty;
//open the file from local file in the app server
var streamMaster = this.OpenFile(sourceFileURL);
fcimaster = new FileCreationInformation();
fcimaster.ContentStream = streamMaster;
fcimaster.Url = targetName;
fcimaster.Overwrite = true;
//add the file to the target folder (in our case, custom folder inside Master Page Library)
Microsoft.SharePoint.Client.File newFile = clientContext.Web.GetFolderByServerRelativeUrl(targeFolder).Files.Add(fcimaster);
clientContext.Load(newFile, Newfile => Newfile.ServerRelativeUrl);
clientContext.ExecuteQuery();
serverRelativeURL = newFile.ServerRelativeUrl;
//return the URL of uploaded file
return serverRelativeURL;
}
[/csharp]
This method will be able to handle upload of any type of file that is given to it. But master pages and layouts need special handling as they need their content types to be set to make sure that SharePoint can recognize them as master page or as a layout.
[csharp]
private void UpdateMasterPage(string relativeURL, string CheckInComments, string PublishComment, string desc, ClientContext clientContext, List gallery)
{
//get the file we are updating
File file = clientContext.Web.GetFileByServerRelativeUrl(relativeURL);
Microsoft.SharePoint.Client.ListItem item = file.ListItemAllFields;
clientContext.Load(file);
clientContext.Load(item);
clientContext.ExecuteQuery();
//if we get the file and not error is returned
if (item != null)
{
//load the file
clientContext.Load(item.File);
clientContext.ExecuteQuery();
//if not checked out
if (item.File.CheckOutType == CheckOutType.None)
{
//checkout the file
item.File.CheckOut();
}
//Get the ASP NET Master Page Content Type using helper function
var masterPageCT = GetContentType(clientContext, gallery, "ASP NET Master Page");
//Set Content Type, UI version, and Description. Check in and Publish
item["ContentTypeId"] = masterPageCT.Id.ToString();
item["UIVersion"] = Convert.ToString(15);
item["MasterPageDescription"] = desc;
item.Update();
item.File.CheckIn(CheckInComments, CheckinType.MajorCheckIn);
item.File.Publish(PublishComment);
clientContext.Load(item);
clientContext.ExecuteQuery();
}
}
[/csharp]
Here we are using a helper method, which finds a particular content type from a given SPList. Here we require “ASP NET Master Page” content type from “Master Page Gallery” so that we can attach it to our uploaded master page.
[csharp]
private ContentType GetContentType(ClientContext ctx, List docs, string contentType)
{
//get all content types from the given list
ContentTypeCollection listContentTypes = docs.ContentTypes;
//find the required content type
ctx.Load(listContentTypes, types => types.Include (type => type.Id, type => type.Name, type => type.Parent));
var result = ctx.LoadQuery(listContentTypes.Where(c => c.Name == contentType));
ctx.ExecuteQuery();
//get the first content type
ContentType targetDocumentSetContentType = result.FirstOrDefault();
//and return it
return targetDocumentSetContentType;
}
[/csharp]
The code till now will upload our master page into the folder inside Master Page Gallery and set it’s content type to ensure that SharePoint recognize it as a master page.
Now we need to set this master page as the default master page for our web. But as we have 2 different master pages (1 for search center and other for team site), we need to know which type of web is this and then set the master page accordingly.
The method below checks for the template of the current given web and returns true, if it is a search center.
[csharp]
private bool IsSearchCenter(Web web)
{
return web.WebTemplate.ToUpperInvariant().Equals("SRCHCEN");
}
[/csharp]
When we have determined the type of website, we can update the custom master page accordingly using the method below:
[csharp]
private void SetMasterPage(Web web, string url)
{
web.CustomMasterUrl = url;
web.Update();
web.Context.ExecuteQuery();
}
[/csharp]
This was about the master page, we also have a custom page layout of type “Welcome Page” that we need to upload and setup for SharePoint to recognize it as a page layout. The following method does the exact thing.
[csharp]
private void UpdatePageLayout(string relativeURL, string CheckInComments, string PublishComment, string desc, ClientContext clientContext, List gallery)
{
//get the file we need to update
File file = clientContext.Web.GetFileByServerRelativeUrl(relativeURL);
Microsoft.SharePoint.Client.ListItem item = file.ListItemAllFields;
clientContext.Load(file);
clientContext.Load(item);
clientContext.ExecuteQuery();
//if we found the file
if (item != null)
{
//If not checked out, Check out File
clientContext.Load(item.File);
clientContext.ExecuteQuery();
if (item.File.CheckOutType == CheckOutType.None)
{
item.File.CheckOut();
}
//Get the Page Layout Content Type using helper function
//This will specify that this file is a Page Layout
var pageLayoutCT = GetContentType(clientContext, gallery, "Page Layout");
//We also need to tell to SharePoint that it will be associated with "Welcome Page" publishing content type
var AssociatedpageLayoutCT = GetContentType(clientContext, clientContext.Web, "Welcome Page");
//Set Content Type and Associated Content type, Check in and Publish
item["ContentTypeId"] = pageLayoutCT.Id.ToString();
//Update the Associated Content type to ‘Welcome Page’ content type
item["PublishingAssociatedContentType"] = string.Format(";#{0};#{1};#", AssociatedpageLayoutCT.Name, AssociatedpageLayoutCT.Id.ToString());
item.Update();
item.File.CheckIn(CheckInComments, CheckinType.MajorCheckIn);
item.File.Publish(PublishComment);
clientContext.Load(item);
clientContext.ExecuteQuery();
}
}
[/csharp]
These are the 2 types of files which need special handling. All other files like .js and .css files can just be uploaded and then published. Another method was created to update all other files to a published state.
[csharp]
private void UpdateOtherFiles(string relativeURL, ClientContext clientContext, string CheckInComments, string PublishComment)
{
File file = clientContext.Web.GetFileByServerRelativeUrl(relativeURL);
Microsoft.SharePoint.Client.ListItem item = file.ListItemAllFields;
clientContext.Load(file);
clientContext.Load(item);
clientContext.ExecuteQuery();
item.File.Publish(PublishComment);
clientContext.Load(item);
clientContext.ExecuteQuery();
}
[/csharp]
Finally, all the helper methods are in-place, we need to build our app and call-out these methods to provision the files required. In the sample app, I had built a folder structure as below:
As you can see I have renamed master pages and layout page as “.txt” files. This is to ensure that when we are trying to read the files for uploading into SharePoint, our webserver (app server) will not try to execute then but return us the actual file contents.
Now let’s bring everything together and see how all these helper methods come together to help us provision the files.
I had written this code in Page_Load event handler of the default.aspx in the app for quick testing, but ideally you should be provisioning the files in the “App Installed” event handler and reverting back your updates in “Handle App Uninstalling” event handler.
Let’s get back to work now.
[csharp]
//Get the context token from the helper (you get this automatically when you create a SharePoint app in Visual Studio)
var contextToken = TokenHelper.GetContextTokenFromRequest(Page.Request);
//we need the host web, which has been sent as part of request by SharePoint to our app
var hostWeb = Page.Request["SPHostUrl"];
//this is the folder name we will be creating in the "Master Page Gallery"
var folderName = "Custom_Branding";
//the folder URL in SharePoint where we will be uploading all the files
var TargetFolderURL = "_catalogs/masterpage/Custom_Branding";
//keep track if the folder has been created in gallery or not
var IsFolderCreatd = false;
//keep track of the url of uploaded files
string url = string.Empty;
//get the client context using which we will be making all the calls
using (var clientContext = TokenHelper.GetClientContextWithContextToken(hostWeb, contextToken, Request.Url.Authority))
{
//ensure that folder exists in "Master Page Gallery"
bool isFolderThere = false;
if (FolderExists(clientContext))
{
// Folder is not Present so Create a Folder
isFolderThere = CreateSPFolder("Master Page Gallery", folderName, clientContext);
}
else
{
//the folder already exists
isFolderThere = true;
}
//if we have the folder
if (isFolderThere)
{
//get the objects required
Site site = clientContext.Site;
Web web = clientContext.Web;
List gallery = site.RootWeb.Lists.GetByTitle("Master Page Gallery");
//make sure we have the WebTemplate property of web available to us
// (need to check if this is a search center or not)
clientContext.Load(web, w => w.WebTemplate);
clientContext.ExecuteQuery();
//UpLoad the custom.Master and set its properties to be a master page
url = UploadFile("/Branding/custom.master.txt", TargetFolderURL, "custom.master", clientContext);
UpdateMasterPage(url, "My check-in comment", "My publishing comment", "My description", clientContext, gallery);
//if current web is not search center, we set custom.master as the Publishing Master
if(!IsSearchCenter(web))
{
SetMasterPage(web, url);
}
// Uploading the custom_Search.master
url = UploadFile("/Branding/custom_Search.master.txt", TargetFolderURL, "custom_Search.master", clientContext);
UpdateMasterPage(url, "My check-in comment", "My publishing comment", "My description", clientContext, gallery);
//if current web is a search center, we set custom_Search.master as the Publishing Master
if (IsSearchCenter(web))
{
SetMasterPage(web, url);
}
//UpLoading logo.png and publishing it
url = UploadFile("/Branding/logo.png", TargetFolderURL, "logo.png", clientContext);
UpdateOtherFiles(url, clientContext, "My check-in comment", "My publishing comment");
//UpLoading custom.css
url = UploadFile("/Branding/custom.css", TargetFolderURL, "custom.css", clientContext);
UpdateOtherFiles(url, clientContext, "My check-in comment", "My publishing comment");
//UpLoading custom.js
url = UploadFile("/Branding/custom.js", TargetFolderURL, "custom.js", clientContext);
UpdateOtherFiles(url, clientContext, "My check-in comment", "My publishing comment");
//Uploading CustomWebPartPageLayout.aspx
url = UploadFile("/Branding/CustomWebPartPageLayout.aspx.txt", TargetFolderURL, "CustomWebPartPageLayout.aspx", clientContext);
UpdatePageLayout(url, "My check-in comment", "My publishing comment", "My page layout description", clientContext, gallery);
}//end of isFolderThere
} //end of using
[/csharp]
Woh! that was a long post and if you are still awake and following the code, you should have all the files properly provisioned in your SharePoint site and have all branding working as required.
The code to remove the provisioned files would be just the reverse of this along with a few more checks around if your master pages and layouts are still in use, you cannot remove them. Are you ready to take it up as an assignment?
Any questions, shoot a comment or contact me over email/social media and I would be more than happy to answer.
Happy Coding 🙂
-Manpreet
P.S.: As for all code in this blog, it is a quick demonstration of how a particular requirement can be achieved. This code does not contain error handling or handle edge cases, nor is this code production ready.
hi manpreet
hope you are well. Would you mind send me this project to have a look
thanks
Hi,
Sorry been out of touch with SharePoint for sometime now. Let me have a quick look and if I can find the source and will share in the blog.
Regards,
Mannu Alag
Hi, great article, this may be exactly what I am after.
If possible, I too would like to see the project, if it’s available?
Many thanks!
Hi Jai,
Sorry, been out of touch with SharePoint for long time and don’t have access to that source code now. But all the code is there in the blog, just needs to be put in the right project.
Let me know if you still need help and will try to find a SharePoint dev machine and get it done.
Regards,
Manpreet