MOSS MVP

I've moved my blog to http://blog.falchionconsulting.com!. Please update your links. This blog is no longer in use--you can find all posts and comments at my new blog; I will no longer be posting to this site and comments have been disabled.

Tuesday, June 17, 2008

Presetting Lookup Field Values via a Link

On a recent project I had a need to relate one list to another.  The first list was a document library which stored project specs and the second was a list that contained user provided scores that were used to rank the projects.  One simple requirement I had was to be able to provide a link to enter a score for a given project and have the lookup field corresponding to the project preset with the value of the field passed in.  I also had to hide certain fields from from the input form based on certain business rules.

To accomplish these goals I created a custom content type which contained my fields necessary for tracking the scores.  The individual fields are not important but note the values of the FormTemplates' elements:

   1: <ContentType ID="0x01001E7ED379D9C6A5458B184D277D8452DA" Name="Project Score" Group="Custom Content Types" Version="12">
   2:   <Folder TargetName="_cts/Project Score" />
   3:   <XmlDocuments>
   4:     <XmlDocument NamespaceURI="http://schemas.microsoft.com/sharepoint/v3/contenttype/forms">
   5:       <FormTemplates xmlns="http://schemas.microsoft.com/sharepoint/v3/contenttype/forms">
   6:         <Display>ListForm</Display>
   7:         <Edit>ProjectScoreListForm</Edit>
   8:         <New>ProjectScoreListForm</New>
   9:       </FormTemplates>
  10:     </XmlDocument>
  11:   </XmlDocuments>
  12:   <FieldRefs>
  13:     <FieldRef ID="{76497df6-2ed1-4da6-bff7-6e370c1963ab}" Name="Project" />
  14:     <FieldRef ID="{e797a104-a6b2-4d8a-8c81-e3a85522cca5}" Name="NeedMoreInfo" />
  15:     <FieldRef ID="{6cf08cc1-1d8e-4234-87f9-9f70a9366f0c}" Name="StrategicAlignment" />
  16:     <FieldRef ID="{51da6c79-7385-463d-8cf9-f44cb43f3cc2}" Name="DirectPayback" />
  17:     <FieldRef ID="{e7617985-7eec-4225-b2fd-3dfc5f96816c}" Name="ProcessImpact" />
  18:     <FieldRef ID="{3cd5a678-1223-47b2-9e4a-d4f3e7ac1109}" Name="TechnicalArchitecture" />
  19:     <FieldRef ID="{885b8c33-7f8e-4564-bd0a-3d1d145a5cc7}" Name="Risk" />
  20:     <FieldRef ID="{0f94f88f-e8b8-4589-b45c-63bd38dc2a91}" Name="TotalScore" ShowInEditForm="False" ShowInNewForm="False" />
  21:     <FieldRef ID="{73094a66-235c-4d77-9900-4501ea8fe622}" Name="PercentAligned" ShowInEditForm="False" ShowInNewForm="False" />
  22:     <FieldRef ID="{52578FC3-1F01-4f4d-B016-94CCBCF428CF}" Name="_Comments" />
  23:   </FieldRefs>
  24: </ContentType>

By specifying a custom value for the "Edit" and "New" elements I was able to use a custom delegate control.  To create the custom delegate control I copied the RenderingTemplate control with the ID of ListForm from the 12\Template\ControlTemplates\DefaultTemplates.ascx file and put it into a new file.  I then modified it to use a custom ListFieldIterator that I created.  The following is the delegate control:

   1: <%@Control Language="C#" %>
   2: <%@Assembly Name="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
   3: <%@Register TagPrefix="SharePoint" Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" namespace="Microsoft.SharePoint.WebControls"%>
   4: <%@Register TagPrefix="wssuc" TagName="ToolBar" src="/_controltemplates/ToolBar.ascx" %>
   5: <%@Register TagPrefix="wssuc" TagName="ToolBarButton" src="/_controltemplates/ToolBarButton.ascx" %>
   6: <%@Assembly Name="Demo, Version=1.0.0.0, Culture=neutral, PublicKeyToken=f801e7437a76c41d" %>
   7: <%@Register TagPrefix="Demo" Assembly="Demo, Version=1.0.0.0, Culture=neutral, PublicKeyToken=f801e7437a76c41d" namespace="Demo"%>
   8:  
   9: <SharePoint:RenderingTemplate ID="ProjectScoreListForm" runat="server">
  10:     <Template>
  11:         <SPAN id='part1'>
  12:             <SharePoint:InformationBar ID="InformationBar2" runat="server"/>
  13:             <wssuc:ToolBar CssClass="ms-formtoolbar" id="toolBarTbltop" RightButtonSeparator="&nbsp;" runat="server">
  14:                     <Template_RightButtons>
  15:                         <SharePoint:NextPageButton runat="server"/>
  16:                         <SharePoint:SaveButton runat="server"/>
  17:                         <SharePoint:GoBackButton runat="server"/>
  18:                     </Template_RightButtons>
  19:             </wssuc:ToolBar>
  20:             <SharePoint:FormToolBar ID="FormToolBar2" runat="server"/>
  21:             <TABLE class="ms-formtable" style="margin-top: 8px;" border=0 cellpadding=0 cellspacing=0 width=100%&gt;
  22:             &lt;SharePoint:ChangeContentType ID="ChangeContentType2" runat="server"/>
  23:             <SharePoint:FolderFormFields ID="FolderFormFields1" runat="server"/>
  24:             
  25:             <Demo:ProjectScoreListFieldIterator runat="server"/>
  26:             
  27:             <SharePoint:ApprovalStatus ID="ApprovalStatus2" runat="server"/>
  28:             <SharePoint:FormComponent ID="FormComponent2" TemplateName="AttachmentRows" runat="server"/>
  29:             </TABLE>
  30:             <table cellpadding=0 cellspacing=0 width=100%&gt;&lt;tr><td class="ms-formline"><IMG SRC="/_layouts/images/blank.gif" width=1 height=1 alt=""></td></tr></table>
  31:             <TABLE cellpadding=0 cellspacing=0 width=100% style="padding-top: 7px"><tr><td width=100%&gt;
  32:             &lt;SharePoint:ItemHiddenVersion ID="ItemHiddenVersion2" runat="server"/>
  33:             <SharePoint:ParentInformationField ID="ParentInformationField1" runat="server"/>
  34:             <SharePoint:InitContentType ID="InitContentType2" runat="server"/>
  35:             <wssuc:ToolBar CssClass="ms-formtoolbar" id="toolBarTbl" RightButtonSeparator="&nbsp;" runat="server">
  36:                     <Template_Buttons>
  37:                         <SharePoint:CreatedModifiedInfo runat="server"/>
  38:                     </Template_Buttons>
  39:                     <Template_RightButtons>
  40:                         <SharePoint:SaveButton runat="server"/>
  41:                         <SharePoint:GoBackButton runat="server"/>
  42:                     </Template_RightButtons>
  43:             </wssuc:ToolBar>
  44:             </td></tr></TABLE>
  45:         </SPAN>
  46:         <SharePoint:AttachmentUpload ID="AttachmentUpload1" runat="server"/>
  47:     </Template>
  48: </SharePoint:RenderingTemplate>

The following is the custom list field iterator that I built (minus the business logic):

   1: using Microsoft.SharePoint;
   2: using Microsoft.SharePoint.WebControls;
   3:  
   4: namespace Demo
   5: {
   6:     public class ProjectScoreListFieldIterator : ListFieldIterator
   7:     {
   8:         /// <summary>
   9:         /// Creates the child controls.
  10:         /// </summary>
  11:         protected override void CreateChildControls()
  12:         {
  13:             base.CreateChildControls();
  14:  
  15:             // Add the javascript necessary for setting the project lookup field to the passed in value.
  16:             const string script1 = @"<script type=""text/javascript"">
  17:                 _spBodyOnLoadFunctionNames.push(""fillDefaultValues"");
  18:                  
  19:                 function fillDefaultValues() {
  20:                   var qs = location.search.substring(1, location.search.length);
  21:                   var args = qs.split(""&"");
  22:                   var vals = new Object();
  23:                   for (var i=0; i < args.length; i++) {
  24:                     var nameVal = args[i].split(""="");
  25:                     var temp = unescape(nameVal[1]).split('+');
  26:                     nameVal[1] = temp.join(' ');
  27:                     vals[nameVal[0]] = nameVal[1];
  28:                   }  
  29:                   setLookupFromFieldName(""Project"", vals[""projectId""]);
  30:                 }
  31:                  
  32:                 function setLookupFromFieldName(fieldName, value) {
  33:                   if (value == undefined) return;
  34:                   var theSelect = getTagFromIdentifierAndTitle(""select"",""Lookup"",fieldName);
  35:                   
  36:                 // if theSelect is null, it means that the target list has more than
  37:                 // 20 items, and the Lookup is being rendered with an input element
  38:                   
  39:                   if (theSelect == null) { 
  40:                     var theInput = getTagFromIdentifierAndTitle(""input"","""",fieldName);
  41:                     ShowDropdown(theInput.id); //this function is provided by SharePoint 
  42:                     var opt=document.getElementById(theInput.opt);
  43:                     setSelectedOption(opt, value);
  44:                     OptLoseFocus(opt); //this function is provided by SharePoint 
  45:                   } else {
  46:                     setSelectedOption(theSelect, value);
  47:                   }
  48:                 }
  49:                  
  50:                 function setSelectedOption(select, value) {
  51:                   var opts = select.options;
  52:                   var l = opts.length;
  53:                   if (select == null) return;
  54:                   for (var i=0; i < l; i++) {
  55:                     if (opts[i].value == value) {
  56:                       select.selectedIndex = i;
  57:                       return true;
  58:                     }
  59:                   }
  60:                   return false;
  61:                 }
  62:                  
  63:                 function getTagFromIdentifierAndTitle(tagName, identifier, title) {
  64:                   var len = identifier.length;
  65:                   var tags = document.getElementsByTagName(tagName);
  66:                   for (var i=0; i < tags.length; i++) {
  67:                     var tempString = tags[i].id;
  68:                     if (tags[i].title == title && (identifier == """" || tempString.indexOf(identifier) == tempString.length - len)) {
  69:                       return tags[i];
  70:                     }
  71:                   }
  72:                   return null;
  73:                 }
  74:                 </script>
  75:                 ";
  76:  
  77:             Page.ClientScript.RegisterClientScriptBlock(GetType(), "SetProjectDefault", script1, false);
  78:         }
  79:  
  80:         /// <summary>
  81:         /// Determines whether the passed in field is excluded from the form.
  82:         /// </summary>
  83:         /// <param name="field">The field.</param>
  84:         /// <returns>
  85:         ///     <c>true</c> if <c>true</c> if the field is excluded; otherwise, <c>false</c>.
  86:         /// </returns>
  87:         protected override bool IsFieldExcluded(SPField field)
  88:         {
  89:             // TODO: Custom business logic
  90:             
  91:             return base.IsFieldExcluded(field);
  92:         }
  93:     }
  94: }

I got the javascript from here: http://www.sharepoint-tips.com/2007/06/using-javascript-to-manipulate-list.html (thanks Ishai for the link).  So all I'm basically doing is just dumping out some simple javascript which looks for a "projectId" parameter in the query string and sets the value of the lookup based on that value.  You can then also override the IsFieldExcluded method to hide certain fields based on your business logic (keep in mind that this will run for every field in the list so make sure your being mindful of performance).

Once I had the input forms customized and deployed I then wanted to be able to modify a view to include a link directly to the form with the project ID preset.  Note the status bar:

image

I could have done this several different ways including adding a hidden field with a custom display pattern that can then be added to the view (this would be the better way to go if the same link is needed in multiple places and you want to be able to deploy it easily via a Feature) but because I was looking for a real simple solution and didn't need reuse of the field I decided to simply modify the CAML of the view (though I may end up changing this later to be a new field).  To do this I used SPCAMLEditor (available on CodePlex) to simply change the ViewBody element to the following:

   1: <ViewBody>
   2:   <HTML><![CDATA[<TR class="]]></HTML>
   3:   <GetVar Name="AlternateStyle" />
   4:   <HTML><![CDATA[">]]></HTML>
   5:   <IfEqual>
   6:     <Expr1>
   7:       <GetVar Name="AlternateStyle" />
   8:     </Expr1>
   9:     <Expr2>ms-alternating</Expr2>
  10:     <Then>
  11:       <SetVar Scope="Request" Name="AlternateStyle" />
  12:     </Then>
  13:     <Else>
  14:       <SetVar Scope="Request" Name="AlternateStyle">ms-alternating</SetVar>
  15:     </Else>
  16:   </IfEqual>
  17:   <Fields>
  18:     <HTML><![CDATA[<TD Class="]]></HTML>
  19:     <FieldSwitch>
  20:       <Expr>
  21:         <Property Select="ClassInfo" />
  22:       </Expr>
  23:       <Case Value="Menu">
  24:         <HTML><![CDATA[ms-vb-title" height="100%]]></HTML>
  25:       </Case>
  26:       <Case Value="Icon">ms-vb-icon</Case>
  27:       <Default>
  28:         <FieldSwitch>
  29:           <Expr>
  30:             <Property Select="Type" />
  31:             <PresenceEnabled />
  32:           </Expr>
  33:           <Case Value="UserTRUE">ms-vb-user</Case>
  34:           <Case Value="UserMultiTRUE">ms-vb-user</Case>
  35:           <Default>ms-vb2</Default>
  36:         </FieldSwitch>
  37:       </Default>
  38:     </FieldSwitch>
  39:     <HTML><![CDATA[">]]></HTML>
  40:     <Field />
  41:     <FieldSwitch>
  42:       <Expr>
  43:         <Property Select="Name" />
  44:       </Expr>
  45:       <Case Value="LinkFilenameNoMenu">
  46:         <HTML><![CDATA[&nbsp;&nbsp;<em><nobr>(<a href="]]></HTML>
  47:         <HttpVDir />
  48:         <HTML><![CDATA[/Lists/ProjectScores/NewForm.aspx?projectId=]]></HTML>
  49:         <ID />
  50:         <HTML><![CDATA[">Score Project</a>)</nobr></em>]]></HTML>
  51:       </Case>
  52:     </FieldSwitch>
  53:     <HTML><![CDATA[</TD>]]></HTML>
  54:   </Fields>
  55:   <HTML><![CDATA[</TR>]]></HTML>
  56: </ViewBody>

The main point of interest is the last FieldSwitch element which looks for a field named LinkFilenameNoMenu (which is the internal field name for the Project field that I'm using) and then adds some additional HTML if a match is found.  In this case I'm using the HttpVDir element and the ID element to construct a simple link tag to the NewForm.aspx file for the scores list (I know, I should be using CSS for the styling).  The result is that I can now click straight to the project scores list while looking at the project documents list and have the project I'm interesting in scoring automatically selected.

If I end up changing this to use a hidden field I'll post the CAML for that in an update.

Update 6/18/2008: Well that didn't take me long to change my mind - I decided to go ahead and add a list field that contains the link rather than modify the view so I pulled the code that I had in the ViewBody element above and added a new field, LinkFilenameAndScore to the ViewFields element.  The LinkFilenameAndScore field is defined as follows:

   1: <Field     ID="{5C5F6D5F-82E6-49de-AC8F-09A50AFA4BA1}" 
   2:         ReadOnly="TRUE" 
   3:         Type="Computed" 
   4:         Name="LinkFilenameAndScore" 
   5:         DisplayName="Name" 
   6:         DisplayNameSrcField="FileLeafRef" 
   7:         Filterable="FALSE" 
   8:         AuthoringInfo="(linked to document with add score link)" 
   9:         StaticName="LinkFilenameAndScore" 
  10:         FromBaseType="TRUE">
  11:   <FieldRefs>
  12:     <FieldRef Name="FileLeafRef" />
  13:     <FieldRef Name="LinkFilenameNoMenu" />
  14:   </FieldRefs>
  15:   <DisplayPattern>
  16:       <Field Name="LinkFilenameNoMenu" />
  17:       <HTML><![CDATA[&nbsp;&nbsp;<em><nobr>(<a href="]]></HTML>
  18:       <HttpVDir />
  19:       <HTML><![CDATA[/Lists/ProjectScores/NewForm.aspx?projectId=]]></HTML>
  20:       <ID />
  21:       <HTML><![CDATA[">Score Project</a>)</nobr></em>]]></HTML>
  22:   </DisplayPattern>
  23: </Field>

I then used the following line of code to add the field to the list (where fieldSchema is a local variable containing the above XML and list is an SPList object representing the documents list):

list.Fields.AddFieldAsXml(fieldSchema, false, SPAddFieldOptions.AddToNoContentType | SPAddFieldOptions.AddFieldInternalNameHint);

Note the SPAddFieldOptions.AddFieldInternalNameHint enum value that is passed in - this tells SharePoint to use the value of the Name attribute as the internal name.  If you omit this it will use the display name (this took me longer than I'd like to admit to figure out).

One I had this field as part of the list I simply had to add the field to the view and I was done except that now the end-user can easily create a new view that contains this same link.

2 comments:

augustine said...

hi, i recently downloaded the wsp packaged version of your extensions and tried installing it but i received an "object not set to an instance" error message in the cmd window followed by "solution not installed". is the link from the left-nav still pointing to a valid copy?

Gary Lapointe said...

The current download is fine - I just did a test of it and was able to install it perfectly. The solution deployment is simply GAC'ing the dll and putting the xml file in the right place - there's no code executing during install so a null reference exception would indicate some issue with your farm. I'd suggest looking at your event logs and ULS logs to see if there is any additional information (you can also just manually install the two files).