Integrating JQuery into SharePoint

I finally had a chance to play with JQuery in the SharePoint environment.

Integrating JQuery is easy. You have two choices: Upload the JQuery api to a document library or put a copy in the 12 hive /TEMPLATE/Layouts folder.

On my development environment I put it in the 12 hive layouts folder. \Layouts\JQuery\

I also installed a JQuery plug-in called Lightbox to have something fun to play with.

JQuery allows the selection of objects in the document object model without having to traverse the DOM to find controls on the page.

For example to select all text boxes on a page and set the font weight to bold use the following line:
$(":text").css("font-weight", "bold");

That’s it --- I can’t wait till my next branding assignment.

With a Content Editor WebPart a JQuery plug-in called “lightbox” and a little script you can have a nice way to browse images without the use of Ajax. Users can click on thumbnails and be able to view and navigate through a list of images.



Content Editor Web Part Script:



<head>
<link rel="stylesheet" type="text/css" href="_layouts/jquery/lightbox/css/jquery.lightbox-0.5.css" media="screen" />
</head>
<div id="gallery">
<a href="_layouts/jquery/lightbox/photos/image1.jpg" title="Image1">
<img src="_layouts/jquery/lightbox/photos/thumb_image1.jpg" width="72" height="72" alt="" />

</a>
<a href="_layouts/jquery/lightbox/photos/image2.jpg" title="Image2">
<img src="_layouts/jquery/lightbox/photos/thumb_image2.jpg" width="72" height="72" alt="" />
</a>

</div>

<script type="text/javascript" src="_layouts/jquery/lightbox/js/jquery.js"></script>
<script type="text/javascript" src="_layouts/jquery/lightbox/js/jquery.lightbox-0.5.js"></script>
<script type="text/javascript">
$(function() {
$('#gallery a').lightBox({
overlayBgColor: 'gray',
overlayOpacity: 0.6,
imageLoading: '_layouts/jquery/lightbox/images/lightbox-ico-loading.gif',
imageBtnClose: '_layouts/jquery/lightbox/images/lightbox-btn-close.gif',
imageBtnPrev: '_layouts/jquery/lightbox/images/lightbox-btn-prev.gif',
imageBtnNext: '_layouts/jquery/lightbox/images/lightbox-btn-next.gif',
containerResizeSpeed: 350,
txtImage: 'Sample Image',
txtOf: 'Sample Lightbox Image'
});
});

</script>






The next edition of Visual Studio will have intellisense built-in for JQuery. For Visual Studio 2008 go here. http://weblogs.asp.net/scottgu/archive/2008/11/21/jquery-intellisense-in-vs-2008.aspx


REFERENCES:


http://philwicklund.com/archive/2009/04/20/an-introduction-to-jquery-for-sharepoint-developers.aspx

http://philwicklund.com/pages/LightboxExample.aspx

http://leandrovieira.com/projects/jquery/lightbox/

http://docs.jquery.com/Downloading_jQuery

http://docs.jquery.com/Tutorials

'Cannot complete this action. Please try again.' when calling SPField.Update() - One solution

I started writing some code for a new SharePoint Feature today and quickly began running into the error:

Cannot complete this action. Please try again.


...on calls to SPList.Update() and SPField.Update(). It appeared to only occur when updating some properties of the SPList and SPField instances and not others.

Most articles I found regarding this error referenced this MS Article. However, that didn't resolve my issue. Neither did setting "AllowUnsafeUpdates" = true on the SPSite and SPWeb objects.

Upon further review of my SharePoint logs, I noticed several instances of the following error message:

Failed to find the parent content type ID="ct-1033-0x01010901004105bb8902fb3f459d93b77e921ce17e" for content type ID="ct-1033-0x01010901004105bb8902fb3f459d93b77e921ce17e00d2c19f98b90dd14eb77883ca6d513d3f"


Turns out, in a separate Feature I had worked on a few days earlier, I had an invalid content type definition (as shown above). I had deactivated this feature in my test site, the content type was no longer in the Site Content Types list, and the test list I was running SPField.Update() against didn't even use this content type. However, for some reason, calls to SPField.Update() (and sometimes SPList.Update()) were still getting blocked by this bad content type. If anyone can explain why, please comment.

I corrected the offending Content Type and everything started working.

Implementing Vanity URLs in SharePoint with the SharePoint URL Redirector

I've updated the CodePlex site for the SharePoint URL Redirector with instructions on how to use it to implement vanity URLs. It's maybe not what I would call full 'vanity URLs' because it doesn't maintain the vanity URL for the duration of the user's session, but it may be the next best thing.

The Word document on the CodePlex site provides detailed instructions, but the basic steps are as follows:
  1. Create a new Web Site that uses a "vanity" host header. Setup the site for anonymous access and a default page of "default.aspx".
  2. In the root folder of your site, add a web.config file, a blank default.aspx file, and a folder named "_app_bin".
  3. Open web.config and paste the following into it:
    <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
    <configuration>
    <system.web>
    <authorization>
    <allow users="*"/>
    </authorization>
    <httpmodules>
    <add type="RDA.SharePoint.UrlMapper, RDA.SharePoint.URLRedirection, Version=1.0.0.0, Culture=neutral, PublicKeyToken=4e209b836e3b9268" name="UrlMapper"/>
    </httpmodules>
    </system.web>
    </configuration>
  4. Under the _app_bin folder, create a file named 'UrlMappings.xml'. Paste the following into it:
    <?xml version="1.0" encoding="utf-8"?>
    <UrlMappings xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
    <UrlMapping ID="4D81994E-2974-46f7-BD30-882BBD048133" Name="SharePoint Vanity" IsActive="true" Order="1">
    <OriginalUrl>http://vanity.sharepoint.local($|/(?&lt;rest&gt;[^$]*))</OriginalUrl>
    <DestinationUrl>http://depts.sharepoint.local/</DestinationUrl>
    </UrlMapping>
    </UrlMappings>

    Then edit the above XML to replace the items marked in yellow. The first is just a GUID. The second is the "vanity" URL to be captured and redirected. The third is the destination URL (the real SharePoint URL). Remeber to use proper placeholders for special XML characters. Ex: &lt;, &gt;, etc...

  5. Repeat the above steps for other web servers if you're load balancing
  6. Create a DNS entry or Host file entry for your vanity URL and you're ready to go!


If you run into trouble, check out the detailed documentation on the CodePlex site here: SharePoint URL Redirector

Have fun!

ARCast.TV: Greg Galipeau on Customizing SharePoint solutions

In this interview, Greg Galipeau discusses with Zhiming Xue about customizing Microsoft Office SharePoint Server (MOSS) based solutions. With demos Greg walks through each of the three customization options -- using the SharePoint Designer tool, using the site templaadmin function inside a SharePoint site, and creating custom site definitions with Visual Studio and other tools. He further talks about what developers can do with web parts, asp.net web applications and workflow activities while customizing SharePoint solutions. Through his consulting experience he shares his thoughts on how to address potential SharePoint customization issues that customers often run into.

Please follow this link to view the video: Galipeau on Customizing SharePoint

Coexistence of Report Viewer Versions 8.x and 9.x in SharePoint

In one of our clients' SharePoint environments, both Report Viewer version 8.x and Report Viewer 9.x were installed. The assemblies can live together in the Global Assembly Cache (GAC) because of the different versions. The development team had created a custom reporting interface solution using the Report Viewer 9.x version(s) of the assemblies and controls. In order for the pages to render properly, an entry under HTTP Handlers within the web configuration (web.config) was required:

<add verb="*" path="Reserved.ReportViewerWebControl.axd" type="Microsoft.Reporting.WebForms.HttpHandler, Microsoft.ReportViewer.WebForms, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />


This was fine. However, the SharePoint usage report page would not render because it requires the Version 8.0.0.0 HTTP Handler entry. The error message actually stated that that the line was missing. It wasn't missing, it was in there:


<add verb="*" path="Reserved.ReportViewerWebControl.axd" type="Microsoft.Reporting.WebForms.HttpHandler, Microsoft.ReportViewer.WebForms, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />


It came first in the HTTP Handler section and we quickly realized that the last entry for the same path wins the battle. There seems to be no way of having two HTTP Handlers for the same path (which make sense). So in our case, either the custom reporting solution was broken or the Site Usage reports would not work; this would not be acceptable for long.

I needed to find away to have both Version 8.0.0.0 and Version 9.0.0.0 work within the same web application. I tried various configuration settings and "hacks" in attempt to get it to work until finally I noticed something in the Reporting Services web config. There was some sort of an assembly redirect to tell IIS (ultimately) to use a different version of an assembly. It wasn't the assembly I was dealing with but I figured I may be able to use the same approach. Therefore, within the SharePoint web.config for port 80, I entered the following within the <runtime> <assembly binding> section under the <system.web> settings :

<dependentAssembly>

<assemblyIdentity name="Microsoft.ReportViewer.WebForms" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />

<bindingRedirect oldVersion="8.0.0.0"

newVersion="9.0.0.0"/>

</dependentAssembly>

Essentially, this was telling SharePoint that "if you are looking for the 8.0.0.0 version, use the 9.0.0.0 instead". This allowed the Site Usage page to at least render! It rendered with all of the web parts however the web parts all contained the same error message. They were now looking for the 9.0.0.0 version of the Microsoft.ReportViewer.ProcessingObjectModel assembly which didn't exist. I looked in the GAC and there was only the Version 8.0.0.0 in there. So I figured this was probably deprecated with the latest Report Viewer updates for SharePoint.

So I decided to be tricky and use the same redirect for the Microsoft.ReportViewer.ProcessingObjectModel assembly but using the opposite version settings:

<dependentAssembly>

<assemblyIdentity name="Microsoft.ReportViewer.ProcessingObjectModel" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />

<bindingRedirect oldVersion="9.0.0.0"

newVersion="8.0.0.0"/>

</dependentAssembly>

This actually worked! The Site Usage reports rendered and the development team's custom reporting interfaces were still functioning. We thought we were going to have to apply MOSS 2007 SP2 and/or SQL Server SP3 and worse case open a case ticket with Microsoft Support - not anymore!

SharePoint URL Redirector available for download on CodePlex

On CodePlex: http://rdacollaboration.codeplex.com/Release/ProjectReleases.aspx?ReleaseId=28073

So, sometime last year, RDA engaged in a project with a client to migrate a SharePoint 2003 environment to SharePoint 2007. After some initial planning, we determined that a one-time content database migration was going to be the best approach. However, we had a few complications to consider:
  1. When using this upgrade path, SharePoint 2007 takes some liberties with the URL structure of the sites once the databases are attached to the 2007 environment and upgraded. For example, the familiar "C" addresses of 2003 (Ex: http:\\sharepoint2003\C10\HR) were converted to "Topics" (Ex: http:\\sharepoint2003\Topics\HR).
  2. The client was not incredibly happy with their existing site taxonomy in general and wanted to take the opportunity to rearrange their sites as part of the upgrade.

The problem: The project would be considered a failure by the user community if everyone lost their IE favorites and links already embedded in content. We needed a solution to redirect URL requests from the old URL schema to the new locations.

Todd Klindt has an excellent article on different options for implementing redirection in SharePoint. Check it out here: http://www.toddklindt.com/blog/Lists/Posts/Post.aspx?ID=48

Upon reviewing the various options, I decided that none of the techniques would really work for the size and scope of what was needed. We needed:

  1. A way to define redirection farm-wide - for hundreds of sites. Not just one-off rules for redirecting single pages or sites.
  2. A way to redirect all URLs for ALL content under a site - not just the first page of the site. If the user linked to an image 15 folders deep in a site, that link should still function even if the site is moved or renamed.
  3. Immediate redirection - no 5 second wait for redirection pages. Ideally, the user shouldn't even realize they've been redirected.
So, I decided to build the SharePoint URL Redirector. I've released a binary, documentation and source code on CodePlex here:

http://rdacollaboration.codeplex.com/Release/ProjectReleases.aspx?ReleaseId=28073

The SharePoint URL Redirector features:
  • Regular Expression based rules - Permits massive flexibility in redirecting content requests. Entire sites of content to be redirected to other locations in a single rule. Not just single pages.
  • Test Driven Design - Built-in testing interface simplifies the design and upkeep of redirection rules. Immediately see conflicting rules or URLs that are no longer valid.
  • Centralized Rule Management - UI for maintaining rules is integrated into SharePoint Central Administration.
  • Immediate Redirection - Redirection is immediate. Users typically don't even notice that they've been redirected from their original URL. No annoying "You'll be redirected in 5 seconds" pages.
  • Scalable - Creatively caches rules and translations to maintain page-load performance. Has been tested with hundreds of redirection rules utilized with no perceived difference in page load times.
  • Ability to copy rules - Rules can easily be copied to other SharePoint web applications.
  • Chainable - Complex redirection can be performed by chaining multiple rules.


In the end, using this solution, we successfully provided seamless redirection for all content in over 350 sites (and many many more sub-sites). Most user's have never noticed a difference. Since then, several other RDA'ers have utilized it on their projects for other reasons such as:

  • Accommodating Site Moves - If you ever have to relocate a site to a new site collection because it's gotten too large and needs its own content databases, you can redirect the old URL scheme to the new one. No need to have to explain to users why you moved their site and broke their IE favorites.
  • Fill in holes in the URL scheme of your site - For example, if all of your sites are located under http://company.com/sites/* (a wildcard managed path), then there is no page directly under http://company/sites. Redirect that URL to a site directory or home page for a better overall experience for users of your environment.
  • Vanity URLs - Configured properly, you could setup a vanity URL at http://finance.company.com that redirects the user to the real site at http://depts.company.com/sites/finance. Sometime soon, I'll post an article on exactly how to do this.

So, please check it out. I highly suggest reading the documentation posted on CodePlex with the tool, and utilizing the test-driven approach to defining rules. It's really fun to see the green indicator light up after you successfully define and test a rule! Also, if you're unfamiliar with Regular Expression syntax, a pretty good overview can be found here: http://www.codeproject.com/KB/dotnet/regextutorial.aspx

VB.Net Support

Up to Version 1.04, WSP Builder was only compatible with C#. This version now adds compatibility for the VB.Net language. Enterprises that are working with VB.Net now can reap the rewards of using WSPBuilder without having to code in C#.

Worflow Foundation EventHandlingScope Activity

This post briefly describes the EventHandlingScope activity in Microsoft Workflow Foundation.

The EventHandlingScopeActivity is a composite activity that can contain multiple event handlers as well as a child activity. The child activity will run concurrently with the defined EventDrivenActivity activities. The event handlers may be triggered more than once and in any order.

If you were to put a Sequence Activity in a EventHandlingScope activity the Sequence will run in parallel with the defined event handlers. The child activity can run to completion without an event being fired.

There are four views in the EventHandlingScope Activity

1. EventHandlingScope - This contains the child Activity - usually a Sequence or Parallel activity.
2. View Cancel Handler
3. View Fault Handlers
4. View Event Handlers - EventDriven activities will go here.

Multiple Event Handlers can be added to the Event Handlers of a EventHandlingScope activity.


The EventDriven activity in turn functions as a container for child activities.
Control will be passed from the EventHandlingScope activity to the workflow only when the child activity has completed and every triggered event handler has been completed.



For what purpose could you use an EventHandlingScopeActivity?
Scenarios:
1. Use a delay activity in a EventDriven activity to put a time constraint on the child activity of the EventHandlingScope activity. You can terminate the workflow, send a reminder email, release control to the workflow etc..
2. The EventHandlingScope activity is used with SharePoint Workflow Modifications. Here is a link to a WebCast on how to build Workflow Modifications in SharePoint


3. You can use EventDriven activities to trigger other EventDriven activites. This could allow one to jump from one EventDriven activity to another based on conditions.

Redirect User After Submission of New or EDIT List Form in SharePoint

I recently had the requirement to redirected uses back to a MOSS sites main page after creating a new list item or editing an existing one.  The default behavior in MOSS is to you are redirect back to the list.  Thankfully MOSS embeds the URL that will be used after clicking the OK button in the URL of the new or edit form.  This URL parameter is called Source.  Knowing this it was possible to write a JavaScript function that could be added to the ASPX page to either update or insert this value associated with the parameter.

Here is the function originally written by another RDA Consultant Viktor Dolezel and modified by myself to work in additional cases. 

<script type="text/javascript">

_spBodyOnLoadFunctionNames.push("redirectAfterSave");

function redirectAfterSave() {
var newUrl = "%2Fsite%2Fdefault%2Easpx";

//parse the form action query string into a map
var qs = document.forms[0].action.substring(1, document.forms[0].action.length);
var args = qs.split("&");
var vals = new Object();
for (var i=0; i < args.length; i++) {
var nameVal = args[i].split("=");
vals[nameVal[0].toLowerCase()] = nameVal[1].toLowerCase();
}

//if source doesn't exists in the original form action string, add it,
//otherwise replace it
if (!vals["source"]) {

if(args.length > 1) {
document.forms[0].action+= "&";
} else {
document.forms[0].action+= "?";
}
document.forms[0].action += "Source=" + newUrl;

} else {
document.forms[0].action = document.forms[0].action.toLowerCase().replace(
"source=" + vals["source"],
"Source=" + newUrl );
}
}
</script>




I manually replaced the characters / and . in my URL with %2F and %2E respectively.  Also I did not have to provide the full URL just the name of the site and MOSS filled in the remainder of the URL



On word of caution, it is against best practices to directly edit the ASPX pages for out of the box list.  So use this post at your own risk in those cases.  In my case we had created custom list definitions so this did not apply to us.

Hosting and Updating Excel Documents in a Custom Task Form

I was recently developing a workflow that required users to review an Excel spreadsheet and provide some feedback. It was actually a little more complex than that but you get the idea. Rather than use the default task form with a link to the spreadsheet, we decided to embed the spreadsheet directly into the custom ASPX task form. I won’t go into the details of creating a deploying a custom ASPX task form (that’s been covered already) but adding in the Excel Web Renderer turned out to be fairly simple.

First, setup a trusted file location in Excel Services for the document library where you plan to save the spreadsheets. Next, In your ASPX file, reference the appropriate assembly:

<%@ Register Tagprefix="ExcelWeb" Namespace="Microsoft.Office.Excel.WebUI" Assembly="Microsoft.Office.Excel.WebUI, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>

Then you can add the control at the appropriate location on the form:

<ExcelWeb:ExcelWebRenderer id="excelWebRenderer" runat="server" WebPart="true" __WebPartId="{4F1D37E5-354C-4818-9A29-6B202A1FA56E}"> 
    <WebPart xmlns:xsi="
http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://schemas.microsoft.com/WebPart/v2">
        <Title>$Resources:xlsrv,dwp-title;</Title>
        <Description>$Resources:xlsrv,dwp-description;</Description>
        <FrameType>None</FrameType>
        <IsIncluded>true</IsIncluded>
        <PartOrder>1</PartOrder>
        <FrameState>Normal</FrameState>
        <AllowRemove>false</AllowRemove> 
        <AllowZoneChange>false</AllowZoneChange>
        <AllowMinimize>false</AllowMinimize>
        <IsVisible>true</IsVisible>
        <DetailLink />
        <HelpLink />
        <Dir>Default</Dir>
        <PartImageSmall />
        <MissingAssembly /> 
        <PartImageSmall>/_layouts/images/ewr023.gif</PartImageSmall>
        <PartImageLarge>/_layouts/images/ewr055.gif</PartImageLarge>
        <IsIncludedFilter />
        <Height>350</Height>
        <Width>100%</Width>
        <ExportControlledProperties>false</ExportControlledProperties>
        <ConnectionID>00000000-0000-0000-0000-000000000000</ConnectionID>
        <AutomaticPeriodicDataRefresh xmlns="Microsoft.Office.Excel.WebUI">Optional</AutomaticPeriodicDataRefresh>
    </WebPart>
</ExcelWeb:ExcelWebRenderer>

Define a variable in the code behind class for the ASPX form to refer to the Excel Web Renderer control:

Protected Microsoft.Office.Excel.WebUI.ExcelWebRenderer excelWebRenderer;

You can then load the spreadsheet in the OnLoad of the code behind:

SPListItem task = null;
string strListID = Request.QueryString["List"];
SPList List = Web.Lists[new Guid(strListID)];
task = List.GetItemById(Convert.ToInt32(Request.Params["ID"]));
string workbookUri = m_task.Web.Url + "/" + workflow.ParentItem.File.Url;
excelWebRenderer.WorkbookUri = workbookUri;

 

clip_image001

Since the embedded document is read-only the user will not be able to make changes. In our scenario we did want to gather a single comment and place it into the original spreadsheet. This is possible directly from the workflow code. Make sure you have a web reference added to Excel Services (http://servier/site/_vti_bin/ExcelService.asmx) and use the following code in the OnTaskCompleted or OnTaskChanged event of your workflow:

SPFile excelFile = this.workflowProperties.Item.File;
excelService.ExcelService svc = new ExcelService.ExcelService();
ExcelService.Status[] stats;
string workbook = this.workflowProperties.Web.Url + "/" + this.workflowProperties.Item.File.Url;
string sessionId = svc.OpenWorkbook(workboook, String.Empty, String.Empty, out stats);

object[] values = new object[1];
object[] currentRow = new object[3];
currentRow[0] = onWssTaskChanged_AfterProperties1.AssignedTo;
currentRow[1] = "'" + DateTime.Now.ToShortDateString();
currentRow[2] = onWssTaskChanged_AfterProperties1.ExtendedProperties[SPBuiltInFieldId.Comments].ToString();
values[0] = currentRow;

//If expecting feeback from multiple users, keep a count and increment the last two parameters of SetRangeA1 for each user.
svc.SetRangeA1(sessionId, "Review Comments", string.Format("A{0}:C{1}", 2, 2), values);
byte[] workbook = svc.GetWorkbook(sessionId, ExcelService.WorkbookType.FullWorkbook, out stats);

excelFile.CheckOut();
excelFile.SaveBinary(workbook, false);
excelFile.Update();
excelFile.CheckIn("Update with comments from " + task.TaskCompletedBy.DisplayName);

svc.CloseWorkbook(sessionId);

This example places some data (the comments entered on the task form) into a sheet named “Review Comments” on the original workbook.  When the workflow is complete, the feedback from all users is nicely formatted on a second sheet in the workbook for historical purposes.  Not something that is useful in every review scenario but a nice trick to be aware of.