CodeKicks.com
Focus on Microsoft Technologies - Tutorials, Articles, Code Samples.

Monday, September 15, 2008

Create an Auto complete Custom Field Type with Ajax for ASP.NET

image There’s been a lot of discussion about the question if MOSS 2007 should support Ajax for ASP.NET or not. We’ve heard a lot of people say that it’s nice to have, but that there are other issues that should have gotten more priority. Other people are unimpressed with Ajax for ASP.NET altogether, because they say: "This possibilities have been around for ages, I could have done that since the days of XML data islands!" And yet other people don’t like entire the concept of Ajax, claiming it doesn’t solve customer problems that are really crucial. Be that as it may, MOSS service pack 1 officially happens to support Ajax for ASP.NET and we love it and we use it. Our personal favourite is the Autocomplete behaviour and in this article we’ll show how to build a custom field type (CFT) that uses it.

What are we doing?

We had this article lying around for some time, and never got around to finishing it. We’ve finished it now, so here goes...

In this article we’ll create a custom field type that offers suggestions to the end user while he types. Before accomplishing this, we need to enable ASP.NET for Ajax support in MOSS 2007. Once that’s set up and ready to go, we’ll create a SharePoint list that holds all available suggestions which makes it easy for end users to modify the list of suggestions. After that, we’ll create a simple web service that is able to retrieve data from this list and supports ASP.NET for Ajax. This web service will act as the end point to which the CFT communicates. Next, we’ll build the CFT, add required configuration information for SharePoint and try it all out.

Enable ASP.NET for Ajax

Enabling ASP.NET for Ajax is a process that can be quite painful. We’ve done this a couple of times for SPS 2003, MOSS 2007 beta and the final version of MOSS 2007 by creating an ASP.NET for Ajax web site and taking a good look at its web.config file and comparing it with the web.config file of our SharePoint installation. We had to do this because we were in the process of writing several chapters about SharePoint and Ajax for different books. As we don’t do this often, we think it would be okay to plug those books a bit here:

  • Pro SharePoint 2007 Development Techniques (written in its entirety by us).
  • Pro SharePoint 2003 Development Techniques (written in its entirety by us).
  • Developer's Guide to the Windows Sharepoint Services V3 Platform (lead author: Todd Bleeker, we were contributing authors).

For more information about one of these books, check Amazon (or another online book vendor) or our web site at http://www.lcbridge.nl/publications.htm. If you want to build ASP.NET for Ajax solutions for MOSS 2007, we think our book "Pro SharePoint 2007 Development Techniques" would be a valuable addition to your library. Anyways, enough of the sales pitch.

Luckily, as far as ASP.NET for Ajax support in SharePoint goes, things have improved over time. Mike Ammerlaan of the Microsoft SharePoint team wrote an extensive blog post about the topic of Ajax-enabling MOSS that guides you through the process of enabling ASP.NET for Ajax. Although it is still tiresome to make these changes manually, the blog post does a great job of walking you through it. You can find more information at http://sharepoint.microsoft.com/blogs/mike/Lists/Posts/Post.aspx?ID=3.

And even better... On CodePlex you can find a feature that is a part of the SharePoint 2007 Features project called the Ajax.Config feature. Currently, we like to use this feature to take care of the enabling of ASP.NET for Ajax for us. This is a web application level feature that uses the Feature event receiver to update and Ajax enable the web.config file of a SharePoint web application automatically once the feature is activated. You can download it at http://www.codeplex.com/features.

We suggest you read the blog post of Mike Ammerlaan or try out the Ajax.Config feature and go ahead and enable ASP.NET for Ajax on your SharePoint machine.

Suggestions list

We’ll create a SharePoint list that holds all available suggestions that will be made to the end user. A web service, discussed in detail in section "Suggestions web service" will be used to retrieve suggestions from this list. To create this list (which we’ll call "Suggestions"), just choose a SharePoint site, create a custom list and fill its Title column with the following test data:
- Alabama
- Alaska
- Arizona
- Arkansas
- California
- Colorado
- Connecticut
- Idaho
- Illinois
- Indiana
- Iowa

This gives you some test data to play around with once everything is set up and done.

Suggestions web service

Now let’s discuss the creation of a web service that supports ASP.NET for Ajax. The example is still an old-school web service, not a WCF service. We’ve decided to do this because there are a couple of issues that need to be resolved before you’re able to use WCF in all popular usage scenarios within SharePoint. The ones we can think of on top of our head are these:

  • You need to adjust the web.config file again to support this (see a href="http://blogs.msdn.com/bgeorgi/archive/2007/01/06/calling-a-wcf-web-service-from-sharepoint.aspx" target="_blank">http://blogs.msdn.com/bgeorgi/archive/2007/01/06/calling-a-wcf-web-service-from-sharepoint.aspx for more information).
  • You need to ASP.NET for Ajax-enable the WCF service.
  • You need to find a way to virtualize the WCF service so that it’s able to run within a SharePoint context.

Mind you, we’re not saying these are unsolvable issues, but we didn’t have time to try and come up with solutions and we haven’t read about solutions for any of these topics either. As a result, our Suggestions web service will be an old-school web service. The next procedure explains how to create a virtual directory that will hold our Suggestion web service:

  1. Open a command prompt and type inetmgr. This opens the Internet Information Services (IIS) Manager.
  2. Open the [server name] (local computer) node.
  3. Open the Web Sites node.
  4. Locate the child node representing the SharePoint web application where you want to add the Ajax CFT later on.
  5. Right-click this child node and choose New > Virtual Directory. This opens the Virtual Directory Creation Wizard.
  6. Click Next. This opens the Virtual Directory Alias dialog window.
  7. In the Alias textfield, type SuggestionHost.
  8. Click Next. This opens the Web Site Content Directory window.
  9. Browse to the location that will hold the Suggestion web service. We've chosen the location C:\inetpub\wwwroot\SuggestionHost ourselves.
  10. Click Next. This opens the Virtual Directory Access Permissions window.
  11. Accept all default settings and click Next.
  12. This completes the Virtual Directory Creation Wizard. Click Finish.
  13. Right-click the SuggestionHost node and choose Properties. This opens the SuggestionHost Properties window.
  14. Click the Create button.
  15. Click OK.

Now you've successfully created a virtual directory that'll be able to hold our Suggestion web service. Next, we'll create the service itself.

  1. Start VS.NET 2008 (at least, that's the version we're using ourselves).
  2. Choose File > New > Web Site. This opens the New Web Site window.
  3. In the Templates section, choose ASP.NET Web Service.
  4. In the Location drop down list, choose HTTP.
  5. In the Location textfield, type: http://[your_server:your_port]/SuggestionHost.
  6. Click OK.
  7. Right-click the SuggestionHost project and choose Add New Item. This opens the Add New Item - http://[your_server]/SuggestionHost window.
  8. In the Templates section, choose Web Service.
  9. In the Name textfield, type SuggestionService.asmx.
  10. Make sure the Place code in separate file checkbox is selected.

We've created the basics of the Suggestion web service. The SuggestionService.asmx file won't change anymore and should look like this:

We've also created a file called SuggestionService.cs. We won't list this one yet, as it still needs a little work. The first thing that this web service needs is to retrieve all suggestions stored in the SharePoint list discussed in section “Suggestions list”. In the next code listing, we’ll demonstrate how to retrieve all items from our predefined SharePoint suggestions list. We’ll use to SharePoint object model to loop through each of the list entries and add the titles of all items to a list of strings, so we can work on that list later.

Please note: This piece of the code would be a nice place to add some caching, so you won’t have to retrieve the suggestions again from SharePoint.

This code listing shows the suggestion retrieval:private void InitItems()
{
if (_dvItems == null)
{
  DataTable dtItems = new DataTable();
  dtItems.Columns.Add("ListItem", typeof(string));
  using (SPSite objSites = new SPSite(ConfigurationManager.AppSettings["http://[my server]"]))
  {
   SPList objList;
   SPWeb objWeb = objSites.OpenWeb();
   objList = objWeb.Lists["Suggestions"]];
   SPListItemCollection objListItems = objList.Items;
   foreach (SPListItem objListItem in objListItems)
   {
    DataRow drItem = dtItems.NewRow();
    drItem["ListItem"] = objListItem.Title;
    dtItems.Rows.Add(drItem);
   }
  }
  _dvItems = dtItems.DefaultView;
  _dvItems.Sort = "listitem ASC";
}
}

We’ve taken care of the retrieval of suggestions from our SharePoint list, but we haven’t finished our web service yet. The ASP.NET Ajax auto completion web service interface demands a very strict interface that requires a given interface and doesn’t allow a change in parameter names.public List<string> MyMethodName(string prefixText, int count)

The prefixText argument contains the prefix of any match found by the autocompletion web service, the count argument limits the max number of results that are returned to the client and the return type of this method needs to be some type of string array. A generic List collection of strings is allowed, and that’s the return type we’ll use here. Luckily, there’s still one thing you’re allowed to change and that is the method name itself. We’ll use that liberty to the fullest and call our method GetItems(). In this method, we’ll use a data view to filter our results:[WebMethod]
public List<string> GetItems(string prefixText, int count)
{
string strReturn = String.Empty;
try
{
  InitItems();
  string strFilter = String.Format("listitem LIKE '{0}%'", prefixText);
  _dvItems.RowFilter = strFilter;
  List<string> lstItems = new List<string>();
  for (int i = 0; i < _dvItems.Count; i++)
  {
   if (i > count - 1) break;
   lstItems.Add(_dvItems[i]["ListItem"].ToString());
  }
  return lstItems;
}
catch (Exception err)
{
  Trace.Write(err.Message);
  throw;
}
}

This concludes our auto completion web service; the complete code for this web service looks like this:using System;
using System.Collections;
using System.Web;
using System.Web.Services;
using System.Web.Services.Protocols;
using System.Text;
using System.Diagnostics;
using System.Threading;
using System.Web.Script.Services;
using System.Data;
using System.Collections.Generic;
using System.Configuration;
using Microsoft.SharePoint;
/// <summary>
/// Summary description for SuggestionService
/// </summary>
[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
// To allow this Web Service to be called from script, using ASP.NET AJAX, uncomment the following line.
// [System.Web.Script.Services.ScriptService]
public class SuggestionService : System.Web.Services.WebService
{
public SuggestionService ()
{
  //Uncomment the following line if using designed components
  //InitializeComponent();
}
private DataView _dvItems = null;
private string[] _arrItems = null;
private bool _blnInitComplete = false;
List<string> _objSuggestionList = null;
private void InitItems()
{
  if (_dvItems == null)
  {
   DataTable dtItems = new DataTable();
   dtItems.Columns.Add("ListItem", typeof(string));
   using (SPSite objSites = new SPSite(ConfigurationManager.AppSettings["http://[my server]"]))
   {
    SPList objList;
    SPWeb objWeb = objSites.OpenWeb();
    objList = objWeb.Lists["Suggestions"]];
    SPListItemCollection objListItems = objList.Items;
    foreach (SPListItem objListItem in objListItems)
    {
     DataRow drItem = dtItems.NewRow();
     drItem["ListItem"] = objListItem.Title;
     dtItems.Rows.Add(drItem);
    }
   }
   _dvItems = dtItems.DefaultView;
   _dvItems.Sort = "listitem ASC";
  }
}
[WebMethod]
public List<string> GetItems(string prefixText, int count)
{
  string strReturn = String.Empty;
  try
  {
   InitItems();
   string strFilter = String.Format("listitem LIKE '{0}%'", prefixText);
   _dvItems.RowFilter = strFilter;
   List<string> lstItems = new List<string>();
   for (int i = 0; i < _dvItems.Count; i++)
   {
    if (i > count - 1) break;
    lstItems.Add(_dvItems[i]["ListItem"].ToString());
   }
   return lstItems;
  }
  catch (Exception err)
  {
   Trace.Write(err.Message);
   throw;
  }
}
}

Since we’ve already ASP.NET for Ajax-enabled MOSS, we can’t register that stuff again in our web service web.config file. Because of this, if you’re adding this web service to a MOSS virtual server (as opposed to adding a web service to a non-MOSS virtual server), you need to comment the contents of the <httpModules>, <modules> and <handlers> elements. After that, the auto completion web service should work fine. You can download the code for the web.config file of our own auto completion web service here.

Alternatively, we could have implemented this web service using Linq. In that case you’ll be using .NET 3.5 and you’ll have to readjust the web.config file again to Ajax-enable it. We didn’t want to go to the trouble of doing that, so we can’t include that web.config file in this article. However, we did think it was fun to rewrite the service using Linq, which is shown in the next code listing:

Please note: If you want to use the Linq implementation instead of the data view implementation, you’ll have to change the web.config file yourself!!!using System;
using System.Web;
using System.Web.Services;
using System.Web.Services.Protocols;
using System.Diagnostics;
using System.Threading;
using System.Web.Script.Services;
using System.Data;
using System.Collections.Generic;
using System.Configuration;
using Microsoft.SharePoint;
using System.Linq;
using System.Xml.Linq;
/// <summary>
/// Summary description for KeywordService
/// </summary>
[WebService(Namespace = "http://loisandclark.suggestionservice/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[ScriptService]
public class SuggestionService : System.Web.Services.WebService
{
private DataView _dvItems = null;
private string[] _arrItems = null;
private bool _blnInitComplete = false;
List<string> _objSuggestionList = null;
private void InitItems()
{
  if (_blnInitComplete) return;
  _objSuggestionList = new List<string>();
  if (_dvItems == null)
  {
   DataTable dtItems = new DataTable();
   dtItems.Columns.Add("ListItem", typeof(string));
   using (SPSite objSites = new SPSite("http://myserver"))
   {
    SPWeb objWeb = objSites.OpenWeb();
    SPList objList = objWeb.Lists["Suggestions"];
    SPListItemCollection objListItems = objList.Items;
    foreach (SPListItem objListItem in objListItems)
    {
     _objSuggestionList.Add(objListItem.Title);
    }
   }
   _blnInitComplete = true;
  }
}
[WebMethod]
public List<string> GetItems(string prefixText, int count)
{
  try
  {
   InitItems();
   IEnumerable items = (from String name in _objSuggestionList
   where name.ToLower().StartsWith(prefixText.ToLower())
   orderby name
   select name).Take(count);
   return new List<string>(items);
  }
  catch (Exception err)
  {
   Trace.Write(err.Message);
   throw;
  }
}
}

Creating a control

In this section, we’ll show a user control that can be used as a SharePoint Custom Field Type (CFT). The user control is responsible for creating the user interface of the CFT. We’ll include a ScriptManager control in it, that is required for ASP.NET for Ajax.

Please note: Since you can only add a single ScriptManager on a page, and since the control already contains one, using this implementation, you can’t add another (or two instances of the same) ASP.NET for Ajax-enabled control to a set of metadata. If you want to support multiple ASP.NET for Ajax-enabled controls, you need to make sure the ScriptManager is only added once to the page.<%@Control Language="C#" Debug="true" %>
<%@Assembly Name="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@Register TagPrefix="SharePoint" Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=
71e9bce111e9429c" namespace="Microsoft.SharePoint.WebControls"%>
<%@Register TagPrefix="ajaxToolkit" Assembly="AjaxControlToolkit" Namespace="AjaxControlToolkit" %>
<SharePoint:RenderingTemplate ID="KeywordEditRenderingTemplate" runat="server">
<Template>
  <ajaxToolkit:ToolkitScriptManager runat="server" ID="ScriptManager1" />
  <asp:TextBox ID="txtKeyword1" MaxLength="100" Width="385"
  runat="server"/>
  <ajaxToolkit:AutoCompleteExtender
  runat="server"
  ID="autoComplete1"
  TargetControlID="txtKeyword1"
  ServicePath="http://[your server]/SuggestionHost/SuggestService.asmx"
  ServiceMethod="GetItems"
  MinimumPrefixLength="1"
  CompletionInterval="1000"
  EnableCaching="true"
  CompletionSetCount="5">
  </ajaxToolkit:AutoCompleteExtender>
</Template>
<SharePoint:RenderingTemplate>

You need to copy SuggestionControl.ascx to the 12 hive TEMPLATE\CONTROLTEMPLATES folder, by default located at:

C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\TEMPLATE\ControlTemplates

Okay, that takes care of the UI for the CFT. You’ll also need to create an assembly that implements functionality required by the CFT. This assembly needs to be strong named because it must be deployed in the GAC and it needs to contain a class that represents the CFT. Such a class needs to inherit from base classes called SPField* located in the Microsoft.SharePoint namespace. The base class you choose depends on the functionality you need. For example, you could inherit from base classes like SPFieldAttachments, SPFieldChoice, SPFieldLink, and SPFieldDateTime. The following code shows an example implementation:using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.SharePoint;
using Microsoft.SharePoint.WebControls;
namespace CustomFieldTypeSuggestion
{
/// <summary>
/// Custom field type for keywords.
/// Suggestion CFT's offer Ajax-enabled assistance while the end user types in the value of a property
/// of a document library item.
/// The values provided by the assistance mechanism are stored in a SharePoint list.
/// </summary>
public class SuggestionField : SPFieldText
{
#region ctors
  /// <summary>
  /// Inits object
  /// </summary>
  public SuggestionField(SPFieldCollection fields, string fieldName)
  : base(fields, fieldName)
  { }
  /// <summary>
  /// Inits object
  /// </summary>
  public SuggestionField(SPFieldCollection fields, string typeName, string displayName)
  : base(fields, typeName, displayName)
  { }
#endregion
  /// <summary>
  /// Returns a user control as the field control's UI.
  /// </summary>
  public override BaseFieldControl FieldRenderingControl
  {
   get
   {
    return new SuggestionFieldControl();
   }
  }
  /// <summary>
  /// Returns the field value object.
  /// </summary>
  public override object GetFieldValue(string value)
  {
   if (!string.IsNullOrEmpty(value))
   {
    return new SuggestionFieldValue(value);
   }
   else
   {
    return null;
   }
  }
}
}

As you may have noticed, this class uses a web control called SuggestionField control which is added to the SharePoint UI. The implementation contains a text box and looks like this:using System;
using System.Collections.Generic;
using System.Text;
using System.Web.UI.WebControls;
using Microsoft.SharePoint.WebControls;
namespace CustomFieldTypeSuggestion
{
public class SuggestionFieldControl : BaseFieldControl
{
  protected TextBox txtKeyword1 = null;
  protected SuggestionFieldValue fieldValue = null;
  /// <summary>
  /// Returns the name of the ASCX template to use.
  /// </summary>
  protected override string DefaultTemplateName
  {
   get { return "KeywordEditRenderingTemplate"; }
  }
  /// <summary>
  /// Uses Field Value object to compress and extract
  /// the various decomposible elements of the keyword CFT.
  /// Every keyword CFT contains 5 keywords.
  /// </summary>
  public override object Value
  {
   get
   {
    EnsureChildControls();
    fieldValue = new SuggestionFieldValue();
    fieldValue.Keyword1 = txtKeyword1.Text.Trim();
    return fieldValue;
   }
   set
   {
    EnsureChildControls();
    fieldValue = value as SuggestionFieldValue;
    txtKeyword1.Text = fieldValue.Keyword1;
   }
  }
  /// <summary>
  /// Adds all required child controls to the keyword CFT.
  /// </summary>
  protected override void CreateChildControls()
  {
   base.CreateChildControls();
   // If the FieldMetadata Field has been created
   if (Field != null)
   {
    switch (ControlMode)
    {
     case SPControlMode.Display:
      // Display mode is handled by a CAML RenderPattern
      break;
     case SPControlMode.New:
      AssociateControlsFromUserControl(TemplateContainer);
      break;
     default:
      AssociateControlsFromUserControl(TemplateContainer);
      break;
    }
   }
  }
  /// <summary>
  /// Get each textbox from the User Control and
  /// associate it with the control's variable
  /// </summary>
  private void AssociateControlsFromUserControl(TemplateContainer usercontrol)
  {
   txtKeyword1 = usercontrol.FindControl("txtKeyword1") as TextBox;
   txtKeyword1.CssClass = this.CssClass;
  }
}
}

There’s also a class called SuggestionFieldValue that represents the values of the CFT.

Please note: Originally, we planned to create a CFT that would contain multiple values. Because of that, it inherits from SPFieldMultiColumnValue.

The next code listing shows the implementation of the SuggestionFieldValue class:using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.SharePoint;
namespace CustomFieldTypeSuggestion
{
public class SuggestionFieldValue : SPFieldMultiColumnValue
{
  private const int numberOfFields = 1;
#region ctors
  /// <summary>
  /// Inits object
  /// </summary>
  public SuggestionFieldValue(): base(numberOfFields)
  { }
  /// <summary>
  /// Adds new field in the SPFieldCollection
  /// </summary>
  public SuggestionFieldValue(string value) : base(value)
  { }
#endregion
  /// <summary>
  /// Retrieves value of the first keyword of the keyword CFT. Every keyword CFT contains a total of 5 keywords.
  /// </summary>
  public string Keyword1
  {
   get { return this[0]; }
   set { this[0] = value; }
  }
}
}

Now, you need to compile this assembly, add it to the GAC and make sure the System.Web.Extensions and AjaxControlKit assemblies are also present in the GAC.

fldtypes_Suggestion.xml

You must use CAML to define where SharePoint can find the CFT. You can define CFTs at different scopes: list level or farm level. If you define a CFT at the list level, you need to adjust the schema.xml file of that list. If you’re defining a CFT at the farm level, you’ll need to create a file starting with "fldTypes_*" and add it to the TEMPLATE\XML folder of the 12 hive.

In this article, we’ve created a file called fldtypes_suggestion.xml that defines a farm level CFT. The next code listing shows its implementation:<?xml version="1.0" encoding="utf-8" ?>
<FieldTypes>
<FieldType>
  <Field Name="TypeName">Suggestion</Field>
  <Field Name="ParentType">MultiColumn</Field>
  <Field Name="TypeDisplayName">Suggestion display name</Field>
  <Field Name="UserCreatable">TRUE</Field>
  <Field Name="Sortable">TRUE</Field>
  <Field Name="Filterable">TRUE</Field>
  <Field Name="FieldTypeClass">
   CustomFieldTypeSuggestion.SuggestionField,CustomFieldTypeSuggestion,
   Version=1.0.0.0,Culture=neutral,PublicKeyToken=1b824d71fdeef6b0
  </Field>
  <RenderPattern Name="DisplayPattern">
   <Switch>
    <Expr>
     <Column />
    </Expr>
    <Case Value="">
    </Case>
    <Default>
     <Column SubColumnNumber="0" HTMLEncode="TRUE"/>
     <HTML><![CDATA[<br />]]></HTML>
    </Default>
   </Switch>
  </RenderPattern>
</FieldType>
</FieldTypes>

You need to copy fldtypes_suggestion.xml to TEMPLATE\XML folder of the 12 hive, by default located at:

C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\TEMPLATE\XML

Demo

Finally, this leaves us with the end result. After following the steps in this article, you can go to a list and create a new column of the type "Suggestion display name". You’ll get a warning stating that you need to be aware that your own CFT cannot be edited from most client programs and might block the programs from saving documents to this library. So, that’s a limitation that you need to consider.

After this, you’re done. When you start editing list items, you’ll get Ajax-enabled typing support. For instance, if we start typing "i", our Suggestions web service comes up with several suggestions, as can be seen in the following screen shot:

If we type further, and type "in" we’ve only added a single state to our list that starts with those letters, "Indiana". As a result, that’s the only suggestion offered by our Suggestion web service, as can be seen in the following screenshot:

So there you have it, an AutoComplete Custom Field Type with Ajax for ASP.NET.

Post a Comment

N & M Bruggeman said...

Hello, what's this? First rip our content, change a picture and don't credit us as all? That's real nice... The org blog post can be found at: http://www.lcbridge.nl/vision/2008/ajax-cft.htm