Using SPServices and jQuery to include social rating control on _layouts page

So, SharePoint 2010 comes with a lot of cool, new social features, including an option allowing users to rate content. Interesting stuff, as I had a requirement where I would need the possibility for users to rate articles (PDF documents) in a document library. The requirement consisted of having a page which is basically a consolidated view of the article itself and a few related social features, one of which being the social rating function. Whenever a user wishes to read the article, a dialog frame pops up with the aforementioned page.

I opted for using a _layouts based page which would take the ID of the article as a parameter to display the contents accordingly. Now, since SharePoint 2010 comes with a standard rating function, I figured I should use the out of the box control, to reduce the amount of customization.

The standard control used by SharePoint is the AverageRatingFieldControl class, residing in the Microsoft.SharePoint.Portal.WebControls namespace of assembly Microsoft.SharePoint.Portal.

I started out by adding the required tag prefix registration and by adding the control itself to the main placeholder of the page:

<%@ Register Tagprefix="SharePointPortal" Namespace="Microsoft.SharePoint.Portal.WebControls" Assembly="Microsoft.SharePoint.Portal, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
 
<SharePointPortal:AverageRatingFieldControl ID="PageRatingControl" FieldName="AverageRating" runat="server" ControlMode="Display" />

This would actually render the control, but I wasn’t able to actually rate the current item, nor would it display the current rating. That makes sense, as it has no way of knowing which item is supposed to be used. Using a disassembler I found out that the control operates under the assumption of apparently the list item context being available. However, as this is a _layouts-based page, neither the list or list item context is available, so that leaves us with an issue.

I decided to see if I could somehow manually include the script required for the control to work. Disclaimer: The following description feels a bit like a dirty hack, even though I got it to work in the end. The decision whether to actually use this, I leave entirely to you. I do intend to incorporate all of this in a custom web control, so it’ll be easier to use.

First of all, we need to JavaScript code required for the control. Add the following to the PlaceHolderAdditionalPageHead placeholder:

<script type="text/javascript">
function RatingsLoader_RatingsCtrl_<%= PageRatingControl.ClientID %>() {
	var ratingsData = new RatingsCommonData('<img src=\'/_layouts/images/loading16.gif\'> Loading...',
	'Average Rating',
	'Number of Ratings',
	'My Rating',
	'Thank you. Your rating has been submitted and will be processed soon.',
	'Not available',
	'Not rated yet',
	'Click to assign a rating (0-5)',
	'',
	'<%= this.WebId %>',
	'<%= this.WebUrl %>',
	'<%= this.SiteId %>',
	'\u002f_layouts\u002fImages\u002fRatingsNew.png',
	'\u002f_layouts\u002fImages\u002fRatingsEmpty.png');
 
	var ratingsControl = new RatingsControl('<%= this.ArticlePathEncoded %>', '<%= this.ArticleTitle %>', 'RatingsCtrl_<%= PageRatingControl.ClientID %>', ratingsData);
}
 
ExecuteOrDelayUntilScriptLoaded(RatingsLoader_RatingsCtrl_<%= PageRatingControl.ClientID %>, 'ratings.js');
</script>
 
<script src="/_layouts/ratings.js?rev=KT%2FEk3FDXaspEd0aPPHuhQ%3D%3D" type="text/javascript"></script

Note that I’m actually pulling a few properties from the code-behind of the page:

public string SiteId
{
    get { return SPContext.Current.Site.ID.ToString(); }
}
 
public string WebId
{
    get { return SPContext.Current.Web.ID.ToString(); }
}
 
public string WebUrl
{
    get { return SPHttpUtility.EcmaScriptStringLiteralEncode(SPHttpUtility.UrlPathEncode(SPContext.Current.Web.Url, true, true)); }
}

ArticleTitle, ArticlePath and ArticlePathEncoded are properties that respectively contain the values of the document title, full path to the PDF and encoded full path, like this:

ArticleTitle = article.Title;
ArticlePath = article.GetFullUrl(SPContext.Current.Web);
ArticlePathEncoded = SPHttpUtility.EcmaScriptStringLiteralEncode(SPHttpUtility.UrlPathEncode(ArticlePath, true, true));

When you load the page this time, rating the requested article should actually be possible now. Picking a rating will correctly register the value and it will be picked up the next time the rating synchronization job runs.

However, we’re not there yet. The value of the control is actually pulled from its underlying field type and this value also determines the CSS class which is rendered in the control. Since again we don’t have the corresponding item context, the control has no way of retrieving the required value.

This is were SPServices comes into play. With SPServices, we can utilize jQuery to access SharePoint’s web services, and in this case, the SocialDataService. This service has a method GetRatingAverageOnUrl, which returns the average social rating for a given URL.

Initially the call from SPServices would result in a service error: “Object reference not set to an instance of an object”. For a solution for this problem, please see my other blog post.

So, to get it to work, include the following JavaScript code after the previously mentioned code block (note: change the paths to the jQuery and SPServices libraries to your locations):

<script src="/_layouts/scripts/jQuery/jquery-1.6.2.min.js" type="text/javascript"></script>
<script src="/_layouts/scripts/SPServices/jquery.SPServices-0.6.2.min.js" type="text/javascript"></script>
 
<script type="text/javascript">
$(function () {
	$().SPServices({
		operation: "GetRatingAverageOnUrl",
		async: false,
		url: "<%= this.ArticlePath %>",
		completefunc: function (xData, status) {
			if(status != 'success') {
				return;
			}
 
			var result = $(xData.responseXML).find("[nodeName='Average']").first();
			var avg = parseFloat(result.text());
 
			$(".ms-currentRating img").addClass(getClassNameForRating(avg));
		}
	});
 
	function getClassNameForRating(rating) {
                if(isNaN(rating) || rating <= 0)
		        return "";
		if (rating < 0.25)
			return "ms-rating_0";
		if (rating < 0.75)
			return "ms-rating_0_5";
		if (rating < 1.25)
			return "ms-rating_1";
		if (rating < 1.75)
			return "ms-rating_1_5";
		if (rating < 2.25)
			return "ms-rating_2";
		if (rating < 2.75)
			return "ms-rating_2_5";
		if (rating < 3.25)
			return "ms-rating_3";
		if (rating < 3.75)
			return "ms-rating_3_5";
		if (rating < 4.25)
			return "ms-rating_4";
		if (rating < 4.75)
			return "ms-rating_4_5";
		else
			return "ms-rating_5";
	}
});
</script>

ArticlePath is again the property defined in the code-behind, like mentioned earlier. This should be the full path to the document. The value we need comes with the response XML of the web service call, in an element called <Average />.

The method getClassNameForRating(rating) takes a float value as a parameter and returns the corresponding CSS class. I actually pulled this code from the rating web control using a disassembler.

The last step now is to set the CSS class to the corresponding rating image, which is defined by the $(".ms-currentrating img") selector.

Reload the page and you should now see the appropriate amount of stars being displayed by the control!

The end result is a social panel like this:

Social panel in dialog frame

Error getting average ratings from social data web service?

Currently I’m working on an application that requires me to retrieve the average social rating for a given document library item. Despite having properly set up the User Profile service application, I would consistently run into the same error when I tried to either access the SocialRatingManager.GetAverage() method from a console application. The following is the stacktrace for this error:

System.NullReferenceException: Object reference not set to an instance of an object.
   at Microsoft.Office.Server.Administration.UserProfileApplicationProxy.get_ApplicationProperties()
   at Microsoft.Office.Server.Administration.UserProfileApplicationProxy.get_PartitionIDs()
   at Microsoft.Office.Server.Administration.UserProfileApplicationProxy.IsAvailable(SPServiceContext serviceContext)
   at Microsoft.Office.Server.SocialData.SocialDataManager..ctor(SPServiceContext serviceContext)
   at Microsoft.Office.Server.SocialData.SocialRatingManager..ctor(SPServiceContext serviceContext)
   at ConsoleTest.Program.Main(String[] args) in C:\Projects\ConsoleTest\ConsoleTest\Program.cs:line 38

In addition to attempting to run this from a console application, I also have a _layouts page on which I tried to get the same result, although getting it using SPServices through the Socal Data web service this time. Unfortunately to no avail, as I was getting pretty much the same error there as well (note that the stacktrace points to my console app, since I wanted to access the web service directly to rule out any potential issues in SPServices; there weren’t any, both tests resulted in the same error):

System.Web.Services.Protocols.SoapException: Server was unable to process request. ---> Object reference not set to an instance of an object.
   at System.Web.Services.Protocols.SoapHttpClientProtocol.ReadResponse(SoapClientMessage message, WebResponse response, Stream responseStream, Boolean asyncCall)
   at System.Web.Services.Protocols.SoapHttpClientProtocol.Invoke(String methodName, Object[] parameters)
   at ConsoleTest.SocialDataWS.SocialDataService.GetRatingAverageOnUrl(String url) in C:\Projects\ConsoleTest\ConsoleTest\Web References\SocialDataWS\Reference.cs:line 795
at ConsoleTest.Program.Main(String[] args) in C:\Projects\ConsoleTest\ConsoleTest\Program.cs:line 53

After a long investigation using dotPeek to go through the SharePoint assemblies, I noticed a call being made to a WCF service which attempts to initialize the profile property cache. Every time a call was made to this service, the following error would appear in the ULS log:

Exception occured while connecting to WCF endpoint: System.ServiceModel.Security.SecurityAccessDeniedException: Access is denied.

Weird, an “access denied” error even though my account is a farm administrator. Apparently, the service application has a different set of administrators and permissions, which you can set here:

  1. In Central Administration go to ‘Manage service applications’;
  2. Select the User Profile Service Application;
  3. In order to solve the SocialRatingManager error, I had to do the following:
    Click Permissions from the ribbon and add the account you’re using to run the console application, giving it ‘Full Control’
  4. To solve the web service (and SPServices) error, do the following:
    Click Administrators from the ribbon and add your account, giving it ‘Manage Social Data’ permissions.
These settings resolved my issues and I was able to correctly use the service from my page. Hope this helps anyone else running into the same vague errors with the social data service.

SharePoint 2007 to 2010 site upgrade: problems and solutions

Recently, our project was in the process of migrating an existing SharePoint 2007 sub-site with the publishing features enabled, to a separate site collection and convert it to SharePoint 2010 as well, with the use of a sharepoint project management system.

The plan was to use stsadm to create peer to peer texting of the sub-site, which in turn could be imported again on an acceptance environment in a newly created web application with separate content database. I’d make the necessary adjustments to get the site to work as a site collection and then the content database was to be copied to the SharePoint 2010 development environment. We chose this course of action, as the site was part of a much larger site collection, which didn’t need to be migrated to SharePoint 2010 as of this moment.

Now, I already did my fair share of reading up on the topic and it turns out, converting a sub-site into a site collection when the publishing features are enabled, isn’t exactly supported. To add on top of that the migration to SharePoint 2010, things were looking rather grim.

First of all, getting the export to be imported correctly on the acceptance environment took a few attempts (version numbers of SharePoint solutions didn’t match, Terminal Services sessions being terminated in the middle of an import (yes, that actually happened), imports that were successful according to the tool, but when the site content was checked, most of the lists and libraries didn’t actually contain any content). But after a while we had that up and running, so I could replace the missing page layouts and master pages, make the necessary changes to the web.config file, so the site was ready for migration to SharePoint 2010.

The migration process involved copying the content database to SharePoint 2010, attaching it to a newly created web application running the upgrade. Funnily enough, that actually ran without any significant issues. It was when I actually start working on the site, a whole array of bizarre errors was encountered. This blog post’s purpose is to outline these errors and provide the solutions I’ve found during the two days I’ve been banging my head against the desk trying to figure it out:

Invalid SPListItem. The SPListItem provided is not compatible with a Publishing Page.

I seemed to be getting this error after creating a new page and then saving and closing the page; it would then throw this error.

Solution: Deactivate the site “SharePoint Server Publishing” feature and activate it again.

One or more field types are not installed properly. Go to the list settings page to delete these fields.

This error would occur when I opened the “Site Content and Structure” page and also after attempting to deactive the “SharePoint Server Publishing” feature on the site (related to the error above).

Apparently this is caused by a missing field in the hidden Relationships List on the site. This list is created by Publishing Resources and is used to store variations relationships. Note that the error I mentioned didn’t actually have to do with variations, as I’m not even using that on the site.

The upgrade process should have (at least I assume so), created an additional column called GroupGuid in this list, but in my case it hadn’t done so. Unfortunately its field type is supposed to be Guid and this type cannot be selected from the user interface when creating a new column.

Solution:
Use this tool to export the list on a working site and import it again on the site having issues:

http://spdeploymentwizard.codeplex.com

Be sure to add the following entry to the application config, as the Relationships List is hidden by default.

<appSettings>
<add key="ListsNotForExport" value="Cache Profiles" />
</appSettings>

The “Cache Profiles” value is in there because the application seemed to ignore a blank value; it would then still hide the Relationships List:

Also, uncomment the section that mentions using the tool with SharePoint 2010.

Start the tool, and follow these steps:

  1. Select “Export” and enter the URL of the working site on the first page of the wizard and click Next;
  2. Locate “Relationships List” in the tree and right-click the item; select Export -> Include all descendants and click Next;
  3. On the ‘Export Settings’ page, I only filled out the ‘Export folder’ and base filename fields. Use settings appropriate to your situation and click Next;
  4. On the next page, click the ‘Finish’ button to start the export;
  5. Once completed, start the tool again but then with “Import” as action. Enter the URL of the non-working site and click Next;
  6. Select the import file created earlier, leave the other settings to their default values and click Next;
  7. On the next screen, click ‘Finish’ to start the import process.
  8. Go to the site that has problems and access the following URL:
    http://<site URL>/Relationships%20List/AllItems.aspx
  9. Delete the old item that was in the list. This should be the item that doesn’t have a value for the GroupGuid field;
  10. All done, the issue should now be resolved.

Error_pagetitle

An error occurred while getting the items.

List does not exist.

The page you selected contains a list that does not exist. It may have been deleted by another user.

I was getting this error when I opened the “Create list” dialog (the one with the Silverlight control on it). I was actually getting a similar error when deploying a solution from Visual Studio:

Error occurred in deployment step ‘Recycle IIS Application Pool’: The list does not exist.

Solution:
  1. Open up the SharePoint 2010 Management Shell
  2. Run the following cmdlet:
    get-spcontentdatabase
  3. Copy the ID of the database that you’re having problems with
  4. Then run this cmdlet:
    upgrade-spcontentdatabase -id <ID>
  5. That should resolve the issue.
Fixing the publishing page layout URLs when restoring from a subsite to a site collection
Since the site we migrated, originally was a sub-site of a larger site collection, the page layout URLs of existing pages needed to be updated to their respective new locations. After the migration, everything was still pointing to the old site collection’s master page gallery.
Solution:
Create a new console application (remember to set the target framework to .NET 3.5 and the platform target to ‘Any CPU’) and copy in the following code:
static void Main(string[] args)
{
    try
    {
        using (SPSite site = new SPSite(""))
        {
            using (var web = site.OpenWeb())
            {
                FixPageLayoutsUrl(web);
            }
        }
    }
}
 
private static void FixPageLayoutsUrl(SPWeb web)
{
    var spPubWeb = PublishingWeb.GetPublishingWeb(web);
    var pages = spPubWeb.PagesList;
 
    foreach (SPListItem item in pages.Items)
    {
        var pubPage = PublishingPage.GetPublishingPage(item);
 
        if (null == pubPage || null == pubPage.ListItem)
        {
            continue;
        }
 
        var url = new SPFieldUrlValue(Convert.ToString(pubPage.ListItem[FieldId.PageLayout]));
        const string urlPart = "/relative/url/tositecollection";
 
        if (url != null &amp;&amp; !String.IsNullOrEmpty(url.Url) &amp;&amp; url.Url.Contains(urlPart))
        {
            url.Url = url.Url.Replace(urlPart, "");
 
            Console.WriteLine(pubPage.Name);
            pubPage.CheckOut();
            pubPage.ListItem[FieldId.PageLayout] = url;
            pubPage.ListItem.UpdateOverwriteVersion();
 
            pubPage.ListItem.File.CheckIn("Fixed page layout URL.", SPCheckinType.MajorCheckIn);
        }
    }
}

Enter the URL of the site collection on which you’re having problems. Also, replace the urlPart part with a value appropriate to your situation. You may need to rewrite a part of the code there to make it work for your environment. The general idea is to replace the list item’s PageLayout column with the new URL.

Could not create a new page; the process in the dialog would hang and eventually result in a timeout

The same thing would happen when accessing “Page layouts and site templates” from the Site Settings page.

The problem may have been related to the fact that this was a migration from a sub-site to a site collection. The initial ite collection had its available page layouts restricted and this setting may have been included in the export, causing the problem.

Solution:

Run the following piece of code from a commandline utility and do an iisreset after:

static void Main(string[] args)
{
    try
    {
        using (SPSite site = new SPSite(""))
        {
            using (var web = site.OpenWeb())
            {
                var publishingWeb = PublishingWeb.GetPublishingWeb(web);
                publishingWeb.AllowAllPageLayouts(true);
                publishingWeb.Update();
            }
        }
    }
}

Replace the site collection URL with a value appropriate to your environment.

Critical entry in the event log when attempting to open the ‘Create list’ page in SharePoint

Insufficient SQL database permissions for user ‘Name: <UserID> SID: <SID> ImpersonationLevel: Impersonation’ in database ‘<database>’ on SQL Server instance ‘<instance>’. Additional error information from SQL Server is included below.

The EXECUTE permission was denied on the object ‘proc_GetProductVersions’, database ‘<database>’, schema ‘dbo’.

Solution:

  1. Open SQL Server Management Studio and connect to the proper server instance
  2. In the database server, expand the database on which you’re having problems and navigate to Programmability – Stored Procedures – dbo.proc_GetProductVersions.
  3. Open the stored procedure’s properties.
  4. On the new screen, select ‘Permissions’ on the left and click the Search button.
  5. On the new screen, click Search, select the [WSS_Content_Application_Pools] role and click OK.
  6. Click OK again.
  7. On the first popup screen, select the role, check ‘Execute’ permission and click OK.
Thinking about it, perhaps most of the problems might have been fixed after running the Powershell content database upgrade again, but I didn’t get a chance to test that separately.
I’ll try to update this post should I encounter any more issues. Hopefully this helps anyone who’s struggling with migrating a SharePoint 2007 to 2010.

Alternative to getting SP2010 to work in IE6

Since Microsoft does not support the use of IE6 in conjunction with SharePoint 2010, users still using this browser will have problems accessing the site. Google Chrome seems to generally be able to render SharePoint 2010 just fine and Google offers a plug-in for Internet Explorer which allows sites to be rendered in a Google Chrome Frame within Internet Explorer.

A new version of the plug-in is available, which no longer requires admin privileges in order for it to be installed. The plug-in can be found here.

In order for this to work, the following modifications have to be made to the master page of the site:

Add a tag to the <head> element

<meta http-equiv="X-UA-Compatible" content="chrome=1">

Disable unsupported browser warning (optional)

Locate the following tag: <SharePoint:WarnOnUnsupportedBrowsers runat="server" />

Change it this code: <SharePoint:WarnOnUnsupportedBrowsers runat="server" Visible="false"/>

When Internet Explorer users open the site, the Chrome Frame plug-in will load and render the page instead.

Now, your mileage may vary of course for being able to use this, but for when you’re in an organization that is still using Internet Explorer 6 and is either in the process of upgrading or doesn’t have any plans yet, this could be a simple alternative for the transition period instead of having to resort to thin-clients or MED-V.

Modify and repackage SharePoint STP files

There have been a few occassions where SharePoint site templates (STP files) we’ve been using in client environments suddenly would stop working after upgrading WSP files, notably after removing certain features from the WSPs. Whenever you’d create a new site based on such a template, you get an error saying “The template you have chosen is invalid or cannot be found”.

Now you’d think you could fix this by just creating a new STP file based on your template site, which then wouldn’t include the feature. However, I’ve encountered cases where this error would still occur, so you need to make sure to use the split pdf. There is a little trick you can use to remove the missing features from the STP file. This is also useful when you receive a non-working STP file from for example a client who has a different environment (extra features) than your own.

The idea is to extract the STP file, remove the missing features from the manifest and repackage the file. In order to do this, use the following steps.

Since an STP file is just a renamed cabinet file, you can just extract its contents by using a ZIP client. Copy the file to your target environment on which you’re trying to use the template.

Create a temporary folder anywhere on your system. Rename the file to .cab and extract the contents to the folder you just created. Navigate to the folder and open the manifest.xml file in a text editor.

Now, in the manifest file, you should see the following elements:

<WebFeatures>

<SiteFeatures> (I believe this one is optional though)

There should be a list of <Feature /> elements in each of these blocks. You can probably safely skip the features with GUIDs starting with 00bfea as these are SharePoint out-of-the-box features.

For any of the other feature GUIDs, you need to verify that they actually exist in your target environment. I’m not sure if there is any quick way to get an overview of all feature GUIDs in a farm, so here’s the convoluted way. Go to the following folder:

C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\12\TEMPLATE\FEATURES

For each of the feature GUIDs, search in *.xml files and use the following search phrase:

ID="{GUID}"

Where {GUID} is the GUID of the elements in the manifest file.

If you can’t find a hit for a search phrase, then this is one of the features that’s not available in the environment. Remove any of these feature elements from the manifest file and save the file when done for all of the feature elements.

Repeat this process if you do have a <SiteFeatures> element in your manifest file.

Now, it’s time to repackage the contents of the cabinet file into a new STP file. Windows comes with a utility called makecab that you can use to create a cabinet file from files in a directory. The only thing is, you need a DDF file as an input parameter.

In your temporary folder you created earlier, create a new text file and name it files.ddf and open this file in a text editor. Add the following text to the file:

.OPTION EXPLICIT
.Set CabinetNameTemplate=newtemplate.stp
.set DiskDirectoryTemplate=CDROM
.Set CompressionType=MSZIP
.Set UniqueFiles="ON"
.Set Cabinet=on
.Set DiskDirectory1=.
.Set CabinetFileCountThreshold=0
.Set FolderFileCountThreshold=0
.Set FolderSizeThreshold=0
.Set MaxCabinetSize=0
.Set MaxDiskFileCount=0
.Set MaxDiskSize=0

This will basically instruct makecab to construct a new file called newtemplate.stp with a couple of settings. The threshold and size parameters allow for cabinet files larget than 1.5MB.

Now we need to add the files to be included to the DDF file. Open a command prompt and go to your temporary folder. Execute the following command:

dir /b

This will print a bare list of the files in the directory, very easy to copy and paste into your DDF file. Be sure to only include the following files:

manifest.xml
*.000

Your DDF should now look something like this:

.OPTION EXPLICIT
.Set CabinetNameTemplate=newtemplate.stp
.set DiskDirectoryTemplate=CDROM
.Set CompressionType=MSZIP
.Set UniqueFiles="ON"
.Set Cabinet=on
.Set DiskDirectory1=.
.Set CabinetFileCountThreshold=0
.Set FolderFileCountThreshold=0
.Set FolderSizeThreshold=0
.Set MaxCabinetSize=0
.Set MaxDiskFileCount=0
.Set MaxDiskSize=0

00000000.000
01000000.000
10000000.000
11000000.000
15000000.000
21000000.000
25000000.000
31000000.000
35000000.000
40000000.000
50000000.000
60000000.000
70000000.000
90000000.000
b1000000.000
c0000000.000
c1000000.000
d0000000.000
f0000000.000
g0000000.000
h0000000.000
k0000000.000
p0000000.000
q0000000.000
r0000000.000
s0000000.000
u0000000.000
u4000000.000
v0000000.000
v4000000.000
w4000000.000
x4000000.000
z4000000.000
manifest.xml

Save the file and go back to your command prompt. Execute the following command:

makecab /f files.ddf

When it’s done, go back to the temporary file where you should now see your newtemplate.stp file.

Upload this file to your SharePoint site collection and test whether you can now create a new site based on this template. If all went well, your template should be working now!

Using the SP2010 Client Object Model to update a list item

In the previous post I wrote about having a custom ribbon button which handles a postback event. In the conclusion I also wrote that so far I haven’t been able to figure out how to retrieve the selected item when the postback occurs. So I have been looking at alternatives to implement what I want, and decided to go with the new client object model so that I don’t need a postback anymore.

First, we need to create a new empty SharePoint element in your SharePoint 2010 project in Visual Studio. The contents of the file is as following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <CustomAction Id="TicketResolvedRibbonCustomAction"
                RegistrationId="28582"
                RegistrationType="List"
                Location="CommandUI.Ribbon"
                Rights="ManageLists"
                Sequence="25"
                Title="Mark as Resolved">
    <CommandUIExtension>
      <CommandUIDefinitions>
        <CommandUIDefinition Location="Ribbon.ListItem.Actions.Controls._children">
          <Button Id="TicketResolvedRibbonButton"
                  Alt="Mark this ticket as resolved."
                  Description="Mark this ticket as resolved."
                  Sequence="25"
                  Command="ResolveTicketCommand"
                  Image32by32="/_layouts/images/Homburg.TicketDesk/ticketresolved.png"
                  LabelText="Mark as Resolved"                  
                  TemplateAlias="o1" />
        </CommandUIDefinition>
      </CommandUIDefinitions>
      <CommandUIHandlers>
        <CommandUIHandler Command="ResolveTicketCommand"
                          EnabledScript="javascript:
                             function enableResolveTicketButton()
                             {
                               var items = SP.ListOperation.Selection.getSelectedItems();
                               return (items.length == 1);
                             }
                             enableResolveTicketButton();"
                          CommandAction="javascript:TicketDeskMarkTicketAsResolved('{SelectedItemId}');" />
      </CommandUIHandlers>
    </CommandUIExtension>
  </CustomAction>
  <Control Id="AdditionalPageHead" ControlSrc="~/_controltemplates/TicketResolvedRibbonDelegate.ascx" Sequence="25"/>
</Elements>

So, compared to the element in my previous post, you’ll now notice that there is a CommandUIHandlers element and I’ve specified a couple of things. First, I want to make sure that the button is only available when an item is actually selected. That’s where the EnabledScript attribute is for.

The CommandAction attribute specifies the JavaScript method that will be called when the button is clicked. In this case, it’s a method that’s defined in the user control that’s linked at the bottom of the XML definition. Now you can use some predefined parameters in your method calls, one of them being the ID of the current selected item: {SelectedItemId}.

Next, we need the user control, so create a new user control in the SharePoint CONTROLTEMPLATES folder in Visual Studio. In this case, we won’t be needing anything in the code-behind file, everything occurs in the ASCX file itself.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<SharePoint:ScriptLink Name="SP.js" runat="server" LoadAfterUI="true" Localizable="false" />
<SharePoint:FormDigest ID="FormDigest1" runat="server" />
 
<script language="ecmascript" type="text/ecmascript">
var ticketsList;
var web;
var context;
var ticketId;
var ticketItem;
var ticketStatusField = "TicketStatus";
var ticketDoneStatus = "Done";
var ticketResolvedStatus = "OK";
 
function TicketDeskMarkTicketAsResolved(itemId)
{
    ticketId = itemId;
 
    context = new SP.ClientContext.get_current();
    web = context.get_web();
    ticketsList = web.get_lists().getByTitle("Tickets");
    ticketItem = ticketsList.getItemById(ticketId);
 
    // This will make sure the contents of the list and list item are actually loaded
    context.load(ticketsList);
    context.load(ticketItem);
    context.executeQueryAsync(OnTicketsListsLoaded);
}
 
function OnTicketsListsLoaded()
{
    var currentStatus = ticketItem.get_item(ticketStatusField);
 
    // There's no need to continue this method when the status is already set to resolved or the ticket has been completed.
    if (currentStatus == ticketResolvedStatus || currentStatus == ticketDoneStatus)
    {
        return;
    }
 
    // Set the ticket status field to the Resolved value
    ticketItem.set_item(ticketStatusField, ticketResolvedStatus);
    ticketItem.update();
 
    // Submit the query to the server
    context.load(ticketItem);
    context.executeQueryAsync(OnTicketUpdated, OnError);    
}
 
function OnTicketUpdated(args)
{
     // Nothing really needed here other than refreshing the page to see that the change has been made
    window.location.href = window.location.href;
}
 
function OnError(sender, args)
{
    alert(args.get_message());
}
</script>

Going through the code, we see a couple of things. First we need a reference to SP.js, so we can actually use the client object model. The FormDigest tag is there so we can perform an update of a list item.

I’m declaring a couple of variables so I can use those in the various methods I have. The first method is the one that’s being called from the XML element created in the first step.

In this method, I’m setting the context that’s used to retrieve the web and list we’re working with. Based on the ID that’s supplied, I’m loading the list item on which the update should be performed. The list and list item are then loaded in the context after which a query is executed asynchronously, to retrieve the data. A callback method is defined for when the query has completed.

In the callback method, I’m doing a couple of checks to see whether an update is required. Then, by using set_item(), you can specify the field name and the value to update an item. The context is loaded again with the updated item and the update query is submitted to the server.

This is a very simple example of how to use the SP2010 client object model to perform an update on a list item. This can easily be extended with more complex business logic, to suit other needs.

Ribbon buttons with postback in SP2010

For this Ticket Desk project I’m doing, I wanted to extend the default Ribbon UI with a new button that quickly allows the application admins to quickly mark a ticket as resolved. This is just a status field on the list item itself and you could even argue to just edit the item. However, since there’s a workflow involved and I want to hide the status field on the new and edit forms from ticket submitters, I’m providing the functionality via a ribbon button.

Now, in SharePoint 2007, I was used to doing something similar, but then I’d be using a custom action with a ControlAssembly and ControlClass specified. The class specified would just inherit from WebControl and I’d implement the IPostBackEventHandler interface and be on my way. I tried something similar in SP2010, however I got no dice. So, apparently things are a bit different in SP2010, let’s find out what exactly.

First, let me outline what we need to get this to work:

  • CustomAction element that defines the ribbon button
  • User control which will load the required JavaScript and handle the postback
  • JavaScript file containing the page component

Right, I’m going to assume you know how to set-up an SP2010 project in Visual Studio 2010 and how to add the different types of items. First, create a new Empty Element which will contain the following XML:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <CustomAction Id="TicketResolvedRibbonCustomAction"
                RegistrationId="28582"
                RegistrationType="List"
                Location="CommandUI.Ribbon"
                Rights="ManageLists"
                Sequence="25"
                Title="Mark as Resolved">
    <CommandUIExtension>
      <CommandUIDefinitions>
        <CommandUIDefinition Location="Ribbon.ListItem.Actions.Controls._children">
          <Button Id="TicketResolvedRibbonButton"
                  Alt="Mark this ticket as resolved."
                  Description="Mark this ticket as resolved."
                  Sequence="25"
                  Command="ResolveTicketCommand"
                  Image32by32="/_layouts/images/TicketDesk/ticketresolved.png"
                  LabelText="Mark as Resolved"                  
                  TemplateAlias="o1" />
        </CommandUIDefinition>
      </CommandUIDefinitions>
    </CommandUIExtension>
  </CustomAction>
  <Control Id="AdditionalPageHead" ControlSrc="~/_controltemplates/TicketResolvedRibbonDelegate.ascx" Sequence="25"/>
</Elements>

Basically, this XML element defines that we have a custom action, which is bound to a List with template ID “28582″. This is a custom list definition that’s deployed with my solution, but you can also bind this to a content type for example. Furthermore, the location is on the ribbon and the rights required here are ManageLists. The CommandUIDefinition element specifies that the location of the button will be the list item tab, in the Actions group. There is a command specified here as well, which we will later need in our code-behind of the user control.

Note that I’m not specifying any CommandUIHandlers here. This is not necessary since we’re handling the event on the server-side here, instead of client-side. So, instead of the handler, we define a user control that will be loaded in the AdditionalPageHead section of the masterpage. The location is linked to the default SharePoint CONTROLTEMPLATES folder, for which you can create a mapping in Visual Studio.

Next, we need the page component JavaScript file. Create a SharePoint mapped folder in Visual Studio which links to LAYOUTS, if you haven’t done that so far. In this folder, create a new JavaScript file called “PageComponent.js”. The contents of the file is as following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
function ULS_SP()
{
    if (ULS_SP.caller)
    {
        ULS_SP.caller.ULSTeamName = "Windows SharePoint Services 4";
        ULS_SP.caller.ULSFileName = "/_layouts/TicketDesk/PageComponent.js";
    }
}
 
Type.registerNamespace('RibbonCustomization');
 
// RibbonApp Page Component
RibbonCustomization.PageComponent = function ()
{
    ULS_SP();
    RibbonCustomization.PageComponent.initializeBase(this);
}
 
RibbonCustomization.PageComponent.initialize = function ()
{
    ULS_SP();
    ExecuteOrDelayUntilScriptLoaded(Function.createDelegate(null, RibbonCustomization.PageComponent.initializePageComponent), 'SP.Ribbon.js');
}
 
RibbonCustomization.PageComponent.initializePageComponent = function ()
{
    ULS_SP();
 
    var ribbonPageManager = SP.Ribbon.PageManager.get_instance();
 
    if (null !== ribbonPageManager)
    {
        ribbonPageManager.addPageComponent(RibbonCustomization.PageComponent.instance);
        ribbonPageManager.get_focusManager().requestFocusForComponent(RibbonCustomization.PageComponent.instance);
    }
}
 
RibbonCustomization.PageComponent.refreshRibbonStatus = function ()
{
    SP.Ribbon.PageManager.get_instance().get_commandDispatcher().executeCommand(Commands.CommandIds.ApplicationStateChanged, null);
}
 
RibbonCustomization.PageComponent.prototype = 
{
    getFocusedCommands: function ()
    {
        ULS_SP();
        return [];
    },
 
    getGlobalCommands: function ()
    {
        ULS_SP();
        return getGlobalCommands();
    },
 
    isFocusable: function ()
    {
        ULS_SP();
        return true;
    },
 
    receiveFocus: function ()
    {
        ULS_SP();
        return true;
    },
 
    yieldFocus: function ()
    {
        ULS_SP();
        return true;
    },
 
    canHandleCommand: function (commandId)
    {
        ULS_SP();
        return commandEnabled(commandId);
    },
 
    handleCommand: function (commandId, properties, sequence)
    {
        ULS_SP();
        return handleCommand(commandId, properties, sequence);
    }
}
 
// Register classes
RibbonCustomization.PageComponent.registerClass('RibbonCustomization.PageComponent', CUI.Page.PageComponent);
RibbonCustomization.PageComponent.instance = new RibbonCustomization.PageComponent();
 
// Notify waiting jobs
NotifyScriptLoadedAndExecuteWaitingJobs("/_layouts/TicketDesk/PageComponent.js");

Note that I’m referring to the “/_layouts/TicketDesk” location, since that’s the name of my project. You may need to change the location here to match your situation. From what I’ve gathered so far, this is object-oriented JavaScript that actually powers the ribbon button and can be considered as a template required for ribbon buttons. You can create your own implementations for the methods defined in the prototype, but in this case it’s not needed, so we’re going to leave the file as it is.

Now, on to the user control. In Visual Studio, create a mapping to the SharePoint CONTROLTEMPLATES folder. Add a new user control item in this folder. The ASCX file itself doesn’t require any contents, so we’re gonna go ahead and move to the source file.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
namespace TicketDesk.CONTROLTEMPLATES
{
    public partial class TicketResolvedRibbonDelegate : UserControl, IPostBackEventHandler
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            var commands = new List<IRibbonCommand>
                           {
                               // The command name here matches the command name of the ribbon button
                               new SPRibbonPostBackCommand("ResolveTicketCommand", this,
                                                           "TicketResolvedRibbonButtonEvent",
                                                           null, "true")
                           };
 
            // Register ribbon scripts
            var spRibbonScriptManager = new SPRibbonScriptManager();
 
            spRibbonScriptManager.RegisterGetCommandsFunction(Page, "getGlobalCommands", commands);
            spRibbonScriptManager.RegisterCommandEnabledFunction(Page, "canHandleCommand", commands);
            spRibbonScriptManager.RegisterHandleCommandFunction(Page, "handleCommand", commands);
 
            InitRibbonScriptManager(commands);
        }
 
        private void InitRibbonScriptManager(IEnumerable<IRibbonCommand> commands)
        {
            // Register scripts
            ScriptLink.RegisterScriptAfterUI(Page, "SP.Runtime.js", false, true);
            ScriptLink.RegisterScriptAfterUI(Page, "SP.js", false, true);
 
            // Register initialize function
            var manager = new SPRibbonScriptManager();
            var methodInfo = typeof(SPRibbonScriptManager).GetMethod("RegisterInitializeFunction", BindingFlags.Instance | BindingFlags.NonPublic);
 
            methodInfo.Invoke(manager,
                new object[]
                    {
                        Page,
                        "InitPageComponent",
                        "/_layouts/TicketDesk/PageComponent.js",
                        false,
                        "RibbonCustomization.PageComponent.initialize()"
                    });
 
            // Register ribbon scripts
            manager.RegisterGetCommandsFunction(Page, "getGlobalCommands", commands);
            manager.RegisterCommandEnabledFunction(Page, "commandEnabled", commands);
            manager.RegisterHandleCommandFunction(Page, "handleCommand", commands);
        }
 
        public void RaisePostBackEvent(string eventArgument)
        {
            Page.Response.Write(eventArgument);
        }
    }
}

Upon loading of the user control, we’re going to add a new ribbon command, of type SPRibbonPostBackCommand. The command name here matches the command name of the button specified in the XML element earlier. We’re also specifying an event ID in the constructor, so when the postback occurs, we can make sure we’re handling the proper event. You could also specify event arguments in the constructor here.

Next, we need to instantiate a new SPRibbonScriptManager object. We’re using that to register the JavaScript methods that we’ve defined in the previous step. Note that the function names here refer to methods in the JS prototype.

The page component file now needs to be registered to SharePoint. For a reason completely beyond me, the method required to do so, is not publically accessible through the API, so we’ll need reflection to that.

For sake of demonstration purposes, the RaisePostBackEvent method isn’t doing a lot here other than writing the event arguments to the page. Deploy your project and go to the list where your ribbon button should be visible and click it. You should now see that a postback is taking place and the event arguments should be written on top of the page.

The only thing I haven’t really figured out yet is how to actually pass the ID of the item you’ve selected, hopefully I’ll be able to write something about that in a follow-up post.

Open a list form dialog from the Quick Launch in SP2010

One of the new cool features to streamline the user interface in SharePoint 2010 is the use of dialog windows, for example when creating or editing list items. Right now I’m on a project for a client where the goal is to create a ticket desk application in SharePoint. You could also consider using this video chat api.

Users should be able to quickly create new tickets without too much clicking through the application. The usual process would be to first navigate to the list itself, click Items under List Tools in the ribbon, expand the New Item button and select the content type of the ticket they wish to create. So that’s 4 steps before they can actually do anything.

Initially I thought, I can add links in the Quick Launch for each ticket content type, so they can reach all options with only one click away. That works obviously, but a problem with this approach is that it just opens the page within the site, not in a dialog window and that’s exactly what makes it look and behave better.

In SharePoint 2010 you can open dialog windows with the following method, available in the client API:

SP.UI.ModalDialog.showModalDialog(options);

More information here: http://msdn.microsoft.com/en-us/library/ff410058.aspx

It takes an object of type SP.UI.DialogOptions as a parameter, which allows you to set the URL, width, height and title of the dialog for example.

So, my initial approach was creating a custom master page in SharePoint Designer, and including a JavaScript method that would take a URL and title as parameters, and then opens the dialog.

function openCreateTicketDialog(formUrl, title)
{
   var options = {
      url: formUrl,
      title: title
   };

   SP.UI.ModalDialog.showModalDialog(options);
}

Apparently, the URL field of a Quick Launch entry doesn’t necessarily have to be a valid URL. You could also include JavaScript in it, which is quite useful for what I’m trying to achieve here. So, for example, you could include the following, which will call the method defined above:

javascript:openCreateTicketDialog('/sites/ticketdesk/Lists/Tickets/NewForm.aspx?IsDlg=1&ContentTypeId=0x0100AE3C77B418BC4C0D9C895BB740C2D7BA0056DC51CECF114FD6B2A7DD64A66C0A45', 'Press Publication');

You see the URL being opened is the new item form of my list, and I’ve included a ContentTypeId parameter to open the proper form and I’ve included the IsDlg=1 parameter, which will open the form in dialog mode. After saving the Quick Launch item and clicking on the newly created link, it indeed opens the form as a dialog!

Now, a potential drawback of this approach is that it requires you to customize the master page. For a scenario where you want as little customisations as possible, you could also perform the following neat little trick, to do away with the new master page:

javascript:function tdql1(){SP.UI.ModalDialog.showModalDialog({url:'/sites/ticketdesk/Lists/Tickets/NewForm.aspx?IsDlg=1&ContentTypeId=0x0100AE3C77B418BC4C0D9C895BB740C2D7BA0056DC51CECF114FD6B2A7DD64A66C0A45', title:'Press Publication'})}tdql1();

What I did here is first declare a method that will actually make the call to the showModalDialog method. I’m using the JSON syntax to declare the object type that’s supplied as the parameter to the method. After the method’s been declared, we’re making the call to it. The reason I’m using a new method, is that when you call SP.UI.ModalDialog.showModalDialog() directly in the Quick Launch URL field, for some reason it just opens a blank page with [object Object()].

This may be a bit of a dirty trick, but it works very well when you want to open a dialog from your Quick Launch directly, without customisations. Do keep in mind that there is a limit to the amount of characters you can enter in the URL field, so the number of options you can supply will depend on the length of your arguments, and vice-versa.

Enjoy!

SharePoint 2010 Console Application Quirks

So, in order to get the schema XML of some field I’ve created on my SharePoint 2010 site, I decided to write a quick console application to retrieve the data.I quickly discovered that apparently some things have changed regarding how to use a console application with SP2010. I figured the code would pretty much be the same as it was in 2007:

const string siteUrl = "http://spfoundation/sites/testsite";

using (SPSite site = new SPSite(siteUrl))
{
   using (SPWeb web = site.OpenWeb())
   {
      SPList list = web.Lists["Test"];
      SPField field = list.Fields["Languages"];

Console.WriteLine(field.SchemaXml);
   }
}

Running this code however, quickly resulted in an error:

The Web application at http://spfoundation/sites/testsite could not be found.

Surely I didn’t make a typo, as I copied the URL from the browser and it works over there. After some looking around for what to do, I figured out there are a couple of things you need to change:

  • You need to run Visual Studio 2010 as an administrator.*
  • The target framework of your application needs to be “.NET Framework 3.5″
  • Because SP2010 is a 64-bit application, the platform target of your application needs to be 64-bit as well. The setting “Any CPU” seems to be working as well.

(*) I installed SP2010 on a workstation running on Windows 7 Professional, so this requirement may be different for scenarios where your Visual Studio is running on a server with SP2010.

SharePoint 2010 Forms Based Authentication Error

I’m currently working at a client who’s wish is to have a SharePoint 2010 site with forms based authentication, to manage the problem of the different locations having different domains.

So, I found this really excellent guide written Donal Conlon, over here: http://donalconlon.wordpress.com/2010/02/23/configuring-forms-base-authentication-for-sharepoint-2010-using-iis7/

I followed every step of it, including the little caveat, which was to reset the default provider for .NET Users to the SharePoint Claims Provider.

However, when I logged on to my site, I was presented with a very interesting exception:

<nativehr>0x8107058a</nativehr><nativestack></nativestack>Operation is not valid due to the current state of the object.

I Google’d the error message and came up with a couple of FBA-related posts, but no real solution. Having followed all the steps in the guide, I wasn’t really sure what was going on exactly. Then I remembered the caveat, and that I initially made a change to the default role provider as well.

So I went back to IIS, changed the default role provider back to the claims authentication one, named “c” and tried the site again. Et voila; there it was, working with forms based authentication!

I didn’t see this mentioned in the guide, so I thought I’d write it down, maybe it helps someone else. Although thinking about it, it makes sense to have to change the role provider back as well, it didn’t occur to me initially :)