Getting Started

Negotiate Between Web Parts

Learn how to create a dynamic application where Web Parts negotiate what data they will share with each other.

Technology Toolbox: Visual Basic; ASP.NET

Consider this (not uncommon) scenario under ASP.NET 2.0. Your user clicks on a button on your ASP.NET 2.0 Web Page, and the page transforms, revealing a list of Web Parts the user can add to the page. As the user adds Web Parts to the page, the page connects compatible Web Parts to each other. The connected Web Parts reach out to each other and start exchanging information. The user has created a personalized application from the components on the page.

In an ideal world, a developer could create Web Parts for this page without having to consult with other developers creating Web Parts to be used on the same page. As the page developer, you add the resulting Web Parts to your page and know the Web Parts will interrogate each other or even negotiate with each other through a set of common interfaces to determine what information they will share. ASP.NET 2.0's Web Parts let you do precisely this by taking advantage of .NET's predefined communication interfaces. I'll show you how to implement a pair of simplified Web Parts that enable you to take advantage of such predefined communication interfaces (see the Go Online box).

The Web Part framework includes four predefined interfaces: IWebPartField, which allows a Web Part to pass a single piece of information; IWebPartRow, which supports passing a set of data; IWebPartTable, for passing a collection; and IWebPartParameters, which allows one Web Part to signal what data it wants and the other Web Part to respond by listing what data can be provided. A Web Part that implements any of these interfaces can communicate with any other Web Part with the same interface.

Implementing these interfaces requires a handful of steps. You begin by using the IWebPartField interface (IWebPartTable and IWebPartRow are almost identical to IWebPartField) as a foundation for creating Web Parts that can interrogate each other, then move onto the more powerful—and slightly more complicated—IWebPartParameters interface which supports negotiation.

All of these interfaces allow the Web Part that requires information (the consumer) to tell the Web Part that has the data (the provider) what function to use to pass data. Implementing IWebPartField, IWebPartRow, or IWebPartTable interfaces in the provider requires adding two members: a method that accepts a single parameter of type delegate and a read-only property that returns a PropertyDescriptor object. These two members work together to provide to the consumer both data and a description of that data from the provider.

The process that the Web Parts follow is simple. In all the interfaces, the consumer calls a method on the provider, passing a reference to a method in the consumer that accepts data from the provider. The provider then calls that method in the consumer to pass data to the consumer. The key distinction between the IWebPartField and the IWebPartRow and IWebPartTable interfaces is the datatype of the parameter passed to the consumer's routine.

Build a Provider
You begin the process of building these dynamic Web Parts by creating a provider that can be interrogated by another Web Part about the data it provides. Start by having the provider implement the IWebPartField interface and the GetFieldValue method required by the interface. The GetFieldValue method accepts a reference to the method in the consumer that will be used to receive data from the provider. This code implements GetFieldvalue to accept a delegate named fld from the consumer. The code also sets a class-level variable (called ConsumerDelegate) to the delegate passed from the consumer:

Public Class WebPartFieldProvider
   Inherits WebControls.WebParts.WebPart
   Implements WebControls.WebParts.IWebPartField
Dim fld As WebControls.WebParts.FieldCallback
Sub GetFieldValue(ByVal fld As _    WebControls.WebParts.FieldCallback) _    Implements WebControls.WebParts. _    IWebPartField.GetFieldValue
   ConsumerDelegate = fld
End Sub
End Class

Elsewhere in the provider's code, the provider can call the consumer's routine whenever the provider has data to pass onto the consumer. The provider must use the delegate's Invoke method to call the consumer's routine through the delegate, passing any parameters that the consumer's routine requires. You can pass the consumer's routine only one parameter for the IWebPartField interface, so this code supplies a single parameter to the Invoke method. The code also checks whether a reference has been passed from the consumer before attempting to invoke the delegate:

If ConsumerDelegate IsNot Nothing Then
   ConsumerDelegate.Invoke(Me.BookData)
End If

This code uses a property on the provider to provide the data passed to the consumer. In this case, the data is a simple property that returns a string value:

ReadOnly Property BookData() As String
   Get
     Return strBookTitle
   End Get
End Property

Of course, you could have just as easily passed the Invoke method a variable or a literal instead of a property on the provider. But passing a property means you can integrate with the Schema property that is the other required member of the IWebPartField interface. The Schema property gives the consumer a way to find out about the information the provider returns. The Schema property must return a PropertyDescriptor, an object that holds information about a property. Passing a property value to the Invoke method enables you to inform the consumer about the data by returning the property's PropertyDescriptor.

This version of the Schema property returns a PropertyDescriptor for the BookData property that you passed to the Invoke method earlier:

ReadOnly Property Schema() _
   As ComponentModel.PropertyDescriptor _
   Implements WebControls.WebParts. _
   IWebPartField.Schema

   Get
     Dim pdc As PropertyDescriptorCollection
     pdc = TypeDescriptor.GetProperties(Me)      Return pdc("BookData")    End Get
End Property

One final step: You need to create a function that returns a reference to the IWebPartField interface on the provider when called. To identify the function to ASP.NET, the function must be decorated with the ConnectionProvider attribute:

<WebParts.ConnectionProvider("WebPartFieldProv")> _
   Public Function ProvideReference() As _
   WebParts.IWebPartField
     Return Me
   End Function

That's it for the key aspects of creating a provider. Next, you need to build a consumer. The code in your consumer code accomplishes four tasks. First, you must get a reference to the IWebPartField interface on the provider. Second, you must provide a routine that the provider can call to pass data. Third, you must pass the provider a reference to that routine. Finally, you must check the Schema property on the provider to determine whether the provider is passing appropriate data.

You accomplish the first task by decorating a method in your consumer with the ConnectionConsumer attribute. That method must accept a single parameter of the type IWebPartField. The host page will call this method automatically when the page connects a provider to your consumer and pass a reference to the IWebPartField interface on the provider. In this routine, you pass the provider a reference to a method on your consumer by calling the interface's GetFieldValue method.

In this code, a routine called IWebPartFieldConsumer has been given the ConnectionConsumer attribute. The code uses the Schema property to check the type of data the provider supplies before passing the reference to the consumers ReceiveData method. The code passes the reference only if the provider's data is of type string:

Public Class WebPartFieldConsumer
   Inherits WebControls.WebParts.WebPart
Dim ifld As WebControls.WebParts.IWebPartField
<WebControls.WebParts. _    ConnectionConsumer("IWebPartField Consumer")> _ Public Sub IWebPartFieldConsumer(ByVal fld As _    WebControls.WebParts.IWebPartField)
Dim pd As PropertyDescriptor    ifld = fld    pd = ifld.Schema    If pd.PropertyType.Name = "String" Then      ifld.GetFieldValue(AddressOf ReceiveData)
   End If
End Sub
End Class

A provider calls the ReceiveData method on the consumer with the Invoke method when it has data to pass to the consumer. It's up to you, what you do with that data in the consumer's ReceiveData method. This code uses it to set a property on the consumer:

Public Sub ReceiveData(ByVal PassedData As Object)
   Me.BookTitle = PassedData
End Sub

Implement Negotiation
You've now created provider and consumer Web Parts that let the consumer check what data is coming from the provider before passing the method that accepts the data. The IWebPartParameters interface takes communication a step further: It supports two-way negotiation between the consumer and provider.

It probably won't surprise you that you need to keep in mind some differences between the IWebPartParameters and IWebPartField interfaces. One difference is trivial: In IWebPartParameters, the equivalent routine to GetFieldValue is GetParametersData.

Another difference: IWebPartField supports passing a single data value, but IWebPartParameters supports passing a named set of values allowing for a virtually unlimited amount of data to be passed. The routine in the consumer that the provider calls supports that difference by accepting an object holding a list of name-value pairs rather than just a single value. Similarly, the Schema property in the provider must return a collection of PropertyDescriptors that describe multiple data items instead of a single PropertyDescriptor.

The most significant difference between the interfaces concerns the SetConsumerSchema method of the IWebPartParameters interface. The SetConsumerSchema method lets the consumer tell the provider what the consumer wants to receive. The consumer accomplishes this by passing a PropertyDescriptorCollection to the SetConsumerSchema method to specify the kind of information that the consumer is looking for.

The provider should use the information received in the SetConsumerSchema method to build the collection that that it passes to the consumer's routine. For example, the provider could loop through the PropertyDescriptorCollection to determine the names and datatypes of the properties to be returned.

This requires a precise sequence of events. First, write your consumer code to call the provider's SetConsumerSchema method so that you let the provider know what data your consumer wants. Next, have your consumer check the provider's Schema property to see which of the data items you requested the provider will actually deliver. Then call the GetParametersData method to pass the consumer routine that the provider will pass data to, but only after you know what you're getting from that method.

This sample code assumes that the consumer requests values for every property on the consumer. To signal this to the provider, the code calls the SetConsumerSchema method and passes a PropertyDescriptorCollection that describes all the properties on the consumer. After passing that information to the provider, the code then performs a simple check of the data returned by the provider's Schema property to see if the count of requested properties matches the count of properties that the provider returns. If there is a match, the consumer calls the GetParametersData method, passing a reference to the routine to be used by the provider (still called ReceiveData):

<WebControls.WebParts.ConnectionConsumer( _
   "IWebPartParameters Consumer")> _
   Public Sub IWebPartParametersConsumer( _
   ByVal prm As _
   WebControls.WebParts.IWebPartParameters)

   Dim pdc As PropertyDescriptorCollection
   Dim iprm As WebControls.WebParts.IWebPartParameters
   iprm = prm
   pdc = TypeDescriptor.GetProperties(Me)    iprm.SetConsumerSchema(pdc)    If iprm.Schema.Count = pdc.Count Then      iprm.GetParametersData(AddressOf _        ReceiveData)    End If End Sub

Your method in the consumer that receives the data from the provider must include some additional logic when you use IWebPartParameters. When the provider calls the consumer's method, you must loop through the object passed to the method, retrieving the data you want. In this example, the method looks for the values named BookTitle and AuthorName. The code then uses that data to set the consumer's BookTitle and BookAuthor properties:

Sub ReceiveData(ByVal BookInfo As IDictionary)
Dim entry As DictionaryEntry
   For Each entry In BookInfo.Keys
     Select Case entry.Key.ToString 
     Case "BookTitle" 
        Me.BookTitle = entry.Value.ToString
     Case "BookAuthor"
       Me.BookAuthor = entry.Value.ToString
     End Select
   Next
End Sub

In the provider Web Part, your SetConsumerSchema method should save a reference to the PropertyDescriptorCollection passed to it by the consumer. This code implements SetConsumerSchema to save the reference in a module-level variable called pcSchemaRequested:

Private pcSchemaRequested As _
   PropertyDescriptorCollection
Sub SetConsumerSchema (ByVal schema As _
   PropertyDescriptorCollection) _
   Implements WebControls.WebParts. _
   IWebPartParameters.SetConsumerSchema
   pcSchemaRequested = schema
End Sub

Your code should loop through the PropertyDescriptorCollection received in the SetConsumerSchema method to check whether your provider can supply that data when the consumer calls your provider's Schema property. You add a PropertyDescriptor describing the data to an array of PropertyDescriptors if your provider can supply the data. After checking all the data requested by the consumer, you convert that array into a PropertyDescriptorCollection and return it to the consumer to indicate which data your provider will return. This version of the Schema property accomplishes those goals:

Private pcSchemaReturned As _
   PropertyDescriptorCollection
Public ReadOnly Property Schema() As _
   System.ComponentModel. _
   PropertyDescriptorCollection _
   Implements WebControls.WebParts. _
   IWebPartParameters.Schema
Get
   Dim pdcMe As PropertyDescriptorCollection
   Dim pdcReturn(pcSchemaRequested.Count) As _
   PropertyDescriptor
   Dim ing As Integer
   pdcMe = TypeDescriptor.GetProperties(Me)
   For Each pd As PropertyDescriptor In _
     pcSchemaRequested
     If pdcMe(pd.Name) IsNot Nothing Then
       ing += 1
       pdcReturn(ing) = pd
     End If
   Next
     pcSchemaReturned = _
       New PropertyDescriptorCollection(pdcReturn)
   Return pcSchemaReturned 
End Get
End Property

Your provider must implement the GetParametersData method to return data to the consumer. The data must be passed using an object that inherits from the Dictionary object. This version uses a StateBag to hold the data specified in the property collection generated in IParmsSchema. The code then passes the StateBag to the consumer's routine:

Sub GetParametersData(ByVal prm As _
   WebControls.WebParts.ParametersCallback) _
   Implements WebControls.WebParts. _
   IWebPartParameters.GetParametersData
Dim dict As New StateBag
Dim prop As PropertyDescriptor
   For Each prop In pcSchemaReturned
   Select Case prop.Name
     Case "BookTitle"
       dict.Add("BookTitle", Me.BookTitle)
     Case "BookAuthor"
       dict.Add("BookAuthor", Me.BookAuthor)
     End Select
   Next
   prm.Invoke(dict)
End Sub

Finally, don't forget to add the function to the provider that returns a reference to the IWebPartParameters interface:

<WebParts.ConnectionProvider("WebPartParmProv")> _
   Public Function ProvideReference() As _
   WebParts.IWebPartParameters
     Return Me
   End Function

Building the kind of dynamic Web Parts applications described at the outset of this article will require implementing provider and consumer Web Parts. However, it is the page developer who must analyze the Web Parts being added to the page and connect them. For example, this code retrieves references to two parts by name, gets references to the collection of connections in each part, and, after determining that the first connections in each collection are compatible, connects the two Web Parts:

Dim prov As BookSiteWP.FindBook
Dim cons As BookSiteWP.DisplayBook
prov = CType(Me.WebPartZone2.WebParts( _
   "Provider1"), BookSiteWP.FindBook)
cons = CType(Me.WebPartZone2.WebParts( _
   "Consumer1"), BookSiteWP.DisplayBook)
Dim cncp As ConsumerConnectionPointCollection
Dim prcp As ProviderConnectionPointCollection
prcp = Me.WebPartManager1. _
   GetProviderConnectionPoints(prov)
cncp = Me.WebPartManager1. _
   GetConsumerConnectionPoints(cons)
If Me.WebPartManager1.CanConnectWebParts( _
   prov, prcp(0), cons, cncp(0)) Then
   Me.WebPartManager1.ConnectWebParts( _
     prov, prcp(0), cons, cncp(0))
End If

This code is only the beginning for the page developer. The CanConnectWebParts method just checks whether two Web Parts share an interface (such as IWebPartField or IWebPartParameters). A real application must organize Web Parts into categories and only make connections between Web Parts that can work together to deliver real business functionality.

The samples described in this article return only a simple string property's value or all the properties on a consumer. A real-world implementation will require you to pass more complex data or create a custom object to specify the data you want to share. These caveats aside, you and your fellow Web Part developers can now concentrate on delivering business functionality, confident that your parts can work together at runtime.

comments powered by Disqus

Featured

Subscribe on YouTube