Practical .NET

SharePoint Web Part Default Interfaces

Because SharePoint lists are automatically turned into connectable Web Parts, you can integrate your own Web Parts with any existing SharePoint list by implementing the default interfaces provided by SharePoint.

To my mind, the coolest part of SharePoint is the ability to give users a way to connect a Web Part to another Web Part so that the two Web Parts can exchange information. Effectively, SharePoint lets users assemble their own application from components you provide. You don't even have to write all the components yourself: SharePoint creates a Web Part for any SharePoint list, so you can drive your Web Parts from any list on your site.

For instance, imagine the Web Parts you might attach to a Task list in SharePoint. Some users might want to select a task and see the contact information for the employee to which the task is assigned (perhaps information retrieved from your employee database using ADO.NET). Some others might want a graph comparing the "percentage complete" numbers for all the tasks in the list. Other users might want to see the information from the task presented in a more readable format (for instance, with the percentage complete shown as a pie graph rather than a number).

You could build a monolithic application that presents all the data (employee information, percentage complete graph of all data, reformatted data and so on). But the better SharePoint solution is to build individual Web Parts for each of these components and let the user assemble the components they want onto the Web Part page of their choice.

Really, connectable Web Parts are a way of applying the principle of a separation of concerns: You're dividing up the code that would be in one monolithic application into multiple classes and Web Parts, each dedicated to doing one thing. And SharePoint provides some default interfaces to make this process even easier to implement. This doesn't mean duplicating code in each of those Web Parts. Where there's shared code, components can hand off shared functionality to a common class that does all the heavy lifting.

I'll show you how to create both a consumer and a provider Web Part that can talk to each other. Then you'll see how to create a consumer Web Part that integrates with Web Parts that SharePoint generates from its lists using each of the default interfaces (IWebPartRow, IWebPartTable, IWebPartField and IWebPart­Parameters). I'll also discuss some tactics for dealing with the order in which SharePoint calls the methods in your consumer Web Part.

Creating a Consumer and a Provider
Creating a Web Part provider simply requires creating a method that returns a reference to the Web Part and then decorating that method with the ConnectionProvider attribute. When a user uses the SharePoint UI to connect your provider to a consumer Web Part, SharePoint will call your provider's method and capture the reference.

The following code assumes that the Web Part is a Visual Web Part, with an associated UserControl that contains a TextBox named FirstNameTextBox:

Public Class MyProviderWebPart
  Inherits WebPart

 <ConnectionProvider("First Name") > _
Public Function MyProviderMethod() As MyProviderWebPart
  Return Me
End Function

Protected Overrides Sub CreateChildControls()
  Dim control As Control = Page.LoadControl(...path to user control...)
  Controls.Add(control)
End Sub

Public Function ProvideFirstName() As String
  Dim uc As MyProviderWebPartUserControl
  uc = CType(Controls(0), MyProviderWebPartUserControl)
  Dim tb As TextBox
  tb = CType(uc.FindControl("FirstNameTextBox"), TextBox)
  Return tb.TextEnd Function
End Class

The ProvideFirstName method passes the current value of the TextBox to the caller.

Creating a Web Part consumer simply requires adding a method that accepts a reference to some provider Web Part and decorating it with the ConsumerProvider attribute. When, in the SharePoint UI, a user connects your consumer to a provider, SharePoint will pass the reference retrieved from the provider's ConnectionProvider to the consumer's ConnectionConsumer method. Typically, in the ConnectionConsumer method, you'll store the reference to the provider in a class-level field so you can call methods or read properties on the provider elsewhere in your consumer Web Part.

There's one wrinkle here: UI-related events in the consumer Web Part where you want to use the provider's members may be called before the Web Part ConnectionConsumer method is passed the reference to the provider. To deal with this problem, you can just set a Boolean variable in the Web Part methods that use the provider. Then, in the ConnectionConsumer, you can check that Boolean variable and call the appropriate members in the provider.

Listing 1 demonstrates that tactic: The code assumes the consumer is a Visual Web Part with a method (called Button1Click) called from a button's Click event handler in the User Control. The User Control is also assumed to have a label called DisplayFirstNameLabel that's updated with data from the provider. However, rather than call the provider's ProvideFirstName method in the Button1Click method, the code sets a Boolean variable (_Button1_Click) and does all its work in the ConnectionConsumer method.

Implementing Leveraging Interfaces
There's a key enhancement you can make here. One of the points of using Web Parts is the ability to mix and match consumers and providers, which means that you don't want to tie your consumer to a single Web Part. To provide that flexibility, you can define the members you want your consumer to access in an interface that multiple providers can implement. An interface for my previous example would look like this:

Public Interface IMyWebPartInterface
  Function ProvideFirstName() As String
End Interface

Implementing a provider interface doesn't change much in the provider. Members in the provider have to be tied to the interface's members and the ConnectionProvider has to return the interface, as shown here:

Public Class MyProviderWebPart
  Inherits WebPart
  Implements MyWebPartInterface

Public Function GetFirstName() As String _
Implements IMyWebPartInterface.ProvideFirstName
  Return Me.FirstNameTextBox.Text
End Function

 <ConnectionProvider > _
Public Function MyProviderMethod() As IMyWebPartInterface
  Return Me
End Function

End Class

Even less is work is required in the consumer -- the data type for the input parameter to the ConnectionConsumer method has to be set to the interface, as does the variable that holds the reference to the provider:

Dim _prov As IMyWebPartInterface
 <ConnectionConsumer("First Name") >
Public Function MyConsumerMethod(prov As IMyWebPartInterface)
  _prov = prov
End Function

Now the consumer can work with any provider that implements the IMyWebPartInterface, rather than with just the MyProviderWebPart Web Part.

Retrieving a Complete List
What's especially interesting about the Web Part architecture is that the automatically generated Web Parts in SharePoint (for instance, the Web Parts created for every list) are providers that implement all of the default SharePoint interfaces. This allows you to create Web Part consumers that users can tie to any SharePoint list. For instance, if you want to create a Web Part that receives all the ListItems in a Task list, you can create a consumer Web Part that accepts data through the SharePoint IWebPartTable interface.

To create a Web Part that will work with the default IWebPartTable interface, you need to create a ConnectionConsumer method in your Web Part that accepts the IWebPartTable interface. The IWebPartTable interface exposes one property and one method for you to use in your consumer.

The property, called Schema, returns a collection of PropertyDescriptors, which describe the fields in the rows of data that the provider will send to the consumer. Calling the interface's method (GetTableData) does not, as you might think, retrieve table data. Instead, when you call the provider's GetTableData method, you pass a reference to some other method (the "callback method") in your consumer. That callback method is passed the data from the provider as a DataView.

This terrible naming convention -- a method called GetTableData that doesn't actually get you any table data -- is, sadly, typical of the default interfaces. Also, while the data is passed to the callback method as a DataView, the interface defines the input parameter to the callback method as an ICollection.

This example uses the IWebPartTable GetTableData method to return to the provider a reference to another method called ProcessTableData. Setting the _prov variable before passing the callback method to the GetTableData method ensures that the _prov variable is set before the callback method is invoked by the provider:

Dim _prov As IWebPartTable
 <ConnectionConsumer("DataView") > _
Public Sub MyConsumerMethod(prov As IWebPartTable)
  _prov = prov
  prov.GetTableData(AddressOf MyCallbackMethod)
End Sub

The callback method will be called by the provider when the user connects the two Web Parts, and when the user does something with the provider's list. That means the callback method is your best place to do something with the provider's data.

Listing 2 is from a standard Web Part that builds its UI in the CreateChildControls method, rather than using a User Control. In this example, the callback method finds a DataGrid in the Web Part Controls collection and binds the DataView to it. (Because this code is in Visual Basic, I can declare the callback method's parameter as DataView and Visual Basic will do an implicit cast from ICollection to DataView for me.)

If you need to retrieve values from a particular column and are willing to tie your consumer Web Part to just one list, you can hardcode the position of the columns whose values you want to access into the callback method. However, the PropertyDescriptor objects available through the IWebPartTable Schema property provide a convenient way of retrieving values by name instead of position.

First, pass the name of the column you want to the Schema property's Find method to retrieve the column's PropertyDescriptor (if you pass True to the Find method, it does a case-insensitive search for the property name). Once you have the PropertyDescriptor, you can pass its GetValue method a DataRowView from the DataView to retrieve the value for that column.

Here, again, there's the problem with ensuring that the ConnectionConsumer method has been called before you use the provider interface's GetTableData method or Schema property. The PreRender event is guaranteed to be called after the ConnectionConsumer method, so the following code calculates the average value of a column called PercentageComplete in that event and displays the result in a label in the Controls collection:

Protected Overrides Sub OnPreRender(e As System.EventArgs)
  MyBase.OnPreRender(e)
  
  Dim npd As PropertyDescriptor
  npd = _prov.Schema.Find("PercentComplete", True)
  Dim tot As Double
  For Each dr As DataRowView In _dv
    tot += Convert.ToDouble(npd.GetValue(dr))
  Next

  Dim lb As Label
  lb = CType(Controls(2), Label)
  lb.Text = (tot / _dv.Count).ToString        
End Sub

You'll still want to make sure your callback method has been invoked (for instance, if your Web Part hasn't been connected to another Web Part yet, the callback method won't have been called), so check to see if the _prov variable is null before using it.

Processing Individual Rows and Fields
If you don't want to process the whole list, you can use the IWebPartRow interface to allow the user to select a row in the provider Web Part and trigger processing in your consumer Web Part. IWebPartRow has a property called Schema and a method called GetRowData -- which is the worst. Method. Name. Ever. Like the GetTableData method, the GetRowData method does not get you a row of data. As with the IWebPartTable interface, you use the GetRowData method to pass a reference to another method in your Web Part. The provider will call that method whenever the user selects a row in the provider Web Part and passes the selected row in the table to the method.

The IWebPartField interface looks much like the other two interfaces, except that the provider Web Part passes only a single field to the consumer's callback method (the user gets to select a field as part of connecting the consumer to the provider in the SharePoint interface). In most cases, you'll use this field in your consumer Web Part to filter data or retrieve additional data. The following code, for a Visual Web Part, accepts a single string and uses it to update a label in the associated User Control:

 <ConnectionConsumer("Field") > _
Public Sub MyFieldConsumerMethod(iwpf As IWebPartField)
  iwpf.GetFieldValue(AddressOf ProcessFieldData)
End Sub

Private Sub ProcessFieldData(fd As Object)
  Dim uc As WebPartFieldUserControl
  uc = CType(Controls(0), WebPartFieldUserControl)
  Dim lb As Label
  lb = CType(uc.FindControl("DisplayFieldLabel"), Label)
  lb.Text = data
End Sub

You can use the single PropertyDescriptor passed in the Schema property to determine which user-selected property is to be passed to your consumer (or generate an error message if the user made an inappropriate choice).

Controlling the Field Selected
The IWebPartField interface gives the user a lot of flexibility in deciding which field will be passed to the consumer Web Part. The IWebPartParameters interface lets you provide the user with a list of names of values you'll accept. You can look at this list in one of two ways: as a set of hints to the user about what your consumer will accept, or as a way of telling the user how you'll treat the data the user selects from the provider.

The IWebPartParameters interface adds a new method to the Schema and Get*Data members: SetConsumerSchema. You pass this method a PropertyDescriptorCollection containing PropertyDescriptor objects that specify the fields for which you're willing to accept values.

When the user connects the provider Web Part to your consumer, the user will be given two dropdown lists to select from: the provider's list of fields and the names from your list of PropertyDescriptors. The user matches one of the provider's fields with one of your PropertyDescriptors to specify which field from the provider will be passed to your consumer, and associate that data with one of your PropertyDescriptors.

The most flexible way to create a PropertyDescriptorCollection is to pass it an array of PropertyDescriptors, where each PropertyDescriptor is created using the TypeDescriptor CreateProperty method. This example creates a collection containing properties whose names match the fields the user will see in a Task list's fields:

 <ConnectionConsumer("Selected Field") > _
Public Sub MyConsumerMethod(prov As IWebPartParameters)
  Dim pds(1) As PropertyDescriptor
  pds(0) = TypeDescriptor.CreateProperty(Me.GetType(), _
    "Status", GetType(String))
  pds(1) = TypeDescriptor.CreateProperty(Me.GetType(), _
    "PercentageComplete", GetType(Decimal))
  Dim pdc As New PropertyDescriptorCollection(pds)
  prov.SetConsumerSchema(pdc)
  prov.GetParametersData(AddressOf GetRow)
End Sub 

The provider passes to the callback method an object implementing the IDictionary interface. You can check the dictionary to see which of your PropertyDescriptors the user selected and retrieve the value from the provider associated with it. That's what this example does:

Private FilterValue As String
Public Sub GetParameter(dict As IDictionary)
  If dict.Contains("Status") Then
    FilterValue = dict("Status")
  End If
  '...check for and use different values...
End Sub

Using one or more of these default interfaces allows you to empower your users by creating Web Parts that can be integrated with the SharePoint Web Parts. Users can assemble the UI they need from the components that you provide.

comments powered by Disqus

Featured

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube