Export (0) Print
Expand All
Expand Minimize

Creating a Windows SharePoint Services 3.0 Custom Field by Using the EntityPicker

Windows SharePoint Services 3

Summary: Learn to create an advanced custom field for Windows SharePoint Services 3.0 and use built-in search dialogs in SharePoint to your advantage. You will also be shown how to implement custom field settings. (11 printed pages)

Wouter van Vugt, Code Counsel

August 2008

Applies to: 2007 Microsoft Office System, Windows SharePoint Services 3.0

Contents

One of the most popular features in Windows SharePoint Services is the area of extensibility using the list and the concept of a field, or column if you will. Windows SharePoint Services 3.0 includes many built-in field types such as the basic Text Field or the more advanced Lookup Field. In the Windows SharePoint Services framework, you can extend the list of built-in fields with your own customizations. For example, Microsoft Office SharePoint Server 2007 customizes a Windows SharePoint Services field to allow data obtained through the business data catalog to appear as a field in your lists and document libraries. This article describes how you can extend the functionality of Windows SharePoint Services with a custom field type that allows you to look up records in other lists in the site collection with a similar user interface (UI) as found in the business data catalog.

When you create a field, various components must work together to form the final solution in Windows SharePoint Services. In its most basic form you need to define an XML file containing the field definition, various pieces of metadata and the collaborative application markup language (CAML)-based rendering logic you use to display the field. The second requirement is the Microsoft .NET Framework class that implements your customization. While the combination of a single XML file and a class might be appropriate for simple fields such as a text field using a regular expression based validation extension, it is not uncommon to have more advanced requirements. Windows SharePoint Services provides this with an extensible field framework that allows various parts of the field to be altered. For example the user interface you display to edit field values. The custom field implementation presented in this article uses these extensibility points to provide a rich and intuitive UI using the built-in customizable search dialog.

Download the sample code: http://code.msdn.microsoft.com/WSS3EntityPicker

Accompanying this article is code for a new field type called the Record Lookup field. As the name implies, it allows you to look up records in other lists, similar to the built-in lookup field. The main difference with the lookup field is that you can link to an entire record in the targeted list instead of including a value of a single column. Another difference is that you are allowed to include extra columns to accommodate values taken from the target list item. Do not mistake this for a database-like foreign key construction because the values are copied and not added as a reference.

First, let me describe the field as it appears in the Windows SharePoint Services UI, the way it is most commonly used. In the following scenario, a list called Projects is already created and filled in a team site. The sample uses a custom list containing a few extra fields that describe a fictional project.

The first step in showing the Record Lookup field is to create a list called Project Reviews. The Custom List template is also used for this second list.

To allow a project review to refer to an item in the projects list, you add a Record Lookup field. The UI that is presented when the Record Lookup field is configured is displayed in Figure 1. As you can see, the user can choose from a site in the site collection and select a list to target. The main display column is set to the Title column in the target list, and there are two extra columns included. The Record Lookup field creates additional fields to store the values of these two extra columns.

Figure 1. The Record Lookup field editor

The RecordLookupField

The display of the list after the field has been added is shown in Figure 2. Notice the inclusion of three new columns, one for the main display value and two others for the included columns of the target list.

Figure 2. A list using the Record Lookup field

A list using the RecordLookupField

The next obvious step is to add new project reviews. The UI that is presented to adding or edit a project review is displayed in Figure 3 and should be recognizable to anyone who has used Windows SharePoint Services before. You will see the familiar text box that allows you to enter a known project name and the validate and search button beside it.

Figure 3. The list-item editing display

The list item editing display

The text that you type in the text box is validated against the main display column you chose when you added the field. The browse button takes you to a custom Windows SharePoint Services dialog box that allows the search of items in the target list. I explain how this custom dialog box is implemented later in the article. The current implementation allows you to base your search on any of the columns that were selected when the field was added. This search dialog box is displayed in Figure 4. The displayed data is taken directly from the target list by issuing a CAML query against it and is independent of the field settings. It is taken from the default view of the target list.

Figure 4. Looking up a value in a list

Looking up a value in a list

Each field type inside Windows SharePoint Services is comprised of various interconnected components, some you observed in the previous demonstration of the field interface. These components work together to provide a rich environment for creating custom data types specific to your company’s needs. You can use custom fields to implement things such as custom UIs for all the places where the field shows up, custom data storage and validation of custom data. You can also extend the field lifecycle with custom actions such as creating additional related fields during the creation or modification of settings of the field.

The following figure shows the main tasks of the components related to the RecordLookupField.

Figure 5. Components of the RecordLookupField field

Looking up a value in a list

A few new components are used in addition to the XML definition and the field class. Let us review these components in detail before reviewing the more intricate parts of the code.

The XML Field Definition

Windows SharePoint Services knows about your fields because they are defined in XML files deployed to each front-end server in your server farm. You find these XML files inside the \Template\Xml subfolder of the Windows SharePoint Services installation. The term fldtypes always prefixes these field-defining files. For example, fldtypesACME.xml is a valid name for the file. The main field definition file belonging to Windows SharePoint Services is called fldtypes.xml and contains definitions of the basic fields such as the text field or choice field. An important side note is that you should not alter the fldtypes.xml file because Windows SharePoint Services relies on it for built-in features. In all likelihood, updates to Windows SharePoint Services are allowed to overwrite this file. Within one XML file you can define multiple fields, their metadata and rendering logic.

The basic field definition of the Record Lookup field is displayed in the following sample. The TypeName property is the main identifier for the field. It is used when you create fields through code with the CreateField method of the SPFieldCollection class. The TypeShortDescription property is displayed in the UI when the field is created or edited. The XML definition identifies two .NET Framework classes. The first is a class representing the field; the second identifies the field editor control. This field editor control is displayed when the field settings are modified. After a short and incomplete list of metadata settings (there are more settings available) the custom settings appear and the file ends with the CAML-based rendering pattern. This pattern is used to display the field in the list viewing interface.

Code Example 1. XML definition for the RecordLookupField field

<?xml version="1.0" encoding="utf-8" ?>
<FieldTypes>
<FieldType>
<Field Name="TypeName">RecordLookupField</Field>
<Field Name="TypeDisplayName">Record Lookup</Field>
<Field Name="TypeShortDescription">Record Lookup (Records already on this site collection)</Field>
<Field Name="ParentType">Text</Field>
<Field Name="FieldTypeClass">CodeCounsel.SharePoint.Fields.RecordLookupField, CodeCounsel.SharePoint.Fields, Version=1.0.0.0, Culture=Neutral, PublicKeyToken=9e1907c8b7a1abf2</Field>
<Field Name="FieldEditorUserControl">/_controltemplates/RecordLookupFieldEditor.ascx</Field>
<Field Name="UserCreatable">TRUE</Field>
<Field Name="Sortable">TRUE</Field>
<Field Name="Filterable">TRUE</Field>
<PropertySchema>
<Fields>
<Field Name="WebID" Type="Text" Hidden="TRUE" />...
</Fields>
</PropertySchema>
<RenderPattern Name="DisplayPattern">
...``
</RenderPattern>
</FieldType>
</FieldTypes>

The custom settings specific to the RecordLookupField object such as the WebId property and CAML-rendering pattern are important because to access the settings at runtime through the RecordLookupField class (the class implementing the field in the .NET Framework) you first need to define them here. There is also a limited set of allowed data types you can use. Basically, strings and simple primitives are allowed. The CAML-rendering logic is not part of the code example and is discussed later.

The deployment location of the field definition is also an important consideration. Because the XML is deployed to a global directory, all the sites within each Web application on that Web server can access the field definition and then display it in the list column creation interface. There is no clean way of hiding the field definition, so the best that you can do to prevent using the field in certain sites is to use the feature framework to block the field settings interface and disallow creating the field that way.

Using Field Classes

The field class is the main implementation of a field. All fields in Windows SharePoint Services derive either directly or indirectly from the SPField class. The field class provides a storage point for the field settings defined in the XML field definition and allows the modification of these setting through code. The field class also takes part in other tasks such as validating the field value.

There are a few interesting points to know about field classes. First, you should derive from the class which corresponds to the ParentType setting in the XML field definition. You should not derive directly from the SPField class. The mapping is rather obvious. For the parent Text object you need to derive from the SPFieldText object. The class must be public and have two specific public constructors to work properly. This is due to instantiating the field dynamically at runtime using Reflection. Additionally, using fields requires the Windows SharePoint Services object model permission. This permission is demanded from the callers.

Code Example 2. Class representing the custom field

Code Example[CLSCompliant(false)]
[SharePointPermissionAttribute(SecurityAction.Demand, ObjectModel = true)]
public class RecordLookupField : SPFieldText
{
    public RecordLookupField(
        SPFieldCollection fields, string fieldName)
        : base(fields, fieldName)
    {
    }

    public RecordLookupField(
            SPFieldCollection fields, string typeName, 
            string displayName)
        : base(fields, typeName, displayName)
    {
    }

    public override void OnAdded(SPAddFieldOptions op) { }

    public override void OnUpdated() { }

    public override void OnDeleting() { }
}

In the SPField class there are three methods that you can override to customize the behavior for various phases of the field’s lifetime. The code running inside these methods needs to consider various implementation details of Windows SharePoint Services, which are handled in the next section.

Now we look at the first extra component used by the RecordLookupField, the field editor.

Using Field Editors

The XML field definition can define custom settings for the field by applying the <PropertySchema> element, as shown earlier in the sample XML field definition. The Record Lookup field uses these settings to identify the site, list and display columns used for looking up the field values. The editor provides a UI for editing field settings. If implemented correctly, you can also modify the field settings directly in the field class without using field editor, for example in a Windows Forms management application running locally on the Web server.

In its most basic form, a field editor is auto-generated based on the settings defined within the XML field definition. Based on the property type, a specific editing control is available. You can hide certain controls by setting the Hidden property to TRUE in the XML definition.

The auto-generated editor is suitable for the most basic requirements, but for more advanced needs it is also possible to go further into the framework by building your own custom interface using a Microsoft ASP.NET control.

By implementing a UserControl and deploying it to the Windows SharePoint Services control templates folder you can show a custom UI that better serves the requirements of the field. The XML field definition holds a reference to this control. Code example 3 displays a part of the markup for the control that the Record Lookup field uses. Note that the control inherits from a class provided by the solution, and there is no code-behind file specified as you might find in the control directive of a normal ASP.NET user control. The class is deployed to the global assembly cache, which is why the Inherits attribute uses a fully qualified assembly name.

Code Example 3. The field editing UserControl

<%@ Control language="C#" 
    Inherits="CodeCounsel.SharePoint.Fields.RecordLookupFieldEditor, 
        CodeCounsel.SharePoint.Fields, Version=1.0.0.0, 
        Culture=Neutral, PublicKeyToken=9e1907c8b7a1abf2" %>
...        
<asp:DropDownList id="_webField" runat="server" 
    AutoPostBack="true"
    OnSelectedIndexChanged="WebField_SelectedIndexChanged" />...

To interact with Windows SharePoint Services, a field editor UserControl must implement the IFieldEditor interface. Code example 4 shows the basic layout of the RecordLookupFieldEditor. The first notable part is the use of protected members for the controls defined in the ASCX part of the user control. One common pitfall I come across is accessing these controls. You should not resort to using the FindControl method to get to them. Remember that you can create protected instance fields with the same name as the ID of the control to allow ASP.NET to link the instances.

The rest of the sample shows the IFieldEditor implementation. The interaction with Windows SharePoint Services is very basic. There is the InitializeWithField method to initialize the editor with the settings of a specific field and a second method to save the field settings back again. An important note is that the InitializeWithField method runs for each request, the initial request as well as post-back requests. You should handle the new data and old field data accordingly. Also, the field passed to the InitializeWithField method can be a null value. This occurs when you create a new field and there are no settings with which to initialize.

Code Example 4. The code for the field editor control

public class RecordLookupFieldEditor
    : UserControl, IFieldEditor
{
    protected DropDownList _webField = null;
    protected DropDownList _listField = null;
    protected DropDownList _mainDisplayColumnField = null;
    protected CheckBoxList _additionalColumnsField = null;
    protected CheckBox _linkToRelatedRecordField = null;

    bool IFieldEditor.DisplayAsNewSection
    {
        get { return false; }
    }

    void IFieldEditor.InitializeWithField(SPField field)
    {
    }

    void IFieldEditor.OnSaveChange(SPField field, bool isNewField)
    {
    }
}

There are a few interesting implementation notes on the field editor in the next section. Before showing the specific implementation, I will discuss the last component of an advanced field implementation.

Using Field Controls

The field control defines the appearance of the field when editing list item values. Figure 3 displays the one the Record Lookup field uses. As you can observe you can provide your own interface.

When using custom field controls, like the Record Lookup field does, you need to create a composite control that derives from the BaseFieldControl class. A field control is just a normal composite control plus some extras to make it interact with Windows SharePoint Services. The control has access to the value in the list item you are editing and has a property where its own current value is stored. The Value property stores the current value of the editing control, the ItemFieldValue property points to the value in the list item. The Validate method is called to check whether the field value is valid and then the UpdateFieldValueInItem method is called to update the value of the list item.

Code Example 5. The field value editing control

public class RecordLookupFieldControl 
    : BaseFieldControl
{
    public RecordLookupFieldControl()
    {
    }

    public override void Validate()
    {    }

    public override void UpdateFieldValueInItem()
    {
        ItemFieldValue = Value;
    }

    protected override void CreateChildControls()
    {            
    }
}

This concludes the review of the main classes that form the Record Lookup field. the next section of the article describes the code implementation in relationship to Windows SharePoint Services. There are a few intricacies in the implementation that I hope will help you implement your own custom fields.

Your custom field may need some metadata to work correctly, such as the regular expression used by a custom field type implementing regular expression based validation or the site, list, and columns that the Record Lookup field uses. One of the most common things people have difficulty with is storing custom field settings correctly. You should not persist the field settings to your own database; instead they should go into a content database that belongs to the site collection in Windows SharePoint Services. The way this works is through the SchemaXml property of your field class, which stores the values of the field settings as a simple XML structure. All built-in settings are persisted as attributes of the root element, which usually is the only element there. The following code example shows this persistency store. This is taken from an instance of a random list in Windows SharePoint Services by code similar to mySite.Lists[0].Fields[0].SchemaXml.

Code Example 6. The XML persistency store for field settings

<Field ID="{fa564e0f-0c70-4ab9-b863-0177e6ddd247}" 
Type="Text" 
Name="Title" 
DisplayName="Title" 
Required="TRUE" 
SourceID="http://schemas.microsoft.com/sharepoint/v3" 
StaticName="Title" 
FromBaseType="TRUE" 
ColName="nvarchar1" />

The XML looks similar to the XML field definition markup, only the field has a unique ID attached to identify the field in the list instance. Windows SharePoint Services uses the attributes that you see in the example to persist the field-specific state. Using the GetProperty method of the SPField class you can access the value of one of these attributes by name. Notice that the SetProperty method is not present in the SPField class. While you can write your own code to persist to the attributes of SchemaXml by editing the XML directly, you should instead use a different part of the XML data store and the access methods that accompany it.

For custom properties, Windows SharePoint Services creates a separate child element available in the XML structure called customization. You can write objects to this part of the data store using the GetCustomProperty method and the SetCustomProperty method. The following example shows the custom settings part of the XML data store.

Code Example 7. The XML persistency store for custom field settings

<Field 
ID="{388cd95a-ae80-47cc-b487-08c7ee784c71}" 
Type="Text" 
Name="Title" 
DisplayName="Title" 
Required="FALSE" 
SourceID="http://schemas.microsoft.com/sharepoint/v3" 
StaticName="Title" 
ColName="nvarchar1" 
FromBaseType="True">
<Customization>
<ArrayOfProperty>
<Property>
<Name>MyCustomProperty</Name>
<Value p4:type="q1:string" 
xmlns:q1="http://www.w3.org/2001/XMLSchema" 
xmlns:p4="http://www.w3.org/2001/XMLSchema-instance">
The MyCustomProperty value
</Value>
</Property>
</ArrayOfProperty>
</Customization>
</Field>

Now you might wonder why this is something people have difficulty with. The split between Windows SharePoint Services and user settings is reasonable so why would it be difficult? There are a few things to know about the custom property persistency store. The first is the moment when calls to the database occur. The GetCustomProperty method and SetCustomProperty method by themselves do not cause database interaction to occur. You should call the Update method to persist the custom properties to the database. Updating the SchemaXml property directly works differently. Changing the property updates the database immediately without the need to call the Update method. The second interesting point is the persistence format used by Windows SharePoint Services. Internally your objects are serialized to XML using the XmlSerializer class when the field settings are persisted to the database.

Before using the XmlSerializer class, all reference types are converted to a string using the ToString method. (I swear I used trial and error to find this out). This means you can only store rich objects such as Array objects through your own serialization mechanism.

Another difficulty is cross-instance persistence of settings. For me to explain this problem fully, first you need to know the way new fields are processed by Windows SharePoint Services.

Creating Fields

The process of creating fields is a basic one. You first configure a field instance and then add it to the SPFieldCollection object of the list you want to extend with a new column. The mechanism underneath is slightly different. Based on the user’s actions, first an instance of the field is created using the CreateNewField method on the field collection. Next, this instance is configured and finally it is added to the list using the Add method. In the Add method the SchemaXml property is accessed and persisted to the database. When you access the new field using its internal name, you get a second instance. The following code example demonstrates this. The WriteLine call displays the text "False."

Code Example 8. Multiple field instances when creating fields

SPList list = web.Lists["Project Reviews"];
// Create field and alter settings
SPField field = list.Fields.CreateNewField(
    "Text", "My Field");
field.Title = "This is an altered setting";
// Add and retrieve again
string name = list.Fields.Add(field);
SPField field2 = list.Fields.GetFieldByInternalName(name);
// Compare instances
Console.WriteLine(field == field2);

Now let me explain this in relationship to the part of SchemaXml property that stores your settings. As discussed, the root element stores the built-in settings and the Customization element stores the custom settings. It is recommended that you use the Customization element as a good behaving SharePoint citizen. The difficulty occurs when you create and configure a new field before you add it to the SPFieldCollection object. Your configuration is stored in a temporary cache in the field and not in the SchemaXml property just yet. (The cache is flushed when you call the Update method remember?) When adding the field to the field collection internally the SchemaXml property is accessed and stored in the content database, without your custom settings.

You can use the three life-time related events (the OnAdded event, the OnUpdated event, and the OnDeleting event) to work around this situation. Using the OnAdded event and the OnUpdated event, you can call the Update method to move your custom settings from the temporary cache into the SchemaXml property and the content database. But now you run into another difficulty. I demonstrated earlier that Windows SharePoint Services creates separate instances of your field class after initially adding the field to the field collection. The difficulty is that while you have configured the first instance, the lifetime events fire on the second. The event is not named OnAdded for nothing, the database interaction for creating the field has already occurred.

All of this basically means that you store custom settings in a temporary persistency store to survive the creation of a field by Windows SharePoint Services. After, you can use the lifetime event to move the custom setting from the temporary store into the cache and. Then you finish it off with a call to the Update method to move the setting to the SchemaXml persistency store and next to the database. This does mean that two database calls occur.

Persisting Values Across Instances

You can think of many solutions for the temporary storage of field settings, but most are unusable when you want your field to work in Windows Forms management applications that runs on the front-end server. When accessing Windows SharePoint Services from Windows Forms applications there is no ASP.NET context, which means the Application/Session/Request and other ASP.NET caches are not available. One thing that these applications do have in common is a thread that operates it. The temporary persistency store used for storing setting values across instances when adding fields can use thread local storage. The only thing you need to worry about is which property to access during the OnAdded event and OnUpdated event. This is discussed next. Code example 9 shows how a custom field setting is persisted.

Code Example 9. Persisting and retrieving custom field settings

class RecordLookupField
{    ... 
    public string MainDisplayColumn
    {
        get
        {            // Only used during field updates
            return GetCustomProperty(
                "MainDisplayColumn");
        }
        set
        {
            // Used during field creation
            SetNamedThreadData("MainDisplayColumn", value);
            // Used during field updates 
            SetCustomProperty("MainDisplayColumn", value);
        }
    }    void SetNamedThreadData(string propertyName, string value)
    {
        Thread.SetData(
            Thread.GetNamedDataSlot(propertyName), 
            value);
    }
    ...
}

Added, Updated and Deleted Events

The SPField class provides three methods you can override that are called during the lifetime of the field. The OnAdded method and the OnUpdated method are fired after and the OnDeleting method is fired before the three main events in that lifecycle: adding, updating and deleting the field. There are a few intricacies to the implementation based on how settings are stored and there is interdependence between the OnAdded event and the OnUpdated event. Because you need to store your custom settings during the OnAdded event due to behavior discussed earlier, you need to call the Update method. This, in turn, raises the OnUpdated event, which if you are not careful, updates the database again for a third time.

So let us review how this looks for a custom field implementation. The following code is simplified code from the RecordLookupField class.

Example One: Simple Custom Setting

The first example is a setting that uses a simple value that does not require any further calculation. The setting is defined in the XML field definition and the SPField derived class contains a .NET Framework property to access the setting. Because the setting does not require any calculation, the setter of the property stores the field directly in the base class using the SetCustomProperty method and stores the value in the thread-local storage for later retrieval in the OnAdded event.

Code Example 10. A custom setting that does not require calculation

public string MainDisplayColumn
{
    get
    {
        return GetCustomProperty("MainDisplayColumn");
    }
    set
    {
        SetCustomProperty("MainDisplayColumn", value);
        SetNamedThreadData("MainDisplayColumn", value);
    }
}

The OnAdded method running in the second field instance is responsible for transferring the field setting from the temporary thread-based storage to the storage in . The Update method moves the custom setting value from the internal container in the base field to the SchemaXml property and then stores everything in the database. This also makes the new value available in the MainDisplayColumn property getter. Because the Update method is called, the OnUpdated method is prevented from running.

Code Example 11. The OnAdded event for a custom setting that does not require calculation

public override void OnAdded(SPAddFieldOptions op)
{
    _preventUpdate = true;
    string mainDisplayColumn = GetNamedThreadData("MainDisplayColumn");
    SetCustomProperty("MainDisplayColumn", mainDisplayColumn);
    Update();
    ClearNamedThreadData("MainDisplayColumn");
}

The OnUpdated method may look silly right now, but stay with me. When you use calculated settings this method gets bigger, so consider it a piece of framework code. In the update scenario, the user of the field is the one modifying settings and the value of the setting is stored when the user calls Update through code or the Windows SharePoint Services UI. Because there is no creation of multiple field instances during update scenarios, there is no need to transfer the setting from the thread-based storage to the custom property container. However, changes to the property through the setter do add the value to the thread-local storage, which you should clear.

Code Example 12. The OnUpdated event for a custom setting that requires calculation

public override void OnUpdated()
{
    if (_preventUpdate == false)
    {
        _preventUpdate = true;
        ClearNamedThreadData("MainDisplayColumn");
    }
}

Example Two: Calculated Custom Setting

The second example is for a setting that is calculated when you add or update the field, also neatly stored in the Customization part of SchemaXml. Because the value is calculated, the property only has a get-accessor. The data is stored in the Customization part of the settings storage so the GetCustomProperty method is used to access the value.

Code Example 13. A custom setting that requires calculation

public string MainDisplayColumn
{
    get { return GetCustomProperty("MainDisplayColumn"); }
}

The OnAdded method is basically the same as shown earlier only now the value is not stored in the thread based storage but is calculated. Also, clearing the thread-based storage is not necessary because it is not used.

Code Example 14. The OnAdded event for a custom setting that requires calculation

public override void OnAdded(SPAddFieldOptions op)
{
    _preventUpdate = true;
    string mainDisplayColumn = "A calculated value";
    SetCustomProperty("MainDisplayColumn", mainDisplayColumn);
    Update();
}

The OnUpdated method is more significant in this scenario. The field value is recalculated and stored in the custom settings cache. The Update method is called again to transfer the values to the content database.

Code Example 15. The OnUpdated event for a custom setting that requires calculation

public override void OnUpdated()
{
    if (_preventUpdate == false)
    {
        _preventUpdate = true;
        string mainDisplayColumn = "A re-calculated value";
        SetCustomProperty("MainDisplayColumn", mainDisplayColumn);
        Update();
    }
}

Of course, most of the time these two different samples are combined because there are multiple settings defined for the field.

Technically, you can also store your field settings in the root element of SchemaXml. The main reason you may want to do this is that your field settings should be available to drive the CAML-rendering pattern defined in the XML field definition.

Now that we examined how to store custom properties, I want to focus on the search control and dialog box in Windows SharePoint Services, called the Entity Picker.

One of the features that I find highly usable for field implementations is the Entity Picker. The entity picker provides a common UI for a search field using the search box and search dialog.

Figure 6. An entity picker dialog

An entity picker dialog

To implement a custom entity picker you require at least three classes. First of all, you need a class deriving from the EntityEditorWithPicker class, which is an ASP.NET control class. This class provides the search box and the two buttons of the basic search interface seen in Figure 3. Next, you create a class deriving from the PickerDialog class, which implements the dialog part of the UI. You create the top part of the picker dialog using a specific query control, the bottom using a results control. The query control is something you implement yourself and the results control is commonly a TableResultControl object. These three controls work in unison to form the dialog.

An important aspect of working with the entity picker is that an entity picker uses its own data format in the form of PickerEntity objects. These PickerEntity objects form the data shown in the controls. You need to translate the data based on the PickerEntity object to real data for your field. There is a second data conversion required for working with the picker dialog. Search results in the search dialog are provided as an ADO.NET DataTable object. You need to convert the DataTable object to data based on the PickerEntity object, which then converts to your field data later in the process.

A PickerEntity object is a simple data type that has a key, a display name, and a Boolean value indicating whether the PickerEntity is valid, known as a resolved entity. The PickerEntity class also allows you to add custom name/value pair useful for transferring arbitrary metadata for a selected item in the search interface. The RecordPicker uses this to transfer the values of additional display columns. The key of the PickerEntity object stores the GUID identifier of the selected list item.

Let us review some of the details of the components found in the RecordPicker class, the entity picker used by the RecordLookupFieldControl.

Using the RecordPicker Class

The RecordPicker class is the main editing control. By deriving from the EntityEditorWithPicker class you get the search box and buttons for free, forming the familiar search interface.

Figure 7. Built-in SharePoint search field

The built in SharePoint search field

You can set some of the properties in the RecordPicker constructor. One important property is the PickerDialogType that points to the class that implements the dialog part of the UI.

Code Example 16. RecordPicker class

public class RecordPicker
    : EntityEditorWithPicker
{
    public RecordPicker()
    {
        PickerDialogType = typeof(RecordPickerDialog);
        ValidatorEnabled = true;
        AllowTypeIn = true;
        MultiSelect = false;
    }
}

Because the RecordPicker object needs to determine where it needs to lookup records, you need to configure the RecordPicker object with the appropriate settings. This data must available for the search box and the dialog box so storing data in view-state does not suffice. The EntityEditorWithPicker base class provides the CustomProperty property, which you can use to store a string of custom data available to both the dialog and the search box.

To provide richer data than a single string you can simply use object serialization. The RecordPicker object uses this to store a RecordPickerData object containing all the configuration information required.

Code Example 17. Storing custom data for the RecordPicker

RecordPickerData _pickerData = null;
public RecordPickerData PickerData
{
    get
    {
        if (_pickerData == null && 
            String.IsNullOrEmpty(CustomProperty) == false)
        {
            byte[] buffer = Convert.FromBase64String(CustomProperty);
            using (MemoryStream stream = new MemoryStream(buffer))
            {
                BinaryFormatter formatter = new BinaryFormatter();
                _pickerData = 
                 (RecordPickerData)formatter.Deserialize(stream);
            }
        }
        return _pickerData;
    }
    set
    {
        if (value != null)
        {
            _pickerData = value;
            using (MemoryStream stream = new MemoryStream())
            {
                BinaryFormatter formatter = new BinaryFormatter();
                formatter.Serialize(stream, value);
                CustomProperty = Convert.ToBase64String(stream.ToArray());
            }
        }
        else
        {
            CustomProperty = null;
            _pickerData = null;
        }
    }
}

The most important part of the RecordPicker code is the ValidateEntity method. You use this method to validate a PickerEntity object, which represents a selected list item in the targeted list. There are two occasions for this. First, when you press the validate button after entering some text this method is called with an unresolved PickerEntity object that you are then asked to resolve. This method is also called when the OK button is pressed in the field editing page. To prevent validating twice, resolved PickerEntity objects are put aside.

Code Example 18. Validating unresolved entities

public override PickerEntity ValidateEntity(PickerEntity needsValidation)
{
    if (needsValidation.IsResolved)
    {
        return needsValidation; 
    }
    // real validation code
}

When an unresolved entity is passed, the assumption is that the DisplayText property of the PickerEntity object contains the text the user has entered in the search box. You can safely make this assumption because the only time you are asked to validate unresolved entities is when you use the search box. The search dialog provides resolved entities. For the RecordLookupPicker object the first real task is to create a CAML query for the targeted list and to get the list items that match. Because the validate button is expected to find exact matches, the CAML query uses the <Eq> element. The main display column is used for the query.

Code Example 19. Querying the target list for the item using CAML

string fieldText = needsValidation.DisplayText;

SPQuery query = new SPQuery();
query.Query = String.Format(
   @"<Where>
        <Eq>
            <FieldRef Name='{0}'/>
            <Value Type='Text'>{1}</Value>
        </Eq>
    </Where>", 
   PickerData.MainDisplayColumn, fieldText);
_currentMatches = List.GetItems(query);

The last part is checking the results. A resolved entity is returned only when there is exactly one result. A static method serves as a factory because creating a PickerEntity from a SPListItem is a common task in the code of the RecordLookupField.

Code Example 20. Verifying the search result and building a PickerEntity

switch (_currentMatches.Count)
{
    case 0:
        needsValidation.Description = "No match";
        break;
    case 1:
        needsValidation = RecordPicker.CreatePickerEntity(
            _currentMatches[0],
            PickerData.IDFieldName,
            needsValidation.DisplayText,
            PickerData.MainDisplayColumn,
            PickerData.AdditionalDisplayColumns,
            PickerData.AdditionalDisplayColumns,
            _currentMatches[0].UniqueId.ToString());
        break;

    default:
        needsValidation.Description = "Multiple matches";
        break;
}return needsValidation;

Using the RecordPickerDialog Object

To further facilitate in the quest for list-items you can add a reference using the RecordLookupField, the search dialog allows the user to search for items instead of entering them manually in the search box. The RecordPickerDialog class is responsible for this. First, it defines which controls to show for the query and display part of the dialog, and builds the results view in the dialog box. Note that the results displayed in the dialog can differ from what is actually needed as the final field value. You are responsible for the value mapping between the PickerEntity object and field value.

The basic layout of the RecordPickerDialog object is displayed below.

Code Example 21. The RecordPickerDialog constructor

public class RecordPickerDialog
    : PickerDialog
{
    public RecordPickerDialog()
        : base( new RecordPickerQueryControl(),
            new TableResultControl(), new RecordPicker())
    {
    }
}

Because the results control is a table, you need to define columns. The OnPreRender method facilitates this. This is an important part, because later you also define columns in the DataTable object containing the query results. You can have more columns in the DataTable instance than are actually shown in the results control. The RecordPickerDialog object uses this feature to store addition list-item values (ID and title) used for building the final result later on in the lifecycle.

Code Example 22. Building the results table

protected override void OnPreRender(EventArgs e)
{            
    TableResultControl resultControl = (TableResultControl)ResultControl;
    ArrayList columnDisplayNames = resultControl.ColumnDisplayNames;
    ArrayList columnNames = resultControl.ColumnNames;
    ArrayList columnWidths = resultControl.ColumnWidths;
    // Create a display column for each field in the default view
    SPView defaultView = List.DefaultView;
    int percentage = (int)(100 / defaultView.ViewFields.Count);
    foreach (string fieldName in defaultView.ViewFields)
    {
        CreateResultColumn(
            columnDisplayNames, columnNames, 
            columnWidths, percentage, fieldName);
    }            
    base.OnPreRender(e);
}

The RecordPickerQueryControl

The simple query control used by the RecordPickerDialog object provides two controls for issuing queries. You find a DropDownList object where you can search for columns and a TextBox object where you can enter the search query.

Figure 8. The query control in the search dialog

The query control in the search dialog

The first step in the lifetime of the RecordPickerQueryControl is the creation of filter columns displayed in the drop down list. The OnLoad method is used for this. The RecordLookupField object allows you to search for values in each of the configured columns.

Code Example 23. The query control for the RecordPickerDialog

class RecordPickerQueryControl
    : SimpleQueryControl
{
    protected override void OnLoad(EventArgs e)
    {
        EnsureChildControls();
        DropDownList list = base.ColumnList;
        SPList list = this.RecordPickerDialog.List;
        SPField mainDisplayField = list.Fields.GetField(
            PickerData.MainDisplayColumn);
        list.Items.Add(
            new ListItem(
                mainDisplayField.Title,                 mainDisplayField.InternalName));

        foreach (string additionalColumn in
            PickerData.AdditionalDisplayColumns)
        {
            SPField field = list.Fields.GetField(additionalColumn);
            list.Items.Add(
                new ListItem(field.Title, field.InternalName));
        }
        base.OnLoad(e);
    }
}

The next interesting part in the query lifetime is the IssueQuery method. You use this method to query your data store for the user’s searches. The RecordPickerQueryControl object uses this to query the target list using a CAML query. The query is slightly different compared to the one the RecordPicker object uses during validation. The following code example shows the parts necessary for your own field implementations because the rest of the code is rather verbose. Note that the results table should contain the columns defined earlier in the RecordPickerDialog object.

Code Example 24. Issuing a search query

protected override int IssueQuery(
    string search, string groupName, int pageIndex, int pageSize)
{
    DataTable resultsTable = new DataTable();
    // create columns

    // add rows

    RecordPickerDialog.Results = resultsTable;
    return resultsTable.Rows.Count;
}

The final task is to convert a DataRow object into a PickerEntity object used by the rest of the query field infrastructure. The conversion is performed by overloading the GetEntity method. To do this, you instantiate a new PickerEntity object and configure it appropriately. The PickerEntity object has built-in storage facilities for the data key and display text and also serves as a container for extra data. An important aspect is setting the IsResolved property indicating the PickerEntity object was successfully found in the target list. This property blocks the validation method discussed earlier.

Code Example 24. Converting a search item to a PickerEntity

public override PickerEntity GetEntity(DataRow entityDataRow)
{
    if (entityDataRow == null)
    {
        throw new ArgumentNullException("entityDataRow");
    }
    PickerEntity entity = new PickerEntity();
    entity.Key = (string)entityDataRow[IDColumnName];
    entity.IsResolved = true;
    entity.DisplayText = (string)entityDataRow[TitleColumnName];
    entity.Description = (string)entityDataRow[TitleColumnName];
    SPList list = RecordPickerDialog.List;            
    foreach (string viewFieldName in
        RecordPickerDialog.RecordPicker.PickerData.AllDisplayColumns)
    {
        SPField viewField = list.Fields.GetField(viewFieldName);

        entity.EntityData[viewField.InternalName] = 
            entityDataRow[viewField.InternalName];
    }            
    entity.EntityData.Add(
        RecordPickerDialog.RecordPicker.PickerData.IDFieldName, 
        entityDataRow[IDColumnName]);
    return entity;
}

One of the extensibility points of Windows SharePoint Services revolves around custom field implementations encapsulating business requirements in a direct and reusable manner. The Record Lookup field displays how to implement an advanced field for Windows SharePoint Services 3.0 and shows how to make use of advanced infrastructure such as custom settings and the EntityPicker search controls.

Wouter van Vugt is an Open XML MVP active with development of Microsoft Office-based solutions and training for Fortune 500 companies in Europe. He is the founder of Code Counsel, a company targeted to technology evangelization, coaching and training on the .NET Framework inside the EMEA region, with a focus on the European market. Contact him through Code Counsel at wouter@code-counsel.net or check up on his blog at http://blogs.code-counsel.net/wouter.

Show:
© 2015 Microsoft