The Problem
When you add a Lookup column in SharePoint 2010, the default link in the view form that is generated links to the SharePoint item (i.e. the DispForm.aspx page) and not the Document itself, in the case where you have linked to a Document Library.
For example, if you had an Events Calendar with recurring events that had further information stored elsewhere to avoid duplication, users would see the following:
Clicking on the “Further Info” link, in this case “SharePoint Developer Bootcamp” takes the user to the a new window as follows:
This means that users must now click on the “Name” field above to download the document. Hardly a compelling user experience.
Unfortunately there is no field which you can choose when setting up the lookup column that links directly to the document. An oversight in my opinion…
Solution Options
As always there are several options for resolving this issue, as to which one is best for you “it depends”…
1. Adding Workflow to the document library which populates a field containing the link to the document – Jennifer Mason blogged about this
2. Using the (JavaScript) Client Object Model to retrieve the document URL (this is my preferred option outlined below)
3. Extending the Lookup field using Visual Studio and either using the Client Object Model (wrapping the above option up nicely in a repeatable fashion) or server-side code to achieve the same
Using the Client Object Model to retrieve the document URL
The code to actually retrieve the document URL was fairly simple in the end, but it took a fair amount of experimenting to get it working 100%.
1. Include SP.js
The usual steps to work with the JavaScript Client OM are necessary, namely to have the following included somewhere:
<SharePoint:ScriptLink Name="SP.js" runat="server" OnDemand="true" Localizable="false"/>
2. Create New DispForm.aspx
I tend to create a new form from SharePoint Designer, rather than edit an existing one, if I am adding functionality, I’ve named mine CustomDispForm.aspx.
3. Modify XSL & XHTML
The usual loop through the fields is generated automatically and for the lookup column you will get something like this:
<tr> <td width="190px" valign="top" class="ms-formlabel"> <H3 class="ms-standardheader"> <nobr>Further Info</nobr> </H3> </td> <td width="400px" valign="top" class="ms-formbody"> <xsl:value-of select="@Further_x0020_Info"/> </td> </tr>
The actual value in the CustomDispForm.aspx when it is rendered will be something like this by default:
http://[site]/[doclib]/Forms/DispForm.aspx?ID=3&RootFolder=*
However by modifying the XSL to disable output escaping, we have the same data in a different format:
<tr> <td width="190px" valign="top" class="ms-formlabel"> <H3 class="ms-standardheader"> <nobr>Further Info</nobr> </H3> </td> <td width="400px" valign="top" class="ms-formbody" id="further-info"> <xsl:value-of select="@Further_x0020_Info" disable-output-escaping="yes"/> </td> </tr>
The link becomes:
This step was not absolutely necessary, as we could deduce the list from the first link (the [doclib] element), however with the ID we can get this information more easily
In order to target the right location (we’re going to use jQuery to replace the contents of the above link with the link to the document) I’ve also added an ID of “further-info” to the table cell.
4. ECMAScript/JavaScript
The final step is to add the ECMAScript/JavaScript. This can be added anywhere in your CustomDispForm.aspx. I’m not sure what anybody else does to prevent SharePoint Designer from messing up your script by replacing with XHTML entities, but this works for me:
<xsl:text disable-output-escaping="yes"> <![CDATA[ <script type="text/javascript"> SP.SOD.executeOrDelayUntilScriptLoaded(loadDocUrl,'SP.js'); var list; var item; function loadDocUrl() { var linkvalue = $("#further-info a").attr("href"); var listid = linkvalue.match(/&ListId={([^}]*)/)[1]; var itemid = linkvalue.match(/&ID=([^&]*)/)[1]; var ctx = SP.ClientContext.get_current(); var web = ctx.get_web(); list = web.get_lists().getById(listid); item = list.getItemById(itemid); ctx.load(item, 'EncodedAbsUrl'); ctx.executeQueryAsync( Function.createDelegate(this, this.onSuccess), Function.createDelegate(this, this.onFail)); } function onSuccess(sender, args) { var docUrl = item.get_item('EncodedAbsUrl'); $("#further-info a").attr("href", docUrl); } function onFail(sender, args) { alert('Error: '+args.get_message()); } </script> ]]> </xsl:text>
Firstly adding an <xsl:text /> node, disabling output escaping, followed by a CDATA node. May be excessive but I’ve had “issues” before so playing it safe!
The above assumes that jQuery is already in your solution, if not you can just add something like:
<script src="http://ajax.aspnetcdn.com/ajax/jquery/jquery-1.4.4.js" type="text/javascript"></script>
The key items in the above code are:
- Delay executing loadDocUrl until SP.js has loaded (line 4)
- RegEx to extract the list & item IDs (lines 11/12)
- EncodedAbsUrl Internal Field Name for the link to the document (lines 17 & 24)
- EncodedAbsUrl Internal Field Name for the link to the document (repeated for emphasis!)
- jQuery to target the HREF attribute of the link in question
The EncodedAbsUrl is key really – took me a while to work this out, I eventually found it by saving the document library as a template (.stp) with content, downloading & extracting with the Magical 7-zip (great tool!) and taking a look at Manifest.xml:
<Field Name='ServerUrl'>/[doclib]/document.docx</Field> <Field Name='EncodedAbsUrl'>http://[site]/[doclib]/document.docx</Field>
I could have used “ServerUrl” instead, but “EncodedAbsUrl” is, as the name suggests, URL encoded, which means if an obscure browser is being used and there are spaces or other spurious characters in the Doc Lib title I guess the user would still find the document.
Summary
I think this presents an elegant solution to a common problem:
- It can be ready in minutes (just copy the above code)
- It can be applied retrospectively (to any SKU; Foundation, Standard, Enterprise)
- Very low deployment impact (no need to have had a Workflow or Custom Field installed)
- Degrades for users not willing to wait for the content to load, they just get to see the default DispForm. However loading should be fast anyway (Firebug tells me that the query returns a lowly 461 bytes!) and hardly anybody will open a form and click on a link within it quicker than the URL is replaced
The only drawbacks, which are minor in my case:
- Needs to be applied to each instance
- Miniscule “onload” wait (transparent to the user though)
Leave a Reply