Monday, July 21, 2014

Add ListViewWebPart to wiki page with CSOM

Here is how you can add very easily a ListViewWebPart to a SharePoint wiki page. I've used this code in a provider hosted app for SharePoint online, but of course this will work on-premise as well.
In this example I'm using again the powerfull extensions methods of OfficeAMS.

In SharePoint 2013 list view web parts are still the same xsltlistviewwebparts that we are used to work with. The only difference in this new version is that we have the ability to overwrite the rendering on the client side by making use of JS Link.

A little bit of background information: a remote event receiver is binded to a list, OnItemAdded, I'm creating a new SharePoint site which I'm provisioning with CSOM. I need a document library, a page and a ListViewWebPart of the document library on the wiki page.

public SPRemoteEventResult ProcessEvent(SPRemoteEventProperties properties)
{
var result = new SPRemoteEventResult();
// Get the token from the request header. Because this is a .svc remote event receiver we use the current Operationcontext.
var requestProperty = (HttpRequestMessageProperty)OperationContext.Current.IncomingMessageProperties[HttpRequestMessageProperty.Name];
var contextToken = TokenHelper.ReadAndValidateContextToken(requestProperty.Headers["X-SP-ContextToken"], requestProperty.Headers[HttpRequestHeader.Host]);
using (var clientContext = TokenHelper.CreateRemoteEventReceiverClientContext(properties))
{
if (clientContext != null)
{
try
{
//create document library
clientContext.Web.AddDocumentLibrary("Internal documents", true);
//create wiki page
clientContext.Web.AddWikiPage("Site Pages", "Documents.aspx");
//set correct page layout to wiki page
clientContext.Web.AddLayoutToWikiPage("sitepages", WikiPageLayout.TwoColumns, "Documents");
//get new document library
var internallDocLib = clientContext.Web.GetListByTitle("Internal documents");
//add listviewwebparts to page
var internalDocsWebPartEntity = new WebPartEntity
{
WebPartIndex = 2,
WebPartTitle = "Latest Internal Documents",
WebPartZone = "Left",
WebPartXml = string.Format(Globals.ListViewWebPart, internallDocLib.Id, "Latest Internal Documents")
};
//params: pageslibrary url, webpartentity, wiki page, table row, table column, addSpace under web part
clientContext.Web.AddWebPartToWikiPage("sitepages", internalDocsWebPartEntity, "documents.aspx", 1, 1, true);
}
catch
{
//write logging information
}
}
}
}

You'll have noticed that the WebPartXml property of the WebPartEntity contains a reference to my globals class. In this class, I've created an xml snippet which I can reuse if I want to add a xsltlistviewwebpart to a page. Using this snippet requires 2 params, the List.Id and a title for the web part. Feel free to adapt this snippet for your requirements (toolbar, JS Link reference...)

public class Globals
{
//WebParts
public const string ListViewWebPart = "<webParts>" +
"<webPart xmlns='http://schemas.microsoft.com/WebPart/v3'>" +
"<metaData>" +
"<type name='Microsoft.SharePoint.WebPartPages.XsltListViewWebPart, Microsoft.SharePoint, Version=16.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c' />" +
"<importErrorMessage>Cannot import this Web Part.</importErrorMessage>" +
"</metaData>" +
"<data>" +
"<properties>" +
"<property name='ShowWithSampleData' type='bool'>False</property>" +
"<property name='Default' type='string' />" +
"<property name='NoDefaultStyle' type='string' null='true' />" +
"<property name='CacheXslStorage' type='bool'>True</property>" +
"<property name='ViewContentTypeId' type='string' />" +
"<property name='XmlDefinitionLink' type='string' />" +
"<property name='ManualRefresh' type='bool'>False</property>" +
"<property name='ListUrl' type='string' />" +
"<property name='ListId' type='System.Guid, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'>{0}</property>" +
"<property name='TitleUrl' type='string'></property>" +
"<property name='EnableOriginalValue' type='bool'>False</property>" +
"<property name='Direction' type='direction'>NotSet</property>" +
"<property name='ServerRender' type='bool'>False</property>" +
"<property name='ViewFlags' type='Microsoft.SharePoint.SPViewFlags, Microsoft.SharePoint, Version=16.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c'>Html, TabularView, Hidden, Mobile</property>" +
"<property name='AllowConnect' type='bool'>True</property>" +
"<property name='ListName' type='string'>{0}</property>" +
"<property name='ListDisplayName' type='string' />" +
"<property name='Title' type='string'>{1}</property>" +
"</properties>" +
"</data>" +
"</webPart>" +
"</webParts>";
}
view raw Globals.cs hosted with ❤ by GitHub
In a next post, I'll show you how to adapt the view of the webpart.

Friday, July 11, 2014

Register remote event receiver (RER) with CSOM

I'm currently working on a provider hosted SharePoint 2013 app. The aim is to build a self provisioning app by making use of the client side object model  (csom).

In an old fashion way, provisioning of site columns, content types, lists... was done with declarative coding. Unfortunately, declarative host web provisioning isn't possible. Luckily for us developers, the OfficeAMS contributors are working hard on building nice extension methods to speed up SharePoint development. 

in the following example I'm provisioning the host web during the AppInstalled event.
  • creating a new list
  • binding the remote event receiver to the newly created list
The remote event receiver is a .svc service hosted on the remote web of the provider hosted app. In my example I'm using Azure.

//do not forget to reference officeams.core (http://officeams.codeplex.com) dll to call extension methods.
//create settingslist
hostWebClientContext.Web.AddList(100, new Guid("00bfea71-de22-43b2-a848-c05709900100"), "Settings", false);
//get settings list
var settingsList = hostWebClientContext.Web.GetListByTitle("Settings");
//prepare eventreceiver binding
var eventReceivers = settingsList.EventReceivers;
hostWebClientContext.Load(eventReceivers);
hostWebClientContext.ExecuteQuery();
//since this code example is running in a web service class during AppInstalled event,
//we can get the remoteweb url from the WebOperationContext
//remoteWebUrl will result in: "yourServiceBusName.servicebus.windows.net"
var remoteWebUrl = string.Empty;
if(null != WebOperationContext.Current)
remoteWebUrl = WebOperationContext.Current.IncomingRequest.Headers["Host"];
//little trick to be able to debug our event receiver
#if DEBUG
remoteWebUrl = String.Format("https://{0}/3458491647/3050950848/obj/f44563bb-02a0-4a5b-b2da-737023b5a03e", remoteWebUrl);
#endif
var settingsRerAdded = new EventReceiverDefinitionCreationInformation
{
ReceiverUrl = String.Format("{0}{1}", remoteWebUrl, "SettingsEvents.svc"),
ReceiverName = "SettingsEventReceiverAdded",
Synchronization = EventReceiverSynchronization.Synchronous,
SequenceNumber = 10000,
EventType = EventReceiverType.ItemAdded
};
settingsList.EventReceivers.Add(settingsRerAdded);
settingsList.Update();
hostWebClientContext.Load(settingsList.EventReceivers);
hostWebClientContext.ExecuteQuery();
In fact, binding the RER isn't rocket science, but since the ReceiverUrl property is set to the absolute address of the .svc we won't be able to debug this thing. If we want to debug a remote event receiver hosted on Azure we have to make use of an azure service bus.

This is why I'm using the #if DEBUG directives of visual studio. If in debug mode, I'm overwriting the absolute .svc url in the following format:
https://server.servicebus.windows.net/computer/account/obj/guid/EventReceiver.svc 

This raises the question, what are the computer, account and guid parameters in this URL? I figured out these values by opening the .debugapp package that is generated by visual studio during debugging.

While debugging you can find this package in the debug folder of your project.
Extract this .debugapp package and open the AppManifest.xml file in a text editor.
The InstalledEventEndpoint contains all the values your are looking for.


<?xml version="1.0" encoding="utf-8"?>
<!--Created:cb85b80c-f585-40ff-8bfc-12ff4d0e34a9-->
<App xmlns="http://schemas.microsoft.com/sharepoint/2012/app/manifest" Name="YourAppName" ProductID="{6b1a2b10-c511-4484-a0f2-d8d247e1c3e7}" Version="1.6.0.8" SharePointMinVersion="15.0.0.0">
<Properties>
<Title>YourAppName</Title>
<StartPage>https://localhost:44303/Pages/Default.aspx?{StandardTokens}&amp;SPHostTitle={HostTitle}</StartPage>
<UpgradedEventEndpoint>https://YourServiceBus.servicebus.windows.net/3458491647/3050950848/obj/f44563bb-02a0-4a5b-b2da-737023b5a03e/Services/AppEventReceiver.svc</UpgradedEventEndpoint>
<InstalledEventEndpoint>https://YourServiceBus.servicebus.windows.net/3458491647/3050950848/obj/f44563bb-02a0-4a5b-b2da-737023b5a03e/Services/AppEventReceiver.svc</InstalledEventEndpoint>
</Properties>
<AppPrincipal>
<AutoDeployedWebApplication>
<DebugInfo ClientSecret="" AppUrl="" />
</AutoDeployedWebApplication>
</AppPrincipal>
<AppPermissionRequests AllowAppOnlyPolicy="true">
<AppPermissionRequest Scope="http://sharepoint/content/sitecollection" Right="FullControl" />
<AppPermissionRequest Scope="http://sharepoint/social/tenant" Right="FullControl" />
<AppPermissionRequest Scope="http://sharepoint/content/tenant" Right="FullControl" />
<AppPermissionRequest Scope="http://sharepoint/taxonomy" Right="Write" />
<AppPermissionRequest Scope="http://sharepoint/content/sitecollection/web" Right="FullControl" />
</AppPermissionRequests>
</App>
view raw AppManifest.xml hosted with ❤ by GitHub

Thursday, July 3, 2014

Copy a file across site collections with REST

I had to give the users the possibility to copy a file from the SharePoint search results to a document library in another site collection. This requirement resulted in the following custom display template: when clicking the 'Copy to other site collection' button, a div is expanded and shows a text box to provide a new filename and 2 radio buttons to select the target document library.



All the code is based on the example from Mikael Svenson. Since he is copying a file from appweb to hostweb he is using the SP.REquestExector javascript library. When I first read his article I thought this technique wasn't usable for my problem because I was going to copy files within the same domain.

Altough the SP.RequestExecutor library is a cross-domain javascript libary, this library is perfectly usable to use within the same domain. And it is fixed, so the patching described by Mikael isn't required anymore :)

This example was built for SharePoint 2013 online but works on premise as well.

function copyFile(targetSiteUrl, contextId, targetFileName, targetList) {
//get current search result item
var currentItem = window.ctxData[contextId];
// Create a request executor.
var sourceExecutor = new SP.RequestExecutor(currentItem.SPSiteUrl);
var targetExecutor = new SP.RequestExecutor(targetSiteUrl);
// Get the absolute server url.
var serverUrlRegex = new RegExp(/http(s?):\/\/([\w]+\.){1}([\w]+\.?)+/);
var serverUrl = serverUrlRegex.exec(currentItem.SPSiteUrl)[0];
// Get the source file url.
var fileUrl = currentItem.Path.substr(serverUrl.length, currentItem.Path.length - serverUrl.length);
// Get the target folder url.
var folderUrl = targetSiteUrl.substr(serverUrl.length, targetSiteUrl.length - serverUrl.length) + "/" + targetList;
// Get form digest of target site collection
$.ajax({
url: targetSiteUrl + "/_api/contextinfo",
type: "POST",
headers: {
"Accept": "application/json;odata=verbose"
},
success: function (data) {
var digest = data.d.GetContextWebInformation.FormDigestValue;
// Build executor action to retrieve the file data.
var getFileAction = {
url: currentItem.SPSiteUrl + "/_api/web/GetFileByServerRelativeUrl('" + fileUrl + "')/$value",
method: "GET",
binaryStringResponseBody: true,
success: function (getFileData) {
// Get the binary data.
var result = data.body;
// Build executor action to copy the file data to the new location.
var copyFileAction = {
url: targetSiteUrl + "/_api/web/GetFolderByServerRelativeUrl('" + folderUrl + "')/Files/Add(url='" + targetFileName + "." + currentItem.FileExtension + "', overwrite=true)",
method: "POST",
headers: {
"Accept": "application/json; odata=verbose",
"X-RequestDigest": digest
},
contentType: "application/json;odata=verbose",
binaryStringRequestBody: true,
body: getFileData.body,
success: function(copyFileData) {
//show your 'file copied successfully' message
},
error: function(ex) {
//show your 'failed' message
}
};
targetExecutor.executeAsync(copyFileAction);
},
error: function(ex) {
//fail
}
};
sourceExecutor.executeAsync(getFileAction);
},
error: function(ex) {
//fail
}
});
}