Monday, January 25, 2010

Fast Track to SharePoint Data View Web Parts

Whenever you need a viewer in SharePoint to show data from other sites, site-collections, web-services, XML files or databases, the Data Form Web Part (DFWP/DVWP) makes this task relatively simple. Use SharePoint Designer to add a data view web-part and configure it using parameters, paging, sorting and filters. Configure all needed aspects using SPD, set e.g. the item limit using "Common Data View Tasks" as doing so from code later requires more work.

When the data viewer is done, export the finished result to a .webpart file and package it into a feature as any other web-part. I recommend extracting the XSL stylesheet template from the <XSL> element into a file using SPD, and store it in the Style Library and use the XslLink web-part property to reference it.

The main drawback with this simple approach is that the users will have to use the parameters XML editor to change the web-part settings instead of having nice SharePoint tool pane input boxes. Another significant issue is that the data source configuration will be hardcoded. This is especially bad when using web-services, as the web-service URL stored in DataSourcesString selectUrl must be an absolute URL. Thus, you cannot implement, test and deploy the web-part on different hosts without manually changing the value of selectUrl in the .webpart file and recompiling your WSP.

However, there is a simple solution to both problems: subclassing the DataFormWebPart class. As Phil Wicklund shows in Writing your own custom DFWP, this is not hard at all. In addition, his source code on CodePlex shows how to create user friendly tool pane properties and connect it to the DFWP configuration. Note that DFWP is a v3 web-part that inherits from the System.Web.UI.WebControls.WebParts.WebPart class. Thus, ASP.NET 2.0 attributes such as [WebBrowsable] must be used for a property to be displayed in the tool pane.

Add a C# class to your feature and inherit the Microsoft.SharePoint.WebPartPages.DataFormWebPart class in your custom web-part class. All you need to change in your existing web-part configuration is the type name, assembly, version, culture and public key string in the <webPart><metaData> XML.


Remove the DataSourcesString property from your webpart file and set it directly from code instead. If you don't move the data source into code, then DFWP will try to access the web-service before you have a chance to correct the URL. This will most likely result in a HTTP 401 authentication failed or a HTTP 404 not found error. Override the HandleRuntimeException and HandleXslException methods to log and inspect these kind of DFWP errors.

Correcting the hardcoded web-service URL is also quite easy once you've got your own C# web-part to access and override the DataFormWebPart standard behaviour:

protected override void SetDataSourceProperties()
{ 
try
{
string dataSource = _dataSourcesString;
string asmxRoot = SPContext.Current.Site.Url + "/_vti_bin/";
this.DataSourcesString = dataSource.Replace("http://puzzlepart/_vti_bin/", asmxRoot);

this.ParameterValues.Set("LinkGroupName", _linkGroupName);

//this.DataBind();
base.SetDataSourceProperties();
}
catch (Exception ex)
{
Label lblError = new Label{ Text = ex.ToString() };
this.Controls.Add(lblError);
}
}

This code just performs a replace on the hardcoded selectUrl pointing to a SharePoint web-service on my dev box to change the host to the current site-collection root URL. The same approach must be used to set the LoginName of the current user in the data source string. The custom data viewer is now safely deployable to any site, totalling less than 40 lines of code.

Use the ParametersValue collection to change the parameters passed to the XSL. Note how these parameters are separate from those of the data source. The only safe method I've found for applying web-part tool pane properties to e.g. DFWP filtering, is SetDataSourceProperties. OnInit is too early because of viewstate, OnPreRender is to late to for changed parameter bindings to have effect.

Other web-parts such as the Content Query Web Part (CQWP) can of course also be subclassed.

Note how you can emit information about exceptions to the UI by dynamically adding a label to the Controls collection. This saves you from looking at the log files or attach a debugger to diagnose problems.

Tuesday, January 19, 2010

SharePoint Content Types Best Practices

I posted some SharePoint site content type guidelines back in 2008 and this post adds some detailed recommendations on the content type ID attribute and on the SourceID and Name attributes for content types fields.

I often create content types and site columns using the UI because it is simple and straight forward, and then use SharePoint Manager to get a starting point for the feature XML from the SchemaXml panel in SPM. It is just a starting point as you need to modify the XML for it to become valid CAML for defining site content types and site columns. I won't describe this mechanism in details, you can find help on this elsewhere.

You should at least make these changes for site columns:
Creating a new field GUID will ensure that your packaged site columns will not collide with the jump start fields when you activate your feature later on. For the same reason, I recommend prefixing all your jump start names with e.g. "dev" such as in "devContosoWebIngress".

You should at least make these changes for content types:
  • Keep only the fields you added to the content type (it will contain all inherited fields also) 
  • Update the <FieldRef> IDs to reflect the new GUIDs you created for your site columns
  • Create a new <ContentType> ID according to recommended practices (see more below)
  • If your content type ID really requires a new GUID, make sure it does not start or end with zero
ContentType IDs have two main formats, one based on GUIDs for your core content types and one based on simple two-digit numbering for variations on those core types. The core content types should be your immutable base types, while the variations are where the customizations are done. See my "open for extension, closed for modification" recommended Open-Closed practice.

Creating content types using the UI will always create a "core style" ID, even if you just make a content type variant. That is why you always should create a new ID for your custom content types, limiting the number or core content types and use the shorter "variant style" ID whenever possible. Let me explain using an example to show how to inherit a new custom core type "Generic Section Page" from the standard MOSS WCM "Page" type and then create two variations of the core custom type.

The Page type has this ID, called [page] later on:

0x010100C568DB52D9D0A14D9B2FDCC96666E9F2007948130EC3DB064584E219954237AF39

Our new "Generic Section Page" core content type requires a registry format GUID with the {} and - removed. To inherit the Page content type you append 00 and the custom, stripped GUID to the Page ID to create a new core ID:

[page]0042E113FF4B6C402B96F767B61591F882

This core content type ID is analogous to a namespace in .NET code, it is a unique grouping of all content types that have this as their base ID.

Our new section page variants "Section Page A" and "Section Page B" needs no GUIDs, just their own two-digit number appended to the core type, but not the reserved 00 combination:

[page]0042E113FF4B6C402B96F767B61591F88201
[page]0042E113FF4B6C402B96F767B61591F88202

Note that 00 is a marker for inheritance between content types when the IDs are concatenated. Thus, I recommend that your custom content type GUIDs never start or end with zero, as this just makes it harder to know where a specific type starts or ends in an inheritance tree ID as it gets longer and longer. Just a simple thing. Still, looking at content type source feels like looking at the green rain of Matrix code.

[UPDATE] See Content Type IDs on MSDN published in May 2010.

As a side note, research suggests that you need no more than about five to ten core content types to classify and organize your content - just think of the library card taxonomies such as the Dewey Decimal Classification that has been used for centuries.

Regarding naming of site columns; the age old recommendation for SharePoint sites and lists is to create them first without any spaces or special characters in the title to set the name, and then rename the site or list afterwards to set the display title. The same applies to site columns, make sure that the Name attribute is without any spaces or special characters - apply your user friendliness skills to the DisplayName attribute.

If you use the above SchemaXml trick and don't follow the naming best practice, for "Contoso Web Ingress" you get "Contoso_x0020_Web_x0020_Ingress" as the Name. If you think that looks ugly, just wait until you do a rollup using the content query web-part (CQWP) and need to select that field in XSL - then your laziness will result in this:

<xsl:value-of select="@Contoso_x005F_x0020_Web_x005F_x0020_Ingress" />

In this case it would be better to put lipstick on the pig using the CQWP DataColumnRenames setting to rename the encoded name to what it should have been in the first place.

Note that not all underscores will get encoded into _x005F_ such as for the description field of standard SharePoint lists and doc-libs, which has the internal name _Comments. So if you use CQWP to fetch data from e.g. a custom list, it will still have the name _Comments in the item style XSL.