03 tháng 6, 2010

Customize SharePoint standard forms with Data View web part

The Data View webpart is encapsulated control for presenting data and uses a SharePoint data source server control to load data items from List or Document Library or multiple List. This web part also allows user to add / edit / remove data entries in data source. SharePoint Designer is used to add (and modify) Data View web part to an existing .aspx page. In this post, I'd like to point out notably things we need to care when working with Data View web part.

Here is the common scenario we may see client's need that the Data View web part is likely a solution: they want to use SharePoint to store invoice data. They have two List :
  • Invoice : store data for an invoice. The possible columns for this list are : Client (could be lookup field but here I use textbox for simple) , Created Date , Payment Status (Completed / Pending / Invalid)
  • Invoice Detail : store items that going with the an invoice. The columns for this list are : Invoice ID (hidden and managed by our code) , Item Name (text for simple) , Price
And they want something that supports them in entering invoice easily. I thought it's not easy to implement a solution that user could be able to enter invoice details when they're creating a new Invoice in the NewForm.aspx of Invoice . So I went for an alternative way : I will customize the DispForm.aspx of Invoice so that Invoice Detail grid shows up below the Invoice item and user can enter invoice details within in DispForm.aspx. Below is some sample pictures I captured from the demo project (download link is in the end of this post)













Before I start, if you have no idea what's Data View web part, kindly read this blog first to get the insight on how to play with Data View webpart: http://www.endusersharepoint.com/2009/05/12/data-view-web-part-the-basics-insert-a-dvwp-on-your-page/

Now I will create a SharePoint-template project for demonstration and use it through this post. From above analysis, List Definition project will be used







After create the project, I organized the folder structure as 12-hive





From the structure, you see that the feature has 2 list definition (and its instances) for Invoice and InvoiceDetail. To start with, I add some fields on two lists and deploy the feature to web. Below is Invoice and Invoice Detail list after installed.









Ok, Now I'll use SharePoint Designer (SPD) to create Data View WebPart in DispForm.aspx of Invoice. First, I open the DispForm.aspx page of Invoice list





Second, I find the default List Form webpart and put the cursor right after that where the new Data View webpart will be added. You can see that two webparts are within the default WebPartZone control in page.





Then I start adding Data View webpart and apply some settings
  • Add InvoiceID column
  • Allow sorting and filtering on header
  • Allow insert/edit/remove




Select Invoice Detail list as source




Select three columns that represent for invoice detail and insert them to list as Multiple Item View





Now we need to do some customization. Open Common Data View Task dialog, click on Edit Columns link to add InvoiceID column which is used to associate invoice detail to a invoice






Click on Data View properties link




Allow sorting / filtering and inserting ...







At this point, the webpart show all invoice detail but we only want it to show invoice detail going with current viewing invoice. To do that, add filtering value on data source. Let create a parameter to represent for ID of Invoice








Name the new parameter: InvoiceID and make it get data from query string




Configure filtering




Only retrieve Invoice Detail that its InvoiceID field is same value as ID parameter in query string








Because the Data View webpart needs to have list id of Invoice Detail in order to retrieve query data as well as perform update to. By default the SPD creates a parameter that contains the current list id Invoice Detail but we should add some code so that the parameter can work on other environment. Let modify the ListID parameter




We will tell the data view webpart to get the list id from our server control (created soon)




Enter the server control ID, this control isn't available in the .aspx page but we'll create soon




Open the DispForm.aspx in Invoice folder in Visual Studio, on the beginning of PlaceHolderMain, add an ASP.NET label control with ID lblInvoiceDetailId

<asp:Content ContentPlaceHolderId="PlaceHolderLeftNavBar" runat="server"/>
<asp:Content ContentPlaceHolderId="PlaceHolderMain" runat="server">

<asp:Label ID="lblInvoiceDetailId" runat="server" style="display:none;"></asp:Label>

<table cellpadding=0 cellspacing=0 id="onetIDListForm">



We also need to make this page inherit from our class instead of default SharePoint. Go to top of page, modify the Inherits attribute to our code behind (created soon)

<%@ Page language="C#" MasterPageFile="~masterurl/default.master"   
Inherits="DataViewWebPartDemo.Invoice.DispForm,DataViewWebPartDemo,Version=1.0.0.0,Culture=neutral,PublicKeyToken=9f4da00116c38ec5"
meta:webpartpageexpansion="full" meta:progid="SharePoint.WebPartPage.Document" %>



Ok, now we'll create code behind for DispForm.aspx. Add new class in Invoice folder and name it DispForm.aspx.cs








Fill up the file with below code, remember to add Microsoft.SharePoint and System.Web assembly

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.SharePoint;

namespace DataViewWebPartDemo.Invoice
{
public class DispForm : Microsoft.SharePoint.WebPartPages.WebPartPage
{
protected global::System.Web.UI.WebControls.Label lblInvoiceDetailId;

public void Page_Init(object sender, EventArgs e)
{
lblInvoiceDetailId.Text = SPContext.Current.Web.Lists["Invoice Detail"].ID.ToString();
}
}
}



In the code, we assign the current Id of list named Invoice Detail to control so that the data view webpart then use it to retrieve data

You need to modify the web.config (adding SafeControl) after deploying the feature and assembly in order the DispForm.aspx can be run in SharePoint


Save all changes before going on





SPD will generate XSL definition based on what we've done on the UI, so we need to pack those definition to the feature. This can be done by using webpart exporting option on SharePoint UI. Let just create a test item on Invoice list and view it in display view





Now we're going to export what we have done so far on data view webpart






Temporarily save Invoice_Detail.webpart to Desktop




Open schema.xml file of Invoice list in feature





Modify the Form (Type="DisplayForm") element as below
<Forms>
<Form Type="DisplayForm" Url="DispForm.aspx" WebPartZoneID="Main">
<WebParts>
<AllUsersWebPart WebPartZoneID="Main" WebPartOrder="2">
<![CDATA[

<!-- Exported webpart definition goes here -->

]]>
</AllUsersWebPart>
</WebParts>
</Form>
<Form Type="EditForm" Url="EditForm.aspx" WebPartZoneID="Main"/>
<Form Type="NewForm" Url="NewForm.aspx" WebPartZoneID="Main"/>
</Forms>



Then open the downloaded Invoice_Detail.webpart, copy whole content and put inside CDATA. It looks like below

<Forms>
<Form Type="DisplayForm" Url="DispForm.aspx" WebPartZoneID="Main">
<WebParts>
<AllUsersWebPart WebPartZoneID="Main" WebPartOrder="2">
<![CDATA[

<!-- Exported webpart definition goes here -->
<webparts>
<webpart xmlns="http://schemas.microsoft.com/WebPart/v3">
<metadata>
<type name="Microsoft.SharePoint.WebPartPages.DataFormWebPart, Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c">
<importerrormessage>Cannot import this Web Part.</importerrormessage>
</type></metadata>
<data>
<properties>
<property name="MissingAssembly" type="string">Cannot import this Web Part.</property>
<property name="FireInitialRow" type="bool">True</property>
<property name="TitleIconImageUrl" type="string">
............
]]>
</AllUsersWebPart>
</WebParts>
</Form>
<Form Type="EditForm" Url="EditForm.aspx" WebPartZoneID="Main"/>
<Form Type="NewForm" Url="NewForm.aspx" WebPartZoneID="Main"/>
</Forms>



We have finished 80% work so far. Next we have one important thing and it's a little bit problematic: Automate filling the ID of current Invoice item to the Invoice ID field of detail when user enters new detail


This is where SharePoint Designer team comes to help :-). They wrote a blog dealing with problem and it's very helpful :-). Please go and check this blog before going on:
http://blogs.msdn.com/b/sharepoint/archive/2007/06/21/using-javascript-to-manipulate-a-list-form-field.aspx


Now we will employ a little bit JavaScript code to automate filling the value. We will put the javascript code to DispForm.aspx page in Invoice folder in Visual Studio.

<script type="text/javascript">

/////////////////////////////////////////////////////////////
// How they do that: http://blogs.msdn.com/b/sharepoint/archive/2007/06/21/using-javascript-to-manipulate-a-list-form-field.aspx
/////////////////////////////////////////////////////////////

_spBodyOnLoadFunctionNames.push("fillDefaultValues"); // Use JS SharePoint API to register onload event

function fillDefaultValues() {
// Parse the key/value from query string
var qs = location.search.substring(1, location.search.length);
var args = qs.split("&");
var vals = new Object();
for (var i = 0; i < args.length; i++) {
var nameVal = args[i].split("=");
var temp = unescape(nameVal[1]).split('+');
nameVal[1] = temp.join(' ');
vals[nameVal[0]] = nameVal[1];
}

// Assign ID query string to InvoiceID field
setTextFromFieldName("InvoiceID", vals["ID"]);
}

// @fieldName: the field display name
function setTextFromFieldName(fieldName, value) {
// If the webpart isn't in Insert/Edit Mode, the control doesn't exist so using try/catch to prevent error js icon on browser
try {
var theInput = getTagFromIdentifierAndTitle("input", "TextField", fieldName);
if (theInput != null) {
theInput.value = value;
}
}
catch (e) { }
}

function getTagFromIdentifierAndTitle(tagName, identifier, title) {
var len = identifier.length;
var tags = document.getElementsByTagName(tagName);
for (var i = 0; i < tags.length; i++) {
var tempString = tags[i].id;
if (tags[i].title == title && (identifier == "" || tempString.indexOf(identifier) == tempString.length - len)) {
return tags[i];
}
}
return null;
}</script>



From the JavaScript code, You see that we set the ID in query string to the field called "InvoiceID" (of course we can set the value on lookup field too (as original code showed).


Here is the whole picture of what we've done: when the DispForm.aspx run, the DataView web part look for server control with Id lblInvoiceDetailId and use the GUID to know where the Invoice Detail list. Then when user enters new record for detail, its Invoice ID field is filled automatically by javascript. That's interesting :-).

This is end of the post. Here is download link for the source of sample I presented