An overview of the SharePoint 2010 platform
Programming using the new Ribbon user interface
Working with new functionality: Event Receivers
How to use the new object models, such as LINQ and the client object model
Programming using the new Sandbox Solutions architecture
The SharePoint 2010 platform is a large platform. If you think about all the capabilities that SharePoint provides, you can see why it needs to be big. SharePoint provides a user interface, data query and modeling, data storage, and application services as part of the product. For the developer, this provides a sea of riches, but navigating the technologies and establishing when to use one versus the other can be difficult. This chapter presents an overview of these technologies so that you can understand which API or service makes sense for the problem you are trying to solve.
One of the best overviews of the developer ecosystem of SharePoint comes from Microsoft itself. Figure 4-1 shows a diagram from a Microsoft poster that covers the possibilities of the SharePoint platform, the surrounding tools and ecosystem, and the target applications.
The SharePoint surface area is huge, since you not only have the SharePoint set of platform services to build on, but also the Office client integration functionality and APIs, ASP.NET, and web technology to learn, as well as the .NET Framework and its set of technologies. For this reason, being a great web or .NET developer is a good first step to becoming a great SharePoint developer, since both of those technologies are foundational technologies for SharePoint. One thing to note is that SharePoint does not support .NET 4.0, so while you can have it installed on your SharePoint Server, SharePoint or your SharePoint applications cannot take advantage of it.
The first new platform area to explore is the user interface. SharePoint has moved to using the Office Ribbon user interface as part of its web experience. You can turn off the Ribbon by changing the Master Page in your SharePoint environment if you want your own user interface, or want to use SharePoint on the Internet where a Ribbon might not be appropriate. Beyond the Ribbon, SharePoint also implements a new AJAX-style user interface so that multi-select list views, using no refreshes, and other streamlined user interface operations are possible without refreshing the page. In addition, your UI changes now can be applied not only to your application pages but also to the administration pages under the _layouts
folder. There is a new dialog framework as well to make the SharePoint dialogs modal in the web experience. Plus, a new theming infrastructure makes it easy for end users or developers to customize the theme of the site. Lastly, the user interface implements a new status bar and notification area so that end users know what operations SharePoint or your application is performing on your behalf. Let's step through each of these areas and examine the changes.
In terms of platform improvements, one of the biggest areas of investment in SharePoint 2010 was refactoring both Cascading Style Sheets (CSS) and JavaScript used in SharePoint. If you look at the CSS for 2007, it is a large CSS file that is hard to decipher and also is a big payload for the browser to download. With 2010, the CSS has been split into multiple files and supports on-demand downloading so that the CSS file will be downloaded and parsed only when the CSS class is needed for the particular HTML rendering.
Same thing goes for the JavaScript (JS) files in SharePoint. Before, you would have to download a large JavaScript file for your SharePoint site, but with 2010 the JavaScript files have been split apart and also support on-demand downloading. Plus, they have been minified, which means that all the spaces have been removed from the file. Minified files are harder to read for developers, which is why SharePoint ships a debug version of the JavaScript files. SharePoint initializes a ScriptManager control from ASP.NET AJAX to allow the new SharePoint UI to take advantage of the AJAX features in .NET.
Now, you may be wondering how you debug a minified version of the JS files. Navigate to %ProgramFiles%Common FilesMicrosoft Sharedweb server extensions14TEMPLATELAYOUTS
, and you will find the debug versions of the JavaScript files in the same directory as the minified ones. The debug versions will have debug in their name — for example, sp.debug.js
or sp.core.debug.js
. You can browse through these JavaScript files to see the source code. In addition, you can force SharePoint to use these JavaScript files rather than the minified versions by modifying your web.config
, located at %inetpub%wwwrootwssVirtualDirectories80
and adding <deployment retail="false"/>
, to the system.web
section.
Beyond the size changes, the JavaScript has been refactored. A big change is the naming of the JavaScript code to try to delineate between public JavaScript and internal-use-only JavaScript. So, if you see a function called SP.UI.UtilityInternal.CreateButton
, that is not supposed to be used by your code.
One of the biggest pain points in SharePoint 2007 was that you could skin the application pages in your site with your own Master Page, but the administration user interface and any pages under _Layouts would not use the applied Master Page. Rather, they would use the standard SharePoint Master Page affectionately known as "blue and white." With 2010, you can now set whether the pages under _Layouts use the same Master Page as the rest of your site. This will reduce the confusion for your end users when they go from the site to the administration pages for your site and the pages look different, as in 2007. Figure 4-2 shows the setting you need to check in order to enable or disable this functionality through the web application settings in Central Administration. It is on by default and that is probably the way you want to keep it, unless you want your application pages to look different than your site.
The _Layouts pages will use a dynamic Master Page based on the site they are being accessed from. You can use the tokens ~masterurl/default.master
or ~masterurl/custom.master
to reference System or Site Master Pages that you want to use for your pages. If you want to create your own application page, the page must derive from Microsoft.SharePoint.WebControls.LayoutsPageBase
.
For security reasons, there are seven pages that do not reference your custom Master Page unless you explicitly change this through PowerShell or the SharePoint API. The seven pages are accessdenied.aspx
, confirmation.aspx
, error.aspx
, login.aspx
, reqacc.aspx
, signout.aspx
, and webdeleted.aspx
.
To change the Master Page using code, you will want to use the CustomMasterUrl
and MasterUrl
properties on your SPWeb and set those to the Master Page that you want to put in place of the current Master Page. Also, remember to change any CSS that you may need to in order to support your Master Page by using the AlernateCSSUrl
property.
using (SPSite siteCollection = new SPSite("http://intranet.contoso.com")) { using (SPWeb web = siteCollection.RootWeb) { MessageBox.Show(web.CustomMasterUrl.ToString()); web.MasterUrl = "/_catalogs/masterpage/minimal.master"; web.CustomMasterUrl = "/_catalogs/masterpage/minimal.master";
web.Update(); } }
If SharePoint cannot find your referenced Master Page for any of your pages, it will default to the standard SharePoint Master Page so that the site does not break.
There are a few new Master Pages that you should be aware of with SharePoint 2010. Some were added to support the new user interface and some are added to support the older 2007 user interface running in SharePoint 2010. The first is V4.Master
. This Master Page is the default Master Page for the 2010 user interface. It supports the Ribbon and all the new visuals in the 2010 product. One other reason to use V4.Master
is that it implements the ability to display both the full chrome and also no chrome, depending on whether your page is displayed in context in the site with navigation and a Ribbon or as a dialog using the new dialog framework without chrome and without a Ribbon. There is nothing you have to do in order to get this functionality if you use V4.Master
, since SharePoint automatically uses the right CSS classes on your behalf, based on the context your page is being called in. This allows you to use the same page for an application page as well as a dialog page, which is great for reusability.
The next is default.master
. This Master Page is used to support the 2007 user interface and is used by visual upgrade to make your 2010 sites look like 2007 sites. This will make your site appear without a Ribbon and perform like a 2007 site.
One welcome addition is minimal.master
. Every developer either downloads or builds their own minimal.master
so that they can start with a simple Master Page and then build on top of it. Now, minimal.master
ships in the SharePoint box, and you do not have to build it yourself. It is a very stripped Master Page that includes no navigation, so you will want to start adding pieces to the Master Page if you intend to use it in your site.
The last Master Page is simple.master
. It is used on the seven pages that we talked about earlier, and it cannot be customized.
One of the major changes you will have to get used to 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 that are 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. It is used, for example, when you click on styles in Word and you can see all the styles, or in Excel, where you can select cell styles right from the gallery control.
The Ribbon does support the majority of controls that you will 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 in order not to confuse your users. Table 4-1 lists the different controls supported by SharePoint with a description of each.
Table 4.1. SharePoint Ribbon Controls
If you look at the architecture for the Ribbon in SharePoint, you will find that SharePoint makes a lot of usage of AJAX, on-demand JavaScript, caching and CSS layout to implement the Ribbon. One thing you will find is that the Ribbon uses no tables, so it is all 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 out-of-the-box (OOB) controls on existing tabs. In fact, you can entirely replace the Ribbon just by using your own custom Master Page. The Ribbon does support backward compatibility in that any custom actions you created for 2007 toolbars will appear in a custom commands tab in the Ribbon.
To understand how to customize the Ribbon, look through the different actions you normally would 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 allows you to perform your customizations by creating XML definition files. At runtime, the Ribbon runtime 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, you will want to look at using a JavaScript Page Component. The section below 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 SharePoint Server and find the file cmdui.xml
. In that file, you will see the SharePoint default Ribbon implementation, and it is a good template to look at as you implement your own Ribbon controls, because it will help you to understand how certain controls work inside of the SharePoint environment.
If all the different types, elements, and attributes get confusing, take a look at the XSD for the Ribbon by browsing to %Program Files%Common FilesMicrosoft SharedWeb Server Extensions14TEMPLATEXML
and looking at cui.xsd
and wss.xsd
. These XSD files will help you understand what SharePoint is expecting in terms of structure and content to make your custom user interface.
The first way you will look at customizing the Ribbon is by using only XML. 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 is used to display the new Ribbon interface. Even though you are using XML, you want to deploy your custom Ribbon XML using a SharePoint feature. So, the easiest way to get started creating a feature is by using Visual Studio 2010. Make sure to create an Empty SharePoint Project and customize the feature name and deployment path. Ribbon extensions can be Sandbox Solutions, which you will learn about later, so they can run in a restricted environment.
Once you have created your Visual Studio project, you want to create a new feature. Add a new file to your project and create an empty elements file. This is where you will place the XML for your new Ribbon interface. To understand the XML, break it down piece by piece. 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>
First, all of your 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, there are attributes that you can set to specify the specifics for your customization. One key one is the Location
attribute. The Location
attribute tells SharePoint where your customization should appear, such as on a list view, on a form, or everywhere. The pattern match for the Location
attribute is Ribbon.[Tab].[Group].Controls._children
. Table 4-2 outlines the options for the Location
attribute.
Table 4.2. Location Attribute Settings
NAME | DESCRIPTION |
---|---|
| Customization appears everywhere. |
| Customization appears when |
| Customization appears on the edit form. |
| Customization appears on the new form. |
| Customization appears on the display form. |
One other piece to notice in the XML is the RegistrationID
. Combined together, the registration ID and the registration type define what 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 allows you to customize even further when your custom UI will appear, depending on what is displayed in SharePoint.
The Sequence
attribute, which is optional, allows 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, and groups use a sequence in multiples of 10, so avoid multiples of 10. For example, if there is a Ribbon tab with the following groups: Clipboard, Font, Paragraph, then their sequence attributes could be set to 10, 20, and 30, respectively. Then, a new group could be inserted 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.
Let's expand the XML a bit more, since the current XML does nothing because you haven't added any new commands to the user interface. To add commands, you want to 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 allows you to set the location for your UI. In the example, you're adding 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 to not 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!, has two images for use depending on whether it's rendered in 16x16 or 32x32, and calls some JavaScript code to perform the action when the button is pressed. The attributes are self-explanatory, except for 1 - TemplateAlias
. TemplateAlias
controls whether your control is displayed in 16x16 or 32x32. If you set it to o1
, you will get a 32x32 icon, and o2
makes it 16x16. You can define your own template, but most times you will use the built-in values of o1
or o2
.
So, how do you call code from your Ribbon code? You will want to wrap your code in a CommandUIHandler
, where you can put in the CommandAction
attribute, which is inline JavaScript that will handle the action for your button. If you do not want to place your JavaScript inline, you can instead use the ScriptSrc
attribute and pass a URL to your JavaScript file. Figure 4-3 shows our new custom button.
<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>
There may be times when you want to replace existing, built-in controls from your SharePoint deployment and put your own control in its place. In fact, 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 thing is you have to get the Location
attribute set to the exact 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. First, notice the Location
attribute in the CommandUIDefinition
element. It maps exactly to an ID in the cmdUI.XML
file. SharePoint will parse both files and if the same ID is found, the one with the lower sequence will be put into the final XML that is parsed and used to create the Ribbon layout. Also, notice the use of the $Resources
for globalization and pulling from a compressed image. If you look at the formatmap on your server, you will see that it contains lots of icons and the XML code contains the coordinates to pull the new folder icon from the larger image.
<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>
Figure 4-4 shows our replaced button. Even though the icon image is the same, the action performed when the user clicks on the icon is our 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 will 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>
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.
First, fire up your JavaScript debugger and set a breakpoint. Since the Ribbon is implemented in JavaScript, you can set breakpoints in the code in SP.Ribbon.debug.js
. Also, make sure to look at the XML in cmdui.xml
to see if there is a pattern your code resembles so you can model your code on that pattern.
The second thing to check is that the sequence is set correctly and does not collide with other controls. SharePoint uses sequences in multiples of 10 or 100, so make sure that you are not using those multiples.
Make sure to check that the name for your function is the same for your command attribute on your control definition and your CommandUIHandler
. If you get the name wrong, even with the same spelling but different cases, your commands will not fire.
Check the registration for your CustomAction
. Did you register your UI on a document library? When you test your code, are you in a document library? Or did you register on an edit form for announcements? This ties in with the next tip, which applies when you are wondering why your user interface does not appear if you select your list instance as a web part in another page. For example, suppose that you are on your home page and you added in your Shared Documents library as a web part on that page. When you select the document library as the web part, your button does not appear on the menu. The culprit behind this is the toolbar type property for the web part under web part properties. By default, it is set to summary toolbar and you want it to be set to full toolbar, since the summary toolbar will not load any of the customizations for the toolbar.
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 permission from SPBasePermissions
, such as ViewListItems
or ManageLists
.
Beyond permissions, you can also specify whether a person has to 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.
There may be times when 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, 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, as you will see. Figure 4-5 shows a custom tab and group with three controls: two buttons and a combobox.
The code below shows the beginning of the new tab and group. As you can see, the XML looks very similar to earlier XML in creating a button. There is a tab defined that you will learn more about.
<!--Create new Tab and Group--> <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> ...
First, tabs support scaling, so if the page is resized, you can control how your buttons look. Scaling has a MaxSize
node that is the maximum size your buttons will be and a Scaling
node that will be used if the page is resized. A couple of things about the Scaling
node. It has a GroupID
attribute, which should point to the group that the scaling will affect. Second, it has a Size
attribute, which has a descriptor of the style of your group. For example, you can have LargeLarge
if you have two buttons and want both to be large buttons, or LargeMedium
if you want a large and a medium button.
After creating the tab, the code then creates the group, as you can see below. A group can have commands, descriptions, and all the standard attributes that other controls have. A group is a logical container for your controls and will physically lay out the controls in your group with your description at the bottom of the group user interface. Your Group
node will contain the definition for your controls that live within that group.
<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> ...
Once you have your tab and group, you create your controls just as you would if the control were an extension of an existing group. The code earlier showed how to create a button, so the code below shows you how to create a combobox as a control in your group.
A combobox has more commands than a button, since users can interact more with a combobox by selecting options from its list. Also, either you can populate a combobox box statically, as the code does, by creating menu options in the XML, or you can pass a function that SharePoint will call to populate the combobox dynamically. Look at the PopulateDynamically
, PopulateOnlyOnce
, and PopulateQueryCommand
sections of the code, since these combined operate the combobox options.
In addition, you can set attributes, such as AllowFreeForm
and InitialItem
, to control whether users can type values into the combobox and select the initial item in the combobox.
<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>
After your controls, you can handle the commands that your controls need to respond to in your CommandUIHandlers
node. For the complete listing for all the code for the commands, the tab, and the group, please refer to the sample code for this chapter.
With your user interface, you should help guide the user on the usage of your controls. To aid in this, the Ribbon supports ToolTips and also linking out 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, and 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 seen writing code inline in your XML in order to handle your control commands. However, SharePoint does allow you to write more complex handlers for your user interface if you need to. You should default to trying to keep your code in the XML definition if you are creating simple buttons with simple code. However, if you are creating Ribbon extensions that are dynamically populated via code; your Ribbon requires variables beyond the default ones you can get with {SiteUrl}
, {ItemId}
, or other similar placeholders; or your code is so long that it may make sense from a manageability standpoint to break it out separately, then you will want to look at creating a page component.
A page component is a set of JavaScript code that can handle commands from your user interface customizations. Your JavaScript has to 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 will handle in your page component. Additionally, you can define whether your global commands should be enabled or not through the canHandleCommand
function. This is the function you want to use to enable or disable your control. For example, you may want to only enable your control if the context is correct for your control to work, such as an item being selected in the user interface or the right variables are set. If you return false to this function, your user interface will be grayed out.
Lastly, the page component allows you to handle the command so if someone clicks on your button, you can run code to handle that click.
Once you have defined 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.
A couple of points about the following code. First, notice how to get the selected items by using the SP.ListOperation.Selection.getSelectedItems()
method. This is a good way to determine if any items are selected in the user interface so that you can enable or disable your control. You can go a step further and look for particular properties or item types by writing some more code.
Second, you could write more functions to do things like populate your drop-downs dynamically or change the buttons on your user interface. In fact, you can write your Ribbon component to perform a postback to the server that a custom .NET program can handle, so that you can avoid writing JavaScript. If you do this, you will want the command action be a postback command such as CommandAction="javascript:__doPostBack('CustomButton', '{ItemUrl}')"
. Then, on the backend that captures the postback, you can handle the postback in two different ways. First, you can look at the __EVENTTARGET
variable in the page request variables to see if your custom command caused the postback. The other way is to spin up a Ribbon object — make sure to reference Microsoft.SharePoint.WebControls
— and implement the IPostBackHandler
interface. Then, you can check to see if your custom button generated the postback by deserializing the postback event using the SPRibbonPostBackCommand.DeserializePostBackEvent
method, and then checking the ID of the control that generated the event to the ID of the control you were looking for. If they match, handle the event. The first method is simpler and requires less code than the second method.
Figure 4-6 shows the custom button on the Ribbon. Also, notice that there are a custom color picker and other buttons in the figure. 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>
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 4-7 shows the form used to tell SPD how to customize the Ribbon for your list.
There may be times when you 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 where 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.
In order 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; } }
Then, you need to implement a custom page component using JavaScript. This is very similar to the code from earlier in the chapter where you add and register the custom page component. You will notice that the code uses the executeOrDelayUntilScriptLoaded
command, which is part of the SharePoint infrastructure to only load and run 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>"; } }
Contextual tabs actually always exist in 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 here.
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; }
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 that does this, and 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, and also when to focus on the contextual tab and when to yield focus depending on whether the web part is selected or not.
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");
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 appears below the Ribbon tab and to the right and is transient in nature. Both should be used to give the user contextual information without being distracting. For example, if you are editing a page and have not checked it in or published it, the status bar will tell you that only you can see the page. The status bar is for more permanent information, while the notification area is similar to instant message popups or Windows system tray notifications, in that 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 both client- or 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 different preset colors, depending on the importance of the message. To work with the status bar, you want to use the SP.UI.Status
class. It is a pretty simple client-side API, since it contains only five methods, and you can find their definitions in SP.debug.js
. On the server side, you will want to use the SPPageStatusSetter
class, which is part of the Microsoft.SharePoint.WebControls
namespace. That API is even simpler in that you call one method — AddStatus
. You can also use the SPPageStateControl
class to work with the status bar on the server side. Table 4-3 outlines the five methods for the client side.
Table 4.3. SP.UI.Status
NAME | DESCRIPTION |
---|---|
| Method that allows 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. |
| Method that appends status to an existing status. You need to pass the status ID, title, and HTML you want the new status appended to. |
| Updates an existing status message. You need to pass the status ID and the HTML payload for the new status message. |
| Allows you to set the priority color to give a user a visual indication of the status messages' meaning, 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. |
| Removes the status specified by the status ID you pass to this method from the status bar. |
| Removes all status messages from the status bar. You can pass a Boolean that specifies whether to hide the bar or not. Most times, you will 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. Once you do this, you will see what appears in Figure 4-8.
<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/>
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 was a need to give users feedback that their pages and actions were completed. The notification area does this by telling the user that the page is loading or that a save was successful, which used to be indicated by a postback and page refresh.
The notification API is limited in that you can just create and remove notifications. Table 4-4 describes the methods for SP.UI.Notify
that work with notifications, with sample code below the table.
Table 4.4. SP.UI.Notify
NAME | DESCRIPTION |
---|---|
| Method that allows you to pass your HTML payload, whether the notification is sticky or not (which when set to |
| 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"/>
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 4-9 shows a custom dialog in SharePoint 2010.
The implementation of the dialog is that your contents are loaded in an iframe in a floating div. The dialog is modal, so the user can't get to other parts of SharePoint from the dialog. 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 will see the implementation for the dialog framework. The framework has a JavaScript API that you can program against to have SharePoint launch and load your own dialogs. The way you do this is 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 4-5.
Table 4.5. Parameters for the SP.UI.showModalDialog method
NAME | DESCRIPTION |
---|---|
| The width of the dialog box as an integer. If you don't specify a width, SharePoint will autosize the dialog. |
| The height of the dialog box as an integer. If you don't specify a height, SharePoint will autosize the dialog. |
| Boolean that specifies whether to have SharePoint autosize the dialog. |
| x coordinate for your dialog. |
| y coordinate for your dialog |
| Boolean that specifies whether to allow the Maximize button in your dialog. |
| Boolean that specifies whether to show your dialog maximized by default. |
| Boolean to specify whether to show the Close button in the toolbar for the dialog. |
| URL for SharePoint to load as the contents for your dialog. |
| A |
| Title of your dialog. |
| The function SharePoint will call back to when the dialog is closed. You create a delegate to this function for this option with the |
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. First, if you are going to use URLs, take a look at the SP.Utilities.Utility
namespace. This namespace has a number of utilities to help you find the right places from which to grab your URLs no matter where your code is running. One utility you will see used in the code is SP.Utilities.Utility.getLayoutsPageUrl('customdialog.htm'
), which gets the URL to the _layouts folder so that the custom dialog HTML file can be retrieved.
Another tip is that dialogs support the Source=url querystring
variable like the rest of SharePoint. So, if you want to have SharePoint redirect to another page, you can specify the source along the query string and SharePoint will respect that.
Looking at the following code, you will see the function OpenDialog
. 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 use 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 will be 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.
If you look at the CloseCallback
function, you will see that it gets the result and any return value. The result will be 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'), } }
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 the code for the dialog loaded by SharePoint. A couple of things to note in the code: First, there are two buttons for OK and Cancel, respectively. If you look at the onclick
event handlers for the button, you will notice that they use methods from the window.frameElement
object. By using this object, you can get methods from the dialog framework. As you can see, commitPopup
will return OK, and cancelPopUp
will return Cancel as the result of your dialog. Table 4-6 shows the methods you want to use from the frameElement
.
<p> <img src="/_layouts/1033/images/DefaultPageLayout.gif" 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" />
One of the advancements in 2010 is a new theming infrastructure. With 2007, if you wanted to change the user interface, you had to do a mixture of changes from Master Pages to CSS to trying to hack inline styles contained in the product. With 2010, this is all simplified, since all styles are moved out into CSS files and certain styles are replaceable using the new theming infrastructure. Plus, rather than creating themes by hand, you can create themes using Office applications, such as PowerPoint, which makes it easy for end users to create new themes to apply to their sites.
Much of the SharePoint user interface supports theming. Supported elements include:
Ribbon
Site title, icon, and description
Secondary title, description, and view name
List item selection and bulk editing highlighting
ECB menu
Quick Launch
Tree control
Top navigation bar
Site Actions menu
Welcome menu
Breadcrumb control
Layout pages (Site Settings Menu)
Popup dialogs
Error messages/pages
Web part chrome/Tool pane
RTE Editor
Search control
Forms
In order to support theming, SharePoint processes the CSS and supporting images that you create. For example, SharePoint can add effects to your images, such as a gradient and rounded corners, if you provide the special theming markup to your elements. This special markup to support theming needs to be placed in your CSS file, and your CSS file has to be placed in a themable location, which is the content database or, more frequently for custom solutions, in the %Program Files%Common FilesMicrosoft SharedWeb Server Extensions14TEMPLATELAYOUTS1033STYLESThemable
folder.
The CSS processor works by looking for particular markups using CSS comments and then performing the actions specified by that markup to replace the CSS style with whatever theme is applied to the site. For example, suppose that you had a CSS declaration such as:
.major-font { /* [ReplaceFont(themeFont: "MajorFont")] */ font-family: Verdana, MS Sans Serif, Sans-Serif; } .minor-font { /* [ReplaceFont(themeFont: "MinorFont")] */ font-family: cursive; } .bg-image1-lt1dk1
{ /* [RecolorImage(lightThemeColor: "Light1", darkThemeColor: "Dark1")] */ background-image: url("../images/bl_Navbar_Gd_Default.jpg"); } .class { /*[ReplaceColor(BackgroundColor1)]*/ Color:#FFFFFF; }
Notice the markup before each of the CSS declarations. Because of these markups, SharePoint would replace the fonts, recolor the image, and change background color if a new theme was selected that used different elements than the ones specified.
To support theming, SharePoint has enhanced the site theme user interface so that you can preview your changes before you actually make them. Figure 4-10 shows the new site theme administration interface.
The three commands that you can perform are ReplaceColor
, ReplaceFont
, and RecolorImage
. Each of these commands has parameters you can specify to customize the command. Table 4-7 describes these commands.
Table 4.7. Theme Commands
NAME | DESCRIPTION |
---|---|
| Replaces the color of the CSS rule with the specified color. You can specify advanced parameters, such as making colors lighter or darker by a certain percentage. For example, |
| Replaces the |
| Recolors the image. This only works for "Light1", darkThemeColor: "Dark1")] */ background- image: url("../images/bl_Navbar_Gd_Default.jpg"); If you want to recolor the image by blending, filling, or tinting it, you can use the optional method parameter, such as: /* [RecolorImage(themeColor:"Light2",method:"Filling" )] */ background:url("/_layouts/images/qlbgfade.png") repeat-x left top; Or /* [RecolorImage(themeColor:"Light2",method:"Tinting")] */ background-image:url("/_layouts/images/bgximg.png"); |
Because a lot of elements support themes, including web parts, you will want to make sure that when you design SharePoint applications, you keep theming in mind. This means that you should move away from using CSS inline styles, since these will not be themable by the engine. Instead, if you use CSS files and appropriately mark up those CSS styles using the theme attributes, then your custom applications will be themable using the built-in SharePoint infrastructure. The extra work of marking up your CSS is worth it for the time savings of not having to write your own theming interface for your applications, plus you will get better user interface integration by not having your application ignore the theme when it changes in the SharePoint product.
To work with themes, there is a new class in the Microsoft.SharePoint.Utilities
namespace, called ThmxTheme
, that provides methods and properties that make programming with themes easier. This class allows you to create new themes and query existing themes in the system. Table 4-8 outlines the important methods and properties of the ThmxTheme
class with supporting sample code below the table to show you how to use this class.
Table 4.8. ThmxTheme Class
NAME | DESCRIPTION |
---|---|
| Forces the styles for the theme to be applied to the SPWeb specified. |
| Returns a collection of themes as |
| Gets the Theme URL for the specified SPWeb object. |
| Sets the theme URL for the SPWeb specified to the string in the second parameter. |
| Opens the theme either using a stream or using an |
| Saves the theme back to the stream. |
| Property that specifies the accent color for the theme. You set it by setting the Please note that there are many other similar properties such as |
using (SPSite site = new SPSite("http://intranet.contoso.com")) { //Get all the themes for the site foreach (ThmxTheme theme in ThmxTheme.GetManagedThemes(site)) { //Get Azure hyperlink color if (theme.Name == "Azure") { MessageBox.Show(theme.HyperlinkColor.DefaultColor.Name + " " + theme.HyperlinkColor.DefaultColor.ToString()); } }
//Upload a new theme from the file system FileStream fs = File.Open( "c:\users\administrator\desktop\test.thmx",FileMode.Open); ThmxTheme newtheme = ThmxTheme.Open( fs,FileMode.Open, FileAccess.ReadWrite); newtheme.Name = "My Test Theme"; newtheme.HyperlinkColor.DefaultColor = Color.Black; newtheme.Save(); }
There are a number of new list, view, and event enhancements in SharePoint 2010. 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, there are new events that you can take advantage of with SharePoint 2010 — for example, when new sites and lists are added. Let's 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, since SharePoint uses lists itself for its own functionality and ability to run. With 2010, there are new list enhancements and even new tools that you can take advantage of to work with your custom lists.
One enhancement, support for large lists and list throttling, already has been discussed in Chapter 3, so refer to that chapter to understand that enhancement.
Before diving into the new enhancements in lists, you need to first look at the tools used to create your lists. The tools of choice are SharePoint Designer (SPD) and Visual Studio (VS). Both are good choices, depending on what you are trying to do. If you want barebones, down to the metal, XML-style creation of lists, then Visual Studio will be your choice. 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 would be a better choice if you are interested in a GUI editor.
Diving into SPD, SPD makes it easy to work with your lists, whether it's creating new lists or modifying your existing lists. SPD can make quick work of your columns, views, forms, content types, workflows, and even custom actions for your list. 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 will have to give up some control, since SPD does not allow 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 4-11 shows the List Settings user interface for SPD.
With Visual Studio, you can create list definitions and list instances. List definitions are a built-in project type for Visual Studio.
One word of warning, don't expect nice designers when you create a list definition. Instead, get ready to work with some XML. The nice thing about the list definition project in Visual Studio is that it allows you to create a list instance at the same time. Plus, your application is deployed as a feature, so you can reuse the list definition and instance in many different sites. If you need the ultimate in flexibility, VS is your tool of choice for creating list definitions and customizing lists.
One common complaint about SharePoint is how it does not 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 now can 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 very complex data model, you will want to use SQL Server and surface SQL Server through SharePoint, using Business Connectivity Services (BCS) and external lists.
The way that list relationships work is you create a lookup between your lists. One new thing about lookups in a list is that you can retrieve more than just the identifier and can retrieve 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 that are related to the parent item, or cascade the delete from the parent list to the child list. Figure 4-12 shows the user interface for setting the properties of the lookup column to enforce relationship behaviors.
If you restrict the delete, SharePoint will throw an error telling the user that there is an item in the related list that exists and will cancel deleting the error, as shown in Figure 4-13.
If you cascade the delete, SharePoint will perform a transacted delete of the related items in the related list.
Please note that 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 will not support the referential integrity features such as cascading delete. Also, referential integrity will not be enforced for a lookup that you allow to have 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
.
If you look at the SPWebApplication
class, you will see two properties that affect relationships. The first property is CascadeDeleteMaximumItemLimit
, which allows you to specify as an integer the maximum number of cascaded items that SharePoint will delete. By default, this value is 1000 items. The other property is CascadeDeleteTimeoutMultiplier
, which allows you to specify as an integer the timeout, which is 120 seconds by default.
To find lookup fields, you can use the GetRelatedFields
method of your list, which returns a 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 is the ability to do list validation using formulas. This is more of an end user or power user feature, but for simple validation scenarios, developers will find this feature easy to use, and quick to write formulas rather than writing 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 4-14 shows setting the formula, and Figure 4-15 shows the custom error message that appears when the formula does not validate.
One of the easiest ways to understand what 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 will use the SPList.ValidationFormula
and SPField.ValidationFormula
properties to get and set your formulas.
Another new feature of lists is the ability to ensure uniqueness for the values in your columns. SharePoint would previously allow you to not 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, since 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 datasources 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 exist in the other list, will be returned in the result set.
The code below 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 are joining 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. Once you get back your results, you will have to 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 for lists is the ability to customize the default forms for your list items. SharePoint 2010 moves 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 2010, you can modify the New, Display, and Edit forms. For more on forms, take a read through Chapter 9.
When you use the Ribbon option to edit the form in InfoPath, InfoPath will automatically be launched, connect to your list, and display your form as shown in Figure 4-16.
The biggest change with views in 2010 is the change of the technology used to display views. 2010 uses the SharePoint Designer XsltListViewWebPart
as the default view web part for viewing lists. There are a number of reasons why this is much better than 2007. First, XSLT views allow you to replace your use of CAML to create views, and can move to using standards-based XSLT to define your view. Second, performance is better than 2007 with the new XSLT view. Third, editing with SPD is easier, since the XSLT view technology is an SPD technology. Lastly, the same view technology is used for all SharePoint lists, including standard SharePoint lists and external lists.
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 4-17 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, you will use the SPView
object and SPViewCollection
. You can add new views, modify existing views, or delete views. There are a few properties that you will be interested in. One is the DefaultView
off the SPList
object; this property returns an SPView
object, which is the default view for your list. From there, you can use the RenderAsHTML
method, which will return the HTML that your view will render. You can also use PropertiesXml
, Query
, SchemaXml
, and Xsl
, which return the properties, query, schema, and XSL used in your list, respectively.
With 2010, there are six new events that you can take advantage of, including WebAdding
, WebProvisioned
, ListAdding
, ListAdded
, ListDeleting
, and ListDeleted
. This is in addition to the existing events that were introduced in SharePoint 2007, such as the ItemAdding
, ItemUpdating
, and ItemUpdated
events. 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, custom error pages and redirection, and finally, impersonation enhancements.
As part of SharePoint 2010, there are six new events that you can take advantage of. These events allow you to capture creation and provisioning of new webs and the creation and deletion of lists. Table 4-9 goes through each of the events and what you can use them for.
Table 4.9. New 2010 Events
NAME | DESCRIPTION |
---|---|
| A synchronous event that happens before the web is added. Some URL properties may not exist yet for the new site, since the new site does not exist yet. |
| A synchronous or asynchronous after-event that occurs after the web is created. You make the event synchronous or asynchronous by using the |
| A synchronous event that happens before a list is created. |
| A synchronous or asynchronous after-event that happens after a list is created but before being it is presented to the user. |
| A synchronous event that happens before a list is deleted. |
| A synchronous or asynchronous after-event that happens after a list is deleted. |
Using these events is the same as writing event receivers for any other types 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 4-18 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. Once 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 to the event log the properties for the event. 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 using the Cancel
property and setting it to false. These events will fire even in the Recycle Bin, so if you restore a list or delete a list, you will 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); } //Log out to the event log string source = "WebEventCustomLog"; string logName = "Application"; if (!EventLog.SourceExists(source)) { EventLog.CreateEventSource(source, logName); } try { EventLog.WriteEntry(source, sb.ToString()); } catch (Exception e) { } } } }
To support the new events, SharePoint has added a new registration capability for registering your event receivers, using the <Receivers>
XML block. The new capability includes registering your event receiver at the site collection level by using the new Scope
attribute and setting it 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 will work 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 just have the receiver work on the root site by using the RootWebOnly
attribute on the <Receivers>
node. The last new enhancement is the ListUrl
attribute, which allows you to scope your receiver to a particular list by passing in the relative URL.
With 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 be not ideal. With 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.
With 2007, you can cancel events and return an error message to the user, but that provides limited interactivity and not much help to the user beyond what your error message says. 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 allows you to have more control of what the users see, and you can try to help them figure out why their action is failing. The custom error pages and redirection will only work for pre-synchronous events, so you cannot do this for post-synchronous events such as ListAdded
. Plus, this will only work with browser clients. Office will just put up 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
.
properties.Cancel = true; properties.Status = SPEventReceiverStatus.CancelWithRedirectUrl; properties.RedirectUrl = "/_layouts/mycustomerror.aspx";
The last area of enhancement for events is in the impersonation that events support. SharePoint runs your events in the context of the user who triggered the event. Generally, this is okay, but there may be certain times when you want to let a user perform actions on lists, libraries, or the system that the current user does not have permissions to do. In most cases, you would use SPSecurity
's RunwithElevatedPrivileges
method. However, you may want to revert to the originating user on some operations. With 2010, the event property bag contains the OriginatingUserToken
, UserDisplayName
, and UserLoginName
, which you can use to revert to the original user. The following code shows how, in a ListAdding
event, you can elevate some code to the system account and then revert some code to the original user by using these properties.
public override void ListAdding(SPListEventProperties properties) { LogListEventProperties(properties); StringBuilder sb = new StringBuilder(); SPSecurity.RunWithElevatedPrivileges(delegate() { using (SPSite site = new SPSite(properties.SiteId)) { // Running under the "system account" now // Perform operations sb.AppendLine("SiteURL: " + site.Url); sb.AppendLine("Impersonating: " + site.Impersonating.ToString()); //Get the web to get the current user using (SPWeb web = site.OpenWeb()) {
sb.AppendLine("Name and LoginName: " + web.CurrentUser.Name.ToString() + " " + web.CurrentUser.LoginName.ToString()); } LogImpersonation(sb.ToString()); } }); //Clear our stringbuilder sb.Length = 0; sb.Capacity = 0; //Now access the site with the original user token using (SPSite originalsite = new SPSite(properties.SiteId, properties.OriginatingUserToken)) { //Running under the original user account that generated the event //Perform operations sb.AppendLine("SiteURL: " + originalsite.Url); sb.AppendLine("Impersonating: " + originalsite.Impersonating.ToString()); //Get the web to get the current user using (SPWeb web = originalsite.OpenWeb()) { sb.AppendLine("Name and LoginName: " + web.CurrentUser.Name.ToString() + " " + web.CurrentUser.LoginName.ToString()); } LogImpersonation(sb.ToString()); } base.ListAdding(properties); } private void LogImpersonation(string valueToLog) { string source = "ListEventCustomLog"; string logName = "Application"; if (!EventLog.SourceExists(source)) { EventLog.CreateEventSource(source, logName); } try { EventLog.WriteEntry(source, valueToLog); } catch (Exception e) { } }
When it comes to SharePoint, working with, manipulating, and displaying data is one of the most 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. Since SharePoint can surface its data in so many different ways, whether that is through the browser, inside of Office, in applications running on the SharePoint server, or in applications running off the SharePoint server, there are a number of different 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 are writing your application to run on or off the SharePoint server. Your choices in 2010 are: LINQ, Server OM, Client OM, or REST. Of course, you can continue to use the web services APIs of SharePoint, but you will want to look at moving to the client OM rather than using that technology. Table 4-10 goes through the pros and cons of each data access technology.
Table 4.10. Data Access Technologies
PROS | CONS | |
---|---|---|
LINQ | Entity-based programming Strongly typed Supports joins and projections Good tools support and IntelliSense | Server-side only New API, so new skills required Pre-processing of list structure required, so changing list could break application |
Server OM | Familiar API Works with more than just list data | Server-side only Strongly typed |
Client OM | Works off the server Easier than web services API Works in Silverlight, JavaScript and .NET More than just list data | New API Weakly Typed |
REST | Standards-based URL-based commands Strongly typed | Only works with lists and Excel |
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 allows 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 being passed back to the server.
The first step in getting 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 will parse your lists and generate the necessary classes for you that you can import into your Visual Studio projects. You can find SPMetal at %ProgramFiles%Common FilesMicrosoft Sharedweb server extensions14 BIN
. 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 pre-build for your project so that the latest version of your entity classes are always included.
Using SPMetal is straightforward for the common scenarios that you will want to do. It does support XML customization, but most of the time you will find that you do not need to customize the default code generation. Table 4-11 shows the SPMetal command-line parameters that you can pass.
Table 4.11. SPMetal Command-Line Parameters
NAME | DESCRIPTION |
---|---|
| Absolute URL of the website you want SPMetal to generate entity classes for. |
| The relative or absolute path of the location where you want the outputted code to be placed. |
| The programming language you want generated. The value for this can be either |
| The namespace you want used for the generated code. If you do not specify this property, SPMetal will use the default namespace of your VS project. |
| SPMetal will use the client object model if you specify this parameter. |
| Allows you to specify |
| The password that SPMetal will use to log on as the user specified in the |
| Specifies whether you want your objects to be serializable or not. By default, this parameter is none, so they are not. If you specify unidirectional, then SPMetal will put in the appropriate markup to make the objects serializable. |
| 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 over 3000 lines long! You will definitely want to use SPMetal to generate this code and tweak SPMetal as you need to in order 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>
Once you have your generated SPMetal code imported into VS, it's time to make sure you have the right references set up to use that code. You will want to add two references at a minimum. The first is a reference to Microsoft.SharePoint
, which is the general SharePoint namespace. The second is a reference to the specific SharePoint LINQ assembly using Microsoft.SharePoint.Linq
. This will add all the necessary dependent LINQ assemblies to your project.
The DataContext
object is the object that provides the heart of your LINQ programming. Your DataContext
object will be named whatever you named the beginning of your generated file from SPMetal. For example, if you had SPMetal create a LINQDemo.cs
file for your outputted code, your DataContext
object will be LINQDemoDataContext
. To create your DataContext
object, you can pass along the URL of the SharePoint site to which you want to connect.
Once you have your DataContext
object, you can start working with the methods and properties of that object. The DataContext
will contain all your lists and libraries as EntityList
properties. You can retrieve these lists and libraries and then work with them. Table 4-12 lists the other methods and properties you will use from the DataContext
object.
Table 4.12. Common Methods and Properties on DataContext Object
As part of SPMetal, you will get autogenerated typed data classes and relationships using the Association
attribute. This allows you to use strongly typed objects for your lists and also to do queries across multiple lists that are related by lookup fields. As you will see in the examples, this makes programming much cleaner and also allows you 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 will throw 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 SharePoint will error on:
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); }
As you can see in the code, you first need to get your DataContext
object. From there, you define your LINQ query as you do against any other datasource. Once you have the results, you can enumerate them using a foreach
loop. If you want, you can also use the ToList
method to return a generic list that you can perform LINQ to Object operations on.
You can add where
clauses to your queries to perform selection. For example, if in the query above you wanted to select only orders that were more than $1000 dollars, you would change the query to the following one:
var orderresults = from orders in context.Orders where orders.Total > 1000 select orders;
For the next example, the query will perform a simple INNER join between two lists that share a lookup field. Since CAML now supports joins, this is supported in LINQ as well. A couple of things to note. First, you'll notice that you get the EntityList
objects for the two lists that you will join, so you can use them in the query. Then, in the query, you just use the join operator to join the two lists together on a lookup field. From there, the code uses the ToList
method on the query results so that you can get back a LINQ to Object collection that you can iterate over.
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 allows you to add, delete, and update your data in SharePoint. Since LINQ is strongly typed, you can just create new objects that map to the type of the new objects you want to add. Once 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. Since LINQ works asynchronously from the server with a local cache, you need to call SubmitChanges
after all modifications to data through LINQ.
To update items, it's a matter of 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
.
Since 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 general exceptions. The code that follows shows how to code for all of 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. Once 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: KeepChanges
, KeepCurrentValues
, or OverwriteCurrentValues
. KeepChanges
keeps the new values since retrieval, even if they are different than the database values, and keeps other values the same as the database. Another choice is KeepCurrentValues
, which keeps the new values since retrieval, even if they are different from the database values and keeps other values the same as they were retrieved, even if they conflict with the database values. OverwriteCurrentValues
keeps the values from the database and discards any changes since retrieval. You need to call SubmitChanges
after resolving any conflicts to save changes.
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 for you, 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 would execute 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 that is 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;
OUTPUT:
<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"><!--List
Name: Customers--><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 are just querying the list and are not planning to add, delete, or update items in the list. This will make your queries perform better, since LINQ will not 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.
There are still definite times when you should revert to using CAML directly. One scenario is where performance is paramount. LINQ makes CAML programming much easier, but no matter how LINQ is optimized, it will add some overhead to your code. Another example is if you have large amounts of adds, deletes, or updates that you need to perform. CAML will provide better performance in this scenario.
If you wanted to program on the client side in SharePoint 2007, you had in reality one choice of API, which was the web services API. While functional, the web services API was not the easiest API to program against, and while it was easy to program the web services API from Windows Forms, programming it from JavaScript or Silverlight was difficult at best. With the growth of client-side technologies, such as .NET CLR–based clients (e.g., 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.
The client OM is really two object models. One works with .NET-based clients, such as Windows Forms, WPF, or Silverlight, since these clients can handle the results in .NET objects, while ECMAScript/JavaScript will get back the JSON response. Figure 4-19 shows the way the client object model works.
One principal of the client object model is to minimize network chatter. When working with the client OM, Fiddler will be a key tool to help you troubleshoot any issues, since the client OM batches together 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 will make your application perform better. In addition, you will want to write asynchronous code with callbacks when working with the client OM so that your user interface doesn't block when users perform actions, which is what they are used to when working with web-based applications.
In terms of API support, the client OM supports a subset of the server object model, so you will find access to lists, libraries, views, content types, web parts, and users/groups as part of the object model, but it does not have coverage of all features, such as the taxonomy store or BI. Figure 4-20 shows the major objects in the client OM.
There is also a difference in the namespaces provided by the .NET and ECMAScript object models. Since you will 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 4-13 shows the different namespaces for the two client OMs.
Table 4.13. Supported Namespaces in Client OMs
.NET MANAGED | ECMASCRIPT |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
| |
|
|
|
|
|
|
|
|
To show you how to map your understanding of server objects to the client, Table 4-14 shows how server objects would be named in the client OMs.
Table 4.14. Equivalent Objects in Server and Client OMs
SERVER OM | .NET MANAGED | ECMASCRIPT |
---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Before diving into writing code with the client OM and adding references in VS, you first need to understand where these DLLs are located and some of the advantages of the DLLs, especially size. As with other SharePoint .NET DLLs, you will 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 over a hefty 15MB.
Since 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 will find three relevant JS files, SP.js
, SP.Core.js
, SP.Ribbon.js
, and SP.Runtime.js
. Of course, when you are debugging your code, you will want to use the debug versions of these files, such as SP.debug.js
, since 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 1MB.
Lastly, Silverlight is a little bit different in that it has its own implementation of the client OM for Silverlight specifically. You can find the Silverlight DLLs at %Program Files%Common FilesMicrosoft SharedWeb Server Extensions 14TEMPLATELAYOUTSClientBin
. You will find two files, Microsoft.SharePoint.Client.Silverlight
and Microsoft.SharePoint.Client.Silverlight.Runtime
. Combined the files also come under 1MB in size.
Microsoft is finalizing the distribution of these files, since on client machines you may need to be able to distribute the files, depending on whether the machine has the required DLLs already installed from other applications, such as Microsoft Office 2010. Until this is finalized, for your development machines, you can either develop on your server or copy the correct files to your development machine.
Depending on the type of application you are writing, the way you reference the different client OMs will vary. 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 4-21 shows adding a reference inside of Visual Studio.
When it comes to the ECMAScript object model, the way you will reference this is by using the ScriptLink
control, which is part of the Microsoft.SharePoint.WebControls
namespace, to add a reference to the ECMAScript files. 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 your code will run in. With the client OM, by default, your code will run in the context of the Windows authenticated user. Since many web applications support forms-based authentication, the client OM supports this as well. You will have to 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;
At the heart of all your code is the ClientContext
object. This is the object that you will 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 in order to open your site, but with the ECMAScript API, you can pass a relative or blank URL in your constructor and SharePoint will either find the relative site or use the current site as the site you want to open.
One quick note on ClientContext
is that, if you look at the implementation, you will notice that it inherits from IDisposable
. This means that you will want to properly dispose of your ClientContext
objects either 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.
Looking at 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 4-15 shows the important methods and properties for the ClientContext
class.
Table 4.15. Methods and Properties for the ClientContext Class
NAME | DESCRIPTION |
---|---|
| Call this method to dispose of your object after you are done using it. |
| After loading all the operations for your site, such as queries, call this method to send the commands to the server. |
| Available in the ECMAScript object model, this allows 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. |
Allows you to load your query using the method syntax of LINQ and will fill the object you pass. You can also just pass an object without a query to return just the object, such as | |
| Use this to return a collection of objects as an |
| Gets or sets the authentication mode for your object. The values can be |
| Use this property to set the username and password for your forms authentication to authenticate against your site. |
| Get or set the timeout for your requests. |
| Gets the site collection associated with the |
| Gets the URL of the site that the |
| Gets the website that the |
As you will see in the sample code throughout this section, you will 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 will want to perform with the client OM, to show you how to use it.
To retrieve items from SharePoint, the easiest way to get back your list is to just use the Load
method to load the object into the client OM. For example, if you wanted 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();
One thing to note is that if you try to use any of the other objects below the requested site, you will get an error saying that the collection is not initialized. For example, if you try to retrieve the lists in the site, you will get an error. 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 will return a large set of properties and hydrate your objects with these properties. For performance reasons, you may not want to have it do that if you are only using 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 letting 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 our lists only the Title property, since that is all we use 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 be wondering what the difference is between Load
and LoadQuery
. Load
hydrates the objects in-context, so if you pass a Web
object to your Load
method, SharePoint will fill in that object with the properties of your SharePoint web. LoadQuery
does not fill in the objects in-context, so it returns an entirely new collection. The LoadQuery
method is more complex, but it also is more flexible. In certain cases, it allows the server to be more effective in processing your queries. Plus, you can query the same object collection multiple times and have different result sets for each query. For example, you can have one query that returns all lists with a certain title, while another collection returns lists with a certain number of items. You can destroy these objects also out of context. With the Load
method, the objects are tied to the client context, so they are only destroyed and are eligible for garbage collection when the client context is destroyed.
The LoadQuery
method is very 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 into the ViewXml
property the CAML query that you want to perform. 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 also use LINQ to query your lists. To do this, you create your query and put it in a variable. Next, using the LoadQuery
method you pass your LINQ query. Then, you can call the ExecuteQuery
method to execute your query and iterate through the results.
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 for your list, such as the title and the type. 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, use the Fields
collection for your list and define the XML in the AddFieldAsXml
property. This property takes your XML, a Boolean that specifies whether to add the field to the default view, and AddFieldOptions
, such as adding the field to the default content type.
Once you have added your field, you can create list items by first creating a ListItemCreationInformation
object, and pass that to the AddItem
method, which will return a ListItem
object representing your new item. Using this object, you can set the properties for your item. Make sure to call the Update
method when you are done modifying your properties.
When you are done with all your changes, make sure to call the ExecuteQuery
method to have the client OM send your changes back to the server.
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 is that when you are deleting items from a collection, you will want to 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 working with lists, libraries, and items, is 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 off 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, you use the UserCreationInformation
object and set the properties on that object, such as Title
, LoginName
, and other properties. Then, you call the Add
method on your UserCollection
object to add the user, and ExecuteQuery
to submit the changes. Since this is very similar to the steps 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 of the code so far that we have looked at 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 will see how to 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 will call back on 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. Once it's complete, you 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 very similar to 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 will use string expressions to define your basic queries. Also, ECMAScript is always asynchronous, so you have to use delegates and create callback functions for the success and failure of your call into the client OM. The final piece, as you see in the code that follows, is that you need to reference the WebControls
namespace from the Microsoft.SharePoint assembly
, reference the ECMAScript client OM in SP.js
or SP.debug.js
, using a SharePoint:ScriptLink
control, and finally put a SharePoint:FormDigest
on your page for security reasons, if you want to be able to write or update to the SharePoint database.
<%@ 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>
Working with Silverlight is also similar to working with the .NET client, except for two major differences. First is that you will want to perform all your operations asynchronously, so this requires you to create delegates and program success and failure methods. Second, since your code runs on a background thread, you will need to get on the user interface thread before you attempt to write to the UI. The easiest way to do this is to wrap your code with the BeginInvoke
method of the Dispatcher
object. This method guarantees that your code will run on the Silverlight UI thread. If you do not do this, you will receive a threading error. One other thing to remember is to 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 runtime.
The last bit you need to know about Silverlight is how to get your applications deployed. They need to run in a trusted area of SharePoint, which could be in a SharePoint library or in the ClientBin
directory. For the ClientBin
, the easiest way to get your code there 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 will learn about later, to create a feature that copies the file up to your SharePoint site using a Module
with a File
reference in your Elements
manifest.
One thing to watch out for is caching of your Silverlight application while you are developing it. Make sure that an old version is not loaded by updating the AssemblyVersion
and FileVersion
in your AssemblyInfo
file in VS. You may also have to clear your browser cache. Another recommendation is to change something in the UI to make sure that your application is the latest; this allows you to tell visually.
Also, if you are working across domains, you will 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 are working in the same domain, you do not have 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 will have to use the ClientAccessPolicy.XML
file to allow those calls. Figure 4-22 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); } } }
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 ADO.NET Data Services, formerly known as Astoria. If you don't know what REST is, 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. First, you will want to make sure you install the ADO.NET data services on the SharePoint 2010 Server where you are developing using REST. REST is implemented in your _vti_bin
directory by accessing http://yourserver/_vti_bin/ListData.svc
, so if you connect to that URL and get a 404 error, you do not have the ADO.NET Data Services technologies installed. Second, if you are connecting to your REST services for SharePoint from Internet Explorer (IE), you will want to turn off Feed Reading View in IE so that you get the raw XML returned from SharePoint. You can find this under Tools
The easiest way to get started 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 4-23. You will see the XML returned for all your lists in your site.
If you have never worked with REST before, there are two ways in SharePoint that you can return your data. First, there is ATOM, which returns XML and is a standard. Then, there is JavaScript Object Notation (JSON), which returns your data using JSON markup so that you can parse that data using JavaScript objects. JSON is good if you want to turn the returned data into Javascript objects. 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.
Since REST uses a standard URL-addressable format and uses 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 4-16 lists some examples of URL addresses.
Table 4.16. Methods and Properties for the ClientContext Class
TYPE | EXAMPLE |
---|---|
List of Lists |
|
List |
|
Item |
|
Single Column |
|
Lookup Traversal |
|
Raw Value Access (no markup) |
|
Sorting |
|
Filtering |
|
Projection |
|
Paging |
|
Inline Expansion (Lookups) |
|
Since Visual Studio has built-in support for using ADO.NET Data Services, programming with REST starts with adding a service reference in your code. In this reference, point to your ListData.svc
URL in _vti_bin
. Please note that in the beta, you will have to change the reference VS adds from System.Data.Service.Client
to Microsoft.Data.Services.Client
by removing the reference and adding a new reference to %Program Files (x86)ADO.NET Data Services V1.5 CTP2in
. Then, you will have to create new proxy classes by running in a command prompt DataSvcUtil.exe /uri:"
http://URL/_vti_bin/ListData.svc
" /out:Reference.cs
. This will create a C# file that you will use to replace the existing Reference.cs
in your project, which you will find in the file directory for your project, not in the user interface, unless you turn on Show All Files in Solution Explorer.
From there, you should see your lists in the Data Source window inside of Visual Studio, as shown in Figure 4-24. If you do not see your datasource in the window, right-click your service reference and select Update Service Reference. You will have to go back and change the System.Data.Services.Client
reference again to Microsoft.Data.Services.Client
.
From there, you can add a new Object
datasource to your project, so you can work with a subset of the lists, such as binding the datasource to a datagrid in your code. You can do this by creating a new Object
datasource and selecting the lists you are interested in, as shown in Figure 4-25.
You can drag and drop your datasource onto your form, and Visual Studio will create and bind a grid to your datasource. You can also use LINQ to program against your REST datasource. Figure 4-26 shows a databound grid against a SharePoint REST datasource.
Since the code for programming using REST is very similar to programming using the rest of the client OM, there is a quick example below of adding an item to your SharePoint list using REST. You will notice 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(); }
Unfortunately, external lists are not supported with the ADO.NET Data Services and REST. If you look at your lists using REST, you will find that your external lists will not appear in your list results. This is a deficiency that you will have to work around by using other methods, such as the client OM, to access external lists.
You may also be wondering about JQuery support in SharePoint, since REST supports putting out JSON objects that you can load with JQuery, as do other parts of SharePoint. While SharePoint itself does not include a JQuery library, you can easily link to JQuery in your SharePoint solutions. One thing to note is that 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.3.2.js" type="text/javascript"></script>
Often developers who want to build solutions on SharePoint can't, because they require administrator access to SharePoint and their solutions must be deployed as a full-trust solution, which could affect the stability of the server if they write bad code. For these reasons, IT administrators do not allow developers to write code against SharePoint 2007. With Sandbox Solutions in SharePoint 2010, the server administrator can allow site administrators to deploy code and developers to write code and still protect the integrity of the server. Sandbox Solutions are self-regulating, since there are quotas for resource usage and the server will shut down any solutions that exceed their quota.
With Sandbox Solutions, you can build a subset of all the solutions you can build in SharePoint. Solutions that require extensive privileges are not allowed in the sandbox because of the limited nature of the sandbox. The following list gives you the types of solutions you can build with Sandbox Solutions.
Content Types
Site Columns
Custom Actions
Event Receivers
Feature Receivers
InfoPath Forms Services (not admin-approved, that is, without codebehind)
JavaScript, AJAX, jQuery, REST, or Silverlight Applications
List Definitions
Site Pages (but no application pages with code behind)
Web parts (but not visual web parts)
Before a Sandbox Solution can be 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 Sandbox 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
.
If you look at the architecture for Sandbox Solutions, there are three main components when executing your solution. First, there is the User Code Service (SPUCHostService.exe
). This service decides whether the server where this service is running will participate in Sandbox Solutions. SharePoint has a modular architecture for Sandbox Solutions where you can run them on your WFEs or you can dedicate separate servers for executing your Sandbox code. If the User Code Service is running on a machine, Sandbox Solutions can run on that machine. When you troubleshoot your Sandbox Solutions, the first thing to check is to make sure that this service is running on a SharePoint server in your farm.
From an architecture standpoint, SharePoint allows you to pin the execution of the Sandbox Solution to the server that received the web request. This means that the User Code Service must run on all your Web Front Ends (WFEs) in your farm. While this provides easy administration, since you do not have to create separate servers for Sandbox Solutions or remember which servers the service runs on, it does limit your scalability because the WFEs have to process other web requests while running the Sandbox Solutions.
Your other option is to run requests by solution affinity. You set up application servers in your SharePoint farm that run the User Code Service and are not processing web requests. SharePoint will route Sandbox Solutions to these servers rather than have the solution run on your WFE.
The second component and next process is the Sandbox Worker Process (SPUCWorkerProcess.exe
). This is the process where your code executes. As you can tell, it is not part of w3wp.exe
, which is one reason why you don't have to reset your entire site when you deploy a Sandbox Solution. If debugging does not work for your sandbox, you can always manually attach the debugger to this process, but be forewarned that SharePoint may kill your debugging session in the middle if you take too long or exceed one of the quotas that is set on the sandbox.
The last component and process is the Sandbox Worker Proxy (SPUCWorkerProcessProxy.exe
). Given that SharePoint has the service application architecture, this proxy allows Sandbox Solutions to tie into that infrastructure.
Sandbox does implement a subset of the Microsoft.SharePoint
namespace. Sandbox Solutions do allow you to use full trust proxies to access other APIs or capabilities, for example, accessing network resources, but OOB the following capabilities from the Microsoft.SharePoint
namespace are supported:
Microsoft.SharePoint
, except
SPSite
constructor
SPSecurity
object
SPWorkItem
and SPWorkItemCollection objects
SPAlertCollection.Add
method
SPAlertTemplateCollection.Add
method
SPUserSolution
and SPUserSolutionCollection
objects
SPTransformUtilities
Microsoft.SharePoint.Navigation
Microsoft.SharePoint.Utilities
, except
SPUtility.SendEmail
method
SPUtility.GetNTFullNameandEmailFromLogin
method
Microsoft.SharePoint.Workflow
Microsoft.SharePoint.WebPartPages
, except
SPWebPartManager
object
SPWebPartConnection
object
WebPartZone
object
WebPartPage
object
ToolPane
object
ToolPart
object
One common question you may be asking yourself is "If I can't access local resources such the hard drive on the server or network resources except for SharePoint, how do I get at external data like a database or Twitter or some other external datasource?" Well, you can use external lists and BCS in SharePoint to access external data, since Sandbox 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 quickly use the external lists in your Sandbox Solutions to read and write to that external data.
Sandbox 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 allows 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 Sandbox Solutions for this, rather than content editor web parts, makes the control reusable and easier to distribute.
You can find a lot of information about Sandbox Solutions under the UserCode
folder in your SharePoint root. If you look at the web.config
file located there, you will see that Sandbox Solutions are restricted by an OOB CAS policy. By default, you cannot access anything outside of the SharePoint object model. You should not modify these permission levels and instead should use full trust proxies to allow your sandbox code to perform allowed operations that it does not have by default. The exact permission levels are:
SharePointPermission.ObjectModel
SecurityPermission.Execution
AspNetHostingPermission.Level = Minimal
Beyond the default CAS policy, SharePoint also implements an API block list. Imagine the scenario where you find some code exploiting your sandbox using a particular API from SharePoint. You may want to block API across your environment. This is where the API block list comes in. A new object was added to SPWebService
called RestrictedObjectModel
. This object contains a collection of restricted types that implements an add method, so you can add new API methods to block. For the add method, you need to pass in the type and method you want to block. For example, if you wanted to block the Update
method of the SPWeb
object you would call SPWebService.RestrictedObjectModel.RestrictedTypes.Add(typeof(SPWeb), "Update")
. By default, nothing is blocked.
One of the nice features of VS 2010 is that it supports Sandbox Solutions. When you create a new SharePoint project, for projects that support Sandbox Solutions, VS will give you the option of deploying your solution as a Sandbox Solution, as shown in Figure 4-27. In addition, VS will limit the API set in IntelliSense to just the APIs that work for Sandbox Solutions. VS does not do a compile-time check if you are using restricted APIs, since you program against the full SharePoint namespace and at runtime, your code is limited. So, if you ignore IntelliSense and write to APIs that are not supported in the sandbox, you will not get a compile-time error but instead will get a runtime error. One trick around this is to reference the Microsoft.SharePoint.dll
under the Assemblies folder under the UserCode folder in the SharePoint hive. That will limit the APIs you can use. You MUST change back the reference to the full Microsoft.SharePoint.dll
before deployment. This may be fixed in the released version of Visual Studio 2010.
One of the powerful features of the sandbox is the monitoring. There are site collection quotas that administrators can set up for all Sandbox Solutions running in that site collection. These quotas stop the Sandbox Solutions from overloading the server, such as maxing out the CPU or pegging the database. Resource calculations are not instantaneous so that you may have to wait a bit during development to see the quota usage change. In addition, there is a daily timer job that aggregates server resource usage, resets any solutions that have exceeded their quota so that they can run again, and deletes old resource usage records.
Farm administrators decide how many resource points a site collection receives in Central Administration. If you look at the Configure Quota and Locks under Application Management in the section called User Solutions Resource Quota, you will see where a farm administrator can set the resource quota for Sandbox Solutions. This is shown in Figure 4-28.
Since the resource quota is for the site collection's Sandbox Solutions, one bad apple ruins the bunch. If Sandbox Solutions eat up all the resources, no Sandbox Solutions will run on that site collection for the rest of the day. So, it's a good idea to make sure that you're writing good code; otherwise, you could use all the resources, even those for other developers running in the same site collection.
In terms of monitoring, SharePoint tracks 14 different counters and tries to normalize across them. For example, how many points should be a millisecond of CPU execution time as compared to the number of SharePoint database queries you make? How do you normalize across different counters to make an aggregate that makes sense? Well, it's easy to see how SharePoint attempts to do it. You can look at using PowerShell by using the SPUserCodeService
object. The following PowerShell code returns all the counters measured by Sandbox Solutions:
$snapin = Get-PSSnapin | where-object { $_.Name -eq 'Microsoft.SharePoint.PowerShell' } if ($snapin -eq $null){ write-host "Loading SharePoint PowerShell Snapin..." -foregroundcolor Blue add-pssnapin "Microsoft.SharePoint.PowerShell" write-host "SharePoint PowerShell Snapin loaded." -foregroundcolor Green } [Microsoft.SharePoint.Administration.SPUserCodeService]::Local.ResourceMeasures # The ReadKey functionality is only supported at the console (not is the ISE) if (!$psISE) { Write-Host -NoNewLine "Press any key to continue. . . " $null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") Write-Host "" } write-host "Completed Run" -foregroundcolor Blue
If you bubble up this list, you get the counters and metrics in the bulleted list below. The way to read the list is as the type of counter and then how many counts must occur or the time to count as a single resource point. For example, if you make 20 SharePoint database calls through your calls to different SharePoint APIs, that counts as one resource point.
AbnormalProcessTerminationCount: 1
CPUExecutionTime: 3600
InvocationCount: 100
PercentProcessorTime: 85
ProcessCPUCycles: 100000000000
ProcessHandleCount:10000
ProcessIOBytes: 10000000
ProcessThreadCount: 10000
ProcessVirtualBytes: 100000000
SharePointDatabaseQueryCount: 20
SharePointDatabaseQueryTime: 120
UnhandledExceptionCount: 50
UnresponsiveprocessCount: 2
The counters are customizable in that you could bump them up or down using the object model any of the counters. For example, if you wanted to allow 40 database calls, you could change that by using the SharePoint object model. However, it's a little like changing search relevancy algorithms yourself; it may cause unintended consequences, so try the default restrictions to see if they meet your needs before you modify them.
Also, there are absolute limits. Absolute limits will terminate a solution even if the resource limits have not been hit. For example, UnresponsiveprocessCount
has an absolute limit of 1. This means that, if your Sandbox Solution is not responding, it will be terminated immediately. Then, two points will be added to the aggregate. The solution could try to run again, but it will be terminated if it becomes unresponsive and again two resource points will be added to the aggregate.
If there are solutions that just keep breaking, administrators can block them from running either by using the object model or, more easily, by selecting the solution by browsing for it and putting in a message that tells the user why the solution can't run.
When it comes to managing your solutions, there are two key areas to take a look at beyond monitoring. One is solution validation, which allows you to be proactive in validating solutions. You decide what to check on the solution and, if it fails validation, it is not allowed to be activated in the site collection. The second is full-trust proxies. These proxies allow Sandbox Solutions to call more functionality than what the sandbox provides, but in a managed way.
To work with solution validation, you just need to inherit from the SPSolutionValidator
class. Once you write your code for this class, you add it to the SPUserCodeService SolutionValidators
collection using either the API or PowerShell. Following is sample code for the solution validator:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Runtime.InteropServices; using Microsoft.SharePoint; using Microsoft.SharePoint.Administration; using Microsoft.SharePoint.UserCode; using System.IO; namespace SolutionValidation { [Guid("7158c574-9881-42b3-9116-2575485af534")] public class SolutionValidator : SPSolutionValidator { //Create constant to pass to constructor private const string solutionValidatorName = "Solution Validator"; //Create constant for Validator Signature private const int sigValue = 1; public SolutionValidator() { //Blank Constructor } public SolutionValidator(SPUserCodeService userCodeService) :base(solutionValidatorName, userCodeService) { this.Signature = sigValue; } public override void ValidateSolution( SPSolutionValidationProperties properties) { //Validate your solution such as checking the code files in the solution foreach (SPSolutionFile solutionFile in properties.Files.Where( f => String.Equals( Path.GetExtension(f.Location), ".dll", StringComparison.InvariantCultureIgnoreCase))) { //Perform Validation of files } base.ValidateSolution(properties); properties.Valid = false;
properties.ValidationErrorMessage = "Illegal Solution"; properties.ValidationErrorUrl = "/_layouts/SolutionValidation/SolutionValidationError.aspx"; } public override void ValidateAssembly( SPSolutionValidationProperties properties, SPSolutionFile assembly) { //You can open the assembly using OpenBinary base.ValidateAssembly(properties, assembly); properties.Valid = true; } } }
A couple of things in the code: First, you can see where the code inherits from the SPSolutionValidator
class. In addition, you need to create your own GUID to assign to your class, since you will use that GUID when you remove the solution validator from SharePoint in your feature deployment.
From there, you can see the blank constructor and another constructor that overloads the base constructor to assign a signature value to your validator. If you update the validator, you will want to update the signature number.
For the implementation, the code has ValidateSolution
and ValidateAssembly
methods. The ValidateSolution
method gets passed a SPSolutionValidationProperties
collection, which contains a number of properties for your SharePoint solutions, such as the files in the solution. You can perform validation in this method and, if the solution is valid, set the Boolean Valid
property to True
. If the solution is not valid, set the Valid
property to False
. You can also specify the ValidationErrorMessage
and ValidationErrorUrl
, which is the message to display and, if you want, the URL of the error message page. You can use the mapped folder feature in Visual Studio to add a custom ASP.NET page to your Layouts folder to display your error message.
For the ValidateAssembly
method, you can validate individual assemblies in the solution. You get the assembly, and you can open it using the OpenBinary
method, write all the bytes, and look at the bits contained in it. If the assembly is valid, set the Valid
property to True
.
Once you have your implemented class, you need to create a feature that deploys your solution validator. It must be a farm-level solution, since you want to validate all solutions in your farm. As part of the feature, you will want to write code for your feature event receiver to turn on your solution validator and turn it off when the feature is activated. The following code does this. Note how the same GUID is used in the FeatureDeactivating
event that you used to mark up your class. Figure 4-29 shows the solution validator in action, denying a solution the right to activate.
public override void FeatureActivated(SPFeatureReceiverProperties properties) { //Add solution Validator using our class name SPUserCodeService.Local.SolutionValidators.Add(
new SolutionValidator(SPUserCodeService.Local)); } public override void FeatureDeactivating( SPFeatureReceiverProperties properties) { //Remove our solution validator using our GUID SPUserCodeService.Local.SolutionValidators.Remove(new Guid( "7158c574-9881-42b3-9116-2575485af534")); }
The last area we will look at is creating full-trust proxies. Full-trust proxies allow you to extend Sandbox Solutions without throwing everything out of the sandbox. For example, you may need to access a network resource to get at data but not want to make the full solution a fully trusted solution; instead you want to provide a limited proxy to just that network resource. With full-trust proxies, you can provide an API just to the network resource and allow your Sandbox Solutions to call through that API to your resource.
To create a full-trust proxy, you need to implement a class that inherits from the SPProxyOperation
class. Then, you need to figure out the arguments that you want passed to your proxy by creating a serializable class that inherits from the SPProxyOperationsArgs
class. Once you have done this, generate the DLL and put this DLL into the global assembly cache (GAC). Then, you can register the DLL with SharePoint and call it using the SPUtility.ExecuteRegisteredProxyOperation
method.
The following code creates a class that mimics accessing a network resource. It passes a fake username and password, and the full-trust proxy returns an array of data. You could imagine that you make a call to ADO.NET or other data access technologies to perform a real database call. Notice in the code that you need to create a serializable class that implements the proxy arguments. The calling Sandbox Solution then displays the data from the datasource that is accessed by using the full-trust proxy.
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.SharePoint.UserCode; namespace FullTrustProxyDLL { public class AccessDatabase : SPProxyOperation { public override object Execute(SPProxyOperationArgs args) { if (args != null) { ProxyArgs proxyArgs = args as ProxyArgs; //Get the user name string userName = proxyArgs.userName; //Get the password string password = proxyArgs.password; //Access the datasource here string[] results = { "A", "B", "C" }; return results; } else return null; } } [Serializable] public class ProxyArgs : SPProxyOperationArgs
{ public string userName { get; set; } public string password { get; set; } } }
In order to make your proxy callable from partially trusted code applications, you need to add a line to your AssemblyInfo.cs
, as shown here:
//Allow partially trusted callers [assembly: System.Security.AllowPartiallyTrustedCallers]
Once you have compiled your proxy DLL, you need to register it in the GAC. Once that is done, you can register it with SharePoint. The following PowerShell script performs this step:
$snapin = Get-PSSnapin | where-object { $_.Name -eq 'Microsoft.SharePoint.PowerShell' } if ($snapin -eq $null){ write-host "Loading SharePoint PowerShell Snapin..." -foregroundcolor Blue add-pssnapin "Microsoft.SharePoint.PowerShell" write-host "SharePoint PowerShell Snapin loaded." -foregroundcolor Green } $userCodeService = [Microsoft.SharePoint.Administration.SPUserCodeService]::Local $assemblyName = "FullTrustProxyDLL, Version=1.0.0.0, Culture=neutral, PublicKeyToken=3098f04d94800e33" $typeName = "FullTrustProxyDLL.AccessDatabase" $proxyOperationType = new-object -typename Microsoft.SharePoint.UserCode.SPProxyOperationType -argumentlist $assemblyName, $typeName userCodeService.ProxyOperationTypes.Add($proxyOperationType) $userCodeService.Update()
Now you can use the proxy in your Sandbox Solution. The following code and Figure 4-30 below shows the full trust proxy being used and how to call the proxy class from your sandbox code:
ProxyArgs proxyA = new ProxyArgs(); proxyA.userName = "TestUser"; proxyA.password = "Password"; string assemblyName = "FullTrustProxyDLL, Version=1.0.0.0, Culture=neutral, PublicKeyToken=3098f04d94800e33"; string typeName = "FullTrustProxyDLL.AccessDatabase"; accessDatabaseButton.Click += (object sender, EventArgs e) => { string[] results; results = (string [])SPUtility.ExecuteRegisteredProxyOperation(assemblyName, typeName, proxyA);
lbl.Text = "First result: " + results[0]; }; Controls.Add(accessDatabaseButton); Controls.Add(lbl);
In this chapter, you have seen how you can use the base services in SharePoint to integrate your applications into the new Ribbon user interface, write event receivers, and even work with the new client OM in your various applications. The SharePoint 2010 platform is a large one with many APIs, so it will take some time for you to absorb all the new APIs. Experimenting with the new APIs is the key to understanding how they work and what their limitations and trade-offs are.
18.191.233.205