Connecting Web Parts
A cool feature of SharePoint web parts is the ability to connect them together. This allows business users to compose their own mash-ups and dashboards of related information. For example, a master/detail display could allow users to select an item in one web part and then see details and related information in other web parts on the page. The only thing is that SharePoint's web part connections run on the server, so a page refresh is required to update the connected web parts.
In this section, you will learn to build Silverlight web parts that can be connected, but since Silverlight runs on the client, the update will be immediate with no need for a page refresh. The strategy to do this is to use a SharePoint server-side web part connection to broker a direct Silverlight connection on the web page. Figure 5.15 shows the web parts in the ConnectedWebParts sample in the code download. When the web parts are connected, anything that's typed into the source web part also appears in one or more connected target web parts.
Figure 5.15 Connected web parts
You can try this on your development machine if you place the two web parts on the page. Edit either web part; then pull down the same drop-down next to the web part title you used to edit the web part. This time, a Connections choice appears to let you connect the web parts as shown in Figure 5.16.
Figure 5.16 Connecting SharePoint web parts
Fortunately, SharePoint allows developers to create any kind of connection they like. In this case the connection is called ISilverlightConnection, and it defines a simple registration method for web parts that wish to connect. Listing 5.10 shows the interface.
Listing 5.10. The ISilverlightConnection Interface
public interface ISilverlightConnection { void RegisterReceiver(string receiverName); }
The ConnectionSource Web Part implements the ISilverlightConnection, and ConnectionTarget consumes it. The strategy is for each ConnectionTarget to register a unique receiver name by calling the RegisterReceiver() method in the source. Both web parts then pass the receiver name to their corresponding Silverlight applications, which can then use Silverlight's messaging API to send messages. The ConnectionSource web part is capable of handling several receiver names if multiple target web parts are connected; go ahead and try this if you like. This is shown in Figure 5.17.
Figure 5.17 Brokering Silverlight communication with a server-side connection
Using Silverlight in Composite Controls
The sad truth is that sandboxed solutions don't allow web part connections, and the Silverlight SharePoint Web Parts used earlier in this chapter use a sandboxed solution. To handle this, the web parts are written from scratch. A Visual Web Part would work, but this is a good opportunity to show you how to use Silverlight in composite controls, as explained in Chapter 2, "Introduction to SharePoint Development." These concepts are used in other web parts later in the book as well as in editor parts, where a visual solution is not available. It's also used in a navigation control in Chapter 13, "Creating Silverlight Navigation," and a field control in Chapter 15, "Creating a Silverlight Field Control," where, again, a composite control is the only option.
Beginning with a farm solution, each web part was added as a simple, nonvisual web part. As you recall from Chapter 2, instead of using a design surface containing ASP.NET controls and HTML, child controls are added in code by overriding a method called CreateChildControls().
To facilitate placing Silverlight on the page, a new SilverlightPlugin web control has been provided in the code download. It contains the same Javascript error handler and <object> tag as the standard Silverlight test page, which you might have noticed in the Custom Silverlight Visual Web Part. This time they're in string constants that contain tokens such as {0} and {1} that hold values for the source, InitParams, and other properties. CreateChildControls() fills in the tokens and adds both the Javascript and <object> tag to the page, as shown in Listing 5.11.
Listing 5.11. CreateChildControls() in the SilverlightPlugin Control
private const string SILVERLIGHT_EXCEPTION_SCRIPT_BLOCK = @" <script type=""text/javascript""> function {0}Error (sender, args) {{ // Boilerplate error handler goes here, same as in any Silverlight // web page. The full code is in the code download. </script>"; private const string SILVERLIGHT_OBJECT_TAG = @" <div style=""overflow-x: hidden; position:relative; width:{0}; height:{1}""> <object data=""data:application/x-silverlight-2,"" type=""application/x-silverlight-2"" width=""{0}"" height=""{1}""> <param name=""source"" value=""{2}""/> <param name=""onError"" value=""{3}Error"" /> <param name=""background"" value=""white"" /> <param name=""minRuntimeVersion"" value=""4.0.50401.0"" /> <param name=""initparams"" value=""{4}"" /> <param name=""autoUpgrade"" value=""true"" /> <!-- Rendering for browsers without Silverlight follows --> <!-- The code download contains the full code for this → </object> <iframe id=""_sl_historyFrame"" style=""visibility:hidden;height:0px;width:0px;border:0px""> </iframe> </div>"; protected override void CreateChildControls() { base.CreateChildControls(); if (Source != null && Source != "") { // Ensure we have set the height and width string width = (this.Width == Unit.Empty) ? "100%" : this.Width.ToString(); string height = (this.Height == Unit.Empty) ? "100%" : this.Height.ToString(); // Render error handling script this.Controls.Add(new LiteralControl( String.Format(SILVERLIGHT_EXCEPTION_SCRIPT_BLOCK, this.ClientID))); this.Controls.Add(new LiteralControl( String.Format(SILVERLIGHT_OBJECT_TAG, width, height, this.Source, this.ClientID, this.InitParameters))); } }
It's important to ensure the Height and Width properties are set on the Silverlight <object> tag, as they both default to zero. Leaving them out will result in a 0x0 pixel Silverlight application that won't show on the page at all.
It would be typical to add Javascript to the page by calling Page.RegisterClientScriptBlock(), but this would preclude using the SilverlightPlugin control in sandboxed solutions in the future because the sandbox does not allow access to the Page object. Instead, the web part's clientID property, which is guaranteed to be unique on the page, is used to make the error handler's method name unique, and the script is generated inline, as in the Silverlight Custom Visual Web Part.
The SilverlightPlugin control shows up in other solutions later in this book in standard (nonvisual) web parts as well as editor parts and navigation and field controls. It makes writing composite controls with Silverlight easy and encapsulates the details about placing Silverlight on the page.
Making the Connection
Listing 5.10 shows the ISilverlightConnection interface used to connect the web parts in this example. The provider (ConnectionSource) web part implements the interface, and the consumer (ConnectionTarget) web part makes use of the interface. In SharePoint the connection provider always implements the interface; in this case, the consumer calls the provider's RegisterReceiver() method, but event handlers are often used to allow information to flow from provider to consumer.
Listing 5.12 shows the ConnectionSource web part. The [ConnectionProvider] attribute tells SharePoint that the connection is available, and the ConnectionInterface() method hands SharePoint an object that implements the ISilverlightConnection interface. Because this web part only supports one kind of connection, the easiest approach is for the web part itself to implement the interface and pass itself back in this method. If you ever want to implement more than one kind of connection provider in a single web part, you'll find yourself having to implement a separate class for each interface and manage them in your web part.
Listing 5.12. ConnectionSource Web Part Implements a Connection Provider
public class ConnectionSource : WebPart, ISilverlightConnection { // Register with SharePoint as a connection provider // The provider name will appear in the connection message, as // in, "Send Keystrokes To" [ConnectionProvider("Keystrokes")] public ISilverlightConnection ConnectionInterface() { return this; } // ISilverlightConnection members void ISilverlightConnection.RegisterReceiver(string receiverName) { EnsureChildControls(); if (silverlightPlugin.InitParameters == null || silverlightPlugin.InitParameters == "") { silverlightPlugin.InitParameters = "SendOn=" + receiverName; } else { silverlightPlugin.InitParameters += "" + receiverName; } } private SilverlightPlugin silverlightPlugin; protected override void CreateChildControls() { base.CreateChildControls(); silverlightPlugin = new SilverlightPlugin(); silverlightPlugin.Source = SPContext.Current.Site.Url + "/ClientBin/SLConnectionSource.xap"; this.Controls.Add(silverlightPlugin); } }
The RegisterReceiver() method begins by calling EnsureChildControls(), which is a method in all ASP.NET controls that checks to see if CreateChildControls() has been called and calls it if it wasn't. That way, the code that follows can be sure that the SilverlightPlugin control has been created.
The code passes the receiver name to its Silverlight application using its InitParam property. This is standard operating procedure in Silverlight: If you want to pass one or more values to Silverlight, place them in the InitParam property in the format name1=value1, name2=value2 and the Silverlight application is presented with a dictionary object containing the name-value pairs in its application startup event. In later chapters you learn how to pass more complex data in a hidden form field on the web page and to pass a reference to the form field in InitParam; for now the receiver name(s) can go in directly. The code uses the convention of a semicolon to separate receiver names, so as more target web parts register themselves the receiver names are simply appended to the InitParam value.
Listing 5.13 shows the ConnectionTarget web part, which registers as a connection consumer. Instead of implementing a method decorated with the [ConnectionProvider] attribute, this web part includes a [ConnectionConsumer] attributed method. As you can see, it uses its own client ID, which is sure to be unique and HTML-safe, as the receiver name, and it registers with the provider and also passes the same ID to its Silverlight application.
listing 5.13. ConnectionTarget Web Part Implements a Connection Consumer
public class ConnectionTarget : WebPart { // Register with SharePoint as a connection consumer // The consumer name will appear in the connection message, as // in, "Get Keystrokes From" [ConnectionConsumer("Keystrokes")] public void GetConnectionInterface (ISilverlightConnection providerPart) { providerPart.RegisterReceiver(this.ClientID); EnsureChildControls(); silverlightPlugin.InitParameters = "ReceiveOn=" + this.ClientID; } SilverlightPlugin silverlightPlugin; protected override void CreateChildControls() { base.CreateChildControls(); silverlightPlugin = new SilverlightPlugin(); silverlightPlugin.Source = SPContext.Current.Site.Url + "/ClientBin/SLConnectionTarget.xap"; this.Controls.Add(silverlightPlugin); } }
Now both the source and target Silverlight applications have the receiver name, so they can communicate directly on the client. Listing 5.14 shows the Application_Startup event in the SLConnectionTarget application; as you can see it simply retrieves the list of receiver names from InitParams and passes them to the main page by setting a public property.
listing 5.14. The Application_Startup Event Passes InitParams to Main Page
private void Application_Startup(object sender, StartupEventArgs e) { MainPage page = new MainPage(); this.RootVisual = page; if (e.InitParams.ContainsKey("SendOn")) { page.SendOnConnectionNames = e.InitParams["SendOn"]; } }
The main page is extremely simple. It consists of a textbox, whose KeyUp event is hooked as shown in Listing 5.15. Each time the event fires, the content of the text box is sent to all receivers.
Listing 5.15. SLConnectionSource Sends Information on the KeyUp Event
internal string SendOnConnectionNames { get; set; } private void messageTextBox_KeyUp(object sender, KeyEventArgs e) { foreach (string receiverName in SendOnConnectionNames.Split(';')) { LocalMessageSender msgSender = new LocalMessageSender(receiverName); msgSender.SendAsync(messageTextBox.Text); } }
The SLConnectionTarget Silverlight application's job is to listen on its receiver and display messages sent to it in a text box. It uses the same Application_Startup code to pass in the receiver name, but instead of sending the main page, it receives as shown in Listing 5.16.
Listing 5.16. SLConnectionTarget Receives and Displays Text
LocalMessageReceiver msgReceiver; internal void SetupReceiver(string receiverName) { msgReceiver = new LocalMessageReceiver(receiverName); msgReceiver.MessageReceived += (s, e) => { Dispatcher.BeginInvoke(() => { this.messageTextBox.Text = e.Message; }); }; msgReceiver.Listen(); this.StatusTextBlock.Text = "This web part is connected."; }
The MessageReceived event handler is called whenever a new message is received and is implemented as an anonymous function as discussed in Chapter 3. In Silverlight, the user interface always needs to be updated on the UI thread, so a second anonymous function is passed Dispatcher.BeginInvoke(), which runs it on the UI thread. Anonymous functions are a big help with all the asynchronous activity in a Silverlight application.
The last thing to do is to listen on the event, by calling the Listen() method. Now any time a user types into the ConnectionSource web part, all connected ConnectionTargets are updated immediately with every key stroke.