Tips and Tricks for Developing with Windows SharePoint Services

 

Marco Bellinaso
Code Architects Srl

December 2005

Applies to:
    Microsoft Windows SharePoint Services
    Microsoft Office FrontPage 2003

Summary: Learn programming techniques that can help make your Windows SharePoint Services Web Parts and applications faster, safer and more reliable, easier to deploy, and even more user-friendly. Author Marco Bellinaso compiled this collection of tips and tricks into a well-organized, single source while working on a large workflow project for Windows SharePoint Services. Reap the benefits of his experience to get more out of your development efforts with Windows SharePoint Services. (26 printed pages)

Contents

Setting Default Values for Web Part Properties in the .dwp File
Writing Web Parts with Preview Support for FrontPage
Customizing the Web Part Shortcut Menu
Using Impersonation to Access Data that the Current User Cannot Access
Referring to Lists and Fields
Making Updates with the AllowUnsafeUpdates Property of SPWeb Class
Filtering a Collection of List Items by Using Views or CAML Queries
Using the DataTable Object to Work with List Items in Memory
Customizing the Context Menu of Document Library Items
Conclusion
About the Author

Introduction

Learn programming techniques that can help make your Microsoft Windows SharePoint Services Web Parts and applications faster, safer and more reliable, easier to deploy, and even more user-friendly. Author Marco Bellinaso compiled this collection of tips and tricks into a well-organized, single source while working on a large workflow project for Windows SharePoint Services. Reap the benefits of his experience to get more out of your development efforts with Windows SharePoint Services.

Setting Default Values for Web Part Properties in the .dwp File

The .dwp files are XML files that are added to Microsoft Windows SharePoint Services Web Part catalogs. These files tell the SharePoint site the Web Part's name and description, which are shown on the catalog sidebar, and also the assembly and type name of the Web Part class, which is necessary to load and instantiate it. In addition to the <Title>, <Description>, <Assembly>, and <TypeName> tags that define these settings, you can add other tags named after built-in or custom Web Part properties, to specify their default value. The following code example shows the content of a sample .dwp file that defines the defaults for the built-in FrameType property (set to None, so that the Web Part does not have borders and title bar) and the custom Operator property.

<?xml version="1.0" encoding="utf-8"?>
<WebPart xmlns="http://schemas.microsoft.com/WebPart/v2" >
   <Title>Calculator</Title>
   <Description>A scientific online calculator</Description>
   <Assembly>MsdnParts</Assembly>
   <TypeName>MsdnParts.Calculator</TypeName>
   <FrameType>None</FrameType>
   <Operator xmlns="MsdnParts">Addition</Operator>
</WebPart>

The only difference between setting a built-in and a custom property is that for the latter case, you also need to specify the xmlns attribute after the property name. Its value must be the same as that used for the Namespace property of the XmlRoot attribute added to the Web Part class, as shown in the following example.

[ToolboxData("<{0}:Hello runat=server></{0}:Hello>"),
   XmlRoot(Namespace="MsdnParts")]
public class Hello : Microsoft.SharePoint.WebPartPages.WebPart
{
   ...
}

Setting the default this way, instead of just from code within the class itself, allows the developer to distribute a single, compiled package. Web site administrators can then change the default values on their own, without requesting a change and recompilation of the source code.

Writing Web Parts with Preview Support for FrontPage

When you try to modify a Web Part from Microsoft Office FrontPage 2003, you may get an error message saying that a preview for that Web Part is not available. To add a preview for FrontPage 2003, a Web Part must implement the Microsoft.SharePoint.WebControls.IDesignTimeHtmlProvider interface. This interface exposes a single method, GetDesignTimeHtml, which returns the static HTML shown from within the editor.

Often, what you can display at design time is not what the Web Part actually displays once deployed, because the output may depend on dynamic queries to a database, on the role of the current user, and on other factors. So, what you return from GetDesignTimeHtml is sample HTML, which should give the designer an idea of what the Web Part will look like.

You can, however, build the resulting HTML according to the Web Part's style properties, so that at least the appearance of the Web Part—if not the content—does not change at run time. In the simplest case, you may even return a static HTML description, saying that in that place, the user will see the real Web Part. Here is an example that shows how to implement the interface to produce a preview representing a descriptive string over a yellow background.

public class Calculator :
   Microsoft.SharePoint.WebPartPages.WebPart, 
   Microsoft.SharePoint.WebPartControls.IDesignTimeHtmlProvider
{
   public string GetDesignTimeHtml()
   {
      return @"<div style=""border: black 1px solid; 
         background-color: yellow"">
         The definitive online calculator will be shown here...</div>";
   }

   // the rest of the Web Part's code...
}

Figure 1 shows the preview from inside FrontPage 2003.

Figure 1. The Web Part preview from inside FrontPage 2003

An alternative solution consists of using a static image as a preview. To do this, you set the GetDesignTimeHtml method to output an <img> tag that refers to it.

This approach has an important limitation: the output is fixed. That is, it does not change according to the values of the Web Part's style properties. An advantage, however, is that you can change the preview image without modifying and recompiling the source code.

If you want to try this approach, add the image to your project, set its Build Action property to "Content", and set a reference for it in the Manifest.xml file. If you have a CAB project in your Microsoft Visual Studio .NET solution and have added the Primary Project's Content Files to it, all the Web Part project's files for which the Build Action property is set to "Content" are included in the resulting .cab file, including the Manifest.xml file and all the .dwp files.

When you run the Stsadm.exe tool, it reads the Manifest.xml file and deploys all the referenced resources (the preview image and other files that you may have, such as documentation or license files) to a directory named as the assembly and located in the path, /wpresources. This is something you must do by hand if you do not use Stsadm.exe and .cab packages.

At this point, in the GetDesignTimeHtml method, you return the HTML code that displays the image, whose virtual path is automatically retrieved through the ClassResourcePath property of the WebPart base class, as shown in the following example.

public string GetDesignTimeHtml()
{
   return string.Format(
      @"<img border=""0"" src=""{0}"">",
      this.ClassResourcePath + "/preview.jpg");
}

Customizing the Web Part Shortcut Menu

By default, a Web Part's shortcut menu shows selections that allow you to minimize, restore, close, or delete the Web Part, or create a connection to another Web Part. You can customize this menu by modifying existing items (to hide or disable them, according to a custom logic) or adding new ones. To do this, you must override the CreateWebPartMenu method of the WebPart base class and access the MenuItems collection of the Web Part's WebPartMenu property. Every single menu item is represented by a MenuItem instance, which exposes properties that allow you to set the item's visibility, enabled state, title, client-side JavaScript, and a reference to a server-side event handler. It is possible to get a reference to an existing item by its index or ID. Here is a list of the default items' IDs:

  • MSOMenu_Minimize
  • MSOMenu_Restore
  • MSOMenu_Close
  • MSOMenu_Delete
  • MSOMenu_Edit
  • MSOMenu_Connections
  • MSOMenu_Export
  • MSOMenu_Help

The next code example does the following things:

  • Gets a reference to the "Minimize" item (ID = MSOMenu_Minimize), disables it, and sets its BeginSection property to true so that a separator line is created between it and the item above it.

  • Creates a new menu item, added to the top of the menu, that posts back to the server when clicked. The server-side event handler sets the Web Part's Text property to a log message.

  • Creates another new menu item, added below the previous new item, that when clicked executes client-side JavaScript code that displays a greeting message for the current user.

    public override void CreateWebPartMenu()
    {
       MenuItem mnuMinimize = this.WebPartMenu.MenuItems.ItemFromID(
          "MSOMenu_Minimize");
       mnuMinimize.BeginSection = true;
       mnuMinimize.Enabled = false;
    
       SPWeb web = SPControl.GetContextWeb(this.Context);
       MenuItem mnuGreeting = new MenuItem(
          "Test Client-side command",
          string.Format(
             "javascript:return alert('Hi {0}, nice work!');", 
             web.CurrentUser.Name));
    
       MenuItem mnuPostBack = new MenuItem(
          "Test server-side command", "MSDNCMD1", 
          new EventHandler(mnuPostBack_Click));
    
       this.WebPartMenu.MenuItems.Insert(0, mnuGreeting);
       this.WebPartMenu.MenuItems.Insert(0, mnuPostBack);
    }
    
    private void mnuPostBack_Click(object sender, EventArgs e)
    {
       this.Text = "Successful postback!";
    }
    

Figure 2 shows the customized shortcut menu.

Figure 2. The customized Web Part shortcut menu

Using Impersonation to Access Data that the Current User Cannot Access

Windows SharePoint Services uses Microsoft Windows integrated security to authenticate users. Windows SharePoint Services is based on the Microsoft ASP.NET infrastructure that allows impersonating the authenticated user so that the page requests run under the current user's context. This allows the administrator to give different permissions, at the Web and list level, to different users and groups. If the user tries to gain access to a page of a list or document library to which they do not have access, they are prompted to insert the user name and password for a user with the proper privileges. A user may also have permission to read data but not to modify it.

Many security checks are done at the object model level. This means that if you access Windows SharePoint Services classes from a custom Web Part or page and you try to read or modify some data, the code runs under the context of the user sending the request. If the user does not have permissions for that operation, calls to the object model classes send back an error code that makes the browser ask for a new user name and password.

At times, however, you may want to do something with the object model that the current user is not allowed to do directly. For example, you may need to read or update data from a lookup list placed in a top-level site and shared among many subsites that the user does not know and does not have access to. Alternatively, you may want to deny to all users the option to upload a file to a document library through the default user interface. You may want to build a custom page to do this and check the user's permissions against your own database. Typically, this is necessary when you create a portal that integrates multiple external services and you have some sort of external single sign-on database. In these cases, you can temporarily impersonate a user that does have all the permissions your code needs to run, and then revert to the original user as soon as your procedure completes.

The LogonUser function of the Microsoft Windows API takes as input the user name, password, and domain of a Windows user and returns (as an output by-ref parameter) a token. This token is used to create an instance of the .NET System.Security.Principal.WindowsIdentity class representing that Windows user. Then, you impersonate the specified user by calling the object's Impersonate method. In the following example, the class wraps the details of the LogonUser function and exposes an easy-to-use method.

class SecurityHelpers
{
   private SecurityHelpers() {}

   [DllImport("advapi32.dll", SetLastError=true)]
   private static extern bool LogonUser(string lpszUsername,
      string lpszDomain, string lpszPassword,
      int dwLogonType, int dwLogonProvider, ref IntPtr phToken);

   [DllImport("kernel32.dll", CharSet=CharSet.Auto)]
   private extern static bool CloseHandle(IntPtr handle);

   public static WindowsIdentity CreateIdentity(
      string userName, string domain, string password)
   {
      IntPtr tokenHandle = new IntPtr(0);

      const int LOGON32_PROVIDER_DEFAULT = 0;
      const int LOGON32_LOGON_NETWORK_CLEARTEXT = 3;

      tokenHandle = IntPtr.Zero;
      bool returnValue = LogonUser(userName, domain, password,
         LOGON32_LOGON_NETWORK_CLEARTEXT,
         LOGON32_PROVIDER_DEFAULT,
         ref tokenHandle);

      if (false == returnValue)
      {
         int ret = Marshal.GetLastWin32Error();
         throw new Exception("LogonUser failed with error code: " +  ret);
      }

      WindowsIdentity id = new WindowsIdentity(tokenHandle);
      CloseHandle(tokenHandle);
      return id;
   }
}

The call to the Impersonate method of the WindowsIdentity class returns an instance of WindowsImpersonationContext, whose Undo method cancels the impersonation and reverts to the previous user. The following code example represents a skeleton for impersonating an administrative user, doing some actions that require high-level privileges, and canceling the impersonation.

WindowsImpersonationContext wic = null;
try
{
   wic = SecurityHelpers.CreateIdentity(
      "AdminName", "DomainName", "AdminPwd").Impersonate();
}
finally
{
   if ( wic != null )
      wic.Undo();
}

Of course, you should save the user name, password, and domain settings to a configuration file (typically, web.config, in the case of a Web application) or the registry, and not hard-code these items. Then, you do not have to modify and recompile the code if you need to rename the account or impersonate a different user.

An additional aspect of security in Windows SharePoint Services has to do with Code Access Security (CAS). The .NET runtime can grant different assemblies different permissions (to do I/O on some parts of the local or remote disk drive, run queries against a database, execute unmanaged API functions, and the like), according to the origin, strong name, digital certificate, or other properties of the assembly. For ASP.NET applications, and thus for Web Parts, the available permissions are defined by a policy file referred to in the machine.config file (at the machine level) or the web.config file (at the site level).

Windows SharePoint Services comes with two custom policy files that are added to the standard ones: wss_minimaltrust.config and wss_mediumtrust.config. These policy files are referred to in the web.config file located in the root directory of the IIS site extended by Windows SharePoint Services (typically drive:\Inetpub\wwwroot). By default, the trust level used by Windows SharePoint Services is set to WSS_Minimal (corresponds to wss_minimaltrust.config), which defines very limited permissions. With this trust level, you cannot perform any I/O on the disk (or on the Isolated Storage), access the Windows registry and Event Log, query a database, use Reflection, and more. The WSS_Medium trust level (corresponds to wss_mediumtrust.config) allows you to do more—such as query a Microsoft SQL Server database through the SqlClient managed provider or access a Web service deployed on the same Web server—but the medium level can still be too constraining in many situations. The following code example shows an extract of web.config that defines these settings.

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <!—other SharePoint configuration settings... -->
  <system.web>
    <securityPolicy>
      <trustLevel name="WSS_Medium" policyFile="C:\Program Files\
         Common Files\Microsoft Shared\Web Server Extensions\60\
         config\wss_mediumtrust.config" />
      <trustLevel name="WSS_Minimal" policyFile="C:\Program Files\
         Common Files\Microsoft Shared\Web Server Extensions\60\
         config\wss_minimaltrust.config" />
    </securityPolicy>
    <trust level="WSS_Minimal" originUrl="" />
    <!-- more settings... -->
  </system.web>
</configuration>

This example uses a couple of Windows API functions to impersonate a specific account, but neither of the two Windows SharePoint Services policy files includes the permissions to execute those unmanaged functions. As a developer, you have two possible solutions for this:

  1. Write your own policy file, add a reference to it into web.config, and set the "level" attribute of the <trust> tag to the name of the new <trustLevel> entry. This approach is the safest and most flexible, because it allows you to give a particular Web Part (or maybe all Web Parts with a particular strong name or publisher) just the permissions it actually needs, and nothing more. However, it is also the most difficult to implement, because it requires you to know in detail many permission classes that have to do with CAS.
  2. Set the "level" attribute of the <trust> tag to "Full". In this solution, you assign full-trust (permissions for doing everything) to all assemblies located under the local /bin folder. This could, of course, seem like it decreases the overall level of security, but you should also consider that when you install a Web Part into the GAC (by executing the Stsadm.exe tool with the –globalinstall parameter), you are assigning full trust to it anyway.

A more comprehensive coverage of CAS is beyond the scope of this article, because it is something that has to do with ASP.NET and the .NET runtime in general, and is not specific to Windows SharePoint Services. You can find more detailed information about this topic in the following documents:

Referring to Lists and Fields

There are two ways you can refer to a specific list from the Lists collection of an SPWeb object: by ID or by title. The ID is a GUID, and as such is unique and is generated when the list is created, so you cannot know it at design-time. Using the title seems much easier, because you can decide to title a list with a particular name, which you can expect to find at run time. However, the problem with referring to a list by title is that a Web designer or administrator can change the title of a list and thus break your custom code. The simplest and probably best solution to the problem is to save the list title in the web.config file under the IIS site's root folder, or under your custom application's root folder. You can then retrieve it from code when you need to refer to the list. Here is how to add a new setting to the web.config file.

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
   <appSettings>
      <add key="TasksListTitle" value="CustomTasks" />
   </appSettings>
   
   <system.web>
      ...
   </system.web>
</configuration>

Here is how you retrieve the value and get a reference to the correct list. If you are within a Web context, you can use the following SPControl object.

SPWeb web = SPControl.GetContextWeb(this.Context);

Otherwise, you must instantiate a Web object by using a full URL, as follows.

SPWeb web = new SPSite("http://localhost").OpenWeb();
string listTitle = ConfigurationSettings.AppSettings["TasksListTitle"];
SPList tasks = web.Lists[listTitle];

Every time the administrator renames a list, the administrator must also change the value in the configuration file. A slightly different approach is to store the list ID instead of the title in the web.config file, so that the administrator has to set the value only once, when the list is created. In fact, since the ID is a unique GUID, it can always (and actually does) safely remain the same, even if the list is renamed. Your setting would look like the following.

<add key="TasksListID" value="dba4c9f4-5ad5-4a0e-8463-55f9c839b738" />

From your code, you retrieve the value as a string, and then create a new GUID from it, as follows.

Guid listID = new Guid(ConfigurationSettings.AppSettings["TasksListID"]);
SPList tasks = web.Lists[listID];

To find the ID of a list, you can write a custom ASP.NET page—to be deployed to a folder under the standard /_layouts virtual folder—that executes the following code to print the title and ID of each list for the Web site under whose context the custom page is to run.

SPWeb web = SPControl.GetContextWeb(this.Context);
foreach ( SPList list in web.Lists )
{
   Response.Write("<li>" + list.Title + " - " + list.ID.ToString() 
      + "</li>");
}

You encounter a similar problem when you refer to fields, because they also can be renamed. If you refer to them in code by using a fixed title, you may get an exception because the field is not found. Here, the solution is simpler, because each field can be identified by two means from the Fields collection of the SPList object: by title or by internal name. The title is the string that you see on the screen in the list's page. The internal name can be the same as the title, but it can also be anything else.

There are two important differences between title and internal name. First, there can be multiple fields with the same title, but the internal name must be unique. Second, the title can be renamed, but the internal name cannot. Thus, it is easy to guess that identifying the field by internal name is recommended, because if you know it, you can be sure that it is not renamed. You can be sure that you refer to the right field and not to another one with the same caption.

In addition, when you access the Fields indexer property and pass a string, internally this string is first interpreted as an internal name. Only if there is no field named as such is it used to search the field by title. Therefore, passing the internal name is safer and even faster than using the title.

Here is how to retrieve the mappings between the titles and the internal names. The following code example shows how to loop through all the lists of the current Web and print their fields' titles, internal names, and types.

SPWeb web = SPControl.GetContextWeb(this.Context);
foreach ( SPList list in web.Lists )
{
   Response.Write("<b>" + list.Title + "</b><ul>");
   foreach ( SPField field in list.Fields )
      Response.Write("<li>" + field.Title + 
         " (" + field.InternalName + ") - " + 
         field.TypeAsString + "</li>");
   Response.Write("</ul><p>");
}

As an example, here is the output for the default Announcements list:

  • ID (ID) – Counter
  • Title (Title) – Text
  • Modified (Modified) – DateTime
  • Created (Created) – DateTime
  • Created By (Author) – User
  • Modified By (Editor) – User
  • owshiddenversion (owshiddenversion) – Integer
  • Attachments (Attachments) – Attachments
  • Approval Status (_ModerationStatus) – ModStat
  • Approver Comments (_ModerationComments) – Note
  • Edit (Edit) – Computed
  • Title (LinkTitleNoMenu) – Computed
  • Title (LinkTitle) – Computed
  • Select (SelectTitle) – Computed
  • InstanceID (InstanceID) – Integer
  • Order (Order) – Number
  • GUID (GUID) – Guid
  • Body (Body) – Note
  • Expires (Expires) – DateTime

Alternatively, you can use a free tool from Navigo Systems A/S, called SharePoint Explorer, to retrieve the mappings. This freeware desktop application uses the Windows SharePoint Services object model to provide an easy-to-navigate, treeview-based representation of the virtual server/top-level site/subsites hierarchy deployed on the local server. It also displays a list of properties for each type of object, including the list IDs, the fields' titles and names, and many other properties.

You can use one of the aforementioned two approaches for the built-in lists, but for custom lists, you must decide how to name and title the columns. When you first create a column, the name you specify is used for both the internal name and the title. After you have created a column, you can only rename the title. The internal name cannot contain spaces, accented letters, or other special characters, because when you create a column using one of these things, the characters are replaced with their hexadecimal representation. For example, "Due date" becomes "Due_x0020_date" and "E-mail" becomes "E_x002d_mail". You should use a simple name when you first create the column—for example "DueDate" or "Email"—so that the internal name does not get modified, and then rename the title of the column. When you refer to the column from code, you can still safely get it by passing "DueDate" and "Email" to the Fields indexer.

Making Updates with the AllowUnsafeUpdates Property of SPWeb Class

Updating the properties of a Web, list, or list item requires that one of the following two conditions is true:

  1. A <FormDigest> control instance is placed on the Web Form that is executing the update. This control generates a security token that is validated when the page is posted to the server and the code performing the update runs. This has the limitation that the update can be done only from within a POST request (that is, a postback in ASP.NET pages and Web Parts), and that you can, of course, place this control only on Web pages. You cannot use it from inside other types of applications such as Windows Forms or console programs. The following code shows the control reference and declaration into a Web page.

    <%@ Page language="c#" Codebehind="WebForm1.aspx.cs" 
       AutoEventWireup="false" Inherits="Custom.WebForm1" %>
    ...
    <form id="Form1" method="post" >
       <SharePoint:FormDigest  />
       ...
    
  2. The AllowUnsafeUpdates property of SPWeb is set to true so that the security token validation is not performed. This allows running updates from GET and POST requests and from any type of client application, thus making this the preferred approach in most situations. The following code example shows how to disable the security checks temporarily so that you can rename the current Web's title.

    SPWeb web = SPControl.GetContextWeb(this.Context);
    web.AllowUnsafeUpdates = true;
    web.Title = web.Title + " MOD";
    web.Update();
    web.AllowUnsafeUpdates = false;
    

    Note that once you enable "unsafe" updates at the Web level, you can also perform updates on its child lists and list items.

Filtering a Collection of List Items by Using Views or CAML Queries

Many developers retrieve content from a list just by calling the Items property of an SPList instance, or its GetItems method without any arguments. By doing this, they retrieve all the list items contained in the list, which can easily mean several thousand items. Most of the time, you only need to work with a few elements, but first retrieving the complete collection and then checking items one by one to find those that match a certain condition is definitely the wrong approach. The correct approach is to apply some sort of filtering up front so that you directly retrieve only items that you actually need to work with. That way, you do not waste database, memory, and CPU resources.

You can choose from a couple of approaches to retrieving filtered results, such as using pre-built static views or using Collaborative Application Markup Language (CAML) to define one or more filter conditions. In the former case, you have a view, defined from inside the browser, that filters the data with hard-coded conditions and values. Consider, for example, the "Active Tasks" view of the Tasks lists. You can use an existing view from your code to retrieve only those items that match that view's conditions. A view is represented by the SPView class, and you get a reference to a view through the Views collection of SPWeb by specifying its title. Once you have the SPView object, you can use it as an input parameter when you call the GetItems method of SPList to get back an SPListItemCollection reference, as shown in the following example.

SPWeb web = SPControl.GetContextWeb(this.Context);
SPList tasks = web.Lists["Tasks"];
SPView activeTasks = tasks.Views["Active Tasks"];
SPListItemCollection items = tasks.GetItems(activeTasks);

foreach ( SPListItem item in items )
{
   // process this task item...
}

This approach is acceptable in some situations; however, most of the time you must apply dynamic filters according to the user's input. In this situation, predefined views are not much help. Instead of passing an SPView object to the GetItems method, you pass an SPQuery object, which defines the filtering conditions and optionally the sorting options. The following example shows how to retrieve all the task items whose Title field contains the word "Approve".

SPQuery query = new SPQuery();
query.Query = @"<Where><Contains>
   <FieldRef Name='Title'/><Value Type='Text'>Approve</Value>
</Contains></Where>";

// retrieve the filtered items
SPListItemCollection items = tasks.GetItems(query);

Of course, you typically build the query string dynamically according to the user's search criteria. The query is written in CAML, the XML language used to define the schema of any Windows SharePoint Services object (site, list, field, view, and so forth). The query resides either in the site and list definition XML files (located in the path, drive:\Program Files\Common Files\Microsoft Shared\Web server extensions\60\Template) that serve as templates when creating new sites and lists, or in the content database's Lists table where the lists are configured. The structure of a simple CAML query is defined as follows.

<Where><[Operator]>
   <FieldRef Name='[FieldTitle]'/><Value Type='[FieldType]'>[Value]</Value>
</[Operator]></Where>   

You can replace the [Operator] placeholder with one of the following operators:

  • Eq = equal to
  • Neq = not equal to
  • BeginsWith = begins with
  • Contains = contains
  • Lt = less than
  • Leq = less than or equal to
  • Gt = greater than
  • Geq = greater than or equal to
  • IsNull = is null
  • IsNotNull = is not null

Possible values that can replace [FieldType] are Boolean, Choice, Currency, DateTime, Guid, Integer, Lookup, Note, Text, User, and the others listed in the SPListItem Class****topic in the Microsoft SharePoint Products and Technologies Software Development Kit.

You can also define a query that contains multiple OR/AND conditions. Here is how you can define a query with two OR conditions, to select all items whose Name field is equal to either "Tony" or "John":

<Where>
   <Or>
      <Eq><FieldRef Name='Name'/><Value Type='Text'>Tony</Value></Eq>
      <Eq><FieldRef Name='Name'/><Value Type='Text'>John</Value></Eq>
   </Or>
</Where>

A limitation of the <Or> and <And> blocks is that they can contain just two conditions. If you want to have more, you have to define an <Or> / <And> section that contains an inner <Or> / <And> section in place of one of the two conditions. The following examples show how to add a further possible value for the previously described query.

<Where>
   <Or>
      <Eq><FieldRef Name='Name'/><Value Type='Text'>Tony</Value></Eq>
      <Or>
         <Eq><FieldRef Name='Name'/><Value Type='Text'>John</Value></Eq>
         <Eq><FieldRef Name='Name'/><Value Type='Text'>Mary</Value></Eq>
      </Or>
   </Or>
</Where>

As soon as you need to build more complex queries with even more conditions and different operators, you will start to feel the need for a tool that can make things smoother to write and test. The CAML Builder tool available from U2U Community Tools is freeware that allows you to build the CAML query by selecting the fields and operators from pre-filled list boxes. You can define a single condition, join multiple conditions with AND / OR operators, and preview the results selected from the local content database.

Sometimes it can be tricky to guess the correct type of list field, because it may not be immediately clear whether it is a Choice, Text, or Calculated field, or something else. In this situation, a tool like Ontolica SharePoint Explorer, introduced previously, is useful. You can also use it to look at the XML schemas of existing (built-in or custom) views, which you can use as templates for your own dynamic queries.

This article provides only as much information about CAML as you need to build queries. For a more comprehensive coverage of CAML, with regard to either schema definition or output generation, refer to the Collaborative Application Markup Language section of the Microsoft SharePoint Products and Technologies Software Development Kit (SDK), or download the SharePoint Products and Technologies 2003 Software Development Kit (SDK) to work with programming tasks and samples.

Using the DataTable Object to Work with List Items in Memory

The SPListItemCollection class represents a collection of list items (SPListItem instances) and is returned by either the Items property of SPList, which returns the complete collection of list items, or by the GetItems method of SPList described previously. SPListItemCollection exposes a method called GetDataTable. This method returns an ADO.NET DataTable object that has the same schema as the parent SharePoint list and that is filled with the items of the SPListItemCollection instance. DataTable is useful for a number of reasons:

  • You can easily cache the data in memory (usually with the ASP.NET Cache, HttpApplicationState, and HttpSessionState classes), or add the table to a DataSet. If you add it to a DataSet, you can call its WriteXml method to serialize the items to disk as an XML file and retrieve them later by using the ReadXml method of the DataSet object.
  • You can use the ADO.NET DataView class to have multiple views on the same physical DataTable, each one with different filter conditions and sorting options. Multiple views are useful when you want to filter items or sort them without querying the database (by means of CAML queries, through the object model, as shown previously) multiple times. Using data caching and in-memory sorting and filtering can dramatically increase the performance of your application.
  • You can easily bind a DataTable/DataView to a DataGrid, DataList, Repeater, or any other ASP.NET data-bound control, to have the data rendered to the client from your custom pages or Web Parts. You may choose to write methods that wrap the code to retrieve the list's data and return a DataTable, and then the rest of the .aspx page would not differ at all from a typical .aspx page.

The following code example shows how to retrieve a DataTable from an SPListItemCollection instance, get its default view, apply a filter to it, and output all the items of the resulting view.

SPList tasks = web.Lists["Tasks"];
DataTable table = tasks.Items.GetDataTable();
DataView view = table.DefaultView;
view.RowFilter = "Title LIKE 'Approve'";

for ( int i = 0; i <= view.Count-1; i++ )
   Response.Write(view[i]["Title"].ToString() + "<br>");

The code can be even simpler if you have wrapper methods that hide the code to access the raw data through the Windows SharePoint Services object model.

public DataTable GetTasks()
{
   // if the DataTable is not already in cache, get a reference
   // to the SPList object, retrieve the DataTable from its
   // SPListItemCollection, put it in cache for 10 minutes,
   // and return it. Return it immediately if already in cache.
   if ( this.Cache["TaskItems"] == null )
   {
      SPWeb web = SPControl.GetContextWeb(this.Context);
      SPList tasks = web.Lists["Tasks"];
      DataTable table = tasks.Items.GetDataTable();
      this.Cache.Insert("TaskItems", table, null, 
         DateTime.Now.AddMinutes(10), Cache.NoSlidingExpiration);
      return table;
   }
   else
   {
      return (DataTable) this.Cache["TaskItems"];
   }
}

// ...
DataTable table = GetTasks();
// the rest does not change...

The only limitation of using a DataTable instead of accessing the SPListItem object directly is that there is no built-in method to synchronize a modified DataTable (with edited or added rows) with the original SharePoint list. Thus, you should use this method every time you cache a list's data and need to apply various filters and sorting to it. As the following example shows, you still must retrieve the real SPListItem reference (typically through the GetItemById method of SPList, by passing in the ID value read from a DataRow/DataRowView) when you want to update it.

int taskID = (int) view[rowIndex]["ID"];
SPListItem task = tasks.GetItemById(taskID);
task["Title"] = task["Title"].ToString() + " MOD";
task.Update();
Response.Write(task["ID"].ToString() + " - " + task["Title"].ToString());

Customizing the Context Menu of Document Library Items

Each document in a document library has a context menu with items such as Check In, Check Out, View Properties, Edit Properties, and others. This menu is created by the AddListMenuItems JavaScript function of the Ows.js file located in the path, C:\Program Files\Common Files\Microsoft Shared\web server extensions\60\TEMPLATE\LAYOUTS\1033. The function starts as shown in the following example.

function AddListMenuItems(m, ctx)
{
   if (typeof(Custom_AddListMenuItems) != "undefined") 
   {
      if (Custom_AddListMenuItems(m, ctx))           
         return;
   }
   // ...
}

This code checks whether another function named Custom_AddListMenuItems exists, and if it does, it calls it. That function is not defined by Windows SharePoint Services itself but is a function that you can write. The function serves as an injection point to add your own menu items to the document's standard context menu. If the function returns a value of true, the standard AddListMenuItems exits immediately; otherwise, it adds the default items. Here is an example implementation that adds a new item called "Say hello" and a separator line. The new item command item shows a greeting message when clicked.

function Custom_AddDocLibMenuItems(m, ctx)
{
   CAMOpt(m, 'Say hello', 'alert("Hello there!");
      ', '/images/greeting.gif');
   CAMSep(m);
   return false;
}

As you see, you can add a new item by calling CAMOpt, which takes as input the target menu (received as a parameter by the Custom_AddDocLibMenuItems function), the name of the item, the JavaScript that runs when the item is clicked, and the URL of an image shown to the left of the item. CAMSep adds the separator line.

This example is simple to write and understand but is not realistic. You typically need to add new commands according to a number of rules and conditions that make up your business logic. For example, some commands may be available only to picture documents; others may be available only if the item is not checked out or according to the current user's roles.

You could also retrieve some of this information from the JavaScript code, by using DHTML DOC to read them directly from the page's HTML, or from the ctx parameter that represents the context. However, in more complex cases, you must retrieve the list of available commands from the server, because only there you can run your business logic and perhaps get the commands from a custom database. Typically, you want to do this if you are implementing a workflow solution where each document has its own process state, with commands associated to it.

The solution for this situation is to have the Custom_AddDocLibMenuItems dynamically call a custom ASP.NET page. This page takes the ID of the document library and the specific item on the query string, and returns an XML string containing all the information for the commands available for that particular document. These commands are available according to the document's process status (or some other custom business logic). The returned XML may be something like the following.

<?xml version="1.0" encoding="UTF-8" ?>
<Commands>
   <Command>
      <Name><![CDATA[Say hello]]></Name>
      <ImageUrl><![CDATA[/images/greeting.gif]]></ImageUrl>
      <Script><![CDATA[alert('Hello there');]]></Script>
   </Command>
   ...other commands...
</Commands>

The sample page that generates this XML can return either one or two commands:

  • If the document is an image (its FieldType attribute is "GIF", "BMP", "JPG" or "PNG"), it returns a command that opens the file in a secondary browser window, without toolbars, menu bars, and status bar.
  • For any type of document, it returns a command that, when clicked, runs a search for that document's title on the Google search service.

In reality, you could do both these things directly from the JavaScript function, but this is just an example to show how to write and call server-side logic from the client, for each document's context menu. The following code example shows the sample page's Load event handler.

SPWeb web = SPControl.GetContextWeb(this.Context);
Guid listID = new Guid(this.Request.Params["ListID"]);
int itemID = int.Parse(this.Request.Params["ItemID"]);
SPListItem item = web.Lists[listID].Items.GetItemById(itemID);
string fileUrl = (web.Url + "/" + item.File.Url);
string fileName = item["Name"].ToString();

this.Response.ClearHeaders();
this.Response.ClearContent();
this.Response.Cache.SetCacheability(HttpCacheability.NoCache);
this.Response.AddHeader("Content-type", "text/xml" );

string cmdPattern = @"<Command>
   <Name><![CDATA[{0}]]></Name>
   <ImageUrl><![CDATA[{1}]]></ImageUrl>
   <Script><![CDATA[{2}]]></Script>
</Command>";

this.Response.Write(@"<?xml version=""1.0"" encoding=""UTF-8"" ?>");
this.Response.Write("<Commands>");

string fileType = item["File_x0020_Type"].ToString().ToUpper();
if ( fileType == "BMP" || fileType == "GIF" || 
   fileType == "JPG" || fileType == "PNG" )
{
   string jsOpenWin = "window.open('" + fileUrl + 
   "', '', 'menubar=no,toolbar=no,status=no,scrollbars=yes,resizable=yes');";
   this.Response.Write(string.Format(cmdPattern, "View in new window", 
      Page.ResolveUrl("~/images/preview.gif"), jsOpenWin));
}

string jsSearch = "location.href='http://www.google.com/search?q=" + 
   fileName + "';";
this.Response.Write(string.Format(cmdPattern, "Search on the web", 
   Page.ResolveUrl("~/images/search.gif"), jsSearch));

this.Response.Write("</Commands>");
this.Response.End();

At this point, Custom_AddDocLibMenuItems is written as a generic function that uses the XMLHTTP Microsoft ActiveX object to send a request to the custom ASP.NET page. The function passes the current list and document IDs to the query string and then parses the returned XML. For each <Command> element it finds, it adds a new menu item with the specified name, image, and JavaScript URL, as shown in the following example.

<script language="javascript">
function Custom_AddDocLibMenuItems(m, ctx)
{
   var request;
   var url = ctx.HttpRoot + 
      "/_layouts/CustomMenuItems/GetCommands.aspx?ListID=" + 
      ctx.listName + "&ItemID=" + currentItemID + "&DateTime=" + Date();

   if ( window.XMLHttpRequest )
   {
      request = new XMLHttpRequest();
      request.open("GET", url, false);
      req.send(null);
   }
   else if ( window.ActiveXObject )
   {
      request = new ActiveXObject("Microsoft.XMLHTTP");
      if ( request )
      {
         request.open("GET", url, false);
         request.send(); 
      }
   }
   
   if ( request )
   {   
      var commands = request.responseXML.getElementsByTagName("Command");
      // for each command found in the returned XML, extract the name, 
      // image Url and script, and a new menu item with these properties
      for ( var i = 0; i < commands.length; i++ )
      {
         var cmdName = commands[i].getElementsByTagName(
            "Name")[0].firstChild.nodeValue;
         var imageUrl = commands[i].getElementsByTagName(
            "ImageUrl")[0].firstChild.nodeValue;
         var js = commands[i].getElementsByTagName(
            "Script")[0].firstChild.nodeValue;
         CAMOpt(m, cmdName, js, imageUrl);
      }
      // if at least one command was actually added, add a separator
      if ( commands.length > 0 )
         CAMSep(m);

      // returning false makes SharePoint render the rest of the 
         standard menu
      return false;
   }
}
</script>

Notice that the function appends the current date and time to the query string of the GetCommands.aspx page that is called. This is done so that each request is different from a previous one, to avoid caching of the response XML. This is necessary if you are implementing a workflow solution, and a different user may change the document's state after you load the document library's page. If you consider that the Custom_AddDocLibMenuItems function is called every time the drop-down menu pops up, this trick allows you to retrieve the commands to the real current document state, without refreshing the whole page. Figure 3 shows the customized menu.

Figure 3. The new items for the document's context menu

In this sample implementation, GetCommands.aspx returns commands with simple JavaScript code associated to them. However, in a real workflow, you typically return JavaScript that executes another XMLHTTP request to a page that actually performs some server-side action on the clicked document.

Conclusion

The Windows SharePoint Services object model allows you to have access to its services and data from your own applications and Web Parts, so that you can really integrate Windows SharePoint Services with your information processes and business logic. In this article, you learned some best practices and tricks that, together with the product documentation, can help you write faster and more reliable code, and customize small aspects of the default user interface for workflow integration.

About the Author

Marco Bellinaso works as a consultant, developer, and trainer for Code Architects Srl, and focuses on Web programming with ASP.NET, Windows SharePoint Services, and related technologies. He is a frequent speaker at important Microsoft conferences in Italy, and was a co-author for a number of Wrox Press books including ASP.NET Website Programming. He also writes articles for programming magazines and Web sites, and recently founded the Italian User Group for SharePoint.

This article was produced in partnership with A23 Consulting.