How To Create a Publishing Page in SharePoint Online Using CSOM API

I am working on some provisioning code for SharePoint Online, and I needed to create a publishing page.  After some digging into the CSOM API, here is the code for adding a publishing page in SharePoint Online.

image

using (ClientContext ctx = Login(parentURL))
{
     try
     {
         var web = ctx.Web;
         ctx.Load(web);


        PublishingWeb publishingWeb = PublishingWeb.GetPublishingWeb(ctx, web);
         ctx.Load(publishingWeb);


        if (publishingWeb != null)
         {
             string pageLayoutDisplayName = “ArticleLeft”;
             // Get Publishing Page Layouts
             List publishingLayouts = ctx.Site.RootWeb.Lists.GetByTitle(“Master Page Gallery”);
             ListItemCollection allItems = publishingLayouts.GetItems(CamlQuery.CreateAllItemsQuery());


            ctx.Load(allItems, items => items.Include(item => item.DisplayName).Where(obj => obj.DisplayName == pageLayoutDisplayName));
             ctx.ExecuteQuery();


            ListItem layout = allItems.Where(x => x.DisplayName == pageLayoutDisplayName).FirstOrDefault();
             ctx.Load(layout);


            List pages = ctx.Site.RootWeb.Lists.GetByTitle(“Pages”);
            
             PublishingPageInformation publishingPageInfo = new PublishingPageInformation();
             publishingPageInfo.Name = “test.aspx”;
             publishingPageInfo.PageLayoutListItem = layout;
            
             PublishingPage publishingPage = publishingWeb.AddPublishingPage(publishingPageInfo);


            List publishPageParentList = publishingPage.ListItem.ParentList;
             ctx.Load(publishPageParentList, list => list.EnableModeration);
             ctx.ExecuteQuery();


            publishingPage.ListItem[“PublishingPageContent”] = “test”;
             publishingPage.ListItem.Update();
             publishingPage.ListItem.File.CheckIn(string.Empty, CheckinType.MajorCheckIn);
             publishingPage.ListItem.File.Publish(string.Empty);


             if (publishingPage.ListItem.ParentList.EnableModeration)
                 publishingPage.ListItem.File.Approve(string.Empty);


            ctx.Load(publishingPage);
             ctx.Load(publishingPage.ListItem.File, obj => obj.ServerRelativeUrl);
             ctx.ExecuteQuery();


        }

public ClientContext Login(string URL)
         {
             SecureString passWord = new SecureString();
             foreach (char c in Password.ToCharArray()) passWord.AppendChar(c);
             ClientContext tenantContext = new ClientContext(URL);
             tenantContext.Credentials = new SharePointOnlineCredentials(Username, passWord);
             return tenantContext;
         }


A couple key notes for those interested:

  • Publishing pages require a page layout – in this case “ArticleLeft”.  The publishing page object is looking for the actual list item representing the page layout which requires you finding it and providing it.  This is what the first few lines of code are doing – looking up by Display Name the page layout for ArticleLeft.
  • In order to approve a page, content approval has to be turned on in the list.  If it’s not turned on and you run the “publishingPage.ListItem.File.Approve” method it will generate an exception.  You can check for it being turned on using the “EnableModeration” property.
  • Like most properties in CSOM, you have to explicitly load properties.  If you try to access the property and it hasn’t been loaded you will get an exception.  This is what the line “
  • ctx.Load(publishPageParentList, list => list.EnableModeration);” is doing – it’s requesting that property be explicitly loaded.

  • The pagename property is ithe physical name of the page and it has to end in “.aspx” or you will get an exception.
  • Once you have created the page, you can then set the content of the various fields within the page as well.  In this case, I successfully updated the field “PublishingPageContent” (which is the main content box in the ArticleLeft page template) with the string “test”.  This would allow you to inject any content into those boxes.

I haven’t tested it but I expect that this would also work in SharePoint 2013/2016 on premise as well.

Read More

Provisioning SharePoint Site Collections in Office 365 or SharePoint 2013 through Code

We were working on some provisioning code for creating and deleting site collections in Office 365.  Microsoft provides the SharePoint 2013 Client Components SDK that includes a tenant library for managing activities such as creating and deleting site collections.

The lifecycle of a site collection is three stages, not two: 1) create the site collection; 2) delete the site collection and 3) remove the site collection from the second stage recycle bin.  SharePoint will not allow you to create a site collection with the same URL as an existing site collection even if it is in the recycle bin.  Using the Office 365, there is no ability to empty the second stage recycle bin at all and the default waiting period is 30 days before it is permanently removed, so the only method for removing the Site Collection from the recycle bin is through code.

Creating a Context

In order to use any of the client methods, you will need a client context.  The code for logging into Office 365 looks like this:

public ClientContext Login(string URL)
{
     SecureString passWord = new SecureString();
     foreach (char c in Password.ToCharArray()) passWord.AppendChar(c);
     ClientContext tenantContext = new ClientContext(URL);
     tenantContext.Credentials = new SharePointOnlineCredentials(Username, passWord);
     return tenantContext;
}

public ClientContext LoginTenant()
         {
             return Login(TenantAdminURL);
         }

The URL in this case needs to be the tenant admin URL which is in the format XXX-admin.sharepoint.com where XXX is the name of your tenant.

Creating a Site Collection

Creating a site collection requires a number of properties to supply in the creation process such as the TItle, URL, resources, etc. for your new site collection.  The tenant library provides the methods for creating a site collection.

I created a value object to store these properties like this:

public class SharePointSiteCollection
    {
        public string URL { get; set; }
        public string Title { get; set; }
        public string Owner { get; set; }
        public string RootSiteTemplate { get; set; }

       public int StorageLimit { get; set; }
        public int ResourcePoints { get; set; }
    }

This makes it easier to pass around the configuration information as an entity and will support serialization of the configuration data at some point in the future.

The method for creating a site collection looks like this:

public void CreateSiteCollection(SharePointSiteCollection SiteCollection )
{
     using (ClientContext ctx = LoginTenant())
     {
         //Properties of the New SiteCollection
         var siteCreationProperties = new SiteCreationProperties();
         var tenant = new Tenant(ctx);
         string URL = AssembleAbsoluteURL(SiteCollection.URL);
         siteCreationProperties.Url = URL;
         //Title of the Root Site
         siteCreationProperties.Title = SiteCollection.Title;
         //Email of Owner
         siteCreationProperties.Owner = SiteCollection.Owner;
         siteCreationProperties.Lcid = (uint) LCID;
         //Template of the Root Site. Using Team Site for now.
         siteCreationProperties.Template = SiteCollection.RootSiteTemplate;
         //Storage Limit in MB
         siteCreationProperties.StorageMaximumLevel = SiteCollection.StorageLimit;
         //UserCode Resource Points Allowed
         siteCreationProperties.UserCodeMaximumLevel = SiteCollection.ResourcePoints;
         //Create the SiteCollection
         SpoOperation spo = tenant.CreateSite(siteCreationProperties);
         ctx.Load(tenant);
         //We will need the IsComplete property to check if the provisioning of the Site Collection is complete.
         ctx.Load(spo, i => i.IsComplete);
         ctx.ExecuteQuery();
         Console.WriteLine(“Waiting for site collection to be created.”);
         //Check if provisioning of the SiteCollection is complete.
         while (!spo.IsComplete)
         {
             //Wait for 30 seconds and then try again
             System.Threading.Thread.Sleep(30000);
             spo.RefreshLoad();
             ctx.ExecuteQuery();
         }
         Console.WriteLine(“Site collection created.”);


    }


}

Note the process for waiting for the site collection process to complete – creating a site collection can take a few minutes and the method simply waits until finished before completion.

Deleting a Site Collection

Deleting a site collection can be done using the following code:

public void DeleteSiteCollection(string URL)
         {
             URL = AssembleAbsoluteURL(URL);
             using (ClientContext ctx = LoginTenant())
             {
                 var tenant = new Tenant(ctx);
                 var spo = tenant.RemoveSite(URL);
                 ctx.Load(spo);
                 ctx.ExecuteQuery();


                Console.WriteLine(“Waiting for site to be deleted.”);


                //Check if provisioning of the SiteCollection is complete.
                 while (!spo.IsComplete)
                 {


                    //Wait for 30 seconds and then try again
                     System.Threading.Thread.Sleep(30000);
                     spo.RefreshLoad();
                     ctx.ExecuteQuery();
                 }


                Console.WriteLine(“Site deleted.”);


            }


        }

Removing the Site Collection from the Recycle Bin

The last step is to remove the site collection from the second stage recycle bin. 

public void RemoveSiteFromRecycleBin(string URL)
         {
                 URL = AssembleAbsoluteURL(URL);
                 using (ClientContext ctx = LoginTenant())
                 {
                     var tenant = new Tenant(ctx);
                     var spo = tenant.RemoveDeletedSite(URL);
                     ctx.Load(spo);
                     ctx.ExecuteQuery();


                    Console.WriteLine(“Waiting for Site to be removed from the recycle bin.”);


                    //Check if provisioning of the SiteCollection is complete.
                     while (!spo.IsComplete)
                     {


                        //Wait for 30 seconds and then try again
                         System.Threading.Thread.Sleep(30000);
                         spo.RefreshLoad();
                         ctx.ExecuteQuery();
                     }


                    Console.WriteLine(“Site removed from the recycle bin.”);


                }
         }

Read More

SharePoint 2013 Update Released to Enable Hybrid Search with Office 365

Microsoft has just released the anticipated update for SharePoint 2013 on premise.  Along with fixing some bugs, the key feature for this update is the enablement of improved hybrid search integration with Office 365.

There is also now a hybrid sites feature that consolidates Followed Sites between SharePoint 2013 on premise and Office 365.

image

In the old hybrid search scenario, you can enable searching of on premise content from Office 365 and vice versa.  However, in the old scenario, the indexes are still separate from each other and when you search they are treated as separate.

In the new hybrid search, you can now create a true hybrid index that combines search results from Office 365 and SharePoint on premise in a single index.

image

Read More

Paging through SharePoint 2013 / Office 365 Lists with JavaScript

One of they most basic requirements for customizations in SharePoint 2013 is displaying lists of items.  For example, you might want to have a list of news items in which you control how that list is rendered in the user interface.  There are several ways to do this including search based display templates, CSOM, JSOM, REST, etc.  We have been using all of these approaches in our custom intranets.

One basic requirement we had was to implement a paging system so that end users could click previous, next and randomly seek to any page.

image

Here is how to implement such a mechanism using JavaScript. 

Two Basic Approaches: Fetch Items from a List or Fetch Items from Search

Using the SharePoint JavaScript APIs, there are two basic approaches to obtain a list of items: 1) query the list where the items are stored directly or 2) query the search index to find items. 

Here is an example for fetching items directly from a news list using the JavaScript API.  The query executes a CAML query against a specific news list with a row limit of 2 items per page.

var queryText = “*”;
var rowsPerPage = 2;
var startRow = 1;
var id = “”;
var contentTypeID = “0x0110”;
var category = “”;


var pathField = “Path”;
var titleField = “Title”;
var bodyField = “BodyOWSMTXT”;
var publishedDateField = “PublishedDateOWSDATE”;


var loadNews = function () {
     $(document).ready(function () {
        SP.SOD.executeFunc(‘sp.js’, ‘SP.ClientContext’, function () {
           
                 var ctx = new SP.ClientContext.get_current();
                 var oWeb = ctx.get_web();
                 var oList = ctx.get_web().get_lists().getByTitle(‘Pages’);
                 var viewFields = “<ViewFields><FieldRef Name=’Title’ /></ViewFields>”;
                 var orderBy = “<OrderBy><FieldRef Name=’Created’ /></OrderBy>”;
                 var where = “”;
                
                 category = manageQueryStringParameter(“category”);
                 if (category != “”)
                 {
                     where = “<Query><Where><Eq><FieldRef Name=’News_x0020_Category’ /><Value Type=’TaxonomyFieldTypeMulti’>” + category + “</Value></Eq></Where></Query>”;
                 }
                 var rowLimit = ‘<RowLimit Paged=”TRUE”>’ + rowsPerPage + ‘</RowLimit>’;
                
                 var viewXML = “<View>”+ where + rowLimit + “</View>”;
                
                 var camlQuery = new SP.CamlQuery();
                 camlQuery.set_viewXml(viewXML);


                var collListItem = oList.getItems(camlQuery);
                 ctx.load(collListItem);
                 ctx.executeQueryAsync(onQuerySuccess, onQueryFail);


                function onQuerySuccess() {
                        
                     var listItemInfo = ”;
                     var listItemEnumerator = collListItem.getEnumerator();
                     var firstPageID = null;
                     var lastPageID = null;
                    
                     $(‘#list’).append(“<ul>”);
                            
                     while (listItemEnumerator.moveNext()) {
                        
                         var oListItem = listItemEnumerator.get_current();


                        // set firstPage ID for the item found
                         if (firstPageID == null)
                             firstPageID = oListItem.get_id();
                            
                         listItemInfo = ‘\nID: ‘ + oListItem.get_id() + ‘\nTitle: ‘ + oListItem.get_item(‘Title’);
                         $(‘#list’).append(“<li>” + listItemInfo + “</li>” );
                        
                         // set lastPageID for last possible item
                         lastPageID = oListItem.get_id();
                     }
                    
                        $(‘#list’).append(“</ul>”);
                       
                    }


                function onQueryFail(sender, args) {
                     alert(‘Query failed. Error:’ + args.get_message());
                 }


            });
         });
};
loadNews();


// pull parameters from query field
function manageQueryStringParameter(paramToRetrieve) {
     var queryValue = “”;


    if (document.URL.indexOf(“?”, 0) > 0) {
         var params = document.URL.split(“?”)[1].split(“&”);
         var strParams = “”;
         for (var i = 0; i < params.length; i = i + 1) {
             var singleParam = params[i].split(“=”);
             if (singleParam[0] == paramToRetrieve) {
                 queryValue = singleParam[1];
             }
         }


    }
     return queryValue;
}

Here is an example of the same query but instead of querying the list, we query the search index instead.

var queryText = “*”;
var rowsPerPage = 2;
var startRow = 1;
var id = “”;
var contentTypeID = “0x010100C568DB52D9D0A14D9B2FDCC96666E9F2007948130EC3DB064584E219954237AF3900242457EFB8B24247815D688C526CD44D008B00E60AACCFA944AC4F0B4704E594A4”;
var category = “NewsCategoryChoiceOWSCHCM”;


var titleField = “Title”;
var publishedDateField = “ArticleStartDate”;
var categoryField = “NewsCategoryChoice”;
var category = “”;
var categoryURLParameter = “category”;


var loadNews = function () {
     $(document).ready(function () {
         SP.SOD.executeFunc(‘sp.js’, ‘SP.ClientContext’, function () {
             SP.SOD.executeFunc(“SP.Search.js”, “Microsoft.SharePoint.Client.Search.Query.KeywordQuery”, function () {
           
                 var ctx = new SP.ClientContext.get_current();
                 var oWeb = ctx.get_web();
                 var contextSite = ctx.get_site();
                 var keywordQuery = new Microsoft.SharePoint.Client.Search.Query.KeywordQuery(ctx);
                
                 keywordQuery.set_queryText(“*”);
                 keywordQuery.set_trimDuplicates(true);
                 keywordQuery.set_rowLimit(rowsPerPage);
                
                 var querytemplate = “ContentTypeId:'” + contentTypeID + “*’ “;
                
                 category = manageQueryStringParameter(categoryURLParameter );
                 if (category != “”) {
                     querytemplate += categoryField + ‘:”‘ + category + ‘” ‘;
                 }
                
                 keywordQuery.set_queryTemplate(querytemplate);
                
                 var properties = keywordQuery.get_selectProperties();
                 properties.add(publishedDateField);
                 properties.add(titleField);
                 properties.add(categoryField);


                var searchExecutor = new Microsoft.SharePoint.Client.Search.Query.SearchExecutor(ctx);
                 var results = searchExecutor.executeQuery(keywordQuery);
                 ctx.executeQueryAsync(onQuerySuccess, onQueryFail);


                function onQuerySuccess() {
                        
                     $(‘#list’).append(“<ul>”);
                                                    
                     for (i = 0; i < results.m_value.ResultTables[0].ResultRows.length; i++)
                     {
                         var row = results.m_value.ResultTables[0].ResultRows[i];
                         listItemInfo = ‘\nTitle: ‘ + row[titleField];
                         $(‘#list’).append(“<li>” + listItemInfo + “</li>” );
                     }
                    
                        $(‘#list’).append(“</ul>”);


                     
                    }


                function onQueryFail(sender, args) {
                     alert(‘Query failed. Error:’ + args.get_message());
                 }


             });
         });
     });
};


loadNews();


// pull parameters from query field
function manageQueryStringParameter(paramToRetrieve) {
     var queryValue = “”;


    if (document.URL.indexOf(“?”, 0) > 0) {
         var params = document.URL.split(“?”)[1].split(“&”);
         var strParams = “”;
         for (var i = 0; i < params.length; i = i + 1) {
             var singleParam = params[i].split(“=”);
             if (singleParam[0] == paramToRetrieve) {
                 queryValue = singleParam[1];
             }
         }


    }
     return queryValue;
}

As you can see by the differences in the function, the basic approach is similar but in this case we execute a query using the SharePoint search APIs instead of the list APIs. 

Implementing Previous and Next using SharePoint List Query

It is possible to implement a previous and next paging approach using our list query approach.  The approach requires the following code to be added to our CAML query creation above:

var pagingInfo = ‘Paged=TRUE&p_ID=0’;
var page = manageQueryStringParameter(“p_ID”);
var pagedPrev = manageQueryStringParameter(“PagedPrev”);
if (page != “”)
{
     pagingInfo = ‘Paged=TRUE&p_ID=’ + page;
}
if (pagedPrev == “TRUE”)
{
     pagingInfo = ‘Paged=TRUE&p_ID=’ + page + ‘&PagedPrev=TRUE’;
}

var position = new SP.ListItemCollectionPosition();
position.set_pagingInfo(pagingInfo);

camlQuery.set_listItemCollectionPosition(position);

This code retrieves an ID value of either the last displayed item in the case of Next or the first display item in the case of Prev in order to tell SharePoint where to count backwards or forwards.  In addition, you have to set the pagingInfo to include a PagePrev=True attribute for when your user has clicked the previous button. 

The id value is provided when you iterate through your query’s results.  The basic code I have tracks the first item’s ID and the last item’s ID and appends these parameters to the p_ID parameter to pass into the query string:

while (listItemEnumerator.moveNext()) {
                        
     var oListItem = listItemEnumerator.get_current();


    // set firstPage ID for the item found
     if (firstPageID == null)
         firstPageID = oListItem.get_id();
                            
     listItemInfo = ‘\nID: ‘ + oListItem.get_id() + ‘\nTitle: ‘ + oListItem.get_item(‘Title’);
     $(‘#list’).append(“<li>” + listItemInfo + “</li>” );
                        
     // set lastPageID for last possible item
     lastPageID = oListItem.get_id();
}
                    
$(‘#list’).append(“</ul>”);
                       
$(‘#pages’).append(“<a href=’./news-list?category=” + category + “&p_ID=” + lastPageID + “‘>Next</a>”);
$(‘#pages’).append(“<a href=’./news-list?category=” + category + “&p_ID=” + firstPageID + “&PagedPrev=TRUE’>Prev</a>”);

This works well for previous and next.  However, there is no easy method to implement the random access to specific pages, e.g. by click on page 3 to go directly to page 3.  Previous and Next work relative to the current position in the list but the API doesn’t provide the total number of items in the list that we could use to calculate how many pages we need.  In addition, the IDs are not necessarily in sequential order which we would need to figure out what the ID to specify to go to a specific page. 

Implementing Pages Using SharePoint Search

The SharePoint Search API allows us to implement pages because of two important features:

  • The SharePoint Search API provides a value for the total number of items in the query even when the Row Limit is specified.  For example, your news list might have 200 items in it but you only want to display five items per page.  Unlike the SharePoint List API, the Search API provides the value of 200 in the TotalRows property of the table of results.
  • The SharePoint Search API provides a method called set_startRow which allows you to specify the starting position of the results provided.  Unlike the List API as described above, the Search API’s positions are linear and sequential.

With these two pieces of information, we can implement Previous, Next and calculate the position of each page.  To fetch the page, we add it to the query string and set the start row like this:

page = manageQueryStringParameter(pageURLParameter);
if (page != “”) {
     keywordQuery.set_startRow(page * rowsPerPage);
}

The start row will be the position in the search results based on the current page and the number of results you’re displaying per page.  Since startRow starts at zero, page 1 should be page 0, page 2 should be page 1, etc. 

Calculating previous, next and each page is now straightforward by simply setting the page in the query parameter.  Since we know the total number of rows and the current page, we can enable/disable the previous and next links and create links for each page.

// calculate paging                    
var totalRows = results.m_value.ResultTables[0].TotalRows;
if (page == “”)
     page = 0;
else
     page = parseInt(page);
                    
if (page > 0)
{
       var prevPage = page -1;
     $(‘#pages’).append(“<a href=’./news-list?category=” + category + “&page=” + prevPage + “‘>Prev</a>”);
}                   


var totalPages = totalRows / rowsPerPage;
for (i = 0; i<totalPages; i++)
{
       // count with a 0 but display as a 1
       var pageDisplay = i + 1;
       $(‘#pages’).append(“<a href=’./news-list?category=” + category + “&page=” + i + “‘>” + pageDisplay  + “</a> | “);
}   


if ((page + 1) * rowsPerPage < totalRows)
{
     var nextPage = page + 1;                       
     $(‘#pages’).append(“<a href=’./news-list?category=” + category + “&page=” + nextPage + “‘>Next</a>”);
}

One important note on the total rows property – it can be an estimated total.  The SharePoint Search API provides estimates if the search results are large (e.g. you have thousands of items coming back from the search).  There is a property on the result table called IsTotalRowsExact which you can check to see if the total rows property value is exact or an estimate.

Read More