What’s In This Chapter
If you have not heard about public or private clouds, you must not be reading the news or blogs, or looking at what the different vendors are doing. Software as a service (SAAS) is all the rage in the computing world. Although it has many merits, including ease of deployment, anywhere access, and quick upgrades, at the same time it has a number of obstacles, such as limited offline support, fewer mature development tools, and less control of customization. Regardless of these limitations, many people are looking to the cloud as the next major shift in the computing world. If you have not tried to build applications against a cloud service such as Microsoft Azure or SharePoint Online, start today.
SharePoint Online is, as the name implies, a Microsoft-hosted version of SharePoint. One of the major differences between your on-premises deployment of SharePoint and SharePoint Online is that SharePoint Online does not support the full range of customizations or functionality that you can host on-premises. The reason for this is that your SharePoint deployment is hosted in a multitenant environment, so your SharePoint deployment is shared with other customer deployments. When building solutions on SharePoint Online, you must build nonfarm solutions rather than solutions that require full-trust or farm-level permissions because you do not own the farm in SharePoint Online. Table 5-1 shows examples of no-code solutions versus farm solutions.
No-Code Solution | Farm Solution |
Custom Markup (HTML, ASP.NET, XSLT) | Custom Server Components (coded workflows, timer jobs, and Window Services) |
SharePoint Designer solutions | Visual Studio Solutions (except for Sandboxed Solutions) |
Client-side code, such as Client OMs, including JavaScript and Silverlight | Application pages |
Coding against SharePoint Web Services, and REST Services | Visual Web Parts without VS Power Tools |
One other major difference between online and on-premises is that the cloud version is currently a subset of the full feature. In particular, SharePoint Online does not support PerformancePoint or FAST in the cloud. In addition, when dealing with search technologies, architecture and deployment become more critical as you deploying in the cloud. For example, if you have a hybrid environment in which you want to have SharePoint sites split between on-premises and online, where do you put which site? The answer is that it depends, but either place that you decide to place it, you deal with access across WANs.
As you can see by the previous summary, some of the ways you are used to developing on-premises do not translate to the multitenant cloud. Using server-side code is a no-no when it comes to building applications on which you run SharePoint with other tenants on the server. However, when developing for online use, you have a number of choices for development, including Sandboxed Solutions, SharePoint Designer (SPD), InfoPath, Access, or the SharePoint client object, REST and Web Services models in ASP.NET, Windows Form applications, or Silverlight applications.
Before diving into development on SharePoint Online, first consider deploying and debugging. Sometimes you might find deployment and debugging painful when developing for SharePoint Online, which provides a limited set of capabilities for these two functions as opposed to SharePoint on-premises. For example, because you cannot run Visual Studio on your SharePoint Online server and because SharePoint Online, today, does not support PowerShell, you must to manually copy your WSP files to SharePoint Online and activate them.
In addition, you cannot attach the debugger directly to your processes because VS is not running on your server, and you cannot set up remote debugging on SharePoint Online. Your only options for debugging are to use a local SharePoint Server, write to a list as a log file, or use the Developer Dashboard.
To deploy solutions to SharePoint Online, you need to manually copy your WSP files, which you can have Visual Studio package up for you. Then, you need to activate the solution contained in the WSP file using the SharePoint user interface, as shown in Figure 5-1. From there, you can use your web part, workflow action, or whatever other component it contains that you have created for SharePoint Online.
One feature that helps with debugging in SharePoint Online is that the Developer Dashboard is enabled by default. By clicking the Developer Dashboard icon in the upper-right corner of the SharePoint user interface, you can quickly see what’s happening. However, Sandboxed Solutions do not support SPMonitoredScope, which enables you to add richer tracking of your solution in the sandbox. Figure 5-2 shows the Developer Dashboard running in SharePoint.
Because you are dealing with an online service, you need to become adept at looking at HTTP packets when you debug. For this reason, you need to install Fiddler (or an equivalent tool) on your machines because Fiddler can help you understand the traffic between your client machine and SharePoint Online. When you can look at the traffic, you can discover what happens between the instances and also determine whether your problems are authentication- or identity-related, or are caused by other issues.
For Silverlight or client-side code, your choices for debugging are better because you can use the tools built into Internet Explorer or Firefox to debug your solution.
The following Sandbox web part shows how to log on to a SharePoint list for debugging purposes:
using System; using System.ComponentModel; using System.Runtime.InteropServices; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.WebControls.WebParts; using Microsoft.SharePoint; using Microsoft.SharePoint.WebControls; namespace SharePointOnlineLogging.WebPart1 { [ToolboxItemAttribute(false)] public class WebPart1 : WebPart { private Button logResultsButton = new Button() { Text = “Log Results” }; private Label lbl = new Label(); public WebPart1() { } protected override void CreateChildControls() { logResultsButton.Click += (object sender, EventArgs e) => { SPContext context = SPContext.Current; SPWeb web = context.Web; SPList list = web.GetList(“/Lists/ SharePointOnlineLogging-ListInstance1“); string logResults = DateTime.Now.ToShortDateString() + “ “ + DateTime.Now.ToShortTimeString() + “: Logged from Sandbox Web Part!“; SPListItem newItem = list.AddItem(); newItem[“Title“] = “New Log Result - “ + DateTime.Now.ToShortTimeString(); newItem[“LoggingResult“] = logResults; newItem.Update(); lbl.Text = “Logged Result: “ + logResults; }; Controls.Add(logResultsButton); Controls.Add(lbl); //base.CreateChildControls(); } protected override void RenderContents(HtmlTextWriter writer) { base.RenderContents(writer); } } }
code snippetWebPart1.cs
As you will find, for out-of-the-box (OOB) functionality, SharePoint Online is a viable solution. The areas to watch out for include custom code and unsupported features in the cloud or features that require administrative access to the server. To help understand areas in which using SharePoint in the cloud makes sense, the following scenarios describe where you can use the cloud and where you can’t:
Office 365 is the next generation of the BPOS suite and is the latest offering in Microsoft’s cloud services infrastructure. With Office 365, you get Exchange, SharePoint, Lync, and Office Professional Plus as part of the bundle. Microsoft runs the infrastructure for you by providing you with hosting, billing, backup, antivirus, and anti-spam services, as well as a host of other services that an IT department would normally run on-premises.
So, what’s the difference between SharePoint Online and SharePoint on-premises from a developer perspective? There are a number of differences, with the majority falling into the level of control and the extensibility scenario. For example, because SharePoint Online runs in a multitenant environment, you do not have the same level of access that you do when you run on-premises on your own server. You do not have access to SharePoint Central Administration. You cannot run full-trust solutions, and any pieces of SharePoint that require full server configuration changes, Central Administration access, or full-trust access will not work in SharePoint Online. So, if you find that you need full control of your environment or the ability to write code that accesses all the resources on the SharePoint Server, you may need to continue to deploy SharePoint on-premises or look at implementation options for those components, including possibly migration these to Azure-hosted solutions.
For example, Figure 5-3 shows the tenant administration page in SharePoint Online. This administration page is different and less functional than what you can do in SharePoint’s Central Administration site.
Because SharePoint Online is not an exact replica of SharePoint on-premises, you may be wondering what is supported in SharePoint Online. Table 5-2 shows the developer features available in SharePoint Online. There are also IT and end user features not available in SharePoint Online, so you need to check the SharePoint website for more information about which features are available in SharePoint Online.
Area | SharePoint Online | SharePoint On-Premises |
Browser-only customization (for example, CQWP and CEWP) | Supported | Supported |
SharePoint Designer | Supported but limited BCS support | Supported |
SharePoint Solutions | Sandbox only | SandboxFarm/full trust |
Client object models | Supported | Supported |
Silverlight web part | Supported | Supported |
Web Services | Subset of Web ServicesREST APIExcel services REST API | All Web ServicesREST APIExcel services REST API |
InfoPath forms | Sandbox only | SandboxFull trust |
Workflow and Workflow Activities | DeclarativeSandbox workflow activities only | DeclarativeSandbox and full-trust workflow activities |
Anonymous access | Not supported | Supported |
Mobile control adapters | Not supported | Supported |
Visual Studio | Supported | Supported |
Developer Dashboard | Supported | Supported |
Business Connectivity Services | Supported against WCF Services only | Supported |
Custom authentication | Office 365 IDADFS | Custom authentication supported |
Given the limitations of SharePoint Online, there may be times when you need to build a hybrid solution that encompasses both SharePoint Online and SharePoint on-premises. For example, given that SharePoint Online supports only a limited subset of the Business Connectivity Services (BCS) functionality found on-premises, bridging this gap is not as easy when it comes to data connectivity. However, for applications that need to span across the two environments when writing client-side code, such as Silverlight and JavaScript applications, hybrid solutions can make sense as long as you can overcome the connectivity and identity issues. Another very useful type of hybrid solution involves leveraging both Office 365 and Windows Azure. By combining the two clouds, you can build richer solutions that span the two environments. The rest of this chapter covers integrating SharePoint, both on-premises and online, with the Microsoft Azure technologies.
The rest of this chapter deals with writing SharePoint Online applications. Because SharePoint Online is the same software as SharePoint on-premises, except with some functionality and platform differences, many of the techniques you’ll learn throughout this book work with SharePoint Online. The main hurdle that you run into when writing SharePoint Online applications is in understanding the limitations of SharePoint Online and making sure you understand how to authenticate and authorize using SharePoint Online. Looking at some sample scenarios can help make this clear, so dive in.
One of the major components of Office 365 is the authentication system as we saw in Chapter 3. Office 365 supports direct authentication via a username and password. The Office 365 security system is known as OrgID, which is based off the LiveID technologies. Although it’s similar, you cannot use LiveID in the same way you can use OrgID. The following steps outline where OrgID differs:
When writing SharePoint Online applications, you need to grab the two cookies that represent your tokens and add them to your calls as part of your CookieContainer.
Rather than writing your own code to do all this work, you have two choices.
Both samples are included in the sample applications of this chapter. The main difference is that the Microsoft sample does display a web browser control, as shown in Figure 5-4, and the other sample does not.
You may think you can just fire up Visual Studio and start working directly against SharePoint Online. Unfortunately, there is a limitation in the current SharePoint tools for Visual Studio such that it requires a locally-deployed SharePoint instance for development so that you can’t point your VS projects at your SharePoint Online tenant. Instead, you must develop and debug locally and then deploy your applications to SharePoint Online. With the next release of Visual Studio, Visual Studio 11 (currently in beta), you will be able to deploy SharePoint solutions to a remote SharePoint server.
To make the authentication concepts clear, we will write a sample application that calls the client object model using both sets of samples for performing authentication. The sample can retrieve the title of the website and all the lists in the site, and display them on the screen.
The Microsoft authentication sample can be compiled and referenced in your projects. The only issue with the Microsoft sample is that it does display its own form so you can run into issues trying to use the sample in Windows Forms applications. The following sample references the Microsoft sample and displays the name of the site and all the lists using the client object model:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.SharePoint.Client; using MSDN.Samples.ClaimsAuth; namespace SharePoint_Online_MSFT_Authentication { class Program { [STAThread] static void Main(string[] args) { if (args.Length < 1) { Console.WriteLine ("Please enter a URL for SharePoint Online"); return; } string targetSite = args[0]; using (ClientContext ctx = ClaimClientContext.GetAuthenticatedContext (targetSite)) { if (ctx != null) { ctx.Load(ctx.Web); ctx.ExecuteQuery(); Console.WriteLine ("Your site name is: " + ctx.Web.Title.ToString()); Console.WriteLine("Your lists are: "); ListCollection listCollection = ctx.Web.Lists; ctx.Load( listCollection, lists => lists .Include( list => list.Title, list => list.Hidden) .Where(list => !list.Hidden) ); ctx.ExecuteQuery(); foreach (var list in listCollection) Console.WriteLine(list.Title); } } Console.ReadLine(); } } }
Rather than using a web browser control to capture the username and password, Wictor’s sample takes the username and password as inputs and then uses the cookies to authenticate against the service. The following sample uses the same client object model code but with a different authentication code:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.SharePoint.Client; namespace Wictor.Office365.ClaimsDemo { class Program { static void Main(string[] args) { if (args.Count() != 3) { Console.WriteLine("Syntax: Wictor.Office365.ClaimsDemo.exe url username password"); } MsOnlineClaimsHelper claimsHelper = new MsOnlineClaimsHelper (args[0], args[1], args[2]); using (ClientContext context = new ClientContext(args[0])) { context.ExecutingWebRequest += claimsHelper.clientContext_ExecutingWebRequest; context.Load(context.Web); context.ExecuteQuery(); Console.WriteLine("Name of the web is: " + context.Web.Title); Console.WriteLine("Your lists are: "); ListCollection listCollection = context.Web.Lists; context.Load( listCollection, lists => lists .Include( list => list.Title, list => list.Hidden) .Where(list => !list.Hidden) ); context.ExecuteQuery(); foreach (var list in listCollection) Console.WriteLine(list.Title); } } } }
SharePoint 2010 offers a number of new list, view, and event enhancements that translates into developer enhancements for SharePoint Online. For example, there is support for referential integrity and formula validation in lists. In addition, all views of lists are now based on the XsltListViewWebPart, which makes customization easier. Finally, you can take advantage of new events with SharePoint—for example, when new sites and lists are added. Now dive into these new enhancements.
Lists are the backbone of SharePoint. They’re where you create your data models and your data instances. They’re what your users understand are their documents or tasks. Without lists, your SharePoint site would cease to function because SharePoint uses lists for its own functionality and capability to run. With 2010, you can take advantage of new list enhancements and even new tools to work with your custom lists.
Before diving into the new enhancements in lists, you need to first look at the tools you can use to create your lists. The tools of choice are SharePoint Designer (SPD) and Visual Studio (VS). Both are good options, depending on what you want to do. If you want barebones, down to the metal, XML-style creation of lists, Visual Studio is your best bet. If you would rather work with a GUI, SPD provides a nice interface to work with your lists, whether it is creating columns or views, or customizing your list settings. Of course, you can use the built-in list settings in SharePoint to work with your lists, but SPD is a better choice if you are interested in a GUI editor.
SPD makes it easy to work with your lists, whether it’s creating new lists or modifying existing ones. SPD can make working with your columns, views, forms, content types, workflows, and even custom actions for your list easier. If you need to rapidly create a list or list definition, SPD is going to be the fastest and easiest way to work with your SharePoint lists. You must give up some control because SPD does not enable you to get down to the same level of customization that Visual Studio does, but you trade customizability for speed when you work with SPD. Figure 5-5 shows the List Settings user interface for SPD.
Also with SharePoint Designer, you can edit your list columns and permissions using a graphical interface. Figure 5-6 shows editing SharePoint columns using SharePoint Designer.
With Visual Studio, you can create list definitions and list instances. List definitions are a built-in project type for Visual Studio. Figure 5-7 shows the List definition project in Visual Studio.
One common complaint about SharePoint is that it doesn’t behave like a relational database. For example, if you have a lookup between two lists and you want to have some referential integrity, SharePoint previously would not block or cascade your deletes between your lists. With 2010, SharePoint can now block or cascade your deletes between lists automatically. Now, don’t think SharePoint is going to become your replacement for SQL Server with this functionality. It is implemented more to make simple relationships work, and if you have a complex data model, you want to use SQL Server and surface its data through SharePoint using BCS and external lists.
The way that list relationships work is through the creation of a lookup between your lists. One new thing about lookups is that you can retrieve more than just the identifier, including additional properties from the list such as built-in or custom fields. On the list where you create your lookup, you can enforce the relationship behavior to either restrict deleting parent list items (if items exist in the list) related to the parent item, or cascade the delete from the parent list to the child list. Figure 5-8 shows the user interface for setting the properties of the lookup column to enforce relationship behaviors.
If you restrict the delete, SharePoint throws an error telling the user that an item is in the related list that exists and cancels deleting the error, as shown in Figure 5-9.
If you cascade the delete, SharePoint performs a transacted delete of the related items in the related list.
Through the user interface you cannot create cross-web lookups, but through the object model and by using site columns, you can. Cross-web lookups don’t support the referential integrity features such as cascading deletes. Also, referential integrity won’t be enforced for a lookup that has multiple values.
When working with the object model, you want to use the RelationshipDeleteBehavior property on your SPFieldLookup object. This property takes a value from the SPRelationshipDeleteBehavior enumerator of which the possible values are None, Cascade, or Restrict.
Two Properties affect relationships when you use them with the SPWebApplication class:
To find lookup fields, you can use the GetRelatedFields method of your list, which returns an SPRelatedFieldCollection collection. From this collection, you can iterate through each related field. From there, you can retrieve properties, such as the LookupList that the field is related to, the ListID, the FieldID, or the relationship behavior when something is deleted from the list.
using (SPSite site = new SPSite("http://intranet.contoso.com")) { SPList list = site.AllWebs[""].Lists["Orders"]; SPRelatedFieldCollection relatedFields = list.GetRelatedFields(); foreach (SPRelatedField relatedField in relatedFields) { //Lookup the list for each SPList relatedList = relatedField.LookupList; MessageBox.Show(relatedField.ListId + " " + relatedField.FieldId); //MessageBox.Show("List Name: " + relatedList.Title + " Relationship Behavior: " + relatedField.RelationshipDeleteBehavior.ToString()); } }
Another new list feature gives you the capability to do list validation using formulas. This is more of an end-user or power-user feature, but for simple validation scenarios, developers find this feature easy to use because it is quicker to write formulas than to write code. You can write validation at either the list level or the column level, depending on your needs. SharePoint also supports this approach for site columns that you add to your content types. Figure 5-10 shows setting the formula, and Figure 5-11 shows the custom error message that appears when the formula does not validate.
One of the easiest ways to understand which formulas you can enter into the validation rules is to connect Microsoft Access to your SharePoint list and use the formula editor in Access. SharePoint supports the same formula functions as Access, so you can use string manipulation, logic, financial, conversion, and date/time functionality. In the API, you can use the SPList.ValidationFormula and SPField.ValidationFormula properties to get and set your formulas.
Another new feature of lists is the capability to ensure uniqueness for the values in your columns. Previously, SharePoint didn’t require unique values so that multiple items could have the same value for a field. With uniqueness, SharePoint can use the field as an index to make lookups faster because the field is guaranteed to have a unique value.
Just like a database, SharePoint supports list joins. Again, SharePoint won’t provide as much functionality as a relational database because its data model sits above the bare-metal database, but compared to 2007 the join functionality is a welcome addition. SharePoint can perform left and inner joins but not right joins. An inner join is where you combine the values from the data sources based on the join predicate, such as “show me all employees who are in a particular department based on their department ID,” which joins an employee list and a department list, both of which have department IDs in them. A left join or left outer join just means that anything that appears in the leftmost list, even if it does not have matching entries in the other list, is returned in the result set.
The following code performs a join across two lists on a lookup field. You need to set the Joins property on your SPQuery object with the join you want to perform. In the code, you join on the Customers list, where the customer is the same as the Customer in the Orders list.
Beyond setting the Joins property, you must specify a value for the ProjectedFields property. This property gets fields from the lookup list. You can alias the field by using the Name attribute and tell SharePoint the field name by using the ShowField attribute. When you get your results, you must use the SPFieldLookupValue object to display the values for your projected fields.
SPList OrderList = web.Lists["Orders"]; SPQuery CustomerQuery = new SPQuery(); CustomerQuery.Joins = "<Join Type='INNER' ListAlias='Customers'>" + "<Eq>" + "<FieldRef Name='Customer' RefType='Id' />" + "<FieldRef List='Customers' Name='ID' />" + "</Eq>" + "</Join>"; StringBuilder ProjectedFields = new StringBuilder(); ProjectedFields.Append("<Field Name='CustomerTitle' Type='Lookup' List='Customers' ShowField='Title' />"); ProjectedFields.Append("<Field Name='CustomerAddress' Type='Lookup' List='Customers' ShowField='CustomerNum' />"); CustomerQuery.ProjectedFields = ProjectedFields.ToString(); SPListItemCollection Results = OrderList.GetItems(CustomerQuery); foreach (SPListItem Result in Results) { SPFieldLookupValue CustomerTitle = new SPFieldLookupValue(Result["CustomerTitle"].ToString()); SPFieldLookupValue CustomerAddress = new SPFieldLookupValue(Result["CustomerAddress"].ToString()); MessageBox.Show(Result.Title + " " + CustomerTitle.LookupValue + " " + CustomerAddress.LookupValue); }
One of the new features of lists is that you can customize the default forms for your list items. SharePoint 2010 moved to using web part pages for the default forms, so your customization can be as easy as adding new web parts to the existing default forms, or you can even replace the default forms with your own custom InfoPath forms. With SharePoint 2010, you can modify the New, Display, and Edit forms.
When you use the Ribbon option to edit the form in InfoPath, InfoPath automatically launches. Connect to your list and display your form, as shown in Figure 5-12.
The biggest change with views in 2010 is the technology used to display them. 2010 uses the SharePoint Designer XsltListViewWebPart as the default web part for viewing lists. This is much better than 2007 for a number of reasons:
The easiest way to understand, prototype, and get sample code is to use SPD to design your views and then view the code that SPD creates to work with your XSLT views. For example, you may want to create a view that makes any numbers that meet or exceed a limit turn red, yellow, or green and implements a custom mouseover event. With SPD, this is as easy as using the conditional formatting functionality and the IntelliSense built-in to modify the view. Figure 5-13 shows the editing of the view in SPD.
The following code shows the conditional formatting XSLT that SPD generates for you:
<div align="right" onmouseover="javascript:alert('You moused over!'),"> <xsl:attribute name="style"> <xsl:if test="$thisNode/@Rating. = 3" xmlns:ddwrt="http://schemas.microsoft.com /WebParts/v2/DataView/runtime" ddwrt:cf_explicit="1">background-color: #FFFF00;</xsl:if> <xsl:if test="$thisNode/@Rating. >= 4" xmlsn:ddwrt="http://schemas.microsoft. com/WebParts/v2/DataView/runtime" ddwrt:cf_explicit="1"> background-color:#71B84F;</xsl:if> <xsl:if test="$thisNode/@Rating. <= 2" ddwrt:cf_explicit="1" xmlns:ddwrt=" http://schemas.microsoft.com/WebParts/v2/DataView/runtime"> background-color:#FF0000;</xsl:if> </xsl:attribute>
To work with views programmatically, use the SPView object and SPViewCollection. You can add new views, modify existing views, or delete views. The following are a few properties in which you should be interested:
With 2010, you can take advantage of six new events, including WebAdding, WebProvisioned, ListAdding, ListAdded, ListDeleting, and ListDeleted. This is in addition to the events that were introduced in SharePoint 2007, such as ItemAdding, ItemUpdating, and ItemUpdated. There are also other enhancements beyond new events, including new registration scopes to support the new events, new tools support in Visual Studio, support for post-synchronous events, and custom error pages and redirection.
As part of SharePoint 2010, you can take advantage of ten new events. These events enable you to capture creation and provisioning of new webs, and the creation and deletion of lists. Table 5-3 goes through each of the events and what you can use them for.
Name | Description |
WebAdding | A synchronous event that happens before the web is added. Some URL properties may not exist yet for the new site because the new site does not exist yet. |
WebProvisioned | A synchronous or asynchronous after-event that occurs after the web is created. You make the event synchronous or asynchronous by using the Synchronization property and setting it to Asynchronous or Synchronous. This is located under the Receiver node in the elements.xml file for your feature. |
ListAdding | A synchronous event that happens before a list is created. |
ListAdded | A synchronous or asynchronous after-event that happens after a list is created but before it is presented to the user. |
ListDeleting | A synchronous event that happens before a list is deleted. |
ListDeleted | A synchronous or asynchronous after-event that happens after a list is deleted. |
WorkflowStarting | An event that occurs before a workflow starts. All Workflow events get SPWorkflowEventProperties object that contains information about the workflow. |
WorkflowStarted | An event that occurs after a workflow has started. |
WorkflowPostponed | An event that occurs if a workflow is postponed. |
WorkflowCompleted | An event that occurs after a workflow is completed. |
Using these events is the same as writing event receivers for any other type of events in SharePoint. The nice thing about writing event receivers with SharePoint 2010 is that you have Visual Studio 2010 support for writing and deploying your event receivers. Figure 5-14 shows the new event receiver template in Visual Studio, where you can select the type of event receiver you want to create and the events you want to listen for in your receiver. After you finish the wizard inside of Visual Studio, you can modify your feature definition or your code using the standard Visual Studio SharePoint tools. Plus, with on-click deployment and debugging, it’s a lot easier to get your receiver deployed and start debugging it.
The code that follows shows you how to use the new web events in SharePoint. The code writes the properties for the event to an event log list. The sample applications with this book include the same sample for the new list events, but for brevity only the web event sample code is shown. If you wanted to, you could cancel the before-events, such as WebAdding, ListDeleting, or ListAdding, by setting the Cancel property to false. These events fire even in the Recycle Bin, so if you restore a list or delete a list, you get an event for those actions.
namespace WebEventReceiver.EventReceiver1 { /// <summary> /// Web Events /// </summary> public class EventReceiver1 : SPWebEventReceiver { /// <summary> /// A site is being provisioned. /// </summary> public override void WebAdding(SPWebEventProperties properties) { LogWebEventProperties(properties); base.WebAdding(properties); } /// <summary> /// A site was provisioned. /// </summary> public override void WebProvisioned(SPWebEventProperties properties) { LogWebEventProperties(properties); base.WebProvisioned(properties); } private void LogWebEventProperties(SPWebEventProperties properties) { StringBuilder sb = new StringBuilder(); try { sb.AppendFormat("{0} at {1} ", properties.EventType, DateTime.Now); sb.AppendFormat("Cancel: {0} ", properties.Cancel); sb.AppendFormat("ErrorMessage: {0} ", properties.ErrorMessage); sb.AppendFormat("EventType: {0} ", properties.EventType); sb.AppendFormat("FullUrl: {0} ", properties.FullUrl); sb.AppendFormat("NewServerRelativeUrl: {0} ", properties.NewServerRelativeUrl); sb.AppendFormat("ParentWebId: {0} ", properties.ParentWebId); sb.AppendFormat("ReceiverData: {0} ", properties.ReceiverData); sb.AppendFormat("RedirectUrl: {0} ", properties.RedirectUrl); sb.AppendFormat("ServerRelativeUrl: {0} ", properties.ServerRelativeUrl); sb.AppendFormat("SiteId: {0} ", properties.SiteId); sb.AppendFormat("Status: {0} ", properties.Status); sb.AppendFormat("UserDisplayName: {0} ", properties.UserDisplayName); sb.AppendFormat("UserLoginName: {0} ", properties.UserLoginName); sb.AppendFormat("Web: {0} ", properties.Web); sb.AppendFormat("WebId: {0} ", properties.WebId); } catch (Exception e) { sb.AppendFormat("Exception accessing Web Event Properties: {0} ", e); } } } }
code snippetWebEventReceiver/Receiver1.cs
One quick comment regarding this code is that with the AfterProperties property, which you can use to determine if the item has been modified during an update, you must decode the column name because SharePoint encodes spaces and other special characters in column name, for example a Hello World column having an internal column name of Hello_x0020_World. You need to use the System.XML.XmlConvert.DecodeName to decode and System.XML.XmlConvert.EncodeName to encode these names.
To support the new events, SharePoint now provides a new mechanism for registering your event receivers, using the <Receivers> XML block. This new feature enables you to register your event receiver at the site collection level by using the new Scope attribute and to set the receiver either to Site or Web, depending on the scope that you want for your event receiver. If you set it to Web, your event receiver works across all sites in your site collection, as long as your feature is registered across all these sites as well. You can tell SharePoint to have the receiver work on only the root site by using the RootWebOnly attribute on the <Receivers> node. The last new enhancement is the ListUrl attribute, which enables you to scope your receiver to a particular list by passing in the relative URL.
With SharePoint 2007, all your after-events were asynchronous, so if you wanted to perform some operations after the target—such as an item—was created but before it was presented to the user, you couldn’t. Your event receiver would fire asynchronously, so the user might already see the target, and then if you modified properties or added values, the user experience might not be ideal. With SharePoint 2010, there is support for synchronous after-events, such as listadded, itemadded, or webprovisioned. To make the events synchronous, you need to set the Synchronization property either through the SPEventReceiverDefinition object model if you are registering your events programmatically or by creating a node in your <Receiver> XML that sets the value to synchronous or asynchronous. That’s it if you run SharePoint on-premises. Unfortunately, post-synchronous events are not supported by SharePoint Online and Sandboxed Solutions.
With SharePoint 2007, you could only cancel events and return an error message to the user, but that provided limited interactivity and not much help to the user beyond what your error message said. With 2010 events, you can cancel the event on your synchronous events and redirect the user to a custom error page that you create. This enables you to have more control over what users see, and you can try to help them figure out why their action is failing. The custom error pages and redirection work only for presynchronous events, so you cannot do this for post-synchronous events such as ListAdded. Plus, this works only with browser clients. Office clients show an error message if you cancel the event.
The way to implement custom error pages is to set the Status property on your property bag for your event receiver to SPEventReceiverStatus.CancelWithRedirectUrl, set the RedirectUrl property to a relative URL for your error page, and set the Cancel property to true, as shown in the following code:
properties.Cancel = true; properties.Status = SPEventReceiverStatus.CancelWithRedirectUrl; properties.RedirectUrl = "/_layouts/mycustomerror.aspx";
In SharePoint, one of the major changes to which you must acclimate yourself is the new Ribbon user interface. The Ribbon provides a contextual tab model and a fixed location at the top of the page so that it never scrolls out of view. In terms of controls, if you have worked with the Office client Ribbon, the SharePoint Ribbon has near parity with the client. The areas missing between the client and the server are controls that provide more complex functionality. The best example of a control that is on the client but not on the server is the in-Ribbon gallery control. This gallery is used, for example, when you click Styles button on the Home tab in Word and you can see all the styles in the document, or in Excel, when you click the Cell Styles button in the Home tab right from the gallery control.
The Ribbon does support the majority of controls that you need, and the main unit of organization for these controls is tabs. You can build custom tabs that contain your custom controls. Even though the server can support up to 100 tabs, it is recommended that you try to limit the tabs to 4–7 to avoid confusing your users. Table 5-4 lists the different controls supported by SharePoint with a description of each.
Name | Description |
Button | A simple button that a user can click. |
Checkbox | A check box that can either have a label or not. |
ColorPicker | A grid of colors/styles that the user can use to choose a color. |
ComboBox | A menu of selections that the user can type or select. |
DropDown | A menu of selections that the user can click to select. |
FlyoutAnchor | An anchor button that includes a button that triggers a drop-down menu. |
InsertTable | A 10 x 10 grid of boxes where a user can specify dimensions for a table. |
Label | A line of text. |
Menu | A container for showing pop-ups. You can place it inside other controls that show menus, such as the FlyoutAnchor. |
MenuSection | A section of a menu. You can give the menu a title and controls. |
MRUSplitButton | A split button control that remembers the last item that a user most recently used out of its submenu with that item bubbling up into the “button” part. |
Spinner | Enables the user to enter values and “spin” through them using the up and down arrows. |
SplitButton | A control with a button and a menu. |
Textbox | A box where a user can enter text. |
ToggleButton | A button with an on/off state. |
If you look at the architecture for the Ribbon in SharePoint, you find that SharePoint uses Ajax, on-demand JavaScript, caching, and CSS layout to implement the Ribbon. In addition, the Ribbon uses no tables, so it is all constructed from CSS styling and hover effects that make the Ribbon function. For this reason, you should investigate the CSS classes that the Ribbon uses, especially corev4.css. Look through the styles beginning with ms-cui, which is the namespace for the Ribbon in the CSS file.
The Ribbon is completely extensible in that you can add new tabs or controls, or you can remove the OOB controls on existing tabs. You can entirely replace the Ribbon just by using your own custom Master Page. The Ribbon does support backward compatibility, so any custom actions you created for 2007 toolbars appear in a custom commands tab in the Ribbon.
To understand how to customize the Ribbon, look through the different actions you normally want to perform and the way to achieve those actions. Before diving in, though, you need to get a little bit of grounding in how the architecture of the Ribbon works.
The architecture of the Ribbon enables you to perform your customizations by creating XML definition files. At run time, the Ribbon merges your XML definitions with its own to add your custom Ribbon elements and code to handle interactions. For more complex customizations, such as writing more complex code, consider using a JavaScript page component. The next section looks at both options.
If you want to understand how SharePoint implements its Ribbon XML elements, go to %Program Files%Common FilesMicrosoft SharedWeb Server Extensions14TEMPLATEGLOBALXML on your local SharePoint Server and find the file cmdui.xml. In that file, you see the SharePoint default Ribbon implementation, and it is a good template to look at as you implement your own Ribbon controls because it can help you to understand how certain controls work inside the SharePoint environment.
If all the different types, elements, and attributes are confusing, take a look at the XSD for the Ribbon by browsing to %Program Files%Common FilesMicrosoft SharedWeb Server Extensions14TEMPLATEXML and look at cui.xsd and wss.xsd. These XSD files can help you understand what SharePoint expects for structure and content to make your custom user interface.
When you write your custom XML to define your Ribbon, SharePoint combines your XML changes with its own definitions in cmdui.xml, and the merged version displays the new Ribbon interface. Even though you use XML, you want to deploy your custom Ribbon XML with a SharePoint feature. The easiest way to create a feature is using Visual Studio 2010. To do so, follow these steps:
The snippet that follows shows some XML from a custom Ribbon:
<?xml version="1.0" encoding="utf-8"?> <Elements xmlns="http://schemas.microsoft.com/sharepoint/"> <CustomAction Id="CustomRibbonTab" Location="CommandUI.Ribbon.ListView" RegistrationId="101" RegistrationType="List" Title="My Custom UI" Sequence="5" > </CustomAction> </Elements>
code snippetElements.xml
First, all XML for the Ribbon will be wrapped in a CustomAction node. This tells SharePoint that you want to perform customization of the user interface. For the node, you can set attributes to give the specifics for your customization. One key node is the Location attribute, which tells SharePoint where your customization should appear, such as on a list view, a form, or everywhere. The pattern match for the Location attribute is Ribbon.[Tab].[Group].Controls._children. Table 5-5 outlines the options for the Location attribute.
Name | Description |
CommandUI.Ribbon | Customization appears everywhere. |
CommandUI.Ribbon.ListView | Customization appears when ListView is available. |
CommandUI.Ribbon.EditForm | Customization appears on the edit form. |
CommandUI.Ribbon.NewForm | Customization appears on the new form. |
CommandUI.Ribbon.DisplayForm | Customization appears on the display form. |
One other piece to notice in the XML is the RegistrationID. When combined, the registration ID and the registration type define the set of content you want your custom UI to appear for. The registration type can be a list, content type, file type, or a progID. The registration ID is mostly used with the content type registration type, and it’s where you specify the name of your content type. This enables you to customize even further when your custom UI appears, depending on what displays in SharePoint.
The Sequence attribute, which is optional, enables extensions to be placed in a particular order within a set of subnodes of a node. The built-in tab controls use a sequence of 100, so you want to avoid using any multiples of 100 for your sequence in tabs. Groups use a sequence in multiples of 10, so you should avoid multiples of 10. For example, if you had a Ribbon tab with the following groups—Clipboard, Font, Paragraph—you could set their sequence attributes to 10, 20, and 30, respectively. Then, you could insert a new group between the Clipboard and the Font groups via the feature framework by setting its sequence attribute to 15. A node without a Sequence attribute is sorted last.
Because the current XML does nothing (you haven’t added any new commands to the user interface), you can expand your XML. To add commands, you create a CommandUIExtension element. This CommandUIExtension is a wrapper for a CommandUIDefinitions element, which is a container for a CommandUIDefinition.
The CommandUIDefinition element has an attribute that enables you to set the location for your UI. In the example, you add a new button to the new set of controls in a document library. You can see _children as part of the location that tells SharePoint not to replace a control, but instead add this as a child control on that user interface element.
The CommandUIDefinition element is where you create your user interface elements, whether they are tabs, groups, or individual controls. In this simple example, you create a button that has the label Click me! which has two images. The image that is used depends on whether the button is rendered in 16 × 16 or 32 × 32 size (because the Ribbon has the ability to scale based on the screen resolution), and SharePoint calls JavaScript code to perform the action when the button is pressed. The attributes are self-explanatory, except for one — TemplateAlias. TemplateAlias controls whether your control is displayed in 16 × 16 or 32 × 32. If you set it to o1, you get a 32 × 32 icon, and o2 makes it 16 × 16. You can define your own template, but most times you use the built-in values of o1 or o2.
So, how do you call code from your Ribbon code? You wrap your code in a CommandUIHandler, where you can put in the CommandAction attribute, which is inline JavaScript that handles the action for your button. If you do not want to place your JavaScript inline, you can use the ScriptSrc attribute and pass a URL to your JavaScript file. Figure 5-15 shows the new custom button on the top-left of the Documents Ribbon.
<CommandUIExtension> <CommandUIDefinitions> <CommandUIDefinition Location="Ribbon.Documents.New.Controls._children"> <Button Id="Ribbon.Documents.New.RibbonTest" Alt="Test Button" Sequence="5" Command="Test_Button" LabelText="Click me!" Image32by32="/_layouts/images/ribbon_blog_32.png" Image16by16="/_layouts/images/ribbon_blog_16.png" TemplateAlias="o1" /> </CommandUIDefinition> </CommandUIDefinitions> <CommandUIHandlers> <CommandUIHandler Command="Test_Button" CommandAction="javascript:alert('I am a test!')," /> </CommandUIHandlers> </CommandUIExtension>
code snippetElements.xml
At times you may want to replace existing, built-in controls from your SharePoint deployment and put your own controls in place. You can replace the entire Ribbon if you want. The way to replace an existing control is to insert your own custom control that overwrites the ID of the control you want to replace and also has a lower sequence number. The key point is you must get the Location attribute set to the same ID as the control ID you want to replace.
The following code replaces the new folder button for a document library. There are a couple of things to highlight in this code:
<CustomAction Id="Ribbon.Documents.New.NewFolder.ReplaceButton" Location="CommandUI.Ribbon" RegistrationId="101" RegistrationType="List" Title="Replace Ribbon Button" > <CommandUIExtension> <CommandUIDefinitions> <CommandUIDefinition Location="Ribbon.Documents.New.NewFolder"> <Button Id="Ribbon.Documents.New.NewFolder.ReplacementButton" Command="MyNewButtonCommand" Image16by16="/_layouts/$Resources:core,Language;/images/ formatmap16x16.png?vk=4536" Image16by16Top="-240" Image16by16Left="-80" Image32by32="/_layouts/$Resources:core,Language;/images/ formatmap32x32.png?vk=4536" Image32by32Top="-352" Image32by32Left="-448" ToolTipTitle="Create a New Folder" ToolTipDescription="Replaced by XML Custom Action" LabelText="My New Folder" TemplateAlias="o1" /> </CommandUIDefinition> </CommandUIDefinitions> <CommandUIHandlers> <CommandUIHandler Command="MyNewButtonCommand" CommandAction="javascript:alert('New Folder Replaced.')," /> </CommandUIHandlers> </CommandUIExtension> </CustomAction>
code snippetElements.xml
Figure 5-16 shows the replaced button. Even though the icon image is the same, the action performed when the user clicks the icon is the custom code.
You may be wondering how you use just URLs with token replacements, rather than having to write JavaScript as the payload for your controls. To do this, you use the URLAction node in your CustomAction node. Your URL actions can be simple URLs, or you can use token replacement, such as ListID or ItemID. You can also place inline JavaScript if you want. When you use URL actions, you can make a simple CustomAction node to handle your changes, as shown in the following listing, which adds a new toolbar item to the new announcements form:
<CustomAction Id="SimpleAction" RegistrationType="List" RegistrationId="104" ImageUrl="/_layouts/images/saveas32.png" Location="NewFormToolbar" Sequence="10" Title="Custom Button" Description="This is an announcement button." > <UrlAction Url="javascript:alert('Itemid={ItemId} and Listid={ListId}'),"/> </CustomAction>
code snippetElements.xml
Troubleshooting your custom Ribbon user interface is not as easy as you would think. If you get something wrong, your customizations just do not appear. Even though this can be frustrating, there are a couple of places to start looking to troubleshoot your issues:
As part of the definition of your CustomAction, you can also specify the rights required to view your custom interface. You can specify a Rights attribute, which takes a permissions mask that SharePoint will logically AND together, so the user must have all the permissions to view the new user interface. Permissions can be any from SPBasePermissions, such as ViewListItems or ManageLists.
Beyond permissions, you can also specify whether a person must be a site administrator to view the new user interface. To do this, create a Boolean RequireSiteAdministrator attribute, and set it to true to require the user to be a site administrator. This is useful for an administration-style UI that you do not want every user to see.
Sometimes you want to hide controls rather than replace them. For example, the control may not make sense in the context of your application. To hide UI controls, use the HideCustomAction element and set the attributes to the nodes you want to hide, as shown in the following code:
<HideCustomAction Id="HideNewMenu" Location="Microsoft.SharePoint.StandardMenu" GroupId="NewMenu" HideActionId="NewMenu"> </HideCustomAction>
If you prefer to write code instead of XML, you can use the SharePoint object model to make changes to menu items. This hasn’t changed from the EditControlBlock (ECB) technologies in 2007 and is shown here for completeness:
using (SPSite site = new SPSite("http://intranet.contoso.com")) { using (SPWeb web = site.RootWeb) { SPUserCustomAction action = web.UserCustomActions.Add(); action.Location = "EditControlBlock"; action.RegistrationType = SPUserCustomActionRegistrationType.FileType; action.RegistrationId = "docx"; action.Title = "Custom Edit Command For Documents"; action.Description = "Custom Edit Command for Documents"; action.Url = "{ListUrlDir}/forms/editform.aspx?Source={Source}"; action.Update(); web.Update(); site.Close(); } }
Beyond just creating buttons, you may want to add new tabs and groups. To do this, you just need to create Tab and Group elements in your code. The process is close to the same as adding a button with some minor tweaks. Figure 5-17 shows a custom tab and group with three controls: two buttons and a combo box.
The following code shows the beginning of the new tab and group. As you can see, the XML looks similar to the earlier XML when you create a button. This code also defines a tab:
<!~DHCreate new Tab and Group~DH> <CustomAction Id="MyCustomRibbonTab" Location="CommandUI.Ribbon.ListView" RegistrationId="101" RegistrationType="List"> <CommandUIExtension> <CommandUIDefinitions> <CommandUIDefinition Location="Ribbon.Tabs._children"> <Tab Id="Ribbon.CustomTabExample" Title="My Custom Tab" Description="This holds my custom commands!" Sequence="501"> <Scaling Id="Ribbon.CustomTabExample.Scaling"> <MaxSize Id="Ribbon.CustomTabExample.MaxSize" GroupId="Ribbon.CustomTabExample.CustomGroupExample" Size="OneLargeTwoMedium"/> <Scale Id="Ribbon.CustomTabExample.Scaling.CustomTabScaling" GroupId="Ribbon.CustomTabExample.CustomGroupExample" Size="OneLargeTwoMedium" /> </Scaling> ...
Some things to note about this code:
<Groups Id="Ribbon.CustomTabExample.Groups"> <Group Id="Ribbon.CustomTabExample.CustomGroupExample" Description="This is a custom group!" Title="Custom Group" Sequence="52" Template="Ribbon.Templates.CustomTemplateExample"> <Controls> ...
After you have your tab and group, you create your controls just as you would if the control were an extension of an existing group.
A combo box has more commands than a button has because users can interact more with a combo box by selecting options from its list. Also, you can populate a combo box either statically, as the code does, by creating menu options in the XML, or by passing a function that SharePoint calls to populate the combo box dynamically. Look at the PopulateDynamically, PopulateOnlyOnce, and PopulateQueryCommand sections of the code because these operate the combo box options.
In addition, you can set attributes, such as AllowFreeForm and InitialItem, to control whether users can type values into the combo box and select the initial item in the combo box.
The code earlier showed you how to create a button, so the following code shows you how to create a combo box as a control in your group:
<ComboBox Id="Ribbon.CustomTabExample.CustomGroupExample. Combobox" Sequence="18" Alt="Ribbon.CustomTabExample.CustomGroupExample. Combobox_Alt" Command="Ribbon.CustomTabExample.CustomGroupExample. Combobox_CMD" CommandMenuOpen="Ribbon.CustomTabExample. CustomGroupExample.Combobox_Open_CMD" CommandMenuClose="Ribbon.CustomTabExample. CustomGroupExample.Combobox_MenuClose_CMD" CommandPreview="Ribbon.CustomTabExample. CustomGroupExample.Combobox_Preview_CMD" CommandPreviewRevert="Ribbon.CustomTabExample. CustomGroupExample.Combobox_PreviewRevert_CMD" InitialItem="StaticComboButton1" AllowFreeForm="true" PopulateDynamically="false" PopulateOnlyOnce="true" PopulateQueryCommand="Ribbon.CustomTabExample. CustomGroupExample.Combobox_PopQuery_CMD" Width="125px" TemplateAlias="cust3"> <Menu Id="Ribbon.CustomTabExample.CustomGroupExample. Combobox.Menu"> <MenuSection Id="Ribbon.CustomTabExample.CustomGroupExample.Combobox. Menu.MenuSection" Sequence="10" DisplayMode="Menu32"> <Controls Id="Ribbon.CustomTabExample.CustomGroupExample. Combobox.Menu.MenuSection.Controls"> <Button Id="Ribbon.CustomTabExample.CustomGroupExample. Combobox.Menu.MenuSection.Button1" Sequence="10" Command="Ribbon.CustomTabExample.CustomGroupExample. Combobox.Menu.MenuSection.Button1_CMD" CommandType="OptionSelection" Image16by16="/_layouts/$Resources:core,Language; /images/formatmap16x16.png?vk=4536" Image16by16Top="-48" Image16by16Left="-112" Image32by32="/_layouts/$Resources:core,Language; /images/formatmap32x32.png?vk=4536" Image32by32Top="-192" Image32by32Left="-32" LabelText="StaticComboButton1" MenuItemId="StaticComboButton1"/> <Button Id="Ribbon.CustomTabExample.CustomGroupExample. Combobox.Menu.MenuSection.Button2" Sequence="20" Command="Ribbon.CustomTabExample.CustomGroupExample. Combobox.Menu.MenuSection.Button2_CMD" CommandType="OptionSelection" Image16by16="/_layouts/$Resources:core,Language; /images/formatmap16x16.png?vk=4536" Image16by16Top="-32" Image16by16Left="-112" Image32by32="/_layouts/$Resources:core,Language; /images/formatmap32x32.png?vk=4536" Image32by32Top="-384" Image32by32Left="-352" LabelText="StaticComboButton2" MenuItemId="StaticComboButton2"/> </Controls> </MenuSection> </Menu> </ComboBox>
code snippetElements.xml
After your controls, you can handle the commands to which your controls need to respond in the CommandUIHandlers node. For the complete listing for all the code for the commands, tab, and group, refer to the sample code for this chapter on www.wrox.com.
Your user interface should guide the user on how to use your controls. To aid in this, the Ribbon supports ToolTips and also linking to help topics. Both of these are set using the ToolTip* set of commands such as ToolTipTitle and ToolTipDescription. The following code sets the title, description, help topic, and shows the keyboard shortcut for your control:
ToolTipTitle="Tooltip Title" ToolTipDescription="Tooltip Description" ToolTipShortcutKey="Ctr-V, P" ToolTipImage32by32="/_layouts/images/PasteHH.png" ToolTipHelpKeyWord="WSSEndUser"
So far, you have been writing code inline in your XML to handle your control commands. However, SharePoint does enable you to write more complex handlers for your user interface if you need to. You should try to keep your code in the XML definition if you create simple buttons with simple code. However, if you create Ribbon extensions dynamically populated via code; if your Ribbon requires variables beyond the default ones you can get with {SiteUrl}, {ItemId}, or other similar placeholders; or if your code is so long that it makes sense from a manageability standpoint to break it out separately, you should consider creating a page component.
A page component is a set of JavaScript code that can handle commands from your user interface customizations. Your JavaScript must derive from the CUI.Page.PageComponent and implement the functions in the prototype definition of the RibbonAppPageComponent. As part of this code, you can define the global commands that your page component works with. These are the tabs, groups, and commands, such as buttons, that you handle in your page component. In addition, you can define whether your global commands should be enabled through the canHandleCommand function. This is the function you use to enable or disable your control. For example, you may want to enable your control only if the context is correct for your control to work, such as when an item is selected in the user interface or when the right variables are set. If you return false to this function, your user interface is grayed out.
In addition, the page component enables you to handle the command, so if someone clicks your button, you can run code to handle that click.
After you define all this JavaScript, you need to register your script with the PageManager that SharePoint creates so that SharePoint knows to call the script when actions are performed on the user interface.
Notice a couple of points about the following code:
Figure 5-18 shows the custom button on the Ribbon. Also, the figure has a custom color picker and other buttons. You can see the code to implement these other buttons in the sample code for this chapter.
<CustomAction Id="SharedDocAction" RegistrationType="List" RegistrationId="101" Location="CommandUI.Ribbon.ListView"> <CommandUIExtension> <CommandUIDefinitions> <CommandUIDefinition Location="Ribbon.Documents.New.Controls._children"> <Button Id="CustomContextualButton" Alt="MyDocumentsNew Alt" Command="MyDocumentsNewButton" LabelText="ScriptBlock Button" ToolTipTitle="Tooltip Title" Image16by16="/_layouts/$Resources:core,Language; /images/formatmap16x16.png?vk=4536" Image16by16Top="-80" Image16by16Left="0" Image32by32="/_layouts/$Resources:core,Language; /images/formatmap32x32.png?vk=4536" Image32by32Top="-96" Image32by32Left="-64" ToolTipDescription="Tooltip Description" TemplateAlias="o1"/> </CommandUIDefinition> </CommandUIDefinitions> </CommandUIExtension> </CustomAction> <CustomAction Id="MyScriptBlock" Location="ScriptLink" ScriptBlock=" ExecuteOrDelayUntilScriptLoaded(_registerMyScriptBlockPageComponent, 'sp.ribbon.js'), function _registerMyScriptBlockPageComponent() { Type.registerNamespace('MyScriptBlock'), MyScriptBlock.MyScriptBlockPageComponent = function MyScriptBlockPageComponent_Ctr() { MyScriptBlock.MyScriptBlockPageComponent.initializeBase(this); }; MyScriptBlock.MyScriptBlockPageComponent.prototype = { init: function MyScriptBlockPageComponent_init() { }, _globalCommands: null, buildGlobalCommands: function MyScriptBlockPageComponent_buildGlobalCommands() { if (SP.ScriptUtility.isNullOrUndefined(this._globalCommands)) { this._globalCommands = []; this._globalCommands[this._globalCommands.length] = 'DocumentTab'; this._globalCommands[this._globalCommands.length] = 'DocumentNewGroup'; this._globalCommands[this._globalCommands.length] = 'MyDocumentsNewButton'; } return this._globalCommands; }, getGlobalCommands: function MyScriptBlockPageComponent_getGlobalCommands() { return this.buildGlobalCommands(); }, canHandleCommand: function MyScriptBlockPageComponent_canHandleCommand(commandId) { var items = SP.ListOperation.Selection.getSelectedItems(); if (SP.ScriptUtility.isNullOrUndefined(items)) return false; if (0 == items.length) return false; if (commandId === 'DocumentNewTab'){ return true; } if (commandId === 'DocumentNewGroup'){ return true; } if (commandId === 'MyDocumentsNewButton'){ return true; } return false; }, handleCommand: function MyScriptBlockPageComponent_handleCommand(commandId, properties, sequence) { alert('You hit my button!'), return true; } } MyScriptBlock.MyScriptBlockPageComponent.get_instance = function MyScriptBlockPageComponent_get_instance() { if (SP.ScriptUtility.isNullOrUndefined(MyScriptBlock. MyScriptBlockPageComponent._singletonPageComponent)) { MyScriptBlock.MyScriptBlockPageComponent._singletonPageComponent = new MyScriptBlock.MyScriptBlockPageComponent(); } return MyScriptBlock.MyScriptBlockPageComponent._singletonPageComponent; } MyScriptBlock.MyScriptBlockPageComponent.registerWithPageManager = function MyScriptBlockPageComponent_registerWithPageManager() { SP.Ribbon.PageManager.get_instance().addPageComponent (MyScriptBlock.MyScriptBlockPageComponent.get_instance()); } MyScriptBlock.MyScriptBlockPageComponent.unregisterWithPageManager = function MyScriptBlockPageComponent_unregisterWithPageManager() { if (false == SP.ScriptUtility.isNullOrUndefined( MyScriptBlock.MyScriptBlockPageComponent._singletonPageComponent)) { SP.Ribbon.PageManager.get_instance().removePageComponent( MyScriptBlock.MyScriptBlockPageComponent.get_instance()); } } MyScriptBlock.MyScriptBlockPageComponent.registerClass( 'MyScriptBlock.MyScriptBlockPageComponent', CUI.Page.PageComponent); MyScriptBlock.MyScriptBlockPageComponent.registerWithPageManager(); }"> </CustomAction>
code snippetElements.xml
The easiest way to add a button to the Ribbon or your items is to use SharePoint Designer. Built right into SPD is the ability to add custom actions to your list. SPD can create these actions on the Ribbon forms, such as the display, edit, or new form for a list item, and also on a list item drop-down menu. You can customize the action performed by the button, for example, navigating to a form such as the edit form for the item, initiating a workflow, or launching a URL. In addition, you can use SPD to assign graphics to your icons, set your sequence number, and even set your Ribbon location in the same way you set the Location attribute in the Ribbon XML you saw earlier. Figure 5-19 shows the form used to tell SPD how to customize the Ribbon for your list.
You may want to build a Ribbon user interface and have it automatically appear when a user selects a web part. This is the way the media-player web part works; it displays a new tab in the Ribbon when you select the web part in the user interface. To perform this functionality, you need to add a contextual tab and contextual group to the Ribbon interface through code. You do not use the declarative XML file, but instead place the XML in code and add it programmatically to the Ribbon.
To build a contextual web part, you create your web part as you normally do, but you want your web part to inherit from the IWebPartPageComponentProvider interface. You need to implement the WebPartContextualInfo method of the interface. This method tells the Ribbon which group and tab to activate when the web part is selected.
public WebPartContextualInfo WebPartContextualInfo { get { WebPartContextualInfo info = new WebPartContextualInfo(); info.ContextualGroups.Add( new WebPartRibbonContextualGroup { Id = "Ribbon.MyContextualGroup", VisibilityContext = "WebPartSelectionTest", Command = "MyContextualGroupCMD" } ); info.Tabs.Add( new WebPartRibbonTab { Id = "Ribbon.MyContextualGroup.MyTab", VisibilityContext = "WebPartSelectionTest" } ); info.PageComponentId = SPRibbon.GetWebPartPageComponentId(this); return info; } }
code snippetCustomWebPart.cs
Then you need to implement a custom page component using JavaScript. This is similar to the code you saw earlier in the chapter where you added and registered the custom page component. The code that follows uses the executeOrDelayUntilScriptLoaded command, which is part of the SharePoint infrastructure, to load and run the script on-demand.
private string DelayScript { get { string wppPcId = SPRibbon.GetWebPartPageComponentId(this); return @" <script type=""text/javascript""> //<![CDATA[ function _addCustomPageComponent() { SP.Ribbon.PageManager.get_instance().addPageComponent(new CustomPageComponent.TestPageComponent(" + wppPcId + @")); } function _registerCustomPageComponent() { RegisterSod(""testpagecomponent.js"", ""/_layouts/TestPageComponent.js""); var isDefined = ""undefined""; try { isDefined = typeof(CustomPageComponent.TestPageComponent); } catch(e) { } EnsureScript(""testpagecomponent.js"",isDefined, _ addCustomPageComponent); } ExecuteOrDelayUntilScriptLoaded(_registerCustomPageComponent, ""sp.ribbon.js""); //]]> </script>"; } }
code snippetCustomWebPart.cs
Contextual tabs always exist on the Ribbon but are hidden. To add your tabs and groups to the Ribbon, you need to use the server-side Ribbon API to get the Ribbon and add your custom Ribbon elements, as shown in the following code.
private void AddCustomTab() { Microsoft.Web.CommandUI.Ribbon ribbon = SPRibbon.GetCurrent(this.Page); XmlDocument xmlDoc = new XmlDocument(); //Contextual Tab xmlDoc.LoadXml(this.CuiDefinitionCtxTab); ribbon.RegisterDataExtension(xmlDoc.FirstChild, "Ribbon.ContextualTabs._children"); xmlDoc.LoadXml(this.CuiDefinitionScaling); ribbon.RegisterDataExtension(xmlDoc.FirstChild, "Ribbon.Templates._children"); exists = true; }
code snippetCustomWebPart.cs
To tie it all together, you need to implement the OnPreRender method, which allows the code to add the new Ribbon elements to the page before the page renders. The following code calls the AddCustomTab method to do this; this method also registers the script block that implements the custom page component with the SharePoint client script manager:
protected override void OnPreRender(EventArgs e) { base.OnPreRender(e); //RegisterDataExtensions; add Ribbon XML for buttons this.AddCustomTab(); ClientScriptManager csm = this.Page.ClientScript; csm.RegisterClientScriptBlock(this.GetType(), "custompagecomponent", this.DelayScript); }
The last piece to look at is the custom page component, which implements the functionality to tell SharePoint which commands the page component implements, as well as when to focus on the contextual tab, and when to yield focus depending on whether the web part is selected.
Type.registerNamespace('CustomPageComponent'), //////////////////////////////////////////////////////////////////////////////// // CustomPageComponent.TestPageComponent var _myWpPcId; CustomPageComponent.TestPageComponent = function CustomPageComponent_TestPageComponent(webPartPcId) { this._myWpPcId = webPartPcId.innerText; CustomPageComponent.TestPageComponent.initializeBase(this); } CustomPageComponent.TestPageComponent.prototype = { init: function CustomPageComponent_TestPageComponent$init() { }, getFocusedCommands: function CustomPageComponent_TestPageComponent$getFocusedCommands() { return ['MyTabCMD', 'MyGroupCMD', 'CommandMyJscriptButton']; }, getGlobalCommands: function CustomPageComponent_TestPageComponent$getGlobalCommands() { return []; }, isFocusable: function CustomPageComponent_TestPageComponent$isFocusable() { return true; }, receiveFocus: function CustomPageComponent_TestPageComponent$receiveFocus() { return true; }, yieldFocus: function CustomPageComponent_TestPageComponent$yieldFocus() { return true; }, canHandleCommand: function CustomPageComponent_TestPageComponent$canHandleCommand(commandId) { //Contextual Tab commands if ((commandId === 'MyTabCMD') || (commandId === 'MyGroupCMD') || (commandId === 'CommandMyButton') || (commandId === 'CommandMyJscriptButton')) { return true; } }, handleCommand: function CustomPageComponent_TestPageComponent$handleCommand (commandId, properties, sequence) { if (commandId === 'CommandMyJscriptButton') { alert('Event: CommandMyJscriptButton'), } }, getId: function CustomPageComponent_TestPageComponent$getId() { return this._myWpPcId; } } CustomPageComponent.TestPageComponent.registerClass( 'CustomPageComponent.TestPageComponent', CUI.Page.PageComponent); if(typeof(NotifyScriptLoadedAndExecuteWaitingJobs)!="undefined") NotifyScriptLoadedAndExecuteWaitingJobs("testpagecomponent.js");
code snippetCustomWebPart.cs
Two new additions to the user interface for 2010 are the status bar, which appears right below the Ribbon tab; and the notification area, which is transient in nature, that appears below and to the right of the Ribbon tab. You should use both to give your users contextual information so long as it’s not distracting. For example, if you are editing a page and have not checked it in or published it, the status bar tells you that only you can see the page. The status bar is for more permanent information, whereas the notification area is similar to instant message pop-ups or Windows system tray notifications because notifications pop up and then disappear after a certain amount of time. Notification area messages are inherently more transient in nature than status bar messages.
The status bar is extensible through client- and server-side code. The message you can deliver in the bar is HTML, so it can be styled and contain links and images. In addition, the bar can have four preset colors, depending on the importance of the message. To work with the status bar, you use the SP.UI.Status class, which is a simple client-side API because it contains only six methods, and you can find their definitions in SP.debug.js. On the server side, you should use the SPPageStatusSetter class, which is part of the Microsoft.SharePoint.WebControls namespace. That API is even simpler because you call one method—addStatus. You can also use the SPPageStateControl class to work with the status bar on the server side. Table 5-6 outlines the methods for the client side.
Name | Description |
addStatus | Enables you to pass a title, the HTML payload, and a Boolean specifying whether to render the message at the beginning of the status bar. This function returns a status ID that you can use with other methods. |
appendStatus | Appends status to an existing status. You need to pass the status ID for the existing status being updated, and the title, and HTML you want appended. |
updateStatus | Updates an existing status message. You need to pass the status ID and the HTML payload for the new status message. |
setStatusPriColor | Enables you to set the priority color to give a user a visual indication of the status messages’ meanings, such as green for good or red for bad. You need to pass the status ID of the status you want to change color and one of four color choices: red, blue, green, or yellow. |
removeStatus | Removes the status specified by the status ID you pass to this method from the status bar. |
removeAllStatus | Removes all status messages from the status bar. You can pass a Boolean that specifies whether to hide the bar. Most times, you want this Boolean to be true. |
Programming the status bar is straightforward. The sample code with this chapter includes a snippet that you can add to the HTML source for a Content Editor web part. When you do this, you can see what appears in Figure 5-20.
<script type="text/javascript"> var sid; var color=""; function AppendStatusMethod() { SP.UI.Status.appendStatus(sid, "Appended:", "<HTML><i>My Status Append to " + sid + " using appendStatus</i></HTML>"); } function UpdateStatus() { SP.UI.Status.updateStatus(sid, "Updated: HTML updated for " + sid + " using updateStatus"); } function RemoveStatus() { SP.UI.Status.removeStatus(sid); } function RemoveAllStatus() { SP.UI.Status.removeAllStatus(true); } function SetStatusColor() { if (color=="") { color="red"; } else if (color=="red") { color="green"; } else if (color=="green") { color="yellow"; } else if (color=="yellow") { color="blue"; } else if (color=="blue") { color="red"; } SP.UI.Status.setStatusPriColor(sid, color); } function AppendStatus() { SP.UI.Status.addStatus("Appended:", "<HTML><i> My Status Message Append using atBeginning</i></HTML>", false); } function CreateStatus() { return SP.UI.Status.addStatus(SP.Utilities.HttpUtility.htmlEncode( "My Status Bar Title"), "<HTML><i>My Status Message</i></HTML>", true); } } </script> <input onclick="sid=CreateStatus();alert(sid);" type="button" value=" Create Status"/> <br/> <input onclick="AppendStatus()" type="button" value=”Append Status using atBeginning”/> <br/> <input onclick=”AppendStatusMethod()” type=”button” value=”Append Status using appendStatus”/> <br/> <input onclick=”UpdateStatus()” type=”button” value=”Update Status using updateStatus”/> <br/> <input onclick=”SetStatusColor()” type=”button” value=”Cycle Colors”/> <br/> <input onclick=”RemoveStatus()” type=”button” value=”Remove Single Status”/> <br/> <input onclick=”RemoveAllStatus()” type=”button” value=”Remove All Status”/> <br/>
code snippetCustomDialogandNotifications.txt
Beyond working with status information, you can also customize the notification area on the upper-right side of the screen below the Ribbon. Given that SharePoint is now leveraging a lot of Ajax, there is a need to give users feedback that their pages and actions are completed. The notification area does this by telling the user that the page is loading or that a save was successful, for example, which used to be indicated by a postback and page refresh.
The notification API is limited in that you can create and remove only notifications. Table 5-7 describes the methods for SP.UI.Notify that work with notifications, with sample code following the table.
Name | Description |
addNotification | Enables you to pass your HTML payload, whether the notification is sticky (which when set to false means the notification disappears after 5 seconds), your ToolTip text, and the name of a function to handle the onclick event. The last two parameters are optional. This function returns an ID that you can use with the removeNotification method to remove your notification. |
removeNotification | Removes the notification specified by the notification ID that you pass to this method. |
<script type="text/javascript"> var notifyid; function CreateNotification() { notifyid = SP.UI.Notify.addNotification("My HTML Notification", true, "My Tooltip", "HelloWorld"); alert("Notification id: " + notifyid); } function RemoveNotification() { SP.UI.Notify.removeNotification(notifyid); } </script> <input onclick="CreateNotification()" type="button" value="Create Notification"/><br/> <input onclick="RemoveNotification()" type="button" value="Remove Notification"/>
code snippetCustomDialogandNotifications.txt
Beyond working with notifications and status bars, SharePoint now also offers a dialog framework that you can write code to. The purpose of the new dialog framework is to keep the user in context and focus the user on the dialog rather than all the surrounding user interface elements. With the new dialog framework, dialogs are modal and gray out the screen, except for the dialog that is displayed. Figure 5-21 shows a custom dialog in SharePoint 2010.
The implementation of the dialog takes the form of your contents being loaded into an iframe in a floating div. The dialog is modal, so the user can’t get to other parts of SharePoint from it. Plus, the dialog can be dragged to other parts of the browser window and can be maximized to the size of the browser window.
If you look in SP.UI.Dialog.debug.js, you see the implementation for the dialog framework. The framework has a JavaScript API against which you can program to have SharePoint launch and load your own dialogs. You do this by calling the SP.UI.showModalDialog method and passing in the options you want for your dialog, such as height, width, page to load, and other options. You can see the full set of options in Table 5-8.
Name | Description |
Width | The width of the dialog box as an integer. If you don’t specify a width, SharePoint autosizes the dialog. |
Height | The height of the dialog box as an integer. If you don’t specify a height, SharePoint autosizes the dialog. |
autoSize | A Boolean that specifies whether to have SharePoint autosize the dialog. |
X | The x coordinate for your dialog. |
Y | The y coordinate for your dialog. |
allowMaximize | A Boolean that specifies whether to allow the Maximize button in your dialog. |
showMaximized | A Boolean that specifies whether to show your dialog maximized by default. |
showClose | A Boolean that specifies whether to show the Close button in the toolbar for the dialog. |
url | A URL for SharePoint to load as the contents for your dialog. |
Html | A DOMElement that contains the HTML you want SharePoint to load as the contents for your dialog. This DOMElement is destroyed after use, so make a copy before passing it to SharePoint if you need it after the dialog is destroyed. |
Title | The title of your dialog. |
dialogReturnValueCallback | The function SharePoint calls back to when the dialog is closed. You create a delegate to this function for this option with the createDelegate function in JavaScript. |
Now that you know the options you can pass to the showModalDialog function, programming a dialog is straightforward. A couple of tips before you look at the code:
The following code contains the OpenDialog function. As part of this function, a variable called options is created, which uses the SP.UI.$create_DialogOptions method. This method returns a DialogOptions object that you can utilize to specify your options. In the code, all the options are specified, including the creation of the delegate that points to the function—CloseCallback—that is called after the dialog is called. Then, the code calls the SP.UI.ModalDialog.showModalDialog with the options object that contains the specified options for the dialog.
The CloseCallback function gets the result and any return value. The result is the button the user clicked. SharePoint has an enumeration for the common buttons OK and Cancel that you can check against with the result value—for example, SP.UI.DialogResult.OK or SP.UI.DialogResult.cancel.
function OpenDialog() { var options = SP.UI.$create_DialogOptions(); options.url = SP.Utilities.Utility.getLayoutsPageUrl('customdialog.htm'), options.url += "?Source=" + document.URL; alert('Navigating to dialog at: ' + options.url); options.width = 400; options.height = 300; options.title = "My Custom Dialog"; options.dialogReturnValueCallback = Function.createDelegate(null, CloseCallback); SP.UI.ModalDialog.showModalDialog(options); } function CloseCallback(result, returnValue) { alert('Result from dialog was: '+ result); if(result === SP.UI.DialogResult.OK) { alert('You clicked OK'), } else if (result == SP.UI.DialogResult.cancel) { alert('You clicked Cancel'), } }
code snippetCustomDialogandNotifications.txt
Now that you have seen the code for calling the dialog and evaluating the result, look at what the HTML for the dialog body looks like. The code that follows is for SharePoint to load the dialog. The code contains two buttons for OK and Cancel. The onclick event handlers for the buttons use methods from the window.frameElement object. By using this object, you can get methods from the dialog framework. commitPopup returns OK, and cancelPopUp returns Cancel as the result of your dialog. Table 5-9 shows the methods you want to use from the frameElement.
<p> <img src="/_layouts/1033/images/DefaultPageLayout.tif" alt="Default Page" style="vertical-align: middle"/> <B>Text for your dialog goes here</B> </p> <input type="button" name="OK" value="OK" onclick="window.frameElement.commitPopup(); return false;" accesskey="O" class="ms-ButtonHeightWidth" target="_self" /> <input type="button" name="Cancel" value="Cancel" onclick="window.frameElement.cancelPopUp(); return false;" accesskey="C" class="ms-ButtonHeightWidth" target="_self" />
code snippetCustomDialogandNotifications.txt
Name | Description |
commitPopup | Returns OK as the result of your dialog |
cancelPopUp | Returns Cancel as a result of your dialog |
navigateParent | Navigates to the parent of the dialog |
SharePoint’s Web Services have been around for a number of releases of SharePoint and continue to work with SharePoint Online, with the caveat that you need to pass the correct cookies for authentication. Also, you want to make sure that when you add a reference to the SharePoint Online Web Services, such as lists.asmx, that you add them as a web reference and not a service reference. You can add a web reference by using the Advanced dialog as part of the Add Service Reference dialog. By using the older-style web references, you can access the cookie container, and add the authentication cookies to your calls to SharePoint Online. The following code shows how to add the cookie container and call a SharePoint web service. Whenever possible you should use the SharePoint Client Object Model rather than the legacy SharePoint Web Services. Please remember that the Client Object Model is a subset of the full functionality of the SharePoint API.
//Call Lists Web Service listWS.Lists spList = new listWS.Lists(); spList.Url = "https://thomrizdom.sharepoint.com/_vti_bin/lists.asmx"; spList.CookieContainer = claimsHelper.CookieContainer; XmlNode spLists = spList.GetList("Announcements");
When it comes to SharePoint, working with, manipulating, and displaying data are the important tasks you do as a developer. If you break SharePoint down to its simplest form, it is just an application that sits on top of a database. Because SharePoint can reveal its data in so many ways—whether through the browser, inside Office, in applications running on the SharePoint server, or in applications running off the SharePoint server—there are a number of data technologies you can take advantage of when working with SharePoint. Which one you use depends on your comfort level with the technology required and also whether you write your application to run on or off the SharePoint server. Your choices in 2010 are:
Of course, you can continue to use the Web Services APIs of SharePoint, but for the bulk of your operation you should look at moving to the Client OM or the SharePoint Data Services (an OData service) rather than using that technology. Table 5-10 goes shows the pros and cons of each data-access technology.
Name | Pros | Cons |
LINQ to SharePoint | Entity-based programmingStrongly typedSupports joins and projectionsGood tools support and IntelliSense | Server-side onlyNew API, so new skills requiredPre-processing of list structure required, so changing list could break application |
Server OM | Familiar APIWorks with more than just list data | Server-side onlyStrongly typed |
Client OM | Works off the serverEasier than Web Services APIWorks in Silverlight, JavaScript, and .NETMore than just list data | New APIWeakly typed |
SharePoint Data Service | Standards-basedURL-based commandsStrongly typed | Only works with lists and Excel |
Web Services | Simple to programStandards-based | Weakly typedLegacy API |
With SharePoint 2007, you had to use CAML queries to write queries against the server, using the SPQuery or SPSiteDataQuery objects. You would write your CAML as a string and pass it to those objects, so there were no strongly-typed objects or syntax checking as part of the API. Instead, you would either have to cross your fingers that you got the query right, or use a third-party tool to try to generate your CAML queries. To make this easier, SharePoint 2010 introduces SharePoint LINQ (SPLINQ). By having a LINQ provider, 2010 enables you to use LINQ to write your queries against SharePoint in a strongly typed way with IntelliSense and compile-time checking. Under the covers, the SharePoint LINQ provider translates your LINQ query into a CAML query and executes it against the server. As you will see, you can retrieve the CAML query that the LINQ provider generated to understand what is passed back to the server.
The first step in starting with SPLINQ is generating the entity classes and properties for your lists. Rather than writing these by hand, you can use the command-line tool that ships with SharePoint, called SPMetal. SPMetal parses your lists and generates the necessary classes that you can import into your Visual Studio projects. You can find SPMetal in the %ProgramFiles%Common FilesMicrosoft Sharedweb server extensions14BIN directory. You can run SPMetal from the command prompt, but if you prefer, you can write a batch file that you have Visual Studio run as part of your prebuild for your project so that the latest version of your entity classes are always included.
Using SPMetal is straightforward for performing common tasks that you want to do. It does support XML customization, but most of the time you don’t need to customize the default code generation. Table 5-11 shows the SPMetal command-line parameters that you can pass.
Name | Description |
Web | Absolute URL of the website you want SPMetal to generate entity classes for. |
Code | The relative or absolute path and filename of the location where you want the outputted code to be placed. |
Language | The programming language you want for the generated code. The value for this can be either csharp or vb. SPMetal can look at your code parameter and infer the language you want by the extension of the filename you specify. |
Namespace | The namespace you want to use for the generated code. If you do not specify this property, SPMetal uses the default namespace of your VS project. |
Useremoteapi | SPMetal uses the client object model if you specify this parameter. |
User | Enables you to specify the DOMAINusername, such as /user:DOMAINusername, that SPMetal runs as. |
Password | The password that SPMetal uses to log on as the user specified in the /user parameter. |
Serialization | Specifies whether you want your objects to be serializable. By default, this parameter is none, so they are not. If you specify unidirectional, SPMetal puts in the appropriate markup to make the objects serializable. |
Parameters | Specifies the XML file used to override the parameters for your SPMetal settings. This is for advanced changes. |
The following code snippet shows you some of the generated code, but to give you an idea of the work SPMetal does for you, the complete code for even a simple SharePoint site is more than 3,000 lines! You definitely want to use SPMetal to generate this code and tweak SPMetal to meet your requirements.
/// <summary> /// Use the Announcements list to post messages on the home page of your /// site. /// </summary> [Microsoft.SharePoint.Linq.ListAttribute(Name="Announcements")] public Microsoft.SharePoint.Linq. EntityList<AnnouncementsAnnouncement> Announcements { get { return this. GetList<AnnouncementsAnnouncement>("Announcements"); } } /// <summary> /// Create a new news item, status or other short piece of information. /// </summary> [Microsoft.SharePoint.Linq.ContentTypeAttribute(Name="Announcement", Id="0x0104")] [Microsoft.SharePoint.Linq.DerivedEntityClassAttribute (Type=typeof(AnnouncementsAnnouncement))] public partial class Announcement : Item { private string _body; private System.Nullable<System.DateTime> _expires; #region Extensibility Method Definitions partial void OnLoaded(); partial void OnValidate(); partial void OnCreated(); #endregion public Announcement() { this.OnCreated(); } [Microsoft.SharePoint.Linq.ColumnAttribute(Name="Body", Storage="_body", FieldType="Note")] public string Body { get { return this._body; } set { if ((value != this._body)) { this.OnPropertyChanging("Body", this._body); this._body = value; this.OnPropertyChanged("Body"); } } } [Microsoft.SharePoint.Linq.ColumnAttribute(Name="Expires", Storage="_expires",FieldType="DateTime")] public System.Nullable<System.DateTime> Expires { get { return this._expires; } set { if ((value != this._expires)) { this.OnPropertyChanging("Expires", this._expires); this._expires = value; this.OnPropertyChanged("Expires"); } } } }
One thing you may realize is that SPMetal, by default, does not generate all the fields for your content types. So, you may find that fields such as Created or ModifiedBy do not appear in the types created by SPMetal. To add these fields, you can specify them in a Parameters.XML. The example that follows adds some new fields to the contact content type:
<?xml version="1.0" encoding="utf-8"?> <Web xmlns="http://schemas.microsoft.com/SharePoint/2009/spmetal"> <ContentType Name="Contact" > <Column Name="CreatedBy" /> <Column Name="ModifiedBy"/> </ContentType> </Web>
After you generate SPMetal code imported into VS, it’s time to make sure you have the right references set up to use that code. You want to add two references at a minimum:
This adds all the necessary dependent LINQ assemblies to your project.
The DataContext object provides the heart of your LINQ programming. Your DataContext object is named after whatever you named the beginning of your generated file from SPMetal. For example, if you had SPMetal create a LINQDemo.cs file for your generated code, your DataContext object becomes LINQDemoDataContext, but you can override this in your parameter.xml file. To create your DataContext object, you can pass along the URL of the SharePoint site to which you want to connect.
When you have your DataContext object, you can start working with that object’s methods and properties. The DataContext contains all your lists and libraries as EntityList properties. You can retrieve these lists and libraries and then work with them. Table 5-12 lists the other methods and properties you can use from the DataContext object.
Name | Description |
GetList<T> | Returns the list of the specified type, for example, . |
Refresh | Refreshes the datasource. |
RegisterList | Enables you to register a list by registering a new name and new URL (if needed) as a replacement for the old name. This is helpful if a list has been renamed or moved and you do not want to rewrite your code. |
ChangeConflicts | Returns a ChangeConflictCollection, which is a list of conflicts from your transactions. |
DeferredLoadingEnabled | A Boolean that gets or sets whether LINQ should defer loading your objects until they are needed. |
Log | Gets the CAML query generated by LINQ. This is a good way to view what LINQ is generating on your behalf. |
ObjectTrackingEnabled | Gets or sets whether changes to objects are tracked. If you are just querying your site, for performance reasons, you should set this to false. |
Web | Gets the full URL of the SharePoint website the DataContext object is connected to. |
As part of SPMetal, you get autogenerated typed data classes and relationships using the Association attribute. This enables you to use strongly typed objects for your lists and also to do queries across multiple lists that are related by lookup fields. This makes programming much cleaner and also enables you to catch compile-time errors when working with your objects, rather than runtime errors.
To query and enumerate your data, you need to write LINQ queries. When you write your queries, you need to understand that LINQ translates the query into CAML, so if you try to perform LINQ queries that cannot be translated into CAML, SharePoint throws an error. SharePoint considers these inefficient queries, and the only way to work around them is to use LINQ to Objects and perform the work yourself. The following is a list of the unsupported operators that cause errors in SharePoint:
The simplest query you can write is a select from your list. The following code performs a select from a list and then enumerates the results:
var context = new LinqDemoDataContext("http://intranet.contoso.com"); var orderresults = from orders in context.Orders select orders; foreach (var order in orderresults) { MessageBox.Show(order.Title + " " + order.Customer); }
From the following in the code, you can see that you need to:
var orderresults = from orders in context.Orders where orders.Total > 1000 select orders;
For the next example, the query performs a simple inner join between two lists that share a lookup field. Because CAML now supports joins, this is supported in LINQ as well. In the code that follows, you’ll notice:
var context = new LinqDemoDataContext("http://intranet.contoso.com"); EntityList<OrdersItem> Orders = context.GetList<OrdersItem>("Orders"); EntityList<CustomersItem> Customers = context.GetList<CustomersItem> ("Customers"); var QueryResults = from Order in Orders join Customer in Customers on Order.Customer.Id equals Customer.Id select new { CustomerName = Customer.Title, Order.Title, Order.Product }; var Results = QueryResults.ToList(); if (Results.Count > 0) { Results.ForEach(result => MessageBox.Show(result.Title + " " + result.CustomerName + " " + result.Product)); } else { MessageBox.Show("No results"); } }
LINQ enables you to add, delete, and update your data in SharePoint and even send items to the recycle bin. Because LINQ is strongly typed, you can just create new objects that map to the type of the new objects you want to add. After the new object is created, you can call the InsertOnSubmit method and pass the new object or the InsertAllOnSubmit method and pass a collection of new objects. Because LINQ works asynchronously from the server with a local cache, you need to call SubmitChanges after all modifications to data through LINQ to actually have the changes propagated back to SharePoint.
Updating items involves updating the properties on your objects and then calling SubmitChanges. For deleting, you need to call DeleteOnSubmit or DeleteAllOnSubmit, passing either the object or a collection of objects, and then call SubmitChanges. Recycling works much the same as deleting.
Because LINQ does not directly work against the SharePoint store, changes could be made to the backend while your code is running. To handle this, SharePoint LINQ provides the ability to catch exceptions if duplicates are present, if conflicts are detected, or if general exceptions occur. The code that follows shows how to code for all these cases.
One thing to note is that the code uses the ChangeConflict exception and then enumerates all the ObjectChangeConflict objects. Then, it looks through the MemberConflict objects, which contain the differences between the database fields and the LINQ object fields. After you decide what to do about the discrepancies, you can resolve the changes with the ResolveAll method. The ResolveAll method takes a RefreshMode enumeration, which can contain one of three values:
try { EntityList<OrdersItem> Orders = context.GetList<OrdersItem>("Orders"); OrdersItem order = new OrdersItem(); order.Title = "My LINQ new Order"; order.Product = "Chai"; //Add a lookup to Customers EntityList<CustomersItem> Customers = context.GetList<CustomersItem>("Customers"); var CustomerTempItem = from Customer in Customers where Customer.Title == "Contoso" select Customer; CustomersItem CustomerItem = null; foreach (var Cust in CustomerTempItem) CustomerItem = Cust; order.Customer = CustomerItem; Orders.InsertOnSubmit(order); context.SubmitChanges(); //Delete the item Orders.DeleteOnSubmit(order); context.SubmitChanges(); } catch (ChangeConflictException conflictException) { MessageBox.Show("A conflict occurred: " + conflictException.Message); foreach (ObjectChangeConflict Items in context.ChangeConflicts) { foreach (MemberChangeConflict Fields in Items.MemberConflicts) { StringBuilder sb = new StringBuilder(); sb.AppendLine("Item Name: " + Fields.Member.Name); sb.AppendLine("Original Value: " + Fields.OriginalValue); sb.AppendLine("Database Value: " + Fields.DatabaseValue); sb.AppendLine("Current Value: " + Fields.CurrentValue); MessageBox.Show(sb.ToString()); } } //Force all changes context.ChangeConflicts.ResolveAll(RefreshMode.KeepChanges); context.SubmitChanges(); } catch (SPDuplicateValuesFoundException duplicateException) { MessageBox.Show("Duplicate value found: " + duplicateException.Message); } catch (SPException SharePointException) { MessageBox.Show("SharePoint Exception: " + SharePointException.Message); } catch (Exception ex) { MessageBox.Show("Exception: " + ex.Message); } }
If you want to inspect the CAML query that LINQ is generating, you can use the Log property and set that to a TextWriter or an object that derives from the TextWriter object, such as a StreamWriter object. If you are writing a console application, the easiest way to set the Log property is to set it to the Console.Out property. From there, you can retrieve the CAML query that SharePoint LINQ executes on your behalf. The following code shows how to write a log file for your LINQ query using the Log property and then shows the CAML query generated by LINQ:
var context = new LinqDemoDataContext("http://intranet.contoso.com"); context.Log = new StreamWriter(File.Open("C:\SPLINQLog.txt", FileMode.Create)); EntityList<OrdersItem> Orders = context.GetList<OrdersItem>("Orders"); EntityList<CustomersItem> Customers = context.GetList<CustomersItem>("Customers"); var QueryResults = from Order in Orders join Customer in Customers on Order.Customer.Id equals Customer.Id select new { CustomerName = Customer.Title, Order.Title, Order.Product }; context.Log.WriteLine("Results :"); var Results = QueryResults.ToList(); if (Results.Count > 0) { Results.ForEach(result => context.Log.WriteLine(result.Title + " " + result.CustomerName + " " + result.Product)); } else { context.Log.WriteLine("No results"); } context.Log.Close(); context.Log = null;
Please note that the log will only work for queries and not for inserts, updates, or deletes. The output for the previous code is as follows:
<View><Query><Where><And><BeginsWith><FieldRef Name="ContentTypeId" /><Value Type="ContentTypeId">0x0100</Value></BeginsWith><BeginsWith><FieldRef Name="CustomerContentTypeId" /><Value Type="Lookup">0x0100</Value></BeginsWith></And></Where><OrderBy Override="TRUE" /></Query><ViewFields><FieldRef Name="CustomerTitle" /><FieldRef Name="Title" /><FieldRef Name="Product" /></ViewFields><ProjectedFields><Field Name="CustomerTitle" Type="Lookup" List="Customer" ShowField="Title" /><Field Name="CustomerContentTypeId" Type="Lookup" List="Customer" ShowField="ContentTypeId" /></ProjectedFields><Joins><Join Type="INNER" ListAlias="Customer"><!~DHList Name: Customers~DH><Eq><FieldRef Name="Customer" RefType="ID" /><FieldRef List="Customer" Name="ID" /></Eq></Join></Joins><RowLimit Paged="TRUE">2147483647</RowLimit></View>
One best practice is to turn off object tracking if you just query the list and do not plan to add, delete, or update items in the list. This makes your queries perform better because LINQ doesn’t have the overhead of trying to track changes to the SharePoint objects. The way to turn off object tracking is to set the ObjectTrackingEnabled property to false on your DataContext object.
If you do need to make changes to the list, you can open another DataContext object to the same list with object change tracking enabled. LINQ allows two DataContext objects to point at the same website and list, so you can have one DataContext object for querying and another for writing to the list.
With SharePoint 2007, if you wanted to program on the client side, in reality you had only one API choice—the ASMX Web Services API. Although functional, the Web Services API was not the easiest API to program against, and although it was easy to program from managed code, programming against it from JavaScript or Silverlight was difficult at best. With the growth of client-side technologies, such as .NET CLR–based clients (for example, Windows Presentation Framework [WPF] or Silverlight); new technologies for programming in JavaScript, such as JSON; and the introduction of REST, moving from the Web Services API to a richer API was sorely needed in SharePoint. Welcome the managed client object model, which this chapter refers to as the Client Object Model (Client OM).
The Client OM is actually two object models:
Figure 5-22 shows the way the Client OM works.
One principle of the Client OM is to minimize network chatter. So Fiddler is a key tool for helping you troubleshoot any issues because the Client OM batches its commands and sends them all at once to the server at your request. This minimizes the round-trips and network bandwidth used by the object model and makes your application perform better. In addition, you should write asynchronous code with callbacks so that your user interface doesn’t block when users perform actions.
In terms of API support, the Client OM supports a subset of the server OM, so access to lists, libraries, views, content types, web parts, and users/groups is part of the OM. However, the OM does not provide coverage of all features, such as the taxonomy store or BI data. Figure 5-23 shows the major objects in the Client OM.
There is also a difference in the namespaces provided by the .NET and ECMAScript OMs. Because you extend the Ribbon using script, the ECMAScript OM has a Ribbon namespace, while the managed Client OM does not. Plus, there is a difference in naming conventions for the foundational part of the namespaces. For example, if you wanted to access a site, in the .NET API you would use the Microsoft.SharePoint.Client.Site object, but in ECMAScript you would use SP.Site. Table 5-13 shows the different namespaces for the two Client OMs.
.NET Managed | ECMAScript |
Microsoft.SharePoint.Client.Application | N/A |
N/A | SP.Application.UI |
N/A | SP.Ribbon |
N/A | SP.Ribbon.PageState |
N/A | SP.Ribbon.TenantAdmin |
N/A | SP.UI |
N/A | SP.UI.ApplicationPages |
N/A | SP.UI.ApplicationPages.Calendar |
Microosft.SharePoint.Client.Utilities | SP.Utilities |
Microsoft.SharePoint.Client.WebParts | SP.WebParts |
Microsoft.SharePoint.Client.Workflow | SP.Workflow |
To convert your understanding of server objects to the client, Table 5-14 shows how server objects would be named in the Client OMs.
Before you dive into writing code with the Client OM and adding references in VS, you first need to understand where these DLLs are located and what some of the advantages of the DLLs, especially size. As with other SharePoint .NET DLLs, you can find the .NET DLLs for the Client OM located under %Program Files%Common FilesMicrosoft SharedWeb Server Extensions14ISAPI. There are two DLLs for the managed OM, Microsoft.SharePoint.Client and Microsoft.SharePoint.Client.Runtime. If you look at these DLLs in terms of size, combined they are under 1MB. Compare that with Microsoft.SharePoint, which weighs in at more than a hefty 15 MB.
Because the ECMAScript implementation is different from the .NET one and needs to live closer to the web-based code for SharePoint, this DLL is located in %Program Files%Common FilesMicrosoft SharedWeb Server Extensions14TEMPLATELAYOUTS. There, you can find four relevant JS files: SP.js, SP.Core.js, SP.Ribbon.js, and SP.Runtime.js. Of course, when you debug your code, you use the debug versions of these files, such as SP.debug.js, because the main versions are crunched to save on size and bandwidth. Also, you can set your SharePoint deployment to use the debug versions of these files automatically by changing the web.config file for your deployment located at %inetpub%wwwrootwssVirtualDirectories80 and adding to the system.web section the following line <deployment retail=“false” />. Again, these files are less than 1 MB.
Lastly, Silverlight is a little bit different in that it has its own specific implementation of the Client OM for Silverlight. You can find the Silverlight DLLs at %Program Files%Common FilesMicrosoft SharedWeb Server Extensions14TEMPLATELAYOUTSClientBin. You can find two files, Microsoft.SharePoint.Client.Silverlight and Microsoft.SharePoint.Client.Silverlight.Runtime. Combined, the files also come under 1 MB in size.
Microsoft has redistributable versions of the .NET and Silverlight object models to install on your client machines. If you have Office 2010 installed on your machine, you don’t need the redistributable version, but if you don’t, you can retrieve the 32-bit and 64-bit versions of the redistributable object model at www.microsoft.com/downloads/en/details.aspx?FamilyID=b4579045-b183-4ed4-bf61-dc2f0deabe47. Given the long URL, it is probably easier to search for SharePoint Client Object Model redistributable.
Depending on the type of application you write, the way you reference the different Client OMs varies. With WPF or WinForms, you use the VS Add Reference user interface to add a reference to the DLLs discussed earlier. From there, you can use the proper statements to leverage the namespaces in your code. The same process is true for Silverlight. Figure 5-24 shows how to add a reference inside of Visual Studio for a regular managed code project.
When it comes to referencing the ECMAScript OM, you use the ScriptLink control, which is part of the Microsoft.SharePoint.WebControls namespace. The following code snippet shows you how to do this:
<SharePoint:ScriptLink ID="ScriptLinkSPDebug" Name="sp.debug.js" LoadAfterUI="true" Localizable="false" runat="server" />
Before you write your first line of code, you need to understand the context that your code will run in. With the Client OM, by default, your code runs in the context of the currently logged-on user. Because many web applications support forms-based authentication, the Client OM supports this as well. You must provide the username and password for the Client OM to use and also set the authentication mode to forms-based authentication on your ClientContext object. The following code shows you how to set the Client OM to use forms-based authentication and set the correct properties to send a username and password:
clientContext.AuthenticationMode = ClientAuthenticationMode.FormsAuthentication; FormsAuthenticationLoginInfo formsAuthInfo = new FormsAuthenticationLoginInfo("User", "Password"); clientContext.FormsAuthenticationLoginInfo = formsAuthInfo;
Remember that SharePoint Online requires that you pass the custom authentication cookies with your Client OM requests so that you cannot use the forms authentication in the previous code to log on to SharePoint Online. Instead, you must use the methods described earlier in this chapter.
At the heart of all your code is the ClientContext object. This is the object that you instantiate first to tell SharePoint what site you want to connect to in order to perform your operations. With the .NET API, you must pass an absolute URL to the client context to open your site, but with the ECMAScript API, you can pass a relative or blank URL in your constructor and SharePoint finds the relative site or uses the current site as the site you want to open.
One quick note on ClientContext is that in managed code, it inherits from IDisposable, which you can tell by looking at the implementation. This means that you must properly dispose of your ClientContext objects by wrapping your code with using statements or by calling Dispose explicitly. If you don’t dispose correctly, you may run into memory leaks and issues. One tool you should use to make sure you dispose of your objects correctly is SPDisposeCheck, which you can find at http://archive.msdn.microsoft.com/SPDisposeCheck.
When working with the constructor for the ClientContext, you can pass in either a string that is the URL to your site or a URI object that contains the URL to your site.
Table 5-15 shows the important methods and properties for the ClientContext class.
Name | Description |
Dispose | Call this method to dispose of your object after you finish using it. |
ExecuteQuery | After loading all the operations for your site, such as queries, call this method to send the commands to the server. |
executeQueryAsync | Available in the ECMAScript object model, this enables you to call a query and pass two delegates to call back to. One is for when the query succeeds, and the other is used when the query fails. |
Load | Enables you to load your query using the method syntax of LINQ and fills the object you pass. You can also pass an object without a query to return just the object, such as Site. |
LoadQuery | Use this to return a collection of objects as an IQueryable collection. This supports both the method and query syntax for LINQ. |
AuthenticationMode | Gets or sets the authentication mode for your object. The values can be Default, FormsAuthentication, or Anonymous. FormsAuthentication is not applicable for SharePoint Online development. |
FormsAuthenticationLoginInfo | Use this property to set the username and password for your forms authentication to authenticate against your site. This property is not applicable for SharePoint Online development. |
RequestTimeout | Gets or sets the timeout for your requests. |
Site | Gets the site collection associated with the ClientContext. |
URL | Gets the URL of the site that the ClientContext is associated with. |
Web | Gets the website that the ClientContext is associated with. |
As demonstrated in the sample code throughout this section, you use the Load or LoadQuery method on the ClientContext object and then call the ExecuteQuery or executeQueryAsync method to execute your query. The rest of this section goes through the different programming tasks you perform with the Client OM to show you how to use it.
To retrieve your list items from SharePoint, you use the Load method to load the object into the Client OM. For example, if you want to load a Web object into the Client OM and then access the properties from it, you would use the following code:
ClientContext context = new Microsoft.SharePoint.Client.ClientContext( "http://intranet.contoso.com"); Web site = context.Web; context.Load(site); context.ExecuteQuery(); MessageBox.Show("Title: " + site.Title + " Relative URL: " + site.ServerRelativeUrl); context.Dispose();
If you try to use any of the other objects below the requested site, you get an error message saying that the collection is not initialized. For example, if you try to retrieve the lists in the site, an error occurs. With the Client OM, you need to be explicit about what you want to load. The following modified sample shows you how to load the list collection and then iterate over the objects in the collection:
//Load the List Collection ListCollection lists = context.Web.Lists; context.Load(lists); context.ExecuteQuery(); MessageBox.Show(lists.Count.ToString()); foreach (Microsoft.SharePoint.Client.List list in lists) { MessageBox.Show("List: " + list.Title); }
By default, SharePoint returns a large set of properties and hydrates your objects with these properties. For performance reasons, you may not want to have it do that if you are using only a subset of the properties. Plus, certain properties are not returned by default, such as permission properties for your objects. As a best practice, you should request the properties that you need rather than let SharePoint retrieve all properties for you. This is similar to the best practice of not doing a SELECT * in SQL Server.
The way to request properties is in your load method. As part of this method, you need to request the properties you want to use in your LINQ code. The following example changes the previous site request code to retrieve only the Title and ServerRelativeURL properties, and for the lists only the Title property, because that is all that’s used in the code:
ClientContext context = new Microsoft.SharePoint.Client.ClientContext( "http://intranet.contoso.com"); Web site = context.Web; context.Load(site, s => s.Title, s => s.ServerRelativeUrl); ListCollection lists = site.Lists; context.Load(lists, ls => ls.Include(l => l.Title)); context.ExecuteQuery(); MessageBox.Show("Title: " + site.Title + " Relative URL: " + site.ServerRelativeUrl); MessageBox.Show(lists.Count.ToString()); foreach (Microsoft.SharePoint.Client.List list in lists) { MessageBox.Show("List: " + list.Title); } context.Dispose();
You may wonder what the difference is between Load and LoadQuery.
The LoadQuery method is similar to the Load method, except that it returns a new collection. The other key difference is that the properties for objects off the client context are not populated with LoadQuery after your LoadQuery call. You need to call Load method to populate these. The following code shows you a good example of this:
ClientContext context = new Microsoft.SharePoint.Client.ClientContext( "http://intranet.contoso.com"); Web site = context.Web; ListCollection lists = site.Lists; IEnumerable<List> newLists = context.LoadQuery(lists.Include( list => list.Title)); context.ExecuteQuery(); foreach (List list in newLists) { MessageBox.Show("Title: " + list.Title); } //This will error out because lists is not populated MessageBox.Show(lists.Count.ToString()); context.Dispose();
In your LoadQuery calls, you can nest Include statements so that you can load fields from multiple objects in the hierarchy without making multiple calls to the server. The following code shows how to do this:
ClientContext context = new Microsoft.SharePoint.Client.ClientContext( "http://intranet.contoso.com"); Web site = context.Web; ListCollection lists = site.Lists; IEnumerable<List> newLists = context.LoadQuery(lists.Include( list => list.Title, list => list.Fields.Include(Field => Field.Title))); context.ExecuteQuery(); foreach (List list in newLists) { MessageBox.Show(" List Title: " + list.Title); foreach (Field field in list.Fields) { MessageBox.Show("Field Title: " + field.Title); } } context.Dispose();
In the Client OM, you can use CAML to query the server as part of the GetItems method. As you see in the code that follows, you create a new CamlQuery object and pass the CAML query that you want to perform into the the ViewXml property. From there, you call the GetItems on your ListCollection object and pass in your CamlQuery object. You still need to call the Load and ExecuteQuery methods to have the client object model perform your query. Also, CAML does support row limits, so you can also pass a <RowLimit> element in your CAML query and page over your results. In the OM, on the ListItemCollection object, there is a property ListItemCollectionPosition. You need to set your CAMLQuery object’s ListItemCollectionPosition to your own ListItemCollectionPosition object to keep track of your paging, and then you can position your query starting point before querying the list and iterate through the pages until there are no pages of content left, as shown here:
ClientContext context = new Microsoft.SharePoint.Client.ClientContext( "http://intranet.contoso.com"); List list = context.Web.Lists.GetByTitle("Announcements"); ListItemCollectionPosition itemPosition = null; while (true) { CamlQuery camlQuery = new CamlQuery(); camlQuery.ListItemCollectionPosition = itemPosition; camlQuery.ViewXml = @" <View> <Query> <Where> <IsNotNull> <FieldRef Name='Title' /> </IsNotNull> </Where> </Query> <RowLimit>1000</RowLimit> </View>"; ListItemCollection listItems = list.GetItems(camlQuery); context.Load(listItems); context.ExecuteQuery(); itemPosition = listItems.ListItemCollectionPosition; foreach (ListItem listItem in listItems.ToList()) { MessageBox.Show("Title: " + listItem["Title"]); } if (itemPosition == null) { break; } MessageBox.Show("Position: " + itemPosition.PagingInfo); }
If you don’t want to use CAML to query your lists, you can use LINQ. To do this:
ClientContext context = new Microsoft.SharePoint.Client.ClientContext( "http://intranet.contoso.com"); var query = from list in context.Web.Lists where list.Title != null select list; var result = context.LoadQuery(query); context.ExecuteQuery(); foreach (List list in result) { MessageBox.Show("Title: " + list.Title); } context.Dispose();
Using the Client OM, you can create lists and items. To do this, you need to use the ListCreationInformation object and set the properties, such as the title and the type, for your list. Your ListCollection object has an Add method that you can call and pass your ListCreationInformation object to in order to create your list.
To create a field, follow these steps:
ClientContext context = new Microsoft.SharePoint.Client.ClientContext ("http://intranet.contoso.com"); Web site = context.Web; ListCreationInformation listCreationInfo = new ListCreationInformation(); listCreationInfo.Title = "New List"; listCreationInfo.TemplateType = (int)ListTemplateType.GenericList; List list = site.Lists.Add(listCreationInfo); Field newField = list.Fields.AddFieldAsXml(@" <Field Type='Text' DisplayName='NewTextField'> </Field>", true, AddFieldOptions.AddToDefaultContentType); ListItemCreationInformation itemCreationinfo = new ListItemCreationInformation(); ListItem item = list.AddItem(itemCreationinfo); item["Title"] = "My New Item"; item["NewTextField"] = "My Text"; item.Update(); context.ExecuteQuery(); context.Dispose();
To delete lists and items, you can use the DeleteObject method. One caveat to remember is that when you delete items from a collection, you should materialize your collection into a List<T> object, using the ToList method, so you can iterate through the list and delete without errors.
ClientContext context = new Microsoft.SharePoint.Client.ClientContext( "http://intranet.contoso.com"); List list = context.Web.Lists.GetByTitle("New List"); CamlQuery camlQuery = new CamlQuery(); camlQuery.ViewXml = @" <View> <Query> <Where> <IsNotNull> <FieldRef Name='Title' /> </IsNotNull> </Where> </Query> </View>"; ListItemCollection listItems = list.GetItems(camlQuery); context.Load(listItems, items => items.Include(item => item["Title"])); context.ExecuteQuery(); foreach (ListItem listItem in listItems.ToList()) { listItem.DeleteObject(); } context.ExecuteQuery(); context.Dispose();
Another feature of the Client OM, beyond enabling you to work with lists, libraries, and items, is that it gives you the ability to work with users and groups. The Client OM includes the GroupCollection, Group, UserCollection, and User objects to make working with users and groups easier. Just as you iterate on lists and items, you can iterate on users and groups using these collections. The Client OM also has access to built-in groups such as the Owners, Members, and Visitors groups. You can access these from your context object using the AssociatedOwnerGroup, AssociatedMemberGroup, and AssociatedVisitorGroup properties, which return a Group object. Remember to hydrate these objects before trying to access properties or User collections on the objects.
To add a user to a group:
Because this is similar to the steps used to create items, the sample code that follows shows you how to query users and groups but not create users:
ClientContext context = new Microsoft.SharePoint.Client.ClientContext( "http://intranet.contoso.com"); GroupCollection groupCollection = context.Web.SiteGroups; context.Load(groupCollection, groups => groups.Include( group => group.Users)); context.ExecuteQuery(); foreach (Group group in groupCollection) { UserCollection userCollection = group.Users; foreach (User user in userCollection) { MessageBox.Show("User Name: " + user.Title + " Email: " + user.Email + " Login: " + user.LoginName); } } //Iterate the owners group Group ownerGroup = context.Web.AssociatedOwnerGroup; context.Load(ownerGroup); context.Load(ownerGroup.Users); context.ExecuteQuery(); foreach (User ownerUser in ownerGroup.Users) { MessageBox.Show("User Name: " + ownerUser.Title + " Email: " + ownerUser.Email + " Login: " + ownerUser.LoginName); } context.Dispose();
All the code shown so far is synchronous code running in a .NET client, such as a WPF, console, or Windows Forms application. You may not want to write synchronous code, even in your .NET clients, so your application can be more responsive to your users, rather than having them wait for operations to complete before continuing to use your application. ECMAScript and Silverlight are asynchronous by default, so you can program them separately, but for .NET clients, you need to do a little bit of work to make your code asynchronous. The main change is that you need to use the BeginInvoke method to execute your code and pass a delegate to that method, which .NET calls when your code is done executing asynchronously. Then, you can do other work while you are polling to see if the asynchronous call is complete. When it’s complete, call the EndInvoke method to get back the result.
public delegate string AsyncDelegate(); public string TestMethod() { string titleReturn = ""; using (ClientContext context = new Microsoft.SharePoint.Client.ClientContext ("http://intranet.contoso.com")) { List list = context.Web.Lists.GetByTitle("Announcements"); context.Load(list); context.ExecuteQuery(); titleReturn = list.Title; } return titleReturn; } private void button1_Click(object sender, EventArgs e) { // Create the delegate. AsyncDelegate dlgt = new AsyncDelegate(TestMethod); // Initiate the asychronous call. IAsyncResult ar = dlgt.BeginInvoke(null, null); // Poll while simulating work. while (ar.IsCompleted == false) { //Do work } // Call EndInvoke to retrieve the results. string listTitle = dlgt.EndInvoke(ar); //Print out the title of the list MessageBox.Show(listTitle);
Using ECMAScript with the client object model is similar to using the .NET object model. The main differences are that you use server-relative URLs for your ClientContext constructor, and the ECMAScript object model does not accept LINQ syntax for retrieving items from SharePoint. Instead, you use string expressions to define your basic queries. Also, ECMAScript is always asynchronous, so you need to use delegates and create callback functions for the success and failure of your call into the Client OM. The final piece, as shown in the code that follows, is that you need to:
<%@ Page Language="C#" %> <%@ Register Tagprefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>ECMAScript Client OM</title> <script type="text/javascript"> function CallClientOM() { var context = new SP.ClientContext.get_current(); this.website = context.get_web(); this.listCollection = website.get_lists(); context.load(this.listCollection, 'Include(Title, Id)'), context.executeQueryAsync(Function.createDelegate(this, this.onQuerySucceeded), Function.createDelegate(this, this.onQueryFailed)); } function onQuerySucceeded(sender, args) { var listInfo = ''; var listEnumerator = listCollection.getEnumerator(); while (listEnumerator.moveNext()) { var list = listEnumerator.get_current(); listInfo += 'List Title: ' + list.get_title() + ' ID: ' + list.get_id() + ' '; } alert(listInfo); } function onQueryFailed(sender, args) { alert('request failed ' + args.get_message() + ' ' + args.get_stackTrace()); } </script> </head> <body> <form id="form1" runat="server"> <SharePoint:ScriptLink ID="ScriptLink1" Name="sp.debug.js" LoadAfterUI="true" Localizable="false" runat="server" /> <a href="#" onclick="CallClientOM()">Click here to Execute</a> <SharePoint:FormDigest runat="server" /> </form> </body> </html>
code snippetClientOM.aspx
You must use the right Client OM DLLs. Silverlight has special DLLs in the %Program Files%Common FilesMicrosoft SharedWeb Server Extensions14TEMPLATELAYOUTSClientBin directory for the core OM and the run time.
To deploy your Silverlight application, the application needs to run in a trusted area of SharePoint, which could be in a SharePoint library or in the ClientBin directory. The easiest way to get your code into the ClientBin directory is to make the output of your project go to this directory. For deploying to SharePoint document libraries, you could manually upload your XAP file to SharePoint and point the Silverlight web part at your manually uploaded XAP. Another way is to use a Sandbox Solution, which you learn about later in this chapter, to create a feature that copies the file to your SharePoint site using a Module with a File reference in your Elements manifest.
Watch out for the caching of your Silverlight application while you develop it. Update the AssemblyVersion and FileVersion in your AssemblyInfo file in VS to ensure that an old version is not loaded. You may also have to clear your browser cache. Another recommendation is to change something in the UI so you can recognize visually that your application is the latest version.
If you use anonymous access and Silverlight, you must modify the SPClientCallableSettings.AnonymousRestrictedTypes property. For example, if you attempt to retrieve list items as an anonymous user, you get an error message stating that the GetItems method has been disabled by the administrator. To enable this method or other methods, you can use the following PowerShell command.
$webapp = Get-SPWebApplication -Identity "http://sharepointsite" $webapp.ClientCallableSettings.AnonymousRestrictedTypes.Remove ([Microsoft.SharePoint.SPList], "GetItems") $webapp.Update()
Also, if you work across domains, you need to understand how to create cross-domain policies using a ClientAccessPolicy.XML file that you host at the root of your website. If you work in the same domain, you do not need to write this policy file, but if you go across domains (for example, if your Silverlight application runs in your domain but calls a service in another domain) you must use the ClientAccessPolicy.XML file to allow those calls. Figure 5-25 shows the Silverlight application in action.
using SP = Microsoft.SharePoint.Client; namespace SPSilverlight { public partial class MainPage : UserControl { IEnumerable<SP.List> listItems = null; public MainPage() { InitializeComponent(); } private void getItemsSucceeded(object sender, Microsoft.SharePoint.Client.ClientRequestSucceededEventArgs e) { Dispatcher.BeginInvoke(() => { //Code to display items //Databind the List of Lists to the listbox listBox1.ItemsSource = listItems; listBox1.DisplayMemberPath = "Title"; }); } private void getItemsRequestFailed(object sender, Microsoft.SharePoint.Client.ClientRequestFailedEventArgs e) { Dispatcher.BeginInvoke(() => { MessageBox.Show("Error: " + e.ErrorCode + " " + e.ErrorDetails + " " + e.Message + " " + e.StackTrace.ToString()); }); } private void button1_Click(object sender, RoutedEventArgs e) { ClientContext context = null; if (App.Current.IsRunningOutOfBrowser) { context = new ClientContext( "http://intranet.contoso.com"); } else { context = ClientContext.Current; } var query = from listCollection in context.Web.Lists where listCollection.Title != null select listCollection; listItems = context.LoadQuery(query); ClientRequestSucceededEventHandler success = new ClientRequestSucceededEventHandler(getItemsSucceeded); ClientRequestFailedEventHandler failure = new ClientRequestFailedEventHandler(getItemsRequestFailed); context.ExecuteQueryAsync(success, failure); } } }
code snippetMainPage.xaml.cs
With SharePoint 2010, you can program against SharePoint and Excel Services using Representational State Transfer (REST). This section covers the core SharePoint REST Services.
SharePoint REST services are implemented using the WCF Data Services, formerly known as the ADO.NET Data Services, and formerly known as Astoria. The easiest way to think about REST is that it provides URL-accessible functionality, so you can query, create, and delete lists and items using just the standard HTTP protocol.
Here are a couple of best practices before getting started with REST in SharePoint 2010:
The easiest way to start with REST in SharePoint is to look at what is returned when you connect to http://yourserver/yoursite/_vti_bin/ListData.svc, as shown in Figure 5-26. You see the XML returned for all your lists in your site.
REST offers two ways to return your data in SharePoint:
JSON is especially good if you want to turn the returned data into JavaScript objects, but it can be useful in other situations as well. You can specify the type of data you want returned by using the Content-Type header in your request. The tools that work with REST, such as Visual Studio, use ATOM, not JSON, so you need to request JSON specifically if you want your results in that format.
Because REST uses a standard URL-addressable format and standard HTTP methods, such as GET, POST, PUT, and DELETE, you get a predictable way to retrieve or write items in your SharePoint deployment. Table 5-16 lists some examples of URL addresses.
Type | Example |
List of lists | ../_vti_bin/listdata.svc |
List | listdata.svc/Listname |
Item | listdata.svc/Listname(ItemID) |
Single column | listdata.svc/Listname(ItemID)/Column |
Lookup traversal | listdata.svc/Listname(ItemID)/LookupColumn |
Raw value access (no markup) | listdata.svc/Listname(ItemID)/Column/$value |
Sorting | listdata.svc/Listname?$orderby=Column |
Filtering | listdata.svc/Listname?$filter=Title eq ‘Value’ |
Projection | listdata.svc/Listname?$select=Title,Created |
Paging | listdata.svc/Listname?$top=10&$skip=30 |
Inline expansion (lookups) | listdata.svc/Listname?$expand=Item |
Because Visual Studio has built-in support for using WCF Data Services, programming with REST starts with adding a service reference in your code. Follow these steps to use REST in Visual Studio:
Because the code for programming using REST is similar to programming using the Client OM, a quick example of adding an item to your SharePoint list using REST follows. Notice that it uses a call to generate a context for the rest of your calls to leverage so that you can batch commands and send them to server when you need to.
For adding, you call the specific AddTo method for your list, such as AddToAnnouncements. For updating and deleting, you use the UpdateObject and DeleteObject methods and pass in the object you want to delete, which is derived from your item type, such as AnnouncementItem.
RESTReference.HomeDataContext context = new RESTReference.HomeDataContext( new Uri("http://intranet.contoso.com/_vti_bin/listdata.svc")); private void button1_Click(object sender, EventArgs e) { //Populate grid using LINQ context.Credentials = CredentialCache.DefaultCredentials; var q = from a in context.Announcements select a; this.announcementsItemBindingSource.DataSource = q; } private void button2_Click(object sender, EventArgs e) { //Add a new Announcement RESTReference.AnnouncementsItem newAnnounce = new RESTReference.AnnouncementsItem(); newAnnounce.Title = "My New Announcement! " + DateTime.Now.ToString(); context.AddToAnnouncements(newAnnounce); context.SaveChanges(); }
You may also be wondering about jQuery support in SharePoint because REST supports putting out JSON objects that you can load with jQuery, as do other parts of SharePoint. Although SharePoint does not include a jQuery library, you can easily link to jQuery in your SharePoint solutions. This linking does require connectivity to the Internet. Microsoft has made jQuery and a number of other libraries available via the Microsoft Ajax Content Delivery Network. To get the jQuery library from the CDN, use the following statement in your code:
<script src="http://ajax.microsoft.com/ajax/jquery/jquery-1.5.2.js" type="text/javascript"></script>
One of the big complaints about script-based applications is that deployment and maintenance is difficult. This is where the power of SharePoint comes into play. You can create a central script library in SharePoint and use the native functionality of SharePoint to maintain your scripts, such as versioning and meta data. Because SharePoint is URL accessible, you can then link to your script using the URL.
If you would rather store your scripts in the file system, you can use solution packages to deploy your scripts. Where you store your scripts is up to you, but the SharePoint option is most likely easier.
When it comes to leveraging your script, there are a number of options to link to your script. You could add the script link to your Master Page definition so that you know jQuery is referenced on every page, but that presents issues if you have lots of pages that don’t use jQuery. You pay the cost of loading the script without using the script.
If you have never used jQuery before, you are missing out. It is a library that makes programming in JavaScript a lot easier, especially when it comes to manipulating your HTML objects. When executing your jQuery methods, you can use JQuery() or the shorthand $(). Most people use the shorthand $() in their code.
To interact with elements, place the element in parentheses. For example, if you want to append some HTML to an element, you would use the ID of the element with the jQuery Append function, such as $(‘#elementid’).append(‘HTML to Append’);.
Table 5-17 lists some other examples of jQuery functions. This is not the exhaustive list of jQuery functionality, so pick up a resource on jQuery to learn more.
Example | Description |
$(‘#elementID’) | Accesses any element. |
$(‘#elementID’).append(‘value’); | Appends text to the element. |
$(‘#elementID’).html(); | Gets or retrieves the HTML within an element. |
$(‘#elementID’).val(); | Gets the value of inputs. |
$(‘#elementID’).hide(); | Hides the element. |
$(‘#elementID’).show(); | Shows the element. |
var divs = $(‘div’); | Returns all instances of element specified, in this case all divs. |
$(‘#elementID’).addClass(‘className’); | Adds a CSS class to the element. |
$(‘#elementID’).removeClass(‘className’); | Removes the specified class. |
$(‘#elementID’).attr(‘attribute’); | Retrieves the attribute. |
$(‘table[class=”className”]’).each( function () { $(‘#’ + this.id).show(); } ); | Performs an action on multiple elements of the same class. |
After you start using jQuery, you can expand your repertoire of jQuery libraries. One key library to learn more about is jQueryUI, which adds visual elements that you can program using jQuery, such as accordions, datepickers, dialogs, and progress bars. It also includes interactions such as drag and drop and sorting.
Rather than talk about programming with jQuery, this section presents an example. The first example combines jQuery and jQueryUI to add an accordion visual element that turns SharePoint announcements into a nicer visual representation. Notice from this example that you can leverage the SharePoint Client OM with jQuery so all your knowledge of the Client OM moves easily into your jQuery solutions. Figure 5-30 shows the final solution hosted in SharePoint.
The following listing contains the sample code for the application. jQueryUI does most of the heavy lifting so that you do not need to write a lot of code. The key point is to look at how the scripts are linked into the application via the <script> tags. Rather than going to the Internet for the scripts, the application pulls the script from a central script library under the SiteAssets folder.
<!~DH Load JQuery ~DH> <script src= "/SiteAssets/jquery/jquery-1.5.2.js" type= "text/javascript"></script> <!~DH Load JQuery UI ~DH> <link type="text/css" href="/SiteAssets/jQueryUI/css/ui-lightness/ jquery-ui-1.8.11.custom.css" rel="stylesheet" /> <script type="text/javascript" src="/SiteAssets/jQueryUI/js/ jquery-1.5.1.min.jss"></script>/> <script type="text/javascript" src="/SiteAssets/jQueryUI/js/ jquery-ui-1.8.11.custom.min.js"></script>/> <B>Demo - Accordion with JQuery</B> <div class="demoControls"> <button id="btnDemo2" type="button">Accordion me!</button> </div> <div id="accordion"> </div> <script type="text/javascript"> var allDocs; $('#btnDemo2').click(function () { var ctx = new SP.ClientContext.get_current(); var targetList = ctx.get_web().get_lists().getByTitle('Announcements'), var query = SP.CamlQuery.createAllItemsQuery(); allDocs = targetList.getItems(query); ctx.load(allDocs); ctx.executeQueryAsync(Function.createDelegate(this, getAllItemsSuccess), Function.createDelegate(this, getAllItemsFailure)); }); function getAllItemsSuccess(sender, args) { var listEnumerator = allDocs.getEnumerator(); while (listEnumerator.moveNext()) { $('#accordion').append('<div><h3><a href="#">' + listEnumerator.get_current().get_item("Title") + '</a></h3><div>' + listEnumerator.get_current(). get_item("Body") + '</div></div>'), } accordionContent(); } function accordionContent() { $("#accordion").accordion({ header: "h3" }); } function getAllItemsFailure(sender, args) { alert('Failed to get list items. Error: ' + args.get_message() + ' StackTrace: ' + args.get_stackTrace()); } </script>
With all the excitement around CSS3 and HTML5, you may wonder how either one plays in the SharePoint environment. Because both are browser-dependent, a lot of the answer depends on your browser, especially for HTML5. SharePoint 2010 does not leverage HTML5, but if you create content using HTML5 and place it in a SharePoint site, as long as you browse that content with an HTML5-compatible browser, your content should work seamlessly. To see some CSS3 working inside of SharePoint, Figure 5-31 shows a Polaroid sample that takes images from a SharePoint picture library and turns them into dragable Polaroid images. The image URLs are retrieved using the Client OM. Manipulating the images leverages CSS3 and jQuery.
Often developers who want to build solutions for SharePoint can’t because they require access to the SharePoint server directly, and their solutions must be deployed as full-trust solutions, which could affect the stability of the server if there are bugs in the code. For these reasons, IT administrators may not allow developers to deploy their solutions against SharePoint 2007. With Sandboxed Solutions in SharePoint 2010, the server administrator can allow site administrators to deploy solutions while still maintaining the integrity of the server. Sandboxed Solutions are self-regulating because there are quotas for resource usage, and the server shuts down any solutions that exceed their quota.
With Sandboxed Solutions, you can build a subset of the solutions you can build in SharePoint. Solutions that require extensive privileges are not allowed in the sandbox because of its limited nature. The following list gives you the types of solutions you can build with Sandboxed Solutions.
Before a Sandbox Solution can run, a site administrator must upload the solution and activate it in the site. When you upload a Sandbox Solution, you upload it to the Solution gallery.
The Solution gallery contains all your Sandboxed Solutions and displays the resource quota that your solutions are taking both for the current day and averaged over the past 14 days. The Solution gallery is located in _catalogs/solutions.
The Sandboxed Solutions architecture provides three main components to use when executing your solution. Please note that this is for on-premises SharePoint deployments since SharePoint Online does not provide access to this functionality or architecture.
The Sandbox implements only a subset of the Microsoft.SharePoint namespace. Sandboxed Solutions enable you to use full trust proxies to access other APIs or capabilities, for example, accessing network resources. However, out-of-the-box (OOB) the following capabilities from the Microsoft.SharePoint namespace are supported:
One question people commonly ask is, “If I can’t access local resources (such as the hard drive on the server or network resources except for SharePoint), how can I access external data (such as a database, Twitter, or some other external datasource)?” Well, you can use external lists and BCS in SharePoint to access external data because Sandboxed Solutions can access external lists. Of course, you need to have permissions to set up BCS and external lists, but if there are already BCS solutions set up with access to the external datasources that you need, you can use the external lists in your Sandboxed Solutions to read and write quickly to that external data.
Sandboxed Solutions do support iframes, so you can add a literal control to your nonvisual web part and make the text the iframe that you want to display in the control. This enables you to connect to many solutions on the Internet, such as Silverlight or web pages that expose information that you want to display in your environment. Using Sandboxed Solutions for this, rather than content editor web parts, makes the control reusable and easier to distribute.
VS 2010 supports Sandboxed Solutions. When you create a new SharePoint project, for project types that support Sandboxed Solutions, VS gives you the option to create and deploy your solution as a Sandbox Solution. In addition, VS limits the API set in IntelliSense to only those APIs that work for Sandboxed Solutions. VS does not do a compile-time check to determine if you are using restricted APIs because you program against the full SharePoint namespace, and it’s only at run time that your code is limited. So, if you ignore IntelliSense and write to APIs not supported in the sandbox, you don’t get a compile-time error but instead get a run-time error. One tip to avoid this is to reference the Microsoft.SharePoint.dll under the Assemblies folder, under the UserCode folder in the SharePoint hive. That limits the APIs you can use. You must remember to change the reference back to the full Microsoft.SharePoint.dll before deployment.
One of the deficiencies of Visual Studio 2010 OOB is that it doesn’t enable you to use the visual design capabilities to create web parts in the sandbox. Instead, you must hand code all the components of your user interface. The Visual Studio 2010 SharePoint Power Tools add this capability and provide pre-compilation support so that you can view any errors for types or members not allowed in a SharePoint sandbox environment. Figure 5-32 shows how to create a sandboxed visual web part in Visual Studio.
Another option for SharePoint Online development is building declarative workflows in SharePoint Designer. SharePoint Designer enables you to build workflows that you can associate with a list, a site, or a content type. In addition, you can have globally-reusable workflows that allow you to share the workflow to many lists or content types.
Beyond using SharePoint Designer to build your workflow, you can also use Visio to prototype your workflow. Then import that prototype into SharePoint Designer to add your business logic to implement the workflow. Because SharePoint Online supports Visio Services, the Visio representation of the workflow is used on the status page to visualize where the workflow is in process. Figure 5-33 shows prototyping a workflow in Visio and Figure 5-34 shows the Visio visualization status page.
Using the SharePoint Designer workflow designer is straightforward. SharePoint Designer provides a graphical user interface designer to build your workflow. This designer enables you to create your steps in your workflow, set the conditions and actions for the workflow, and pass parameters and initialization variables to your workflow.
The SharePoint Designer workflow designer also enables you to have parallel blocks and embedded steps in your workflow. The only deficiency you will find with the workflow designer is that you cannot have looping inside of your workflow.
In addition, the SharePoint Designer workflow designer enables you to use InfoPath to customize your workflow forms. This means you can customize your input parameters and the forms that appear for your workflow. Figure 5-35 shows the SPD workflow designer.
Although the SharePoint Designer workflow designer comes with a large set of workflow actions OOB, such as sending e-mails, checking in and out documents, setting permissions, looking up users, and other actions, you may still want to extend the designer with your own custom workflow activities that you implement as a Sandbox Solution. Your workflow activities are limited to what Sandboxed Solutions can do, so you cannot access networking, disk, or any other highly elevated tasks.
When creating sandbox workflow actions, you must create a class with a method that accepts a SPUserCodeWorkflowContext as the first parameter. This method must return back a Hashtable object. This hashtable contains the return parameters that you want the workflow to receive from your action. One parameter should be success or failure, and the other parameters can be customized to your business scenario. If you add custom parameters, you need to define these in the WorkflowActions element of your feature’s elements.xml file. The following XML shows how to pass in the workflow context and return back a success or failure string:
<Parameters> <Parameter Name="__Context" Type="Microsoft.SharePoint.WorkflowActions.WorkflowContext, Microsoft.SharePoint.WorkflowActions" Direction="In" DesignerType="Hide"/> <Parameter Name="Result" Type="System.String, mscorlib" Direction="Out" DesignerType="ParameterNames" Description="Success or failure result"/> </Parameters>
SharePoint Online supports InfoPath forms so that you can design the forms that run either in the InfoPath client, in the web browser, or in both. You can use InfoPath forms in SharePoint Online as a standalone form or in a web part using the InfoPath Forms web part. A key change from previous versions of SharePoint is that you can customize the OOB list forms directly in InfoPath.
InfoPath does include a compatibility checker, so you can check your InfoPath forms before you convert them to web-based forms. This checks to make sure that you do not use any capabilities that do not translate to the web-based version of InfoPath.
To give you a taste of what InfoPath can do, Figure 5-36 shows an InfoPath form running in SharePoint Online and Figure 5-37 shows an InfoPath form running as a web part in SharePoint Online.
Another option for application development is using Access and Access Services. If you are already a proficient Access database developer, you can save your Access applications to SharePoint and, similar to InfoPath, have a web-based version of your Access application. Access includes a compatibility checker to see which pieces of your application can convert to Access Services, and which pieces need to continue to run inside of the Access client. Most times, the issues you run into are VBA macros and reporting because Access Services does not support VBA, and SharePoint Online has not enabled Access Services reporting. Figure 5-38 shows a sample Access Services application running in SharePoint Online.
One of the newest features to Office 365, and one that was added after the initial launch of the services, was the ability to connect BCS to external datasources using WCF endpoints. These endpoints can exist in Windows Azure or other locations. The endpoints do not need to connect solely to databases, but can also connect to other types of data such as Web Services and REST datasources. This section shows you the steps necessary to leverage this capability in SharePoint Online.
The first step is to create your WCF service for BCS to connect to. Your endpoint needs to implement methods for SharePoint to call in order to, at a minimum, to read from your datasource, but you can also provide capabilities to write to your datasource.
To create the WCF service, follow these steps:
using System; using System.Collections.Generic; using System.Linq; using System.Web; namespace WCFServiceWebRole1 { public class SharePointBCSRecord { public string CustomerID { get; set; } public string CompanyName { get; set; } public string ContactName { get; set; } public string City { get; set; } } }
using System; using System.Collections.Generic; using System.Linq; using System.Runtime.Serialization; using System.ServiceModel; using System.ServiceModel.Web; using System.Text; namespace WCFServiceWebRole1 { public class Service1 : IService1 { public SharePointBCSRecord[] GetCustomers() { using (CustomerEntities customerRecords = new CustomerEntities()) { var customerRecordItems = (from c in customerRecords.Customers select c); SharePointBCSRecord[] customerArray = new SharePointBCSRecord[customerRecordItems.Count()]; int i = 0; foreach (Customer item in customerRecordItems) { customerArray[i] = new SharePointBCSRecord(); customerArray[i].CustomerID = item.CustomerID; customerArray[i].CompanyName = item.CompanyName; customerArray[i].City = item.City; customerArray[i].ContactName = item.ContactName; i++; } return customerArray; } } public SharePointBCSRecord GetCustomer(string customerID) { using (CustomerEntities customerRecords = new CustomerEntities()) { var customerRecord = (from c in customerRecords.Customers where c.CustomerID == customerID select c).FirstOrDefault(); SharePointBCSRecord returnCustomerRecord = new SharePointBCSRecord(); returnCustomerRecord.CustomerID = customerRecord.CustomerID; returnCustomerRecord.ContactName = customerRecord.ContactName; returnCustomerRecord.CompanyName = customerRecord.CompanyName; returnCustomerRecord.City = customerRecord.City; return returnCustomerRecord; } } } }
using System; using System.Collections.Generic; using System.Linq; using System.Runtime.Serialization; using System.ServiceModel; using System.ServiceModel.Web; using System.Text; namespace WCFServiceWebRole1 { [ServiceContract] public interface IService1 { [OperationContract] SharePointBCSRecord[] GetCustomers(); [OperationContract] SharePointBCSRecord GetCustomer(string customerID); } }
In this chapter, you learned how to build solutions with SharePoint Designer, Visual Studio, and SharePoint Online. SharePoint Online provides a robust development platform with a few limitations because you run in a multitenant environment. However, the possibilities of what you can build on SharePoint Online far outweigh the limitations that you will run into.