Introduction
Have you ever developed a provider / consumer web part pair that would allow a single consumer web part to be connected to multiple provider web parts only to find that ASP.NET / WSS 3.0 / MOSS 2007 only allows a single one-to-one connection? If so, then you have come to the correct location.
Background
A few weeks back, I was developing some new web parts for a SharePoint project I am busy with, and found myself running into a brick wall. The requirement was that when a link was clicked on a list item, some help/information text would be displayed to the right of the list. The requirement also stated that more than one list could be on a single portal page and that the same help/info text web part would be used. I thought this would be easy as I knew the ConnectedConsumerAttribute
class had a property called AllowMultipleConnection
and all I would have to do is set this value to true
; after all, WSS 2.0/MOSS 2003 supported this functionality already. How wrong I was, so after wading through all the forums / blogs / wikis etc., I came to the conclusion that many people were experiencing the same pain as myself. Unfortunately, I have had not been able to find a solution to the problem which has led me to this point. Either the ASP.NET web part implementation is flawed in design, or there is one big defect which needs to be repaired. I think I have come up with a potential solution to the problem (although I thought up several others along the way, this is the slickest).
Using the code
Let me start straight off. This is not a step-by-step how-to to develop a bit of code using VS 2008; I will assume you already know this. I will describe the concepts of the solution using code. My development environment is the following:
- Vista Enterprise
- VSTS 2008
- WSS 3.0 (installed on Vista using Bamboo Solutions install)
- VS Extensions for SharePoint SVCS 1.2
- + plenty of other stuff (for this article, the above is required)
NB: Please remember to start VS with elevated credentials if you are using Vista, and don't forget to change the WSS URL in the project debug properties.
The first thing I did was create a new custom attribute class called ConnectedConsumerInterfaceAttribute
which I use to mark the relevant method on the consumer web part. This method will be invoked by the provider web part.
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
public class ConnectConsumerInterfaceAttribute : Attribute
{
private Type _interfaceType;
public ConnectConsumerInterfaceAttribute(Type interfaceType)
{
_interfaceType = interfaceType;
}
public Type InterfaceType
{
get
{
return _interfaceType;
}
}
}
The next thing I did was to subclass the ASP.NET WebPart
class and create a ProviderWebPart
class. All providers that require to connect to multi-connect consumers will need to inherit from this new class. I added a new protected virtual
method called ConnectedConsumerInvoke
which has the following signature:
protected virtual void InvokeConnectedConsumer<TInterfaceType>(TInterfaceType graph)
This method has all the magic required to invoke the consumer method based on the interface type and whether it is tagged with the ConnectedConsumerInterfaceAttribute
attribute. The method retrieves all the current web part connections using the WebPartManager
object. It then loops through each connection until it can find the connections which belong to the current provider web part (which is this
). Once the connection provider has been identified as the same object, the connection consumer is added to a list. Once the consumer web part list is fully populated, the code loops through each consumer web part and retrieves all the methods of the web part using Reflection. It then checks to see if the methods are tagged with the ConnectedConsumerInterfaceAttribute
attribute. If it is, then the TInterfaceType
type is compared to the ConnectedConsumerInterfaceAttribute.InterfaceType
, and if the same, the retrieved method is invoked and the graph
object is passed in as a parameter. In the code sample attached, I have created a helper class called SPSHelper
which does some of the work described above.
public class ProviderWebPart : WebPart
{
public ProviderWebPart() : base()
{
}
protected virtual void InvokeConnectedConsumer<TInterfaceType>(TInterfaceType graph)
{
WebPart[] consumerWebParts = SPSHelper.GetConnectedConsumerWebParts(
this.WebPartManager, this, typeof(TInterfaceType));
foreach (WebPart consumerWebPart in consumerWebParts)
{
MethodInfo[] methods = consumerWebParts.GetType().GetMethods(
BindingFlags.Public | BindingFlags.Instance);
foreach (MethodInfo method in methods)
{
object[] attributes = method.GetCustomAttributes(
typeof(ConnectConsumerInterfaceAttribute), false);
foreach (ConnectConsumerInterfaceAttribute attribute in attributes)
{
if (typeof(TInterfaceType) == attribute.InterfaceType)
{
method.Invoke(consumerWebPart, new object[] { graph });
break;
}
}
}
}
}
}
I then developed two web parts called InputWebPart
(provider) and DisplayWebPart
(consumer).
public class InputWebPart : ProviderWebPart
{
private TextBox _txtInputText;
private ITextData _textData;
public InputWebPart()
{
}
protected ITextData TextData
{
get
{
if (_textData == null)
{
_textData = new TextDataProvider();
}
return _textData;
}
set
{
_textData = value;
}
}
protected override void CreateChildControls()
{
base.CreateChildControls();
_txtInputText = new TextBox();
this.Controls.Add(_txtInputText);
Button btnSend = new Button();
btnSend.Text = "Send";
btnSend.Click += new EventHandler(btnSend_Click);
this.Controls.Add(btnSend);
}
private void btnSend_Click(object sender, EventArgs e)
{
this.TextData.Text = _txtInputText.Text;
this.InvokeConnectedConsumer<ITextData>(this.TextData);
}
[ConnectionProvider("Text Data")]
public ITextData SetTextData()
{
return _textData;
}
}
public class DisplayWebPart : System.Web.UI.WebControls.WebParts.WebPart
{
private ITextData _textData;
public DisplayWebPart()
{
}
protected ITextData TextData
{
get
{
return _textData;
}
set
{
_textData = value;
}
}
protected override void CreateChildControls()
{
base.CreateChildControls();
if (this.TextData != null)
{
Label lblDisplayText = new Label();
lblDisplayText.Text = this.TextData.Text;
this.Controls.Add(lblDisplayText);
}
}
[ConnectionConsumer("Text Data Consumer", AllowsMultipleConnections = true)]
[ConnectConsumerInterface(typeof(ITextData))]
public void GetTextData(ITextData textData)
{
this.TextData = textData;
this.EnsureChildControls();
}
}
That is the bulk of the code in the sample. Once you load the sample into VS 2008, it will make more sense. To deploy the solution into WSS 3.0, right click the solution file in the VS Solution Explorer and click the Deploy option in the context menu.
I hope this will make your lives a lot easier when developing web parts in the future.