Practical .NET

Integrating SharePoint Web Parts: Providers, Filters and More

Creating connectable Web Parts is a good thing for you and your users -- and the default interfaces that come with SharePoint form the architecture you should use to create those Web Parts.

In my last column, I covered how you can create your own consumer Web Parts to pull data from the Web Parts that come with SharePoint -- the Web Parts that SharePoint generates for every List in your site, for instance. This column is going to extend that column by looking at how to create a provider Web Part, starting with a filtering Web Part that, again, uses one of the default interfaces in SharePoint. A filtering Web Part allows users to enter selection criteria to filter the data displayed in a SharePoint-generated List Web Part.

Here's my point: You should be going beyond creating consumer Web Parts that use the default interfaces, and be creating provider Web Parts that implement those interfaces. The default interfaces, which you should implement and use in your Web Parts, will create an architecture that will allow users to assemble complex applications from simple parts. Creating a provider Web Part to use with your consumer Web Part isn't hard to do, provided you take advantage of the default SharePoint interfaces.

Creating a Filter
For instance: a user drags a List onto a Web Part page. Now the user wants to be able to filter that List down to some specific set by selecting an appropriate filtering Web Part. You just need the right interface in your Web Part. The first step, using Visual Studio 2010, is to add a Web Part to a SharePoint project. Once you've added the Web Part (which can be a Visual Web Part), have it implement the Microsoft.SharePoint.WebPartPages.ITransformable­FilterValues interface, like this (I've included some Imports directives to simplify some code coming up later):

Imports Microsoft.SharePoint.WebControls
Imports Microsoft.SharePoint.WebPartPages 
Imports System.Collections.ObjectModel

Public Class FilterWebPart
  Inherits WebPart
  Implements ITransformableFilterValues

As with any other "connectable" Web Part, you need a connection method -- this method tells SharePoint that your Web Part can be connected to other Web Parts. Unlike the Web Parts discussed earlier, a filtering Web Part is a provider, so its connection method must return a reference to your Web Part. The connection method must be decorated with the correct attribute (ConnectionProvider), and the return type of the method must be a SharePoint interface that holds the methods and properties the two Web Parts will use to communicate. For a filtering Web Part, that interface is ITransformableFilterValues. Here's an example:

 <ConnectionProvider("Percentage") > _
Public Function MyFilterConnection() As ITransformableFilterValues
  Return Me
End Function

The ITransformableFilterValues interface adds, among other members, three read-only properties to your Web Part: AllowAllValue, AllowEmptyValue and AllReadOnlyValues. For now, just have those properties return True so that you can concentrate on the two more-interesting properties in the interface: ParameterName and ParameterValues.

Your ParameterValues method must return a ReadOnlyCollection of values to be used to filter the List. To create that collection, first create a List of strings, add your filter value to the List, then instantiate the ReadOnlyCollection, passing the List. This example hardcodes a value into a List before using it to create a ReadOnlyCollection:

Public ReadOnly Property ParameterValues _
  As System.Collections.ObjectModel.ReadOnlyCollection(Of String) _
    Implements ITransformableFilterValues.ParameterValues
  Get
    Dim lst As New List(Of String)
    lst.Add("ALFKI")
    Dim roc As New ReadOnlyCollection(Of String)(lst)
    Return roc
  End Get
End Property

When the user connects your filter Web Part to a SharePoint List Web Part, the user will be presented with a dialog that includes a DropDownList of all of the columns in the List Web Part. The name that you return from the ParameterName property will be displayed in this dialog, but fundamentally, users can select any column they want to use with your filter. That doesn't mean the ParameterName column is useless, but it does mean that you should use it to provide the user with helpful advice, as in this example:

Public ReadOnly Property ParameterName _
  As String _
    Implements ITransformableFilterValues.ParameterName  
  Get
    Return "Filter on Customer Ids"
  End Get
End Property

You can return multiple values in your filter Web Part GetValues method (in which case you should return True from the AllowMultipleValues property). However, I've never found a SharePoint-generated List Web Part that uses anything other than the first value in the collection. If you don't want to filter the List -- that is, if you want to show all the values -- just return nothing or null from ParameterValues (and, no, the returning True or False from AllowAllValue or AllowEmptyValue doesn't make any difference in the SharePoint-generated Lists, either). Of course, if you create your own Web Part to use with your filter Web Part, you can implement these options, as I'll discuss later.

Creating an IWebPartTable Provider
In last month's column, I introduced consumer and provider Web Parts by creating both kinds of Web Parts using a custom interface that I defined. But later in that column, I suggested it was more useful to create consumer Web Parts using the default interfaces because it would enable users to integrate your consumer Web Parts with the SharePoint-generated Web Parts.

Once you start creating consumers with the default interfaces, it makes sense -- when creating your own provider Web Parts -- to implement those default interfaces (the more, the better) in your provider Web Parts. That way, users have the maximum flexibility in assembling your custom Web Parts. So what does a provider that supports filtering and implements multiple default interfaces look like?

The provider I'll use as my example uses ADO.NET to extract Order data from the Northwind database and dump it into a typed DataTable,using a custom FillBy method on the DataSet holding the DataTable. Using a DataTable simplifies some programming required to fully support the default interfaces, as you'll see. I've created this provider as a Visual Web Part; to facilitate communication between the "code-only" component of my Web Part (where most of my code goes) and the User Control component (which will handle displaying data to the user), in the code-only part of the Web Part I manage the DataSet through a property:

Private _nwTB As New Northwind.OrdersDataTable
Public  Property nwTB As Northwind.OrdersDataTable
  Get
    Return _nwTB
  End Get
  Set(value As Northwind.OrdersDataTable)
    _nwTB = value
  End Set
End Property

Initially, I'll have this Web Part implement just the IWebPartTable interface:

 <ToolboxItemAttribute(false) > _
Public Class OrderProvider
  Inherits WebPart
  Implements IWebPartTable

The IWebPartTable interface requires two members: Schema (which returns a collection of PropertyDescriptors for the data supplied by my Web Part) and GetTableData (which accepts a reference to a callback method in the consumer Web Part). Using an ADO.NET DataTable makes it easy to return a collection of PropertyDescriptors for each column in the Schema property -- just use the GetProperties method of the TypeDescriptor class, passing a reference to the table's DefaultView to get the collection:

Public ReadOnly Property Schema _ 
  As System.ComponentModel.PropertyDescriptorCollection _
    Implements IWebPartRow.Schema
  Get
    Return TypeDescriptor.GetProperties(nwTB.DefaultView(0))
  End Get
End Property

When the consumer calls the provider GetTableData method and passes a reference to one of its methods, I simply store the reference to use later:

Dim tcb As TableCallback
Public Sub GetTableData(callback As TableCallback) _
  Implements IWebPartTable.GetTableData
  tcb = callback
End Sub

Finally, I need a method decorated with the ConnectionProvider attribute to mark my Web Part as a provider that supports the IWebPartTable interface. In the attribute, I provide some text for the SharePoint UI to display ("All Data"), give the connection a unique name ("ConnectionPointIWebPartTable") to allow me to have multiple connection methods in one Web Part, and specify that multiple Web Parts can connect to this point because ... well, why not? That method looks like this:

 <ConnectionProvider("All data", _
  "ConnectionPointIWebPartTable", AllowsMultipleConnections:= True) > _
    Public Function MyConsumerMethodForAllData() As IWebPartTable
  Return Me
End Function

Supporting Filtering
To make filtering the orders from another Web Part possible, I first need to add a method decorated with the ConnectionConsumer attribute and, in that method, save the filtering parameter passed to the method by the filter Web Part in some field. That's what this code does:

Dim fvi As WebPartPages.ITransformableFilterValues
 <ConnectionConsumer("Customer ID", _
  "FilteringConsumerCustomerID", AllowsMultipleConnections:=True) > _
    Public Sub MyFilterConnection( _
      tfv As WebPartPages.ITransformableFilterValues)
  fvi = tfv
End Sub

Now it's time to write the code in the provider to retrieve the data and put it in the DataTable. The following code first looks in the Session object for the DataSet and, if the DataSet isn't there, the code creates the DataSet and adds it to the Session object. That process starts by instantiating the TableAdapter:

nwTB = CType(HttpContext.Current.Session("CustomerData"), _
               NorthwindDS.OrdersDataTable) 
If nwTB Is Nothing Then
  Dim nwtba As New NorthwindDSTableAdapters.OrdersTableAdapter

If there's no filtering Web Part attached to the provider, the code just calls the GetData method on the TableAdapter to get all the orders:

If fvi Is Nothing Then
  nwTB = nwtba.GetData
Else

If there's a filtering Web Part connected, I make a simplifying assumption -- the provider won't accept multiple filter values and, furthermore, I'll throw an exception if the filtering Web Part provides multiple values:

Dim parm As String
If fvi.AllowMultipleValues = True OrElse _
  fvi.ParameterValues.Count  > 1 Then
  Throw New SPException("Multiple values not allowed")
End If

The next set of code retrieves the filter value from the filtering Web Part ParameterValues property. In addition to retrieving the value from the ParameterValues property, the code checks for various conditions that mean all the Orders are to be retrieved (and throws an exception if those conditions are met but the filtering Web Part doesn't support the "All" value):

Dim parm As String
If fvi.ParameterValues Is Nothing OrElse _
   fvi.ParameterValues.Count = 0 OrElse _
   fvi.ParameterValues(0) Is Nothing Then
  If fvi.AllowMultipleValues = False Then
    Throw New SPException("A value must be provided")
  End If
  parm = "All"
Else
  parm = fvi.ParameterValues(0)
End If

The final step is to retrieve data using the parameter (or retrieve all the data) and put the data in the DataTable in the Session object. This code assumes that if the filter value is set to "All" but the "All" value isn't permitted, then "All" must be a CustomerID. The custom FillBy method I added to the DataSet accepts a CustomerID and retrieves the Orders for the Customer with that ID:

If (fvi.AllowAllValue = True And _
    parm = "All") Then
  nwTB = nwtba.GetData
Else
  nwTB = nwtba.GetDataByCustomerID(parm)
End If

With the DataTable now loaded with data, I put the DataTable in the Session object for next time (I've omitted the code to retrieve data if the consumer's filter value changes):

HttpContext.Current.Session("CustomerData") = nwTB

Once this provider Web Part has retrieved its data, it should pass the data to any connected consumer Web Part. Passing the data consists of calling the callback method in the consumer that was passed to the provider Web Part after the consumer was connected. In the IWebPartTable interface, the consumer's callback method accepts a single parameter defined of type Collection. The DataTable Rows collection works here because, as a collection of DataRows, the Rows collection is compatible with the parameter definition for the consumer method. Code like this checks for a consumer and calls its method if connected:

If tcb IsNot Nothing Then
  tcb(nwTB.Rows)
End If

The simplest place to put all this code is "as late as possible" in the Web Part lifecycle. For this example, I overrode the Web Part OnPreRender method, which is automatically executed after virtually all the other code in the Web Part, and put the code there. As you'll see, I'm going to centralize all the data-access code and communication with the consumers in this method.

Adding IWebPartRow
To support another interface, you just need to have the class implement the interface and add another ConnectionProvider method that returns that interface. That's what this method does for the IWebPartRow interface:

Public Class OrderProvider
  Inherits WebPart
  Implements IWebPartTable
  Implements IWebPartRow

 <ConnectionProvider("Current Row", _
  "ConnectionPointIWebpartRow", _
  AllowsMultipleConnections:=True) > _
Public Function MyConsumerMethodForSelectedRow() As IWebPartRow
  Return Me
End Function

The IWebPartRow interface, like the IWebPartTable interface, gives the consumer Web Part the ability to pass a reference to a callback method in the consumer Web Part to the provider by calling a method in the provider. The method in the provider is called GetRowData; all you have to do in that method is accept the reference to the consumer's callback method and store it until you need to pass the consumer some data:

Dim rcb As RowCallback
Public Sub GetRowData(callback As RowCallback) _
       Implements IWebPartRow.GetRowData
  rcb = callback
End Sub

Because the IWebPartRow interface passes a row selected by the user in the provider Web Part to the consumer Web Part, the provider Web Part will typically have a UI that displays the data the provider retrieved and lets the user select a row.

There are lots of ways to create that UI, but the easiest way for me was to create my provider as a Visual Web Part and then drag an ASP.NET GridView onto the UserControl. I set the GridView AutoGenerateSelectButton property to True, to generate a column with a LinkButton in it that allows the user to select a row in the GridView. I also specified in the GridView DataKeyNames property that the OrderId field must be tracked for each row in the GridView.

To link the GridView in the UserControl to the data retrieved in the code-only part of my Visual Web Part, I first need to retrieve a reference to the UserControl Parent and then cast the reference to the type of the code-only portion of my WebPart:

Dim parentWebPart As OrderProvider
Protected Sub Page_Load(ByVal sender As Object, _
  ByVal e As EventArgs) Handles Me.Load
  parentWebPart = CType(Parent, OrderProvider)
End Sub

I used this code in the UserControl PreRender event to move the data into the GridView from the property I set up in the code-only part of the Web Part:

Me.GridView1.DataSource = parentWebPart.nwTB
Me.GridView1.DataBind()

The final step in the UserControl is to add this code to the GridView Selected­IndexChanged event to call a method in the code-only part of the Web Part when the user selects a row. I pass the method the OrderId for the selected row:

parentWebPart.MarkRow( _
  Me.GridView1.SelectedDataKey)

In the method called from my UserControl, I don't pass the selected row to any connected consumer. Instead, in the method called from my UserControl, all I do is retrieve the OrderId passed from the UserControl and store it in a variable:

Dim RowKey As String = String.Empty
Public Sub MarkRow(key As DataKey)
  RowKey = key.Value
End Sub

I use that value in the OnPreRender method where I'm putting the rest of my data-access code. In the OnPreRender method, I check to see if the variable has a value in it. If it does (and if I have a consumer connected through my IWebPartRow interface), I find the matching row in the DataSet and call the method I received from the consumer, passing the row:

If RowKey  < > String.Empty AndAlso _
  rcb IsNot Nothing Then
  Dim selectedRows() As DataRow
  selectedRows = nwTB.Select("OrderId=" & RowKey)
  rcb(selectedRows(0))
End If

And there it is: a custom provider Web Part that supports both filtering and integrating with consumers, using either the IWebPartTable or IWebPartRow interface. By leveraging the default SharePoint interfaces, you can create truly interoperable Web Parts that will empower your users to create the applications that they need.

comments powered by Disqus

Featured

  • AI for GitHub Collaboration? Maybe Not So Much

    No doubt GitHub Copilot has been a boon for developers, but AI might not be the best tool for collaboration, according to developers weighing in on a recent social media post from the GitHub team.

  • Visual Studio 2022 Getting VS Code 'Command Palette' Equivalent

    As any Visual Studio Code user knows, the editor's command palette is a powerful tool for getting things done quickly, without having to navigate through menus and dialogs. Now, we learn how an equivalent is coming for Microsoft's flagship Visual Studio IDE, invoked by the same familiar Ctrl+Shift+P keyboard shortcut.

  • .NET 9 Preview 3: 'I've Been Waiting 9 Years for This API!'

    Microsoft's third preview of .NET 9 sees a lot of minor tweaks and fixes with no earth-shaking new functionality, but little things can be important to individual developers.

  • Data Anomaly Detection Using a Neural Autoencoder with C#

    Dr. James McCaffrey of Microsoft Research tackles the process of examining a set of source data to find data items that are different in some way from the majority of the source items.

  • What's New for Python, Java in Visual Studio Code

    Microsoft announced March 2024 updates to its Python and Java extensions for Visual Studio Code, the open source-based, cross-platform code editor that has repeatedly been named the No. 1 tool in major development surveys.

Subscribe on YouTube