Welcome to MSDN Blogs Sign in | Join | Help

FF at the End of a Content Type ID Kills Update Propagation

I just spent a couple of hours tracking down an issue where our content type update code wasn’t working for some content types.  The problem ended up being that one content type in the hierarchy had a content type ID that ended with “FF” (such as “0x0100EBE43BB291D8438f94E2C48C129366FF”).  None of this type’s child content types were getting updated (for example, the content type with ID “0x0100EBE43BB291D8438f94E2C48C129366FF01” was not updated).

Details

When updating content types and you want those updates to push down (propagate) to child content types, you need to either do that through the SharePoint UI, or write code.  In our case, we have code in the feature receiver that adds new site columns to content types as needed.  We have a hierarchy of content types and when we update a base content type, we want those changes to be pushed down to their children, their children’s children and so on.  We do this using the SPContentType’s Update(true) method.

Even with this, none of the children of one specific content type were getting updated.  After scouring the code for clues and trying various ways of updating, I started to wonder if the ID was “special” in any way since ID’s are parsed by SharePoint (for example, a “00” tells SharePoint that the preceding characters represent the ID of a base content type).  I noticed that it ended in “FF”; all of the other types that were propagating correctly ended with other characters.  So, I changed it to end in “0E”, reactivated the feature, and the propagation worked!

So, it appears that somewhere in the SharePoint code that updates content types, “FF” has some special meaning, telling it, “stop here, don’t propagate any further.”

Posted by Valdon | 0 Comments

Creating Several List Instances in a Single Feature

I’m not sure why I didn’t find this earlier, but I have always put a single list instance definition into a single feature.  However, I recently had to put together several inter-related lists into a feature (no single list was useful by itself).  The first approach was to create each list instance as a hidden feature and then have a visible feature that activated the hidden features through code in the feature receiver.

This just didn’t seem like a good way to maintain this going forward since I ended up with several hidden features, which I don’t really care for (I’d rather everything be visible).  So I tried putting all the list instances into one feature, with subfolders for each list, but I saw strange behavior.  Lists would have fields included that were part of another list definition.  What was going on?

So I started doing some hunting around and it turns out that you can do what I wanted, you just couldn’t take the list templates as they were straight from an export (I always make my lists by creating them in the SharePoint UI and then exporting using SharePoint export or another tool).

First, you need to change the Type attribute of the ListTemplate element in the elementManifest.xml file and, in the same file, change the TemplateType attribute of the ListInstance element to match the new Type value.  It turns out that the ListTemplate Type value only needs to be unique within the feature, so make all of your Type values different (I ended up making all the Type values different across the entire solution so I could easily move list definitions between features until I got ready to release).  It seems that the default export type value is always 100, which confused SharePoint into adding fields from multiple templates with that Type within the same feature.

Posted by Valdon | 0 Comments
Filed under: ,

Removing Extra Workflow Status Column in Default View

When SharePoint starts a workflow on a list for the first time, it has this annoying behavior of adding a workflow status column to the default view of the list.  Often times, this information is of no practical use for end users.  Unfortunately, there appears to be no way of getting rid of this behavior.  So instead, you have to either remove it by hand after the first instance has started, or write custom code.  Luckily the code is fairly simple and can be placed anywhere you want, including inside of a custom code activity within the workflow that is started.

Here's the type of code you need:

// In our case, the internal column name is the first 8 characters of the
// list name followed by " Workflow"
string colName = (WorkflowItemList.Title + " Workflow").Substring(0, 8); SPView defaultView = YourList.DefaultView; if (defaultView.ViewFields.SchemaXml.Contains("\"" + colName + "\"")) { defaultView.ViewFields.Delete(colName); defaultView.Update(); }

Note that I think the internal column name is always the first 8 characters of the list name, but if you have multiple workflows on the same list, it may be different (you may need to export your list and look at the manifest.xml to find the internal field name).  If you find it is different,  you could either hard code the column name or you could loop through all the FieldsRefs in the view and remove any that have a type of "WorkflowStatus".  In any case do not remove the Field from the SPList (this will crash your workflows!); just remove the FieldRef from the SPView.  Once it's gone, SharePoint won't try and add it back unless you remove the workflow association and add it back in.

UPDATE: the internal column name is actually the first 8 characters of the workflow name, which in our case was using the list name followed by " Workflow", but is not always the case.  Change the first line in the sample code to retrieve the workflow name, or hard code the name if it is always the same in your case.

Also, one other thing I tried after this was to explicitly add the workflow status field into the list definition, copying the Field definition directly from the exported list definition manifest file, thinking that we would just preempt SharePoint and add the field ourselves, leaving it out of the default view.  Alas, this did not work as SharePoint, once again, outsmarted us by adding its own field to the column and the default view.

Posted by Valdon | 1 Comments

Using Calculated Fields to Pad Numbers with Leading Zeros

Just a quick note on one way to put leading zeros in a SharePoint list field using a Calculated Field.  A client wanted a column to show the list item's name, followed by a number that was left-padded with zeros to two digits, like:

Addendum-01

Addendum-02

...

Turns out there's no function (at least that I could find) to do this, but using CONCATENATE, REPT and LEN function, I could do it as follows:

Replace [Item Name] with the field name that contains the name and [Item Number] with the field name that contains the item number.

=CONCATENATE([Item Name],"-",REPT("0",2-LEN([Item Number])),[Item Number])

The REPT function will repeat a given string a specified number of times.  By subtracting the length of the number from 2, I could tell it how many leading zeros were needed.

One caveat to this is that I'm not sure if it works with numbers longer than the digit count we want (in this case, greater than 99).  I'm not sure of the behavior of REPT with negative numbers passed in.  In my client's, it isn't an issue, but if you need to, you could add an IF function that checks if it is negative and uses zero if it is.

Posted by Valdon | 0 Comments
Filed under:

How to work around bugs in the SPGridView control

SharePoint has a little known web control called SPGridView.  This control can give you a SharePoint list-like view of data from any data source you can bind to (from a database or other sources).  It also has features to sort, filter and group the data in much the same way users can with SharePoint lists.  For more information on how to use this, Paul Robinson has two great posts: SPGridView and SPMenuField: Displaying custom data through SharePoint lists Part 1 and Part 2.  In addition, Robert Fridén has a good post on Filtering with SPGridView.  However, there are two bugs that can be annoying and cause confusion to users:

  • When both filtering and grouping are enabled, the first group title row sometimes disappears.
  • When both sorting and filtering are enabled, the sort indicator arrow image will sometimes appear in the wrong column.

Working around either of these bugs requires you to create your own web control based on SPGridView and overriding the CreateChildControls method.  Here is the full code listing for the solution to work around both bugs.

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Reflection;
using System.Web.UI;
using System.Web.UI.WebControls;

using Microsoft.SharePoint.WebControls;

namespace AdventuresInConsulting
{
    public class GridViewControl : SPGridView
    {
        protected override int CreateChildControls(System.Collections.IEnumerable dataSource, bool dataBinding)
        {
            // Due to a bug in SPDataView, we need to reset the private field previousGroupFieldValue so the 
            // first grouping row title will consistently show up
            FieldInfo fieldInfo = typeof(SPGridView).GetField("previousGroupFieldValue",
                BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
            if (fieldInfo != null)
                fieldInfo.SetValue(this, null);

            int returnValue = base.CreateChildControls(dataSource, dataBinding);

            // Fix up a bug in the SPGridView where it is putting the sort arrow indicator on the wrong
            // column if AllowFiltering is true
            if (this.AllowFiltering && this.HeaderRow != null && !string.IsNullOrEmpty(this.SortExpression))
            {
                List<string> filterFields = new List<string>(this.FilterDataFields.Split(','));
                if (this.AllowGrouping) filterFields.Insert(0, string.Empty);

                for (int i = 0; i < this.HeaderRow.Cells.Count; i++)
                {
                    DataControlFieldHeaderCell cell = this.HeaderRow.Cells[i] as DataControlFieldHeaderCell;
                    if (cell != null)
                    {
                        // Find the sort image control
                        Image sortImage = null;
                        foreach (Control c in cell.Controls)
                        {
                            sortImage = c as Image;
                            if (sortImage != null && sortImage.ImageUrl == this.SortDirectionImageUrl)
                                break;
                        }

                        // If this field is filterable, make sure the menu image is correct
                        if (i < filterFields.Count && filterFields[i].Trim() != string.Empty)
                        {
                            // If there is also an image control, remove it (we'll add it back in the right place)
                            if (sortImage != null) cell.Controls.Remove(sortImage);

                            // Find the menu control and add or remove the image in there as needed
                            foreach (Control c in cell.Controls)
                            {
                                if (c is Microsoft.SharePoint.WebControls.Menu)
                                {
                                    Microsoft.SharePoint.WebControls.Menu menu = c as Microsoft.SharePoint.WebControls.Menu;
                                    if (this.SortExpression.Equals(this.Columns[i].SortExpression, StringComparison.InvariantCultureIgnoreCase))
                                    {
                                        // Make sure it has the sort indicator
                                        menu.RightImageUrl = this.SortDirectionImageUrl;
                                        menu.ImageTextSpacing = new Unit("2px", CultureInfo.InvariantCulture);
                                    }
                                    else if (menu.RightImageUrl == this.SortDirectionImageUrl)
                                    {
                                        // Make sure it doesn't have the sort indicator
                                        menu.RightImageUrl = null;
                                        menu.ImageTextSpacing = Unit.Empty;
                                    }
                                }
                            }
                        }
                        else
                        {
                            if (this.SortExpression.Equals(this.Columns[i].SortExpression, StringComparison.InvariantCultureIgnoreCase))
                            {
                                // Make sure there is a sort image
                                if (sortImage == null)
                                {
                                    sortImage = new Image();
                                    sortImage.ImageUrl = this.SortDirectionImageUrl;
                                    sortImage.Style[HtmlTextWriterStyle.MarginLeft] = "2px";
                                    cell.Controls.Add(sortImage);
                                }
                            }
                            else if (sortImage != null)
                            {
                                // Remove the sort image if it exists
                                cell.Controls.Remove(sortImage);
                            }
                        }
                    }
                }
            }

            return returnValue;
        }
    }
}

Fixing the group title row disappearance

Making it so that the first group title row doesn't disappear is fairly simple.  I noticed that this problem occurred if the filter or sort caused the first group in the new results to be the same as the last group from the previous results.  This led me to look at how SPGridView was checking for the start of a new group.  Sure enough, it was just checking to see if the value of the group field in the current row was different from the group field value the last time it detected a change.  It saves the last group value in a field called previousGroupFieldValue.  That value was being preserved between calls (presumably so that paging will work correctly).  The easy fix is to reset the last group value.  The problem was that the value was accessed through a private field.  Of course, thanks to .NET reflection, we won't let that stand in our way.  We just grab  the FieldInfo for previousGroupFieldValue and then forcibly set the value.

NOTE: you may need to modify this slightly if paging is enabled.  You probably only want to reset it if the page has not changed.

Fixing the sort indicator arrow image

Fixing the sort indicator arrow image is the more difficult of the two work arounds.  I had hoped to recreate the CreateChildControls method of SPGridView and just avoid the image from coming up in the wrong place.  However, it turns out that SPGridView calls its base class CreateChildControls method and there is no way in C# for a derived class to call its base's base class methods.  So, rather than trying to recreate a whole chain of CreateChildControls methods, I just let SPGridView put the arrows where it wanted to and then fixed them up later.

The code loops through all of the column headers and looks for the sort indicator image either as a standalone Image control, or as the RightImageUrl of the column menu's title.  We remove the image where it doesn't belong and add the image where it does belong.

Note on this bug: the issue arises because enabling grouping causes a hidden column to be added to the front of the grid and the code logic SPGridView uses to decide which column to put the filter indicator on is off by one.  You'd think it'd be off to the left, but it is actually off to the right because whoever wrote the code knew there was a hidden column, but they overcompensated for it (kind of feels like being in a car with a kid learning to drive with power steering for the first time).

Posted by Valdon | 4 Comments
Filed under: , ,

Using Both Field and FieldRef in InfoPath List Loses Data

It appears that if you have both a Field and a FieldRef definition in your SharePoint list definition file that includes XML (InfoPath) field promotion, you can no longer update the field through the list; you must do it through the XML only.

I ran into this interesting bug that took quite some time to track down.  We have several site definitions that have a number of SharePoint custom lists that use a shared set of site columns.  One of the custom lists is a document library containing InfoPath XML files that are edited using Web-based forms.  The list definition includes fields that are "promoted" (synchronized with) the InfoPath XML file such as:

<Field ID="{12345678-9ABC-DEF0-1234-56789ABCDEF0}"
       Type="Text" Name="Sample" StaticName="Sample"
       ColName="nvarchar11" DisplayName="Sample"
       Description="A Sample Field" Group="Samples"
       Required="FALSE" Hidden="FALSE" Customization=""
       ReadOnly="TRUE" RowOrdinal="0"
       Node="/my:myFields/my:Sample"
       SourceID="{A1234567-89AB-CDEF-0123-456789ABCDEF}" />

Note that the Sample field is defined from a site Column with the same ID as the Field.  The site column is defined in the feature identified by the SourceID GUID.  Also, the Node field defines where the value is promoted from (synchronized with) in the XML document.  For an extensive look at using XML with SharePoint lists, see Andrew May's 5-part blog on this topic: http://blogs.msdn.com/andrew_may/archive/2006/06/29/SharePointBeta2XMLDocumentParsers1.aspx.

A nice feature of SharePoint is that when you define a field this way, you can update the XML file and the column value is updated, or you can update the column value and the XML file will be updated.  Except in our case, it wasn't working correctly.  If the XML value were updated, the column value would be updated, but not the other way around.

We had code that would get a value from another system and update the column in the list.  The code would appear to work, without raising any errors, but neither the column value nor the value in XML would be updated.  No errors were logged anywhere that we could find.

After going around in circles trying different ways in code for updating the value, I finally thought to check the schema file for that particular list and compare it against another site's list definition that was working.  It turns out that the field we were trying to update had FieldRef in the <ContentTypes> section of the list definition.

<FieldRef ID="{12345678-9ABC-DEF0-1234-56789ABCDEF0}"
          Name="Sample" DisplayName="Sample"
          Required="FALSE" Hidden="FALSE"
          Customization="" ReadOnly="TRUE"
          Node="/my:myFields/my:Sample" />

The other site where it was working only had a Field definition, not the FieldRef.  After commenting out the FieldRef and creating a new list from that definition, it started working.  I still have no explanation about why it was causing the problem, but it is working.

The last thing we had to do was to fix existing lists.  I wrote some code to open the list, copy the existing values from the column, delete the field, add the field back in using the site column definition and then restore the previous values to the column.

Posted by Valdon | 1 Comments
Filed under: , ,

Getting To the Root (Well Almost) - Navigating the SPWeb Hierarchy

Recently I was faced with the problem of finding an ancestor web (SPWeb object) of the current web.  The ancestor needed to be the one just below the RootWeb.  I quickly put together a solution that worked, but I later realized that it had massive memory "leaks" (technically, they're not memory leaks, but they have the same effect so we'll just call them leaks).  For more information on this common coding problem, see Roger Lamb's excellent blog entry on this topic: http://blogs.msdn.com/rogerla/archive/2008/02/12/sharepoint-2007-and-wss-3-0-dispose-patterns-by-example.aspx

SPWeb nextToTopWeb = SPContext.Current.Web;
while (nextToTopWeb.ParentWeb != null && !nextToTopWeb.ParentWeb.IsRootWeb)
{
    nextToTopWeb = nextToTopWeb.ParentWeb;
}
// Code that uses nextToTopWeb 

The problem is that the ParentWeb method returns an SPWeb object that needs to be disposed and I was calling ParentWeb multiple times without any disposing going on (and in the while expression, it wasn't even returning to a variable).  Also, at the end I was not disposing of nextToTopWeb, which you should do if it got assigned a ParentWeb (which happened when it went at least once through the loop), but should not do if you got it through SpContext.Current (which happened if we start at the root web or one below).  For some reason, it took me a while to wrap my brain around this little problem and I think I've come up with a solution:

SPWeb nextToTopWeb = SPContext.Current.Site.OpenWeb(SPContext.Current.Web.ServerRelativeUrl);
while (true)
{
    SPWeb nextWeb = nextToTopWeb.ParentWeb;
    if (nextWeb == null || nextWeb.IsRootWeb)
    {
        // We found the next-to-top web
        nextWeb.Dispose(); // Line added 1/15/08
        break;
    }
    nextToTopWeb.Dispose();
    nextToTopWeb = nextWeb;
}

// Code that uses nextToTopWeb 
nextToTopWeb.Dispose();

There are probably more elegant ways to do this (code purists won't like using break in an otherwise infinite while loop), but this worked for our project.  The key here is to have an intermediary variable (in this case, nextWeb) that holds a reference to the parent web so the current one can be disposed.  Also, this code should be put in a try...catch...finally block so if there is an error, you can still dispose of topWeb.

I'm also a bit concerned about performance since it is throwing around SPWeb objects like candy at a parade (SPWeb objects are pretty heavy and have sharp corners - not recommended for throwing at bystanders).  There hasn't been an issue thus far, but any suggestions on improving the algorithm are welcome.

Posted by Valdon | 1 Comments

Empty Active Directory Groups do not Show up for SharePoint 2007 Audiences

I ran into an interesting issue at a customer site.  They had dozens of newly created Active Directory groups, but only two of them were showing up when they wanted to create a new audience or specify one of the groups as a target for a list item.  It turns out that SharePoint only shows Active Directory groups that contain users who have a profile imported into SharePoint.  There's a simple work around: add a user to the group who also has a SharePoint user profile.  This is a little inconvenient, since you can't set up audiences or target content using Active Directory groups before you have members in the group, and you may not know who should be members of the group at the time you're setting them up.

The trick is to do a full user profile import before you try to use groups in audiences.  I've also seen some references on other sites that you may need to do 2 or 3 full profile imports before they start working, but we found that one full import did the trick.  However, note that if you change group membership, you need to do a profile import again to see those changes.

So, to make a long story short, here's what we found works:

  1. Create your Active Directory Groups
  2. Add at least one user to each group (and make sure that one user will have their user profile imported)
  3. Run a full user profile import in SharePoint
  4. Create your audiences (you should now see all the groups that have members with profiles)

In development and testing environments, you typically end up doing full profile imports quite a bit since when you change the group membership, you need to do a profile import.  In production, make sure you're doing profile imports often enough to catch group membership changes in a timely manner.

Posted by Valdon | 1 Comments

Lookup Helper Methods

Sometimes when writing custom code for SharePoint, you want to open the list to which another list field is referring (when it uses a lookup field).  I had to do this to put the lookup field values in a custom menu item.  Other times, you just want the SP List Item that another list item is referring to.  You can do this using the SPFieldLookup and SPFieldLookupValue classes.  Below are two helper functions that I wrote to do those two tasks.  They may be useful to others working with lookup lists in SharePoint.

SPList GetLookupList (SPList, string)

This helper function takes the referring list and the lookup field name as parameters and returns the SPList object.  If it can't find the field name or the lookup list it will throw an error with the text indicating what went wrong.

SPListItem GetLookupListItem (SPListItem, string)

This helper function takes the referring list item and the lookup field name as parameters and returns the SPListItem object the item is referring to.  If it can't find the name, the lookup list or the list item, it will throw an error with the text indicating what went wrong.

Code Listing

The following is the code listing for these two methods.  They're static methods in a public class.  Just add this to a new file in your SharePoint project.

C#
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Text;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

using Microsoft.SharePoint;

namespace AdventuresInConsulting
{
    public class LookupHelper
    {
        /// <summary>
        /// Get the list ID used by a lookup field for values
        /// </summary>
        /// <param name="referringListName">The SPList containing the lookup field</param>
        /// <param name="lookupFieldName">The name of the lookup field</param>
        /// <returns>The list used by the lookup field</returns>
        static public SPList GetLookupList(SPList referringList, string lookupFieldName)
        {
            SPList lookupList = null;
            string errMsg = string.Empty;
            try {
                errMsg = "ERROR: Could not find field named " + lookupFieldName + " in list " + referringList.Title;
                SPFieldLookup editionsField = referringList.Fields[lookupFieldName] as SPFieldLookup;
                errMsg = "ERROR: Expected " + lookupFieldName + " to be a lookup field in list " + referringList.Title;
                Guid lookupWebGuid = editionsField.LookupWebId;
                string lookupListId = editionsField.LookupList;
                // The using statement prevents a leak of SPWeb (thanks for the catch Bob!)
                using (SPWeb lookupWeb = SPContext.Current.Site.OpenWeb(lookupWebGuid))
                    lookupList = lookupWeb.Lists[new Guid(lookupListId)];
            } catch (Exception e) {
                throw new Exception(errMsg, e);
            }

            return lookupList;
        }

        /// <summary>
        /// Get the SPListItem the lookup field is pointing to
        /// </summary>
        /// <param name="referringListItem">The SPListItem containing the lookup field</param>
        /// <param name="lookupFieldName">The name of the lookup field in referringListItem</param>
        /// <returns>The SPListItem where the value for the lookup field is stored</returns>
        static public SPListItem GetLookupListItem(SPListItem referringListItem, string lookupFieldName)
        {
            SPListItem lookupListItem = null;
            string errMsg = string.Empty;
            try {
                errMsg = "ERROR: Could not find field named " + lookupFieldName + " that is of type lookup";
                SPFieldLookup lookupField = referringListItem.ParentList.Fields[lookupFieldName] as SPFieldLookup;
                if (lookupField == null) throw new Exception(errMsg);
                SPFieldLookupValue lookupValue = lookupField.GetFieldValue(referringListItem[lookupFieldName].ToString()) as SPFieldLookupValue;
                if (lookupValue == null) throw new Exception(errMsg);
                errMsg = "ERROR: Could not open lookup list";
                SPList lookupList = LookupHelper.GetLookupList(referringListItem.ParentList, lookupFieldName);
                errMsg = "ERROR: Could not find lookup item in list";
                lookupListItem = lookupList.GetItemById(lookupValue.LookupId);
            } catch (Exception e) {
                throw new Exception(errMsg, e);
            }

            return lookupListItem;
        }
    }
}
Posted by Valdon | 3 Comments
Filed under: , ,

Wrapping Text in a SharePoint WebPart Title

An interesting bit of CSS formatting behavior that I ran across recently at a customer site. They wanted to be able to have long titles in some of their web parts, but it would always make the web part zone expand to fit the text rather than wrapping the text. It turns out that the web part zone puts the title in a <nobr> tag.  As anyone who has done it knows, overriding an out-of-box web part or using reflection to modify its behavior can get ugly fast.  Out of curiosity, I suggested they try a CSS class that modifies the style of the <nobr> tag using the white-space:normal style.  They tried:

.ms-standardheader nobr {
    white-space: normal;
}

 

and it worked. Now, this may not be news to all of you CSS gurus out there, but to me it was a surprise that you could override the behavior of a tag, not just override CSS styles. Useful information to keep in mind when branding a site and trying to wrestle web part html into submission.

Posted by Valdon | 3 Comments
Filed under: , ,

Accessing images and other binary files in blob fields through SharePoint BDC

 

There are many applications that store binary data in blob or varbinary fields of a database, including pictures, Office documents, PDFs and other types of documents. Recently, a customer had the need to display, in SharePoint, an image stored in an Oracle database. They were already indexing and displaying the other fields from the database. Since there is no out-of-box support for displaying blobs in SharePoint from the Business Data Catalog (BDC), I decided to see if I could find some way to retrieve the data in a format that could be used by SharePoint.

I discovered that you can get to blob or varbinary fields through the BDC as byte arrays (type byte[] in C#). Once you have this byte array, you can output it directly to the browser, or any other streaming end point as raw data.

In this post, I will walk through how this can be done and how it can be used to display binary documents in SharePoint. The steps are: 1) Creating the BDC application definition file, 2) Creating the ASPX page class, and 3) Displaying the Image on a page.

Products used:

· Microsoft Office SharePoint Server 2007 (MOSS 2007)

· Microsoft Visual Studio 2008 C#

· Microsoft SQL Server 2005

Step 1 – Creating the BDC application definition file

In this article I am using the always-handy AdventureWorksDW sample database that comes with SQL Server 2005. To set up the initial BDC connection to this database, use the BDC Application Definition file from AdventureWorksDW SQL Server 2005 Sample at http://msdn2.microsoft.com/en-us/library/ms494876.aspx.

NOTE: there is a link to a walkthrough from that page, but the walkthrough applies to the AdventureWorks database, while the 2005 update applies to the AdvantureWorksDW database (the data warehouse). The steps still work with some minor changes, but just follow the steps to the point where you have a Products list on a page (select the Key column instead of the ID column for display – I also added the Name column to the list).

NOTE: To make this work with Oracle and other databases, see How to: Adapt the Samples to Connect to Oracle and Other Databases at http://msdn2.microsoft.com/en-us/library/aa673236.aspx.

Once you have the BDC connection working and SharePoint is able to show the Products list, we are ready to add a method to the BDC Application Definition File that will retrieve the blob.

Add a new method called GetProductImage on the Product entity with an instance called GetProductImageInstance of type ViewAccessor. This method will take a parameter that identifies the record to be retrieved and then return the blob field containing the picture.

You can do most of this through the Business Data Catalog Definition Editor which is included in the Microsoft Office SharePoint Server 2007 Software Development Kit (SDK). See Business Data Catalog Definition Editor in SharePoint Server 2007 at http://msdn2.microsoft.com/en-us/library/bb802680.aspx for information on this tool.

NOTE: if you use the BDC Definition Editor, you cannot select byte[] as the return type of the field. You can select byte, but not a byte array; you must export the application definition and edit that part by hand.

While you’re editing the definition file, increment the version of the file in the LobSystem tag (for example, to 1.1.0.0). This way you don’t have to delete the BDC application before importing it again.

The following defines the method and the method instance. Insert these lines in the <methods> section of the Product <Entity…> section:

XML
<Method Name="GetProductImage">
  <Properties>
    <Property Name="RdbCommandText" Type="System.String">SELECT LargePhoto FROM DimProduct WHERE ProductKey=@ProductKey</Property>
    <Property Name="RdbCommandType" Type="System.Data.CommandType, System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">Text</Property>
  </Properties>
  <Parameters>
    <Parameter Direction="In" Name="@ProductKey">
      <TypeDescriptor TypeName="System.Int32" IdentifierName="ProductKey" Name="ProductKey" />
    </Parameter>
    <Parameter Direction="Return" Name="ProductImage">
      <TypeDescriptor TypeName="System.Data.IDataReader, System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" IsCollection="true" Name="ProductImageDataReader">
        <TypeDescriptors>
          <TypeDescriptor TypeName="System.Data.IDataRecord, System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" Name="ProductImageDataRecord">
            <TypeDescriptors>
              <TypeDescriptor TypeName="System.Byte[]" Name="LargePhoto" />
            </TypeDescriptors>
          </TypeDescriptor>
        </TypeDescriptors>
      </TypeDescriptor>
    </Parameter>
  </Parameters>
  <MethodInstances>
    <MethodInstance Type="ViewAccessor" ReturnParameterName="ProductImage" ReturnTypeDescriptorName="ProductImageDataReader" ReturnTypeDescriptorLevel="0" Name="GetProductImageInstance" />
  </MethodInstances>
</Method>

Import the BDC application definition file through the SharePoint Central Administration site the same way it was done in the walk-through.

Step 2 – Creating the ASPX page class

There are several ways to expose the binary data. I’ve chosen one that is straight-forward and fairly efficient, but there may be higher-performing ways to do it. In the case of my customer, the pages were not in a high-use part of their site so simplicity won out over high-performance.

The code overrides the page’s Render method. Using the BDC API, it retrieves the blob field based on URL parameters. Once it does that, it clears the Response output cache, sets the application type, and streams the blob field to the Response. There’s also error handling code to output the error as either an image or as HTML, depending on the mime type that was requested. Note that this code could also be extended to rename the output file based on a field in the data record or a URL parameter, by using the Context.RewritePath method. You could also pull the mime type from the BDC record if it is available there.

Using the tool of your choice, create a new C# class (any other .NET language will work as well, but the sample code shown here is all C#). At the customer, I originally used Visual Studio 2005; for this example, I’ve used Visual Studio 2008. Replace the class file code with the following (using your own namespace):

C#
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Text;
using System.Web.UI;

using Microsoft.Office.Server.ApplicationRegistry.MetadataModel;
using Microsoft.Office.Server.ApplicationRegistry.Runtime;

using Microsoft.SharePoint;
using System.IO;

namespace ValdonBlogSamples
{
    // The GetBlobAs class is used to send blob data from a BDC Entity to
    // the browser as a specified file type.
    // The URL must contain the following parameters:
    //      type - the mime type of the blob (for example "image/jpeg")
    //      app - The name of the BDC application instance to connect to
    //      entity - The name of the BDC entity to access
    //      method - The name of the BDC method instance to call
    //      id - The id of the entity to retrieve
    //      field - The name of the field that contains the blob data.  This
    //          must be declared as type byte[] in the application definition file.
    // Example: /GetBlobAs.aspx?type=image%2fjpeg&app=AdventureWorksDWInstance&
    //      entity=Product&method=GetProductImageInstance&id=1234&field=LargePhoto
    public class GetBlobAs : Page
    {
        public GetBlobAs() : base() { }

        protected override void Render(HtmlTextWriter writer)
        {
            string errMsg = string.Empty;
            string contentType = string.Empty;

            try
            {
                // Get the URL parameters
                errMsg = "Error retrieving the URL parameters. Details: ";
                contentType = Request.Params["type"].ToLower();
                string bdcApplication = Request.Params["app"];
                string bdcEntity = Request.Params["entity"];
                string bdcMethod = Request.Params["method"];
                string entityId = Request.Params["id"];
                string fieldName = Request.Params["field"];

                if (string.IsNullOrEmpty(contentType))
                    throw new Exception("Missing content type (type URL parameter)");
                if (string.IsNullOrEmpty(bdcApplication))
                    throw new Exception("Missing BDC Application Instance (app URL parameter)");
                if (string.IsNullOrEmpty(bdcEntity))
                    throw new Exception("Missing BDC Entity (entity URL parameter)");
                if (string.IsNullOrEmpty(bdcMethod))
                    throw new Exception("Missing BDC Method Instance (method URL parameter)");
                if (string.IsNullOrEmpty(entityId))
                    throw new Exception("Missing BDC Entity ID (id URL parameter)");
                if (string.IsNullOrEmpty(fieldName))
                    throw new Exception("Missing BDC Blob Field Name (field URL parameter)");

                // Get the BDC entity
                errMsg = "Error retrieving the BDC entity. Details: ";
                LobSystemInstance app =
                    ApplicationRegistry.GetLobSystemInstanceByName(bdcApplication);
                Entity entity = app.GetEntities()[bdcEntity];
                MethodInstance method = entity.GetMethodInstances()[bdcMethod];
                Object[] args = method.GetMethod().CreateDefaultParameterInstances(method);
                args[0] = Convert.ChangeType(entityId, args[0].GetType());

                IEntityInstanceEnumerator entities = (IEntityInstanceEnumerator)
                    entity.Execute(method, app, ref args);
                if (!entities.MoveNext())
                    throw new Exception("Unable to find Entity instance from the ID.");

                // Get the field value
                errMsg = "Error retrieving the entity field value. Details: ";
                IEntityInstance entityInst = entities.Current;
                Object field = entityInst[fieldName];
                byte[] bytes = field as byte[];
                if (bytes == null)
                    throw new Exception("Entity field is not the correct type (byte[])");

                // Output the field value with the content type specified
                errMsg = "Error writing to the output. Details: ";
                Response.ClearHeaders();
                Response.Clear();
                Response.ContentType = contentType;
                Response.BufferOutput = true;

                using (BinaryWriter bWriter = new BinaryWriter(Response.OutputStream))
                {
                    bWriter.Write(bytes);
                }
            }
            catch (Exception e)
            {
                if (contentType.StartsWith("image"))
                {
                    OutputErrorAsImage(errMsg + e.ToString());
                }
                else
                {
                    OutputErrorAsHtml(errMsg + e.ToString());
                }
            }
        }

        private void OutputErrorAsImage(string errMsg)
        {
            Response.ClearHeaders();
            Response.Clear();
            Response.ContentType = "image/jpeg";
            Response.BufferOutput = true;

            // Create an image.  Note: you could calculate an image size  
            // based on the size of the text using System.Drawing classes
            Bitmap errBitmap = new Bitmap(300, 300);
            Graphics gdi = Graphics.FromImage(errBitmap);
            gdi.FillRectangle(new SolidBrush(Color.DarkRed), 0, 0,
                errBitmap.Width, errBitmap.Height);

            // Put the message on the bitmap
            gdi.DrawString(errMsg, new Font("Tahoma", 10, FontStyle.Bold),
                new SolidBrush(Color.White),
                new RectangleF(5, 5, errBitmap.Width - 10, errBitmap.Height - 10));

            // Stream the error message image to the web response
            errBitmap.Save(Response.OutputStream,
                        System.Drawing.Imaging.ImageFormat.Jpeg);
        }

        private void OutputErrorAsHtml(string errMsg)
        {
            Response.ClearHeaders();
            Response.Clear();
            Context.Response.ContentType = "text/html";
            Response.Write(@"
                <html>
                <head>
                    <meta http-equiv='Content-Type' content='text/html; charset=utf-8'>
                    <title>Error Retrieving BLOB</title>
                </head>
                <body>
                    " + errMsg + @"
                </body>
                </html>");
        }
    }
}

Make sure you code sign the assembly so it can go in the Global Assembly Cache (GAC) then add it to the GAC using “gacutil.exe /i <filename>”. Alternatively, you can copy the assembly to the SharePoint’s bin directory for development and testing without signing it.

Add the new class to the web.config file as a trusted type.

XML
<SafeControl Assembly="GetBlobAs,Version=1.0.0.0,Culture=neutral,PublicKeyToken=ffffffffffffffff" Namespace="ValdonBlogSamples" TypeName="*" Safe="True" />
 

Replace ffffffffffffffff with your PublicKeyToken (use sn.exe –T <assembly path> to get the token), or if it isn’t code signed, use PublicKeyToken=null.

Create a new ASPX page and set the page class to the newly created class (I used SharePoint Designer and placed the page at the root of the site). The rest of the HTML in the page is not important and can be anything that you choose. I called the page GetBlobAs.aspx.

ASP.NET
<%@ page language="C#" inherits="ValdonBlogSamples.GetBlobAs,GetBlobAs,Version=1.0.0.0,Culture=neutral,PublicKeyToken= ffffffffffffffff" %>
 

You could set up this page to be a nicely formatted error page and fill it out if an error occurs. You could also redirect to the SharePoint error page instead of outputting HTML in the code.

Step 3 – Displaying the Image on a page

The final step is to pull everything together to display the image on a page. On the page you created during the walk-through that displays the Products list, add a Business Data Item web part and select the Product entity type.

With the page in edit mode, on the Products list web part, select edit/Connections/Send Selected item to/Product. This will add a radio button to the list which, when selected, will display the details of the item in the Business Data Item part. This should look something like Figure 1.

BDC Web Parts on a Page

Figure 1 - BDC Web Parts on a Page

Now we are going to edit the Product web part’s XSL to insert the image.

Click the web part’s menu and select Modify Shared Web Part.

In the Data View Properties section, click the XSL Editor… button

Select all the text in the dialog (press Ctrl-a) then copy it (Ctrl-c) to your favorite XSL editor tool.

In the dv_1.rowview template section, add a new table row at the top (or wherever you want it) to display the image.

XML
<tr>
    <td class="ms-descriptiontext ms-alignright">
        <nobr>
        Product Image:
    </nobr>
    </td>
    <td class="ms-descriptiontext ms-alignleft" width="100%">
        <img src="/sites/blogs/GetBlobAs.aspx?type=image%2fjpeg&amp;app=AdventureWorksDWInstance&amp;entity=Product&amp;method=GetProductImageInstance&amp;id={@ProductKey}&amp;field=LargePhoto" />
    </td>
</tr>

 

At this point, you can just copy the file back into the properties of the web part (which is good for quick testing), but I would recommend saving the file and uploading it to the site’s Style Library list. This allows you to reuse the XSL file for multiple instances of the Business Data Item web part. I’ve also found that just putting it in through the XSL Editor doesn’t always stick; it seems to get overwritten at times by the default XSL.

Link the Business Data Item web part to the XSL file by specifying the relative path in the XSL Link field of the Miscellaneous section. For example:

/sites/blogs/Style Library/XSL Style Sheets/ProductImageDisplay.xsl

Click OK and select a Product that has an image (I found that #559 – Chain and #306 – HL Mountain Frame – Black, 38 have images). Your page should now look something like:

BDC Web Parts on a Page2

Figure 2 - BDC Web Parts With an Image

If the database includes a thumbnail image, you could modify the XSL of the Business Data List web part to retrieve that field. You’d only need to add a new method to the BDC Application Definition file to execute the required SQL.

You could also extend the C# code to do some server-side manipulation of the image, such as enforcing a standard size, creating a thumbnail on the fly or adding a watermark.

Conclusion

This solution is fairly generic; it can return blobs of any mime type stored in the database by simply linking to an aspx page and providing the BDC information as URL parameters. If you are already using the BDC to retrieve the Entity, it is easy to add a new method to retrieve blob data.

Posted by Valdon | 5 Comments
Filed under: , , , ,
 
Page view tracker