Synergy
THE COMPANY . CONTACT
Home > Blog > MOSS Blog > Posts > Creating client-side connected web parts in WSS 3.0

 Posts

Creating client-side connected web parts in WSS 3.0
Randy Williams

With the incorporation of the ASP 2.0 Framework in Windows SharePoint Services (WSS) 3.0, creating connected web parts in SharePoint has gotten much easier.  One only needs to create an interface, implement the interface in the provider web part and consume it in the consumer web part.  However, the WebPart class in the .NET Framework (System.Web.UI.WebControls.WebParts namespace) does not support client-side connections.  This is a real draw back since for web parts to exchange information, a formal postback must occur which is both slow and unattractive.  Ideally, we would like to use client-side connections to avoid this post back, providing a much better user experience.  Fortunately, the object model supports this using the older SharePoint WebPart class (Microsoft.SharePoint.WebPartPages namespace).  As is published in the WSS SDK:

“The Windows SharePoint Services 3.0 WebPart class has been rebased to inherit from the Microsoft ASP.NET 2.0 WebPart class, providing a compatibility layer to ensure that Web Parts written for Windows SharePoint Services 2.0 work in Windows SharePoint Services 3.0 without modification. It exists primarily for the purpose of backward compatibility, and secondarily, to provide a small set of features that are not available in the ASP.NET WebPart class.”

Support for client-side connections is just one of these extra features which is the focus of this article.  The article will first introduce a sample application (with source code provided), and will then walk you through the concepts by reviewing the code.  The consumer web part will use AJAX technologies to call into a SharePoint-based Web Service, so you might want to first read my two-part article on
Calling into SharePoint Web Service using AJAX-Enabled Web Parts.  This is a long article, so let’s get started!

Application Overview

The business scenario that this solution addresses is an executive portal.  Imagine a CXO wants to be able to quickly pull information on various customers from his (or her) dashboard.  To address this need, we have a provider web part which presents a list of customers.  When a customer is selected, two consumer web parts illuminate with information on that customer.  Here is a screen shot (click for a larger view):

image

Since we are using client-side, web part connections along with AJAX technology for the consumer web parts, a new customer can be selected and displayed in an instant.  (You can also use the scroll wheel or up/down arrows to select as well—a very nice feature).

The Visual Studio solution will introduce two web parts, one provider (selecting the company) and one consumer.  Customer Sales and Customer Contact from the screen shot are actually two instances of the same consumer web part, each with different configuration settings.  By the end of the article, you will see how it works and have the basic knowledge needed to build your own application like this.

While client-side connected web parts are very powerful, there is additional coding complexity that goes into them.  Part of this is implementing the ICellProvider and ICellConsumer interfaces, and part of this is the necessary client-side (JavaScript) code.  We must implement these older interfaces rather than the newer ones (IWebPartField, IWebPartRow, etc.) because we are using the SharePoint WebPart class.  I chose ICellProvider and ICellConsumer because we are exchanging a single piece of data (the Customer ID of the customer selected).

The solution originated with the code that Microsoft has published in this SDK article.  This example covers a provider and consumer web part supporting both client and server-side connections.  In my opinion, it’s very difficult to follow.  So while I started with that code, I made numerous changes to simplify the implementation.  I think the result is a much more reusable and understandable baseline that you can work from.  I then incorporated AJAX into the consumer web part.

Solution Code

The VS.NET 2005 solution contains two projects, ClientConnecions and wsClientConnections.  ClientConnections contains the provider and consumer web parts created with Visual Studio Extensions for Windows SharePoint Services (VSeWSS).  This project has the Microsoft.SharePoint and System.Web.Extensions (part of ASP.NET AJAX 1.0 Extensions) references added.  wsClientConnections is a Web Service project that returns the HTML displayed in the consumer web parts which is briefly introduced near the end of this article.    For now, let’s look at the Provider web part.

Provider Web Part

The provider web part is implemented in the ClientProvider class and inherits from the base SharePoint WebPart class. Here is the class definition:

//Implement the ICellProvider Interface
[Obsolete]
public class ClientProvider : WebPart, ICellProvider 

Since the SharePoint WebPart class has been marked as obsolete, we decorate the definition with the [Obsolete] attribute to avoid compiler warnings.  This is also done in other methods we override.  We’ll first look at what we need to do to implement the interface, we’ll then look at overriding methods from the base WebPart class.  To develop a client-connected, provider web part, we need to implement these from ICellProvider:

CellConsumerInit
CellProviderInitEventHandler
CellReadyEventHandler

Since these are all for server-side activities, we don’t attach any code to them.  CellConsumerInit is only used for server-side initialization code.  The empty method is shown here:

public void CellConsumerInit(object sender, CellConsumerInitEventArgs cellConsumerInitArgs)
{
} 

Here are the two defined server-side events.  We don’t have any server-side code handling these.

public event CellProviderInitEventHandler CellProviderInit;
public event CellReadyEventHandler CellReady; 


With that done, here are the methods that we override from the SharePoint WebPart base class:

EnsureInterfaces
CanRunAt
PartCommunicationConnect
RenderWebPart
CreateChildControls

The EnsureInterfaces method registers the interface for the web part.  Here is how it looks:

public override void EnsureInterfaces()
{
    RegisterInterface("CustomerProvider_WPQ_",//Interface name       
       InterfaceTypes.ICellProvider,   //Interface type       
       WebPart.UnlimitedConnections,   //Allow multple connections
       ConnectionRunAt.Client,         //Only support client-side        
       this,                           //Server-side interface object       
       "ProviderClient_WPQ_",          //JavaScript object for interface       
       "Send Customer To",             //UI Label seen when connecting
       "Provides a Customer ID");      //Description
}

CustomerProvider_WPQ_ is the interface name we reference in both server and client-side code.  The _WPQ_ naming convention is a token that is used to ensure that this web part gets uniquely named when created.  This becomes important if we have multiple instances of the web part on a page.  This token is replaced using the ReplaceTokens method covered below.  As you can see, the web part gets stamped as a provider, and it allows multiple connections from consumer web parts.  ProviderClient_WPQ_ is the name of the JavaScript object that communicates with the consumer web parts.  This exchange is covered is more detail later.

The CanRunAt method is shown next.  It also marks this web part as onlhy supporting client-side connections.

//Called by framework to mark this web part as supporting only client-side only connections
[Obsolete]
public override ConnectionRunAt CanRunAt()
{
    return ConnectionRunAt.Client;
} 

PartCommunicationConnect is now introduced.  This method is called when a connection is established between provider and consumer web parts.  Even though this is a client-side connection, the web parts are connected in the UI the same way.  We use this method to track the number of connections made.

//Notification to the Web Part that it has been connected.
[Obsolete]
public override void PartCommunicationConnect(string interfaceName,
   WebPart connectedPart,
   string connectedInterfaceName,
   ConnectionRunAt runAt)
{
    ////Check if this is my particular cell interface
    if (interfaceName == "CustomerProvider_WPQ_")
    {
        //Keep a count of the connections
        _connectedCount++;
    }
} 

RenderWebPart is the equivalent of RenderContents in the ASP.NET WebPart.  Here is the code:

//Defines Web Part UI
protected override void RenderWebPart(HtmlTextWriter output)
{ 

    //Is at least one connection make to this provider?
    if (_connectedCount > 0)
    {
        //Call CreateChildControls.  Not needed since we only use HTML controls
        //EnsureChildControls(); 

        //Write out client-side script and HTML
        output.Write(ReplaceTokens(ProviderJavaScript()));
        output.Write("Select the company: ");
        output.Write(ReplaceTokens(@"<select id='SelectList_WPQ_' onchange='SelectListChanged_WPQ_()'>"));
        output.Write("<option value='0' selected>");
        output.Write("<option value='1'>Aloha Manufacturing");
        output.Write("<option value='2'>Australian Bushwalkers");
        output.Write("<option value='3'>Bay Area Conglomerate");
        output.Write("</select>");
    }
    else
        output.Write ("You must connect this web part to make it active.");
} 

Notice how we display a message if no web parts are connected.  When one or more consumer web parts are connected, we write out our client-side JavaScript (shown below) and some HTML.  Of course, you wouldn’t hard code these company names—presumably this would be pulled from an external database or SharePoint list.  Here is where we call the ReplaceTokens method to ensure our _WPQ_ token name gets replaced with a unique value.  Also, a DropDownList server-side control could also be used, but you will need to then extract the generated client ID.  I’ve kept it to HTML controls for simplicity. 

Finally, we introduce CreateChildControls.  Since all of our controls are HTML, this method is empty.

protected override void CreateChildControls()
{
}

Let’s turn our attention to the provider’s JavaScript code, since this is where the real action is.  This code contains the following methods:

MyMain
SelectListChanged_WPQ_()

The script also creates an object named ProviderClient_WPQ_ which matches the name we defined in the RegisterInterface method in the server-side code above.  Conceptually, you can think of this object as being the client-side instance of the web part.  This object is where MyMain is defined as shown here:

//this object name must match client interface name in RegisterInterfaces
var ProviderClient_WPQ_ = new MyCustomerProvider_WPQ_(); 

function MyCustomerProvider_WPQ_()
{
   this.PartCommunicationMain = MyMain;

   function MyMain()
   {
      var customerArgs = new Object();
      customerArgs.Cell = ''; 

      WPSC.RaiseConnectionEvent('CustomerProvider_WPQ_', 'CellReady', customerArgs);
   }
}

MyMain is automatically called when the page containing the web part is loaded.  As you can see above, it calls the RaiseConnectionEvent method.  RaiseConnectionEvent will then call each consumer web part’s MyCellReady method.  MyCellReady is defined in the consumer class which is covered later.  So, in effect, this becomes initialization code to ensure the provider and consumer web parts start off in sync.  In our demo application, since the customer drop down list defaults to a blank entry, this ensures all consumers get an initial blank value (via the customerArgs objectd).

To summarize the sequence of events, if we just have one consumer web part connected, this yields the following events in this order:

(Provider) MyMain
(Consumer) MyCellReady

If we have two consumer web parts, these events are called:

(Provider) MyMain
(Consumer 1) MyCellReady
(Consumer 2) MyCellReady

Note: There are additional methods that can be used.  For example, you can also incorporate initialization code that runs when the page loads.  This code is listed in the downloaded project but excluded here for simplicity.

Let’s turn our attention to the final JavaScript function, SelectListChanged_WPQ_.  This is called when a new customer is selected from our option select.  Here is the code:

function SelectListChanged_WPQ_()
{
   var customerArgs = new Object();
   customerArgs.Cell = document.all('SelectList_WPQ_').selectedIndex; 

   //alert ('Provider SelectListChage fired');
   WPSC.RaiseConnectionEvent('CustomerProvider_WPQ_', 'CellReady', customerArgs);
} 

As you can see, this is similar to MyMain.  Instead of passing a blank customer value, it passes the selected index (the Customer ID) to the consumer web part through the MyCellReady method.  With that, we move on to the consumer web part.

Consumer Web Part

The consumer web part is very similar to the provider.  For the sake of brevity, some of the duplicated code that is the same as the provider will be omited.  Here is the class definition:

//Implement the ICellConsumer Interface
[Obsolete]
public class ClientConsumer : WebPart, ICellConsumer 

ICellConsumer only has a single server-side event we must register.  It is listed here:

public event CellConsumerInitEventHandler CellConsumerInit; 

Remember that we will be using multiple instances of this consumer web part.  To allow the user to set what the web part will display (i.e. the report type), we create a personalizable property as shown here:

[DotNetWebParts.Personalizable(DotNetWebParts.PersonalizationScope.Shared),
DotNetWebParts.WebBrowsable(true),
DotNetWebParts.WebDisplayName("Result type"),
DotNetWebParts.WebDescription("Enter in the type of Customer Web Part Report to see")]
  public CustomerReportType reportType
  {
      get { return _reportType; }
      set { _reportType = value; }
  } 

Note: DotNetWebParts is an alias to the System.Web.UI.WebControls.WebParts namespace.  This is done to prevent name clashes between the .NET and SharePoint WebPart classes.

_reportType is based on an enumeration as shown here.  In a real-world scenario, there would probably more than two choices.

public enum CustomerReportType { Sales, Contacts } 


With that, here we define EnsureIntefaces.  This is almost exactly the same as in the provider:

//Define and register the provider interface for this web part.
[Obsolete]
public override void EnsureInterfaces()
{
    //Registers an interface for the Web Part         
    RegisterInterface("CustomerConsumer_WPQ_",//Interface name
       InterfaceTypes.ICellConsumer,        //Interface type
       WebPart.UnlimitedConnections,        //Allow multiple connections
       ConnectionRunAt.Client,              //Only support client-side 
       this,                                //Server-side interface object
       "ConsumerClient_WPQ_",               //JavaScript object for interface
       "Get Customer From",                 //UI Label seen when connecting
       "Consumes a Customer ID");           //Description
} 

CanRunAt, PartCommunicationConnect and CellProviderInit are nearly identical to what we had in the Provider.  Cell Ready, as shown here must also be implemented.  Since this is a server-side event, there is no code we need write.

//Not needed for client-side web parts, but must still be defined
[Obsolete]
public void CellReady(object sender, CellReadyEventArgs cellReadyArgs)
{
} 

That takes us to RenderWebPart.  This is very similar to the provider.  Here is what we do in the consumer:

//Defines Web Part UI
protected override void RenderWebPart(HtmlTextWriter output)
{
    //Is this web part connected to a provider?
    if (_connectedCount > 0)
    {
        //Call CreateChildControls
        EnsureChildControls(); 

        //Write out client-side script and HTML
        output.Write(ReplaceTokens(ConsumerJavaScript()));
        output.Write(ReplaceTokens("<div id='ConsumerDiv_WPQ_'/>\n"));
    }
    else
        output.Write("You must connect this web part to make it active.");
} 

In this case we do call EnsureChildControls which then calls CreateChildControls if it hasn’t been called yet.  We write out the JavaScript script and define a single div named ConsumerDiv_WPQ_.  As we’ll see this is the container for the HTML that our Web Service generates.  Our final bit of server-side code is CreateChildControls.  This is only used because the consumer web part calls into a SharePoint Web Service.  This code will generate a JavaScript proxy class that allows us to easily call into our web method.  The details on how this works is covered in a separate post of mine linked here:

protected override void CreateChildControls()
{
    //Create script reference for this web's url
    ServiceReference objReference = new ServiceReference(); 

    _wsPath = SPControl.GetContextWeb(HttpContext.Current).Url + "/_vti_bin/ajaxSearch.asmx";
    _wsPath = "http://portal.synergy.com/ajax/_vti_bin/customers.asmx";
    objReference.Path = _wsPath;
    _scriptManager.Services.Add(objReference);
} 

That’s the end of the server code.  Switching to the client, it is also pretty straight-forward.  For this example, the only method involved in web part communication is MyCellReady.  Again, this is invoked by the provider from the RaiseConnectionEvent call.  This method is shown here:

function MyCellReady(sender, customerArgs)
{
    //retrieve cell from provider
    customerID = customerArgs.Cell; 

    //was an actual customer selected
    if (customerID != 0)
    {
        //Ensure correct SharePoint path is used
        wsConnections.Customers.set_path('" + _wsPath + @"'); 

        //Call web service and pass customerID and type of report chosen
        wsConnections.Customers.GetData(customerID, '" + _reportType + @"', Response, Error);
    }
    else
    {
        //clear contents since no customer was selected
        document.all('ConsumerDiv_WPQ_').innerHTML = '';
    }
} 

The cell value from the provider is delivered into the customerArgs object.  As set from the provider, this contains a property called Cell which is how we extract the CustomerID.  From it, it’s just a simple call into the Web Service with a little help from the proxy class generated by the AJAX extensions.  A successful result calls the Response method.  An error response goes to the Error method.  Both are shown here and are self-explanatory.

//get return value from web service
function Response (result)
{
    document.all('ConsumerDiv_WPQ_').innerHTML = result;
} 

//display error from web service
function Error (result)
{
    alert (result.get_message());
}

Web Service Code

The Web Service is the trivial part of the solution, so I won’t spend much time on it.  Here is part of it which determines which block of HTML code to return:

public string GetData (int customerID, CustomerReportType reportType)
{
    if (reportType == CustomerReportType.Sales)
        return (GetSales(customerID));
    else if (reportType == CustomerReportType.Contacts)
        return (GetContact(customerID));
    else
        return ("Unknown report type specified");
} 

The GetSales method just returns a hardcoded image page.  This is where the output of some reporting system, such as Reporting Services could return content.  GetContact just returns sample contact HTML but could be easily generated by calling into SharePoint or an external database.

Conclusion

Well, if you made it this far, good job.  I salute you.  This is a powerful, but an admittedly complex solution to describe.  Remember that you don’t need to understand every little detail to leverage the power behind this.  I recommend you download the VS.NET solution and play with it a little bit to get a better understanding of the moving parts.  With a little practice, you should be much more comfortable with how it works.

To deploy the ClientConnections project, make sure you set the Start browser with URL option to your SharePoint Web application.  The Web Service should auto deploy into the 12 Hive and the GAC.  This is done via a post-build script.  You will need to manually deploy the three sample graphs—see the readme.txt file in the SampleImages folder for instructions.

There are so many different applications for client-side, web part connections, and AJAX just adds another degree of pizzazz to the solution.  So, with that, here is the link to the code.  Have fun and let me know if you have any questions or comments to share.  Till next time...

Technorati:,


<Update 11 Sep 2008>  I have noticed that this solution only seems to work on Internet Explorer.  Testing on both FireFox and Safari yield connection errors between the web parts.  As my schedule allows, I’ll try to find a solution to this. If any else have workarounds or other thoughts, please chime in.

 

Comments

There are no comments yet for this post.
Items on this list require content approval. Your submission will not appear in public views until approved by someone with proper rights. More information on content approval.

Title *


Body *


Posted By *


Attachments

Phone Number CLIENT LOGIN . CHANGE LOCALE . LIVE MEETING LOGIN . BLOG . PRIVACY POLICY . SITE MAP