Processing items with Work Item Timer Jobs in SharePoint 2010

Once in a while you find yourself in a situation where you have to process some items in SharePoint. Using Timer Jobs is only a half of the answer. Find out how to process items using Work Item Timer Jobs in SharePoint 2010.
Page Content

 

2011-10-24-ProcessingItems-01.jpg

Once in a while you find yourself in a situation where you have to process some items in SharePoint. Using Timer Jobs is only a half of the answer. Find out how to process items using Work Item Timer Jobs in SharePoint 2010.

Processing items in SharePoint 2010

One of the things we have probably all done at least once during our SharePoint developer career was to create a solution for processing a number of items, such as ratings, subscriptions, some kind of requests, etc. We have all learned that such processes can take quite a while to complete and it is therefore a good practice to use Timer Jobs for implementing the logic. Because Timer Jobs are being executed outside the w3wp.exe process, they are not only less prone to failures due to process termination but also allow us to move the load away from the Application Pool serving our solution.

When dealing with items you have to have some kind of queue which describes what should be processed. Most frequently such queue contains some input information for the task (rating, username, etc.) but it may also contain ID’s of sites and items to which the task refers.

While implementing queues for Timer Jobs many people choose Lists for storage. And although there is nothing wrong with this approach, it requires some additional work in creating and maintaining the queue List’s schema and all the plumbing for adding and cleaning queued items not to mention supporting upgrades should anything change in the future! There is however an easier way to work with item queues in SharePoint 2010.

Presenting Work Item Timer Jobs

Work Item Timer Jobs (SPWorkItemJobDefinition) are specialized types of Timer Jobs designed for dealing with item queues. And although they have been around for quite some time (available as a part of Windows SharePoint Services v3) they haven’t been that well documented yet and there are not many samples to find of how they can be used.

Creating Work Items

At the base of every Work Item Timer Job is the Work Item: a unit of work that is picked up and processed by the Job when it executes. A work item can be added using the SPSite.AddWorkItem method, eg.:

01 Guid siteId = SPContext.Current.Site.ID; 
02 Guid webId = SPContext.Current.Web.ID; 
03 Guid listId = SPContext.Current.ListId; 
04 int itemId = SPContext.Current.ItemId; 
05 Guid itemUniqueId = SPContext.Current.ListItem.UniqueId; 
06 int currentUserId = SPContext.Current.Web.CurrentUser.ID; 
07 SPSecurity.RunWithElevatedPrivileges(() => { 
08     using (SPSite site = new SPSite(siteId)) { 
09         site.AddWorkItem(Guid.NewGuid(), 
10             DateTime.Now.ToUniversalTime(), 
11             MyJobDefinition.WorkItemTypeId, 
12             webId, 
13             listId, 
14             itemId, 
15             true
16             itemUniqueId, 
17             itemUniqueId, 
18             currentUserId, 
19             null
20             "Hello World"
21             Guid.Empty); 
22    
23 });

Although the number of parameters required by the AddWorkItem method might seem scary it is all way easier than it looks.

First there is the gWorkItemId parameter which uniquely identifies the work item.

The second parameter (schdDateTime) determines when the item should be processed. One important thing to notice here is that the time should be stored in the Universal Time format. Without this you might find yourself creating work items in the past which would prevent the Timer Job from processing them.

The next parameter (gWorkItemType) contains the ID of the work item type. This value is very important as it’s used by the Work Item Timer Job to pick up its work items. The identifier set while creating a Work Item should match the value returned by the SPWorkItemJobDefinition.WorkItemType method.

Next there are a few parameters that can be used for retrieving the List Item to which the Work Item refers. For example when working with ratings you would have the item that has been rated (List Item) and the rating Work Item which contains the rating value. In order to update the average rating on the List Item you would need a reference to the Site and List where the particular item is stored but also the ID of the item itself so that you can retrieve and update it.

Then there is the nUserId parameter which contains the ID of the user who requested the Work Item. This can be useful for tracking purposes.

Next, there are two parameters which can be used for storing input values for the Timer Job and which replace the need for a whole separate List. Those parameters are rgbBinaryPayload and strTextPayload and can be used to store respectively binary and text payloads. When working with ratings you could, for example, store the rating value (eg. 4) as the text payload. In other scenarios you could choose to store more complex objects: either serialized as JSON/XML as the text payload or as a binary payload. Which one you use depends mostly on the type of Timer Jobs that you are building.

Important: there are two things that you should keep in mind when designing your solution based on Work Items. First of all the assembly that contains the code which adds a new Work Item must have Full Trust. Secondly the user who adds the Work Item must be a Site Collection Administrator. In most scenarios you will therefore be adding new Work Items after elevating privileges.

Calling the SPSite.AddWorkItem method results in adding a record to the ScheduledWorkItems table in the Content Database of your Site Collection.

2011-10-24-ProcessingItems-02.jpg

Those Work Items records are cleaned up once the item has been processed by the Timer Job.

Now that we have our Work Item created let’s create a job that will process our Work Items.

Creating a Work Item Timer Job

As mentioned before, Work Item Timer Jobs are specialized types of Timer Jobs. From the deployment perspective they look the same as regular Timer Jobs, so they also need a Feature to be deployed and a schedule to execute. The big difference is in the execution process. Where you would implement the Execute method in a regular Timer Job, you use the ProcessWorkItem method to execute your logic.

The following code snippet presents a sample Work Item Timer Job.

01 public class MyJobDefinition : SPWorkItemJobDefinition { 
02     public static readonly string WorkItemJobDisplayName = "My Work Item Job"
03     public static readonly Guid WorkItemTypeId = new Guid("{CEAAFFA4-4391-40D6-868E-19EDEDB78DD4}"); 
04   
05     public override Guid WorkItemType() { 
06         return WorkItemTypeId; 
07    
08   
09     public override int BatchFetchLimit { 
10         get
11             return 50; 
12        
13    
14   
15     public override string DisplayName { 
16         get
17             return WorkItemJobDisplayName; 
18        
19    
20   
21     public MyJobDefinition() { 
22   
23    
24   
25     public MyJobDefinition(string name, SPWebApplication webApplication) 
26         : base(name, webApplication) { 
27   
28    
29   
30     protected override bool ProcessWorkItem(SPContentDatabase contentDatabase, SPWorkItemCollection workItems, SPWorkItem workItem, SPJobState jobState) { 
31         if (workItem == null || String.IsNullOrEmpty(workItem.TextPayload)) { 
32             throw new ArgumentNullException("workItem"); 
33        
34   
35         try
36             using (SPSite site = new SPSite(workItem.SiteId)) { 
37                 using (SPWeb web = site.OpenWeb(workItem.WebId)) { 
38   
39                     try
40                         SPList list = web.Lists[workItem.ParentId]; 
41                         SPListItem listItem = list.GetItemByUniqueId(workItem.ItemGuid); 
42                         string message = workItem.TextPayload; 
43                         listItem["Message"] = message; 
44                         listItem.SystemUpdate(false); 
45                    
46                     catch (Exception ex) { 
47                         // exception handling 
48                    
49                     finally
50                         // SubCollection(): required to set the ParentWeb property on the WorkItemsCollection 
51                         // Delete(): required to remove the item from the queue 
52                         workItems.SubCollection(site, web, 0, (uint)workItems.Count).DeleteWorkItem(workItem.Id); 
53                    
54                
55            
56        
57         catch (Exception ex) { 
58             // exception handling 
59        
60   
61         return true
62    
63 }

We begin with defining the name for our Timer Job and the Work Item Type. Next, using the BatchFetchLimit property we specify how many items our Timer Job should process in a single run. Depending on the logic of your job this can help you keep your environment from overloading and the job running for too long. Finally, in the ProcessWorkItem method, we implement our logic for processing Work Items. In this example we retrieve the associated List Item and we update the value of its Message field with the message stored as the Text Payload of our Work Item.

As you can see this is all very straightforward and allows you to focus on the real job instead of the plumbing.

Important: One very important thing that you should implement correctly and test thoroughly is cleaning up Work Items after they have been processed. Without this they would be processed every time the Timer Job executes over and over again.

In the code sample above you can see how a processed Work Item is being removed in line 52. Although the ProcessWorkItem method receives the collection of Work Items you cannot just remove the Work Item from it. For the Work Item to be removed the collection has to have a reference to the Parent Web and therefore you have to call the SubCollection method first before calling the DeleteWorkItem method.

Summary

Work Item Timer Jobs are specialized types of Timer Jobs in the SharePoint platform, that have been designed to work with queued items. The mechanics behind the Work Item Timer Jobs contain all the plumbing required for managing queues allowing you to focus on processing items. Work Item Timer Jobs are a great alternative to using custom Lists as they require less maintenance and are a part of the SharePoint platform

How To: Configure SharePoint 2010 BCS connection information with PowerShell

One of the most common things you’d want to change is connection information for your LOBSystem. There is no SharePoint out-of-the-box UI for it, but thankfully PowerShell can access and change BCS LOB System connection configuration.
Page Content

 

If you worked with BCS in SharePoint 2010 you know that you can configure your BCS models and connection information right in the model configuration file, and that’s something developers will create for you. But once your model has been deployed to the server – all of the configuration information stays there. Some of the model details you can change using SharePoint Designer, some others you just can’t get to.

One of the most common things you’d want to change is connection information for your LOBSystem. There is no SharePoint out-of-the-box UI for it, but thankfully PowerShell can access and change BCS LOB System connection configuration.

Just so we’re on the same page, I assume you already have BCS model installed in your farm. Let’s assume your BCS system instance (LobSystemInstance) has the name of My.Project.BCSInstance; this is important to change in the commands below.

To get your LobSystemInstance value, also used in the script below, navigate to Central Administration -> Manage Service Applications -> click Business Data Connectivity Service -> switch the view to External Systems

2011-12-20-SP2010BCSConnections-01.png

In the same screen you will find your External System Name (in our case it’s My.Project.BCSSystem).

To find out External System Instance Name (in my case My.Project.BCSInstance), click on the BCS system name from the screen above and grab a value of External System Instance Name

Also, I assume you’re connecting to a SQLServer machine with the machine name of MySqlserver, you can also change that in the script below.

Open SharePoint 2010 Management Shell and execute the following script, substituting variables with your own.

01 $lob = Get-SPBusinessDataCatalogMetadataObject
02   -BdcObjectType "LobSystem"
03   -Name "My.Project.BCSSystem"
04   -ServiceContext "http://[site URL]"
05
06 $instance = $lob.LobSystemInstances | Where-Object {$_.Name -eq "My.Project.BCSInstance"}
07
08 Set-SPBusinessDataCatalogMetadataObject –Identity $instance
09   –PropertyName "AuthenticationMode" –PropertyValue "PassThrough"
10
11 Set-SPBusinessDataCatalogMetadataObject –Identity $instance
12   –PropertyName "RdbConnection Data Source" –PropertyValue "MySqlserver"
13
14 Set-SPBusinessDataCatalogMetadataObject –Identity $instance
15   –PropertyName "RdbConnection User ID" –PropertyValue "My.Sql.User"
16
17 Set-SPBusinessDataCatalogMetadataObject –Identity $instance
18   –PropertyName "RdbConnection Password" –PropertyValue "MyPassword#!"
19
20 Set-SPBusinessDataCatalogMetadataObject –Identity $instance
21   –PropertyName "RdbConnection Integrated Security" –PropertyValue " "
22
23 $instance.Update()
24 $lob.Update()

That’s it – the script above will connect to the BCS system instance and change any provisioned variables with the ones in the script.

SharePoint Large List Performance: SPMetal vs. SPQuery

I was recently faced with determining the storage mechanism for a custom application. The choices were ‘SharePoint lists vs. SQL tables’. The user interface of this application would be implemented as a set of webparts either way.
Page Content

Key takeaway

Use SPQuery for large lists instead of SPMetal. While testing with a list of 45K items, SPQuery performed at 0.06s compared to SPMetal’s performance at 9.98s.

Background

I was recently faced with determining the storage mechanism for a custom application. The choices were ‘SharePoint lists vs. SQL tables’. The user interface of this application would be implemented as a set of webparts either way. Now SharePoint has the advantage on UI which allows end users to easily interact with the data. SQL on the other hand takes the trophy on performance. So the choice really boiled down to ‘End-User Access vs. Performance’. I really liked the idea of users being able to interact with the data so the obvious question arose: Could SharePoint perform reasonably well when dealing with ‘large’ data sets?

Throttle Levels

What is considered ‘large’ in the SharePoint world? For end users, 5000 items is where SharePoint starts to throttle queries. Using the object model however, queries of over 20,000 items leads to an Expensive Query Exception. This behavior can be overriden by setting the SPQuery.QueryThrottleMode to SPQueryThrottleOption.Override.

Additionally, these limits can be increased from Central Admin. So in order to create a large enough data set, let’s populated a blank custom list with 45,000 rows of data by setting the title column to a unique number. Additionally, for roughly 100 of these list items, let’s set a second column’s value (Column=Campaign) to a predetermined text since that way we can pull these 100 rows as part of a query.

01 using (var context = new ClientDataContext("http://site"))
02 {
03     for (int i = 0; i < 45000; i++)
04     {
05         var newItem = new PerfListItem {Title = i.ToString()};
06             if (i >= 44900)
07                 newItem.Campaign = "ABC";
08             context.PerfList.InsertOnSubmit(newItem);
09     }
10     context.SubmitChanges();
11 }

Column Indexing

Before diving into the test results, let me briefly point out that SharePoint list columns can be indexed for better performance. When dealing with large lists, throttling kicks in on sort, where and join operations if dealing with non-indexed columns. For example, a where clause on a non-indexed column is a resource-intensive operation and SharePoint will throttle the query leading to unexpected results. The correct way to deal with large lists is to index any columns that would be used for any sorting, filtering or lookups. For the purposes of our test, I will be filtering based upon the ‘Campaign’ column so let’s add an index for that. Additionally, let’s add an index for the ‘Content Type’ column since SPMetal queries add a ContentTypeId filter to the underlying CAML.

SPMetal Test

Let me begin by saying that I’m a huge fan of SPMetal. For those who don’t know, SPMetal is a command-line tool that generates entity classes, which are primarily used in LINQ to SharePoint queries. Let’s setup this test as a simple filter query which consequently displays the count of the result set.

1 using (var context = new ClientDataContext("http://site"))
2 {
3     var res = from item in context.PerfList
4               where item.Campaign == "ABC"
5               select item;
6     Console.WriteLine(res.Count());
7 }

The result displayed a whopping count of 100 as expected and the execution time was an average of 9.98s. Test was conducted 3 times and the elapsed time was measured using System.Diagnostics.Stopwatch.

Extracting SPMetal’s underlying CAML

SPMetal’s performance of 9.98s wasn’t exactly stellar so let’s try a native CAML SPQuery instead. Since SPMetal ultimately turns LINQ queries into CAML queries, a fair test would be to try the SPMetal generated CAML query directly against the list. In order to extract the underlying query used by SPMetal, we can assign a TextWriter to the Log property of the data context.

1 var sb = new StringBuilder();
2 context.Log = new StringWriter(sb);
3 //LINQ Query
4 Console.WriteLine(sb.ToString());

This results in the following CAML-

01 <View>
02   <Query>
03     <Where>
04       <And>
05         <BeginsWith>
06           <FieldRef Name="ContentTypeId" />
07           <Value Type="ContentTypeId">0x0100</Value>
08         </BeginsWith>
09         <Eq>
10           <FieldRef Name="Campaign" />
11           <Value Type="Text">ABC</Value>
12         </Eq>
13       </And>
14     </Where>
15   </Query>
16   <ViewFields>
17     <FieldRef Name="Campaign" />
18     <FieldRef Name="ID" />
19     <FieldRef Name="owshiddenversion" />
20     <FieldRef Name="FileDirRef" />
21     <FieldRef Name="Title" />
22   </ViewFields>
23   <RowLimit Paged="TRUE">2147483647</RowLimit>
24 </View>

SPQuery Test

Finally, let’s setup the test for a native CAML SPQuery against the same list.

01 var query = new SPQuery
02                 {
03                     ViewXml = "<View><Query><Where><And><BeginsWith><FieldRef Name=\"ContentTypeId\" />" +
04                     "<Value Type=\"ContentTypeId\">0x0100</Value></BeginsWith><Eq>" +
05                     "<FieldRef Name=\"Campaign\" /><Value Type=\"Text\">ABC</Value></Eq>" +
06                     "</And></Where></Query><ViewFields><FieldRef Name=\"Campaign\" />" +
07                     "<FieldRef Name=\"ID\" /><FieldRef Name=\"owshiddenversion\" />" +
08                     "<FieldRef Name=\"FileDirRef\" /><FieldRef Name=\"Title\" /></ViewFields>" +
09                     "<RowLimit Paged=\"TRUE\">2147483647</RowLimit></View>"
10                 };
11 using (var site = new SPSite("http://site"))
12 {
13     using (SPWeb web = site.OpenWeb())
14     {
15         SPList list = web.Lists["PerfList"];
16         SPListItemCollection items =list.GetItems(query);
17         Console.WriteLine(items.Count);
18     }
19 }

The result? A surprising 0.06s on average! Again the test was conducted 3 times and elapsed the time was measured using System.Diagnostics.Stopwatch.

Machine Specs

For anyone that is interested, these tests were performed on a Windows 7 Client machine with Intel Core 2 Duo 2.66GHz & 8GB RAM.

Conclusion

I finally got the answer to my initial question: It IS possible to both leverage the UI capabilities of SharePoint while working with large data sets. It is also painfully obvious that while SPMetal can save hours upon hours of development time, it clearly comes at a price. A price that gets prohibitively expensive as the data set grows in size. The lesson here is to be cognizant of the performance implications of SPMetal while balancing the need for faster development.

Multi-page forms with the SharePoint Scenario framework

SharePoint 2010 provides the SharePoint Scenario framework which simplifies creating and working with multi-page forms.
Page Content

 

2011-08-20-MultiPageForms-01.jpg

Splitting long forms into multiple pages is a great idea from the usability point of view. The downside is that it requires you to create some sort of mechanism of persisting the data between the pages. The good news is that SharePoint 2010 provides you with a framework for that particular purpose. Find out how to create multi-page forms with the SharePoint Scenario framework.

WhatIs: SharePoint Scenario framework

SharePoint Scenario framework is one of many hidden gems of the SharePoint 2010 platform. Although it’s not very well documented it’s an invaluable asset in your toolbox whenever you need to implement multi-page forms.

The SharePoint Scenario framework allows you to create multi-page forms, where every page can be a separate page in SharePoint (no matter if it’s an Application Page, a Web Part page or a Publishing Page). This is great news in terms of reusability of form sections. Because every page of your form can be a separate Web Part it allows you to build your forms as composite controls rather than giant all-in-one Web Parts. Reusing a particular step across multiple forms is very easy and doesn’t require you to make any changes in the code.

How it works?

A form built using the SharePoint Scenario framework consists of two pieces: the Scenario Definition and form pages. When composing the form it’s up to you to decide whether you want to implement every page as a separate control and a separate page. The great benefit of separating form pages is that you can compose many forms with minimal effort by reusing common form sections. The downside is that it adds some maintenance as you have to manage a number of Web Parts instead of one.

Important: Before you read further you should know that the SharePoint Scenario framework requires ASP.NET Session State to work. There are some interesting resources available on using Session State with SharePoint 2010 including an article by Mark Arend and another one by Todd Carter. You should read both articles before deciding on whether the Scenario framework is suitable to use in your scenario or not.

Scenario Definition

At the foundation of a form built on the Scenario Framework is the Scenario Definition. A scenario definition is an XML file deployed to the {SharePointRoot}\TEMPLATE\Scenarios\{YourScenario} folder which describes the steps in your form (scenario).

The following code snippet shows a sample Scenario Definition for a multi-page form with two pages:

01 <?xml version="1.0" encoding="utf-8"?> 
02 <Scenario xmlns="urn:Microsoft.SharePoint.Administration" Title="Visitor Info" Description="Gather Visitor Information"
03   <Section Title="Section 1"
04     <Page
05       CurrentPageUrl="/visitor-info/Pages/default.aspx"
06       NextPageUrl="/visitor-info/Pages/display.aspx"
07       RedirectOnCancelUrl="/Pages/default.aspx" /> 
08     <Page
09       CurrentPageUrl="/visitor-info/Pages/display.aspx"
10       RedirectOnCompletionUrl="/Pages/default.aspx"
11       RedirectOnCancelUrl="/Pages/default.aspx"/> 
12   </Section
13 </Scenario>

When creating a Scenario Definition for your form there are a few rules that you have to obey. Although the exact XML definition (XSD) is stored in the Microsoft.SharePoint assembly as the Microsoft.SharePoint.Administration.ScenarioXmlConfigurationSchema.xsd resource, here are a few rules that should help you get started:

  • Every Scenario must have a Title and a Description and at least one Section
  • Every Section must have a Title and at least one Page
  • Every Page must have the CurrentPageUrl and the RedirectOnCancelUrl attributes
  • The CurrentPageUrl must be at least 7 chars long and must end with the .aspx extension
  • The RedirectOnCancelUrl must not be empty
  • The last step must have the RedirectOnCompletionUrl attribute which must not be empty

There are a few scenarios already defined in the {SharePointRoot}\TEMPLATE\Scenarios folder which you can explore to get a better idea how to create a Scenario Definition for your multi-page form.

As mentioned before, Scenario Definitions must be deployed to the {SharePointRoot}\TEMPLATE\Scenarios folder. From the project perspective you can do this by adding the Scenarios Mapped Folder to your SharePoint Project:

2011-08-20-MultiPageForms-02.jpg

Important: It is important that you place your Scenario Definition XML file in a folder. The name of that folder will be used by the form as your Scenario ID.

Using the Scenario framework

The SharePoint Scenario framework ships with a very simple API located in the Microsoft.SharePoint assembly in the Microsoft.SharePoint.Administration.SPScenarioContext class.

Whenever you want to interact with the Scenario framework you have to first retrieve a reference to the Scenario Context. You can do this by calling the SPScenarioContext.GetContext(Page, CultureInfo) method:

1 SPScenarioContext context = SPScenarioContext.GetContext(Page,

For this to work the URL of the current page must contain the scenarioId query string parameter with a valid ID pointing to one of the Scenario Definitions deployed to the {SharePointRoot}\TEMPLATE\Scenarios folder.

Once you have the reference to the Scenario context you can start working with the data. For this, the API provides you with two methods: PersistDataToSessionState(string, object) and RetrieveDataFromSessionState(string). Whenever you want to persist some data to use them further in the form, all you have to do is to make a call to the PersistDataToSessionState(string, object) method:

1 SPScenarioContext context = SPScenarioContext.GetContext(Page, CultureInfo.CurrentUICulture); 
2 if (context != null) { 
3     context.PersistDataToSessionState("myKey", someObject); 
4 }

The point to note here is that you can not only store object of primitive types such as int or string but any object that is serializable.

Once you have your object stored in the Scenario Context you will want to retrieve it at some point. For this all you have to do is to call the RetrieveDataFromSessionState(string) method:

1 SPScenarioContext context = SPScenarioContext.GetContext(Page, CultureInfo.CurrentUICulture); 
2 if (context != null) { 
3     myObject = context.RetrieveDataFromSessionState("myKey") as MyType; 
4 }

Finally when you are done working with the data you have to provide your user with the ability to move to the next or the previous step. Every page in the Scenario framework has a reference to a CompletionUrl and CancelUrl. It is up to you to make those URLs point to correct URLs to create the desired flow in your multi-page form.

You can move between steps by calling either the TransitionToTheNextPage(bool) or the TransitionToTheCancelPage(bool) method which will respectively redirect the visitor to the next or the cancel page. Using the boolean parameter you can automatically redirect users to the desired page. If you need to manipulate the URL before redirecting the user, you can pass the false value to the methods and modify the returned URL. For example:

1 SPScenarioContext context = SPScenarioContext.GetContext(Page, CultureInfo.CurrentUICulture); 
2 if (context != null) { 
3     SPUtility.Redirect(context.TransitionToNextPage(false) + "&myParam=myValue", SPRedirectFlags.Static, HttpContext.Current); 
4 }

Now you know how everything works, in theory. Let’s move things into practice.

SharePoint Scenario Framework hands-on

To illustrate the working of the SharePoint Scenario Framework and to see how all pieces fall together let’s create a simple multi-page form.

On the first page we will ask the visitor to enter his first and last name:

2011-08-20-MultiPageForms-03.jpg

On the second and the last page we will display the data entered by the visitor:

2011-08-20-MultiPageForms-04.jpg

We will create the multi-page form on a Publishing Page where every form page will be a separate Publishing Page.

Step 1: Creating the Scenario Definition

Let’s start off by creating a Scenario Definition for our multi-page form.

First let’s add the Scenarios Mapped Folder so that we can deploy our Scenario Definition using a SharePoint Package. For this we use the the Add > SharePoint Mapped Folder… SharePoint Project option:

2011-08-20-MultiPageForms-05.jpg

and we select the {SharePointRoot}\TEMPLATE\Scenarios folder:

2011-08-20-MultiPageForms-06.jpg

After adding the Scenarios Mapped Folder we create a new folder called VisitorInfo and add in it a new file called VisitorInfo.xml with the following contents:

01 <?xml version="1.0" encoding="utf-8"?> 
02 <Scenario xmlns="urn:Microsoft.SharePoint.Administration" Title="Visitor Info" Description="Gather Visitor Information"
03   <Section Title="Section 1"
04     <Page
05       CurrentPageUrl="/visitor-info/Pages/default.aspx"
06       NextPageUrl="/visitor-info/Pages/display.aspx"
07       RedirectOnCancelUrl="/Pages/default.aspx" /> 
08     <Page
09       CurrentPageUrl="/visitor-info/Pages/display.aspx"
10       RedirectOnCompletionUrl="/Pages/default.aspx"
11       RedirectOnCancelUrl="/Pages/default.aspx"/> 
12   </Section
13 </Scenario>

2011-08-20-MultiPageForms-07.jpg

Step 2: Creating wrapper class for storing data

As mentioned before in our sample form we will ask the visitor to enter his first and last name which we will display later on. Although we could store all those values separately we can create a wrapper class for it.

In your SharePoint Project add a new class and call it VisitorInfo. The following code snippet shows the contents of the class:

01 using System; 
02   
03 namespace SPScenarioContextTest { 
04     [Serializable] 
05     public class VisitorInfo { 
06         public static readonly string VisitorInfoPropertyName = "VisitorInfo"; 
07   
08         public string FirstName { get; set; } 
09         public string LastName { get; set; } 
10    
11 }

The VisitorInfoPropertName field contains the name of the key with which we will store the visitor information in the Scenario Context.

Step 3: Creating the data entry form page

Our sample form will contain two pages where the first page shows the data entry form and the second one displays the entered data. To separate the pages from each other and make it possible to reuse the pages across multiple forms we will create a separate Web Part for every form page. Let’s start by creating the first page that allows the visitor to enter his personal information.

2011-08-20-MultiPageForms-08.jpg

Add to your SharePoint Project a new Web Part using the Web Part SPI Template. Call the Web Part DataWriterWebPart and paste the following contents:

01 using System; 
02 using System.ComponentModel; 
03 using System.Globalization; 
04 using System.Web.UI; 
05 using System.Web.UI.WebControls; 
06 using System.Web.UI.WebControls.WebParts; 
07 using Microsoft.SharePoint.Administration; 
08   
09 namespace SPScenarioContextTest.DataWriterWebPart { 
10     [ToolboxItemAttribute(false)] 
11     public class DataWriterWebPart : WebPart { 
12         TextBox FirstName; 
13         TextBox LastName; 
14         IButtonControl SubmitButton; 
15         IButtonControl CancelButton; 
16   
17         protected override void CreateChildControls() { 
18             FirstName = new TextBox(); 
19             Controls.Add(FirstName); 
20   
21             LastName = new TextBox(); 
22             Controls.Add(LastName); 
23   
24             SubmitButton = new Button(); 
25             SubmitButton.Text = "Submit"; 
26             SubmitButton.Click += new EventHandler(SubmitButton_Click); 
27             Controls.Add((Button)SubmitButton); 
28   
29             CancelButton = new Button(); 
30             CancelButton.Text = "Cancel"; 
31             CancelButton.Click += new EventHandler(CancelButton_Click); 
32             Controls.Add((Button)CancelButton); 
33   
34             ChildControlsCreated = true; 
35        
36   
37         void CancelButton_Click(object sender, EventArgs e) { 
38             SPScenarioContext context = SPScenarioContext.GetContext(Page, CultureInfo.CurrentUICulture); 
39             if (context != null) { 
40                 context.TransitionToCancelPage(true); 
41            
42        
43   
44         void SubmitButton_Click(object sender, EventArgs e) { 
45             SPScenarioContext context = SPScenarioContext.GetContext(Page, CultureInfo.CurrentUICulture); 
46             if (context != null) { 
47                 context.PersistDataToSessionState(VisitorInfo.VisitorInfoPropertyName, new VisitorInfo { 
48                     FirstName = FirstName.Text, 
49                     LastName = LastName.Text 
50                 }); 
51                 context.TransitionToNextPage(true); 
52            
53        
54   
55         protected override void RenderContents(HtmlTextWriter writer) { 
56             new Label { 
57                 AssociatedControlID = FirstName.ID, 
58                 Text = "First Name:"
59             }.RenderControl(writer); 
60             FirstName.RenderControl(writer); 
61             writer.Write("<br />"); 
62             new Label { 
63                 AssociatedControlID = LastName.ID, 
64                 Text = "Last Name:"
65             }.RenderControl(writer); 
66             LastName.RenderControl(writer); 
67             writer.Write("<br />"); 
68             ((Button)SubmitButton).RenderControl(writer); 
69             ((Button)CancelButton).RenderControl(writer); 
70        
71    
72 }

As you have seen, our data entry control consists of two text boxes and two buttons. Those are defined in lines 12 – 15 and instantiated in the CreateChildControls method in lines 17 – 35. Both buttons are associated with event handlers (lines 26 and 31) which interact with the SharePoint Scenario framework.

After the visitor has entered his information and clicked the Save button, his data should be persisted and he should be redirected to the next page. To do this we first have to get a reference to the Scenario Context (line 45). Once we have it, we create an instance of the VisitorInfo class (lines 47 – 50) and store it in the Scenario Context (line 47). Finally we redirect the visitor to the next page of the form (line 51).

Should the visitor click the Cancel button then he should be redirected to the cancel page. We can do this by retrieving the Scenario context and redirecting the visitor to the Cancel page (line 40).

Step 4: Creating the data display form page

The last step is to create the data display form page which retrieves the previously entered data and displays it to the visitor.

2011-08-20-MultiPageForms-09.jpg

Add another Web Part to your SharePoint Project using the Web Part SPI Template. Name the Web Part DataReaderWebPart and paste the following contents:

01 using System; 
02 using System.ComponentModel; 
03 using System.Globalization; 
04 using System.Web.UI; 
05 using System.Web.UI.WebControls; 
06 using System.Web.UI.WebControls.WebParts; 
07 using Microsoft.SharePoint.Administration; 
08   
09 namespace SPScenarioContextTest.DataReaderWebPart { 
10     [ToolboxItemAttribute(false)] 
11     public class DataReaderWebPart : WebPart { 
12         IButtonControl FinishButton; 
13         IButtonControl CancelButton; 
14   
15         VisitorInfo visitorInfo; 
16   
17         protected override void CreateChildControls() { 
18             FinishButton = new Button(); 
19             FinishButton.Text = "Finish"; 
20             FinishButton.Click += new EventHandler(FinishButton_Click); 
21             Controls.Add((Button)FinishButton); 
22   
23             CancelButton = new Button(); 
24             CancelButton.Text = "Cancel"; 
25             CancelButton.Click += new EventHandler(CancelButton_Click); 
26             Controls.Add((Button)CancelButton); 
27   
28             ChildControlsCreated = true; 
29        
30   
31         void CancelButton_Click(object sender, EventArgs e) { 
32             SPScenarioContext context = SPScenarioContext.GetContext(Page, CultureInfo.CurrentUICulture); 
33             if (context != null) { 
34                 context.TransitionToCancelPage(true); 
35            
36        
37   
38         void FinishButton_Click(object sender, EventArgs e) { 
39             SPScenarioContext context = SPScenarioContext.GetContext(Page, CultureInfo.CurrentUICulture); 
40             if (context != null) { 
41                 context.TransitionToNextPage(true); 
42            
43        
44   
45         protected override void OnPreRender(EventArgs e) { 
46             base.OnPreRender(e); 
47   
48             SPScenarioContext context = SPScenarioContext.GetContext(Page, CultureInfo.CurrentUICulture); 
49             if (context != null) { 
50                 visitorInfo = context.RetrieveDataFromSessionState(VisitorInfo.VisitorInfoPropertyName) as VisitorInfo; 
51            
52        
53   
54         protected override void RenderContents(HtmlTextWriter writer) { 
55             if (visitorInfo != null) { 
56                 writer.Write("First Name: {0}<br/>", visitorInfo.FirstName); 
57                 writer.Write("Last Name: {0}<br/>", visitorInfo.LastName); 
58                 ((Button)FinishButton).RenderControl(writer); 
59                 ((Button)CancelButton).RenderControl(writer); 
60            
61        
62    
63 }

Just like the data entry control the data display control has two buttons (lines 12 and 13) which have to be instantiated (lines 17 – 29) and linked to event handlers (lines 20 and 25).

While loading the Web Part we try to retrieve previously stored data (lines 45 – 52). We first retrieve the Scenario Context (line 48) and try to retrieve the data (line 50). Later during the render stage (lines 54 – 61) we check if the data has been retrieved (line 55) and render it if possible (lines 56 and 57).

And that’s all! All you have to do now is to build & deploy your project, create a new subsite called Visitor Info (URL: /visitor-info), create Publishing Pages as specified in the Scenario Definition and add the Web Parts.

Important: While testing the form don’t forget to append ?scenarioId=visitorInfo to the URL. Otherwise the form won’t work correctly.

Summary

A common requirement when working with large forms is to split them into multiple steps. SharePoint 2010 provides the SharePoint Scenario framework which simplifies creating and working with multi-page forms. Using the SharePoint Scenario framework you can focus on building forms instead of implementing the plumbing for persisting data between pages. An additional benefit of using the Scenario framework is that you can separate form pages and turn them into building blocks that you can reuse across multiple forms.

Script to provision SharePoint 2010 publishing pages

In this post, I’d like to share a method of provisioning many publishing pages automatically using a PowerShell script.
Page Content

 

In this post, I’d like to share a method of provisioning many publishing pages automatically using a PowerShell script.

An example of when this is handy, is when you need to migrate old company press releases to an archive section. Another bulk provisioning scenario may be when you need to provision a site structure along with pages so end users populate the content on pages when they’re ready. You could do the same with a SharePoint 2010 solution package and deploy your pages as modules. In this example, however, we’ll just be using a script, so no deployment or down time required on the server while our provisioning happens.

I’ll assume you’re running afully configured development environment with SharePoint 2010 publishing infrastructure enabled.

I will be using http://www.contoso.com as my test URL, so if you’re using another host name – replace the respective variables in the script.

Open SharePoint 2010 Management Shell from the Start menu of your development server and execute the following script which provisions one publishing page to the root of your site:

01 $SiteUrl = "http://www.contoso.com"
02 $pageFileName = "TestPage.aspx"
03 Add-PSSnapin "Microsoft.SharePoint.Powershell"
04
05 $SPSite = Get-SPSite | Where-Object {$_.Url -eq $SiteUrl}
06   if($SPSite -ne $null)
07   {
08   $RootWeb = $SPSite.RootWeb
09   $pubWeb = [Microsoft.SharePoint.Publishing.PublishingWeb]::GetPublishingWeb($RootWeb)
10
11   $pageLayout = $pubWeb.GetAvailablePageLayouts() | Where-Object {$_.Title -eq "Blank Web Part page"}
12
13   $page = $pubWeb.GetPublishingPages().Add($pageFileName, $pageLayout)
14
15   $page.Title = $PressReleaseFileName
16   $pageItem = $page.ListItem
17   $pageItem["Comments"]="New page description"
18   $pageItem["PublishingContactName"]="Brad"
19   $pageItem["PublishingContactEmail"]="brads@contoso.com"
20   $page.Update()
21
22   $page=$pubWeb.GetPublishingPages() | Where-Object {$_.Name -eq $pageFileName}
23   $page.CheckOut()
24   $pageItem = $page.ListItem
25   $pageItem["Comments"]="Changed description"
26   $page.Update()
27
28   $page.CheckIn("Checked in by PowerShell script")
29   $page.listItem.File.Publish("Published by PowerShell script")
30   }
31 $SPSite.Dispose()

We start with defining variables and loading a PowerShell snap in.

Next, we get a hold of the current site and create an instance of a publishing site object to work with. We retrieve the Blank Web Part page page layout to create a new page with. Once created, we make changes to some of the page’s properties such as contact name of the creator, etc.

Next, we look at how you can get a hold of the existing page to make changes to it. In our case we connect to the same page instance, which looks a bit redundant.

Adding Items to the User/Welcome Menu in SharePoint 2010

Adding a new menu item to the SharePoint welcome/user menu is fairly straight forward.
Page Content

 

Adding a new menu item to the SharePoint welcome/user menu is fairly straight forward.

Thanks to Sohels blog for pointing me in the right direction.

First create a new empty SharePoint project in Visual Studio 2010, call it “MenuItemProject”.

2011-08-30-AddingItems-01.jpg

Deploy as a farm solution and click finish.

2011-08-30-AddingItems-02.jpg

Add a feature to the project. In the solutions explorer box, right click on the feature node and add feature.

2011-08-30-AddingItems-03.jpg

Also in the solution explorer, right click the top menuItemProject node and add > new item.

2011-08-30-AddingItems-04.jpg

Add an empty Element called “MenuAdditionElement”.

2011-08-30-AddingItems-05.jpg

Once added, open the menuAdditionElement > Elements.xml file.

2011-08-30-AddingItems-06.jpg

Replace the Elements.xml code with the following:

01 <?xml version="1.0" encoding="utf-8"?>
03    <CustomAction
04    Id="myCustomAction"
05    GroupId="PersonalActions"
06    Location="Microsoft.SharePoint.StandardMenu"
07    Sequence="1000"
08    Title="Google"
09    Description="Search away">
10      <UrlAction Url="http://www.google.com"/>
11    </CustomAction>
12 </Elements>

Save the Elements.xml file and then in the solutions explorer double click on the feature1.feature node.

2011-08-30-AddingItems-07.jpg

Ensure the MenuAddition Element is included in the items in the feature.

2011-08-30-AddingItems-08.jpg

Deploy the solution to your SharePoint server.

Once deployed, refresh your site home page and the new menu item should be visible.

2011-08-30-AddingItems-09.jpg

Using minified CSS and JavaScript files everywhere in SharePoint 2010

Find out how to leverage the extensibility capabilities of the SharePoint 2010 platform and support using minified CSS and JavaScript files everywhere.
Page Content

2011-11-27-UsingMinifiedCSS-01.jpg

In my previous post I showed you how you can automate minifying JavaScript and CSS files in Visual Studio 2010. As I also mentioned, SharePoint has limited support for using minified files. But just because something is not available out of the box doesn’t mean it’s not possible. Find out how to leverage the extensibility capabilities of the SharePoint 2010 platform and support using minified CSS and JavaScript files everywhere.

Minified files and the ScriptLink control

Using minified files is a great and easy technique for optimizing your SharePoint solution for performance. Out-of-the-box SharePoint 2010 provides the ScriptLink control which allows you to automatically switch between raw and minified versions of your files. However, it has two flaws. First of all it supports only JavaScript files and just as you can minify JavaScript files, you can also minify your CSS files. Another thing that you have to keep in mind, if you want to use the standard ScriptLink control with minified files, is that it works only for JavaScript files deployed to the LAYOUTS folder on the file system.

Using minified files everywhere

If you want to use minified CSS and JavaScript files everywhere you will need a solution other than the standard ScriptLink control. One solution that you could consider is creating a custom control that you would add to your Master Page and which would automatically switch between the raw and minified version of the asset file depending on the mode in which the Web Application is working. Such control could look as follows:

001 using System; 
002 using System.IO; 
003 using System.Web; 
004 using System.Web.UI; 
005 using Microsoft.SharePoint.Utilities; 
006   
007 namespace Mavention.SharePoint.Controls { 
008     public enum AssetType { 
009         CSS, 
010         JavaScript, 
011         Other 
012    
013   
014     public class AssetLinkControl : Control { 
015         public string Href { get; set; } 
016         public bool WithDebug { get; set; } 
017   
018         protected override void Render(HtmlTextWriter writer) { 
019             if (!String.IsNullOrEmpty(Href)) { 
020                 AssetType assetType = AssetType.Other; 
021                 string href = GetAssetUrl(Href, WithDebug, out assetType); 
022   
023                 switch (assetType) { 
024                     case AssetType.CSS: 
025                         RenderCssLink(href, writer); 
026                         break
027                     case AssetType.JavaScript: 
028                         RenderJsLink(href, writer); 
029                         break
030                
031            
032        
033   
034         public static string GetAssetUrl(string url, bool withDebug, out AssetType assetType) { 
035             string assetUrl = url; 
036             assetType = AssetType.Other; 
037   
038             if (!String.IsNullOrEmpty(assetUrl)) { 
039                 assetUrl = SPUtility.GetServerRelativeUrlFromPrefixedUrl(assetUrl); 
040                 assetType = GetAssetType(assetUrl); 
041   
042                 if (withDebug && HttpContext.Current.IsDebuggingEnabled) { 
043                     assetUrl = GetDebugUrl(assetUrl, assetType); 
044                
045            
046   
047             return assetUrl; 
048        
049   
050         private static AssetType GetAssetType(string url) { 
051             AssetType assetType = AssetType.Other; 
052   
053             if (!String.IsNullOrEmpty(url)) { 
054                 string extension = Path.GetExtension(url); 
055                 switch (extension) { 
056                     case ".css"
057                         assetType = AssetType.CSS; 
058                         break
059                     case ".js"
060                         assetType = AssetType.JavaScript; 
061                         break
062                
063            
064   
065             return assetType; 
066        
067   
068         private static string GetDebugUrl(string url, AssetType assetType) { 
069             string debugUrl = url; 
070   
071             if (!String.IsNullOrEmpty(debugUrl)) { 
072                 string extension = Path.GetExtension(debugUrl); 
073                 switch (assetType) { 
074                     case AssetType.CSS: 
075                         extension = ".css"
076                         break
077                     case AssetType.JavaScript: 
078                         extension = ".js"
079                         break
080                
081   
082                 debugUrl = String.Concat(debugUrl.Replace(extension, String.Format(".debug{0}", extension))); 
083            
084   
085             return debugUrl; 
086        
087   
088         private void RenderJsLink(string href, HtmlTextWriter writer) { 
089             writer.AddAttribute(HtmlTextWriterAttribute.Src, href); 
090             writer.RenderBeginTag(HtmlTextWriterTag.Script); 
091             writer.RenderEndTag(); 
092        
093   
094         private void RenderCssLink(string href, HtmlTextWriter writer) { 
095             writer.AddAttribute(HtmlTextWriterAttribute.Rel, "stylesheet"); 
096             writer.AddAttribute(HtmlTextWriterAttribute.Type, "text/css"); 
097             writer.AddAttribute(HtmlTextWriterAttribute.Href, href); 
098             writer.RenderBeginTag(HtmlTextWriterTag.Link); 
099             writer.RenderEndTag(); 
100        
101    
102 }

How it works

We start with retrieving the URL of the asset (line 21). To provide flexibility when defining URLs we first translate the URL to a server-relative URL by parsing tokens if applicable (line 39). Next we get the type of the asset that we are working with (line 40). While we could create two separate controls: one for CSS files and one for JavaScript files, in this sample we use one control for both types. The last step is to check whether the asset link points to an asset that is provided in two versions (raw and minified) and if the Web Application is in debug mode (line 42).

Using the WithDebug property allows us to use the AssetLinkControl both for referencing files that have already been minified (like the jQuery library) as well as files that we have created ourselves and that are available both as raw and minified files.

If the Web Application is in debug mode we convert the asset link to point to the debug version of the asset (line 43). Finally we return the URL and render it depending on the asset type (lines 23-30).

Using the AssetLinkControl

The AssetLinkControl can be used the best for declaratively registering assets in the Master Page and Page Layouts. To register an asset, all you have to do is to include the following snippet:

1 <Mavention:AssetLinkControl Href="~SiteCollection/Style Library/mavention/css/mavention.css" WithDebug="true" runat="server" />

Depending on the mode in which your Web Application is running, this control will either point to the minified or the debug version of the CSS file.

What the AssetLinkControl is not

Although the AssetLinkControl provides you with great flexibility, it is by no means a replacement for dynamically registering JavaScript or CSS files. The standard SharePoint 2010 CssRegistration and ScriptLink controls provide much more flexibility allowing you to determine how and when your assets should be loaded. Whenever you need to register a CSS or a JavaScript file on runtime you should use those standard controls instead. The great thing about how the AssetLinkControl is built, is that you can use its GetAssetUrl method to build a URL to your asset and then pass it to the standard SharePoint controls. This approach allows you to extend the standard functionality of the SharePoint platform with additional flexibility and optimization for performance.

Previous Older Entries