1. Problem
You have two or more separate
Silverlight applications composing parts of your overall web page, and
you need these applications to exchange data with each other.
2. Solution
Use the local
connection feature in Silverlight 3 to enable communication channels
between these applications and facilitate cross-application data
exchange.
3. How It Works
The local-connection
feature in Silverlight 3 enables you to establish communication channels
between two or more Silverlight applications on the same web page.
3.1. Receiver Registration
In this mode of
communication, an application can act as a sender, a receiver, or both.
To register itself with the communication system as a receiver, the
application has to provide a unique identity, using which messages are
directed to it. This identity is a combination of a receiver name
(expressed using a string literal) and the application's web domain
name, and needs to yield a unique identifier within the scope of the
containing page.
To register itself as a receiver, the application can create an instance of the LocalMessageReceiver class in System.Windows.Messaging, passing in the receiver name as shown here:
LocalMessageReceiver ThisReceiver = new LocalMessageReceiver("ThisReceiverName");
Using this version of the
constructor registers the receiver name as unique in its originating
domain—other receivers in the page can have the same receiver name as
long as they belong to different domains. Registering in this fashion
also allows the receiver to receive messages only from those senders on
the page that originate from the same domain as the receiver.
The local-connection API offers granular control over the message receiving heuristics. An overloaded constructor for the LocalMessageReceiver class is made available with the following signature:
public LocalMessageReceiver(string receiverName,
ReceiverNameScope nameScope, IEnumerable<string> allowedSenderDomains);
The ReceiverNameScope enumeration used in the second parameter has two possible values. ReceiverNameScope.Domain
has the same effect as the previous constructor, requiring that the
receiver name be unique within all receivers on the page originating
from the same domain. However, ReceiverNameScope.Global requires that the receiver name be unique across all receivers on the page, regardless of their originating domain name.
The third parameter, allowedSenderDomains,
enables extending the list of sender domains from which the receiver
can receive messages beyond the receiver's originating domain. Setting
it to LocalMessageReceiver.AnyDomain
allows the receiver to receive messages from any sender on the page,
regardless of the sender's originating domain. You can also set allowedSenderDomains
to a selective list of the domains from which you want to allow message
receipt. The following code shows a receiver being registered as unique
across all receiver domains on the page, with the abilty to receive
messages from senders in two specific domains (http://www.microsoft.com and http://www.silverlight.net):
LocalMessageReceiver ThisReceiver =
new LocalMessageReceiver("ThisReceiverName",
ReceiverNameScope.Global, new List<string>{ "http://www.microsoft.com",
"http://www.silverlight.net"});
3.2. Receiving Messages
When a receiver has been registered, you need to attach a handler to the LocalMessageReceiver.MessageReceived event to receive messages and then call the LocalMessageReceiver.Listen() method to start listening for incoming messages asynchronously. Here is an example:
ThisReceiver.MessageReceived +=
new EventHandler<MessageReceivedEventArgs>((s, e) =>
{
string Msg = e.Message;
//do something with the received message
...
//optionally send a response message
string ResponseMessage = PrepareResponseMessage();
e.Response = ResponseMessage;
});
ThisReceiver.Listen();
The MessageReceivedEventArgs.Message
property contains the string message that was sent. When your code has
processed the message, you can also send a response message back to the
sender in the MessageReceivedEventArgs.Response
property. The response message follows the same rules as any other
local connection message: it must be a string that is less than 1 MB in
size. We talk more about the Response property in a bit.
3.3. Sending Messages
A sender application has no explicit registration process. To send messages to a receiver, you must construct an instance of System.Windows.Messaging.LocalMessageSender as shown here, passing in the receiver name and the receiver domain as parameters:
LocalMessageSender ThisSender =
new LocalMessageSender("SomeReceiver","http://localhost");
You can also pass the value LocalMessageSender.Global
as the second parameter. In that case, the system attempts to deliver
the message to all receivers with the specified name on the page,
regardless of what domain they belong to.
Local-connection messages are always sent asynchronously using the LocalMessageSender.SendAsync() method, as show here:
string MyMessage;
//create a message here
ThisSender.SendAsync(MyMessage);
As you can see, the message being sent is of type String.
In the current version of Silverlight, only string messages less than 1
MB can be sent and received using the local-connection system. This may
seem limiting initially. But consider that you can express any
Silverlight data structure in either JSON or XML strings using the
Silverlight-supplied serialization mechanisms like data-contract
serialization or LINQ to XML XDocument serialization. With that in mind,
this approach allows you to build fairly effective and rich
data-exchange scenarios.
After the message has been sent, or an attempt to do so fails, the LocalMessageSender.SendCompleted event is raised by the runtime. You need to handle the event to do any error handling or response processing, as shown here:
ThisSender.SendCompleted +=
new EventHandler<SendCompletedEventArgs>((s, e) =>
{
if (e.Error != null)
{
//we had an error sending the message - do some error reporting here
}
else if (e.Response != null)
{
//the receiver sent a response - process it here
}
});
Because the send operation
is asynchronous and returns immediately, the local-connection system
does not raise a direct exception to the sender if a send operation is
unsuccessful. Consequently, in the SendCompleted event handler, you should check the SendCompletedEventArgs.Error property of type Exception
for any exception that may be raised in the event of an unsuccessful
send attempt. In case of a send-related error, this may be set to an
instance of System.Windows.Messaging.SendFailedException.
If the send was successful, the SendCompletedEventArgs.Response may contain a response message, depending on whether the receiver sent a response back.
3.4. Request-Response
The Response property is interesting in that it lets you establish a rudimentary request-response correlation using the local connection.
There are no limitations on an
application being both a sender and a receiver at the same time. For an
application to be both a sender and a receiver, you must perform the
appropriate receiver registration and then create both a LocalMessageSender and a LocalMessageReceiver
instance, as shown in the previous sections. One way to send responses
from a receiver back to a sender would be a role-reversal strategy,
where the receiver acts as a sender and the sender acts as a receiver
for the response message path. However, because the order of message
delivery is not guaranteed in the current implementation, this puts the
onus on you to include additional details in the message body, should
you need to correlate a sent message with its response.
The Response properties on the MessageReceivedEventArgs and MessageSentEventArgs types let you circumvent that. MessageReceivedEventArgs also contains a Message property and a SenderDomain property, which let the receiver application accurately pair the right response with the incoming message. MessageSentEventArgs also contains Message and Response properties, in addition to information about the receiver that sent the response through the ReceiverDomain and ReceiverName properties. This allows the sender to accurately pair a receiver response with a specific sent message.
4. The Code
The
application allows you to change the spending in each category to
different values and watch the graph change accordingly. It also lets
you drag any bar in the graph using your mouse and watch the
corresponding value change in the DataGrid, maintaining the same total.
To adapt that sample to this
recipe, you break it into two separate applications. The application
named 7.7 HomeExpenseWorksheet encapsulates the DataGrid-based
worksheet portion of the sample, whereas the 7.7 HomeExpenseGraph
application encapsulates the bar-graph implementation. You then use
local-connection-based messaging between the two applications to
implement the necessary communication.
Figure 1 shows the applications hosted on the same page.
Before we discuss the
local-connection-related code changes, let's quickly look at the XAML
for the expense worksheet application, shown in Listing 1.
Listing 1. XAML for the HomeExpenseWorksheet application in MainPage.xaml
<UserControl x:Class="Recipe7_7.HomeExpenseWorksheet.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:data=
"clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"
Width="300"
Height="600">
<Grid x:Name="LayoutRoot"
Background="White">
<Grid.RowDefinitions>
<RowDefinition Height="0.8*" />
<RowDefinition Height="0.2*" />
</Grid.RowDefinitions>
<data:DataGrid HorizontalAlignment="Stretch"
Margin="8,8,8,8"
VerticalAlignment="Stretch"
HeadersVisibility="All"
Grid.Row="0"
x:Name="dgSpending"
AutoGenerateColumns="False"
CellEditEnded="dgSpending_CellEditEnded">
<data:DataGrid.Columns>
<data:DataGridTextColumn Header="Item"
Binding="{Binding Item,Mode=TwoWay}" />
<data:DataGridTextColumn Header="Value"
Width="100"
Binding="{Binding Amount,Mode=TwoWay}" />
</data:DataGrid.Columns>
</data:DataGrid>
<StackPanel Orientation="Horizontal"
Grid.Row="1"
HorizontalAlignment="Right">
<Button x:Name="btnAddItem"
Margin="3,3,3,3"
Height="30"
Width="85"
Content="Add Item"
Click="btnAddItem_Click" />
<Button x:Name="btnRemoveItem"
Margin="3,3,3,3"
Height="30"
Width="85"
Content="Remove Item"
Click="btnRemoveItem_Click" />
</StackPanel>
</Grid>
</UserControl>
|
The code in Listing 1 shows the only notable changes made to the XAML. As you can see, you add two buttons: clicking btnAddItem adds a new row to the DataGrid, and clicking btnRemoveItem removes the currently selected item from the DataGrid. You also attach a handler to the CellEditEnded event of the DataGrid. We cover the details of the implementations of these handlers later in this section.
To start with the
local-connection implementation, recall that messages are string based.
However, strings are cumbersome to work with, so you define the
application messages as a custom CLR type named Message and then resort to serialization to convert Message instances to string representations before sending them. Listing 2 shows the Message type.
Listing 2. The Message custom type in Messages.cs
using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization;
using System.Text;
namespace Recipe7_7.SD
{
public enum MessageType
{
ItemRemoved,
ItemsValueChanged
}
[DataContract]
public class Message
{
[DataMember]
public MessageType MsgType { get; set; }
[DataMember]
public List<Spending> Items { get; set; }
public static string Serialize(Message Msg)
{
DataContractSerializer dcSer = new DataContractSerializer(typeof(Message));
MemoryStream ms = new MemoryStream();
dcSer.WriteObject(ms, Msg);
ms.Flush();
string RetVal = Encoding.UTF8.GetString(ms.GetBuffer(), 0, (int)ms.Length);
ms.Close();
return RetVal;
}
public static Message Deserialize(string Msg)
{
DataContractSerializer dcSer = new DataContractSerializer(typeof(Message));
MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(Msg));
Message RetVal = dcSer.ReadObject(ms) as Message;
ms.Close();
return RetVal;
}
}
}
|
You handle two kinds
of messages in the local connection implementation between the worksheet
and the graph applications, as defined in the MessageType enumeration. The MessageType.ItemRemoved
value indicates a message that communicates the removal of one or more
items; it is sent from the worksheet to the graph only when rows are
removed from the worksheet. The MessageType.ItemsValueChanged
typed message can be sent in either direction when the values of one or
more items change—either in the worksheet for an existing item or a
newly added item through user edits, or in the graph when the user drags
a bar to resize it.
The Message class contains the MessageType and a list of Items with changed values or a list of Items that were removed. It also defines two static methods that use DataContractSerialization to serialize and deserialize instances of the Message type to and from a string representation. Note that you have the Message class attributed as a DataContract with the Mistyped and Items properties attributed as DataMember.
An individual data item for the application is defined as a class named Spending, and a custom class named SpendingCollection deriving from ObservableCollection<Spending> defines the data collection that initially populates the worksheet and the graph. Listing 3 shows these classes.
Listing 3. Data classes in DataClasses.cs
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Runtime.Serialization;
namespace Recipe7_7.SD
{
public class SpendingCollection : ObservableCollection<Spending>
{
public SpendingCollection()
{
this.Add(new Spending
{
ParentCollection = this,
ID = 1,
Item = "Utilities",
Amount = 300
});
this.Add(new Spending
{
ParentCollection = this,
ID = 2,
Item = "Food",
Amount = 350
});
this.Add(new Spending
{
ParentCollection = this,
ID = 3,
Item = "Clothing",
Amount = 200
});
this.Add(new Spending
{
ParentCollection = this,
ID = 4,
Item = "Transportation",
Amount = 75
});
this.Add(new Spending
{
ParentCollection = this,
ID = 5,
Item = "Mortgage",
Amount = 3000
});
this.Add(new Spending
{
ParentCollection = this,
ID = 6,
Item = "Education",
Amount = 500
});
this.Add(new Spending
{
ParentCollection = this,
ID = 7,
Item = "Entertainment",
Amount = 125
});
this.Add(new Spending
{
ParentCollection = this,
ID = 8,
Item = "Loans",
Amount = 750
});
this.Add(new Spending
{
ParentCollection = this,
ID = 9,
Item = "Medical",
Amount = 80
});
this.Add(new Spending
{
ParentCollection = this,
ID = 10,
Item = "Miscellaneous",
Amount = 175
});
}
public double Total
{
get
{
return this.Sum(spending => spending.Amount);
}
}
}
[DataContract]
public class Spending : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
internal void RaisePropertyChanged(PropertyChangedEventArgs e)
{
if (PropertyChanged != null)
{
PropertyChanged(this, e);
}
}
public override int GetHashCode()
{
return ID.GetHashCode();
}
public override bool Equals(object obj)
{
return (obj is Spending) ? this.ID.Equals((obj as Spending).ID) : false;
}
SpendingCollection _ParentCollection = null;
public SpendingCollection ParentCollection
{
get { return _ParentCollection; }
set
{
_ParentCollection = value;
if (ParentCollection != null)
{
foreach (Spending sp in ParentCollection)
sp.RaisePropertyChanged(new PropertyChangedEventArgs("Amount"));
}
}
}
private int _ID = default(int);
[DataMember]
public int ID
{
get
{
return _ID;
}
set
{
if (value != _ID)
{
_ID = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("ID"));
}
}
}
private string _Item;
[DataMember]
public string Item
{
get { return _Item; }
set
{
string OldVal = _Item;
if (OldVal != value)
{
_Item = value;
RaisePropertyChanged(new PropertyChangedEventArgs("Item"));
}
}
}
private double _Amount;
[DataMember]
public double Amount
{
get { return _Amount; }
set
{
double OldVal = _Amount;
if (OldVal != value)
{
_Amount = value;
if (ParentCollection != null)
{
foreach (Spending sp in ParentCollection)
sp.RaisePropertyChanged(new PropertyChangedEventArgs("Amount"));
}
}
}
}
}
}
|
The only changes worth noting are the addition of an ID property to the Spending class to uniquely identify it in a collection, and the overrides for GetHashCode() and Equals() to facilitate locating or comparing spending instances based on their IDs. The changes are noted in bold in Listing 3.
Now, let's look at the application code. Listing 4 lists the codebehind for the worksheet application.
Listing 4. The MainPage codebehind in MainPage.xaml.cs for the HomeExpenseWorksheet application
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Messaging;
using Recipe7_7.SD;
namespace Recipe7_7.HomeExpenseWorksheet
{
public partial class MainPage : UserControl
{
//data source
SpendingCollection SpendingList = new SpendingCollection();
//create a sender
LocalMessageSender WorksheetSender =
new LocalMessageSender("SpendingGraph",
LocalMessageSender.Global);
//create a receiver
LocalMessageReceiver WorksheetReceiver =
new LocalMessageReceiver("SpendingWorksheet",
ReceiverNameScope.Global, LocalMessageReceiver.AnyDomain);
public MainPage()
{
InitializeComponent();
//bind data
dgSpending.ItemsSource = SpendingList;
//handle message receipt
WorksheetReceiver.MessageReceived+=
new EventHandler<MessageReceivedEventArgs>((s,e) =>
{
//deserialize message
Message Msg = Message.Deserialize(e.Message);
//if item value changed
if (Msg.MsgType == MessageType.ItemsValueChanged)
{
//for each item for which value has changed
foreach (Spending sp in Msg.Items)
{
//find the corrsponding item in the data source and replace value
SpendingList[SpendingList.IndexOf(sp)] = sp;
}
}
});
//handle send completion
WorksheetSender.SendCompleted +=
new EventHandler<SendCompletedEventArgs>((s, e) =>
{
//if error
if (e.Error != null)
{
//we had an error sending the message - do some error reporting here
}
//if there was a response
else if (e.Response != null)
{
//the receiver sent a response - process it here
}
});
//start listening for incoming messages
WorksheetReceiver.Listen();
}
//handle add row button click
private void btnAddItem_Click(object sender, RoutedEventArgs e)
{
//add a new Spending instance to the data source
SpendingList.Add(new Spending() { ParentCollection = SpendingList });
}
//handle a cell edit
private void dgSpending_CellEditEnded(object sender,
DataGridCellEditEndedEventArgs e)
{
//send a message
WorksheetSender.SendAsync(Message.Serialize(
new Message()
{
//message type - Item value changed
MsgType = MessageType.ItemsValueChanged,
//the changed Spending instance
Items = new List<Spending> { e.Row.DataContext as Spending }
}));
}
//remove the selected item
private void btnRemoveItem_Click(object sender, RoutedEventArgs e)
{
//if there is a selected row
if (dgSpending.SelectedItem != null)
{
//get the corresponding Spending instance
Spending target = dgSpending.SelectedItem as Spending;
//remove it from the data source
SpendingList.Remove(target);
//send a message
WorksheetSender.SendAsync(Message.Serialize(
new Message()
{
//message type - Item Removed
MsgType = MessageType.ItemRemoved,
//the item that was removed
Items = new List<Spending> { target }
}));
}
}
}
}
|
As you can see, you start by creating a LocalMessageSender and a LocalMessageReceiver instance, respectively, named WorksheetSender and WorksheetReceiver, as members of the codebehind class. WorksheetSender is created to let you send messages to a receiver named SpendingGraph, which is globally unique across all receivers on the page. WorksheetReceiver registers this application as a receiver named SpendingWorksheet, again with a global namescope, and prepares to receive incoming messages from senders in any domain.
During construction, you attach handlers to WorksheetReceiver.MessageReceived and WorksheetSender.SendCompleted. In the MessageReceived handler, you deserialize the incoming message and then process it. You only handle messages of type MessageType.ItemsValueChanged,
because these are the only types of messages the HomeExpenseGraph
application can generate. As a part of the processing, if you do receive
Spending instances that have changed, you replace them accordingly in the expense worksheet datasource. In the SendCompleted
handler, you show a skeletal set of statements for handling error
conditions and response messages—we leave it as an exercise for you to
implement error handling and response correlation as needed.
In the Click event handler for the Button named btnAddItem, you add a new Spending item to the datasource. However, you do not immediately send a message to the HomeExpenseGraph application, because the Spending item still does not have any meaningful data. Instead, you use the CellEditEnded event handler to send item-change notifications. In that handler, you construct a new Message instance with the changed Spending item as the only item in the Message.Items collection, and you set the MsgType property to MessageType.ItemsValueChanged. You then serialize the message and send it through the WorksheetSender.SendAsync() method.
In the Click handler for btnRemoveItem, you first remove the Spending instance bound to the selected DataGrid row from the datasource collection. Then, you use the same approach to serialize and send a Message instance, with the MsgType property set to MessageType.ItemRemoved.
Let's look at the HomeExpenseGraph application. Listing 5 shows the codebehind for the MainPage in that application.
Listing 5. The MainPage aodebehind in MainPage.xaml.cs for the HomeExpenseGraph application
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Messaging;
using System.Windows.Shapes;
using Recipe7_7.SD;
namespace Recipe7_7.HomeExpenseGraph
{
public partial class MainPage : UserControl
{
//variables to enable mouse interaction
private bool MouseLeftBtnDown = false;
private bool Dragging = false;
Point PreviousPos;
//data source
SpendingCollection SpendingList = null;
//create a sender
LocalMessageSender GraphSender =
new LocalMessageSender("SpendingWorksheet",
LocalMessageSender.Global);
//create a receiver
LocalMessageReceiver GraphReceiver =
new LocalMessageReceiver("SpendingGraph",
ReceiverNameScope.Global, LocalMessageReceiver.AnyDomain);
public MainPage()
{
InitializeComponent();
SpendingList = this.Resources["REF_SpendingList"] as SpendingCollection;
//handle property changed for each Spending - this is used to send item
//value changed messages
foreach (Spending sp in SpendingList)
{
sp.PropertyChanged +=
new System.ComponentModel.
PropertyChangedEventHandler(Spending_PropertyChanged);
}
//handle message receipts
GraphReceiver.MessageReceived +=
new EventHandler<MessageReceivedEventArgs>((s, e) =>
{
//deserialize message
Message Msg = Message.Deserialize(e.Message);
//if value changed
if (Msg.MsgType == MessageType.ItemsValueChanged)
{
//for each changed Spending instance
foreach (Spending sp in Msg.Items)
{
//if it exists
if (SpendingList.Contains(sp))
{
//replace it with the changed one
SpendingList[SpendingList.IndexOf(sp)] = sp;
}
else
{
//add the new one
SpendingList.Add(sp);
}
//handle property changed
sp.PropertyChanged +=
new System.ComponentModel.
PropertyChangedEventHandler(Spending_PropertyChanged);
//force a recalc of the bars in the graph
sp.ParentCollection = SpendingList;
}
}
//item removed
else if (Msg.MsgType == MessageType.ItemRemoved)
{
foreach (Spending sp in Msg.Items)
{
//unhook the event handler
SpendingList[SpendingList.IndexOf(sp)].PropertyChanged
-= Spending_PropertyChanged;
//remove from data source
SpendingList.Remove(sp);
}
//force a recalc of the bars in the graph
if (SpendingList.Count > 0)
SpendingList[0].ParentCollection = SpendingList;
}
});
//start listening for incoming messages
GraphReceiver.Listen();
}
void Spending_PropertyChanged(object sender,
System.ComponentModel.PropertyChangedEventArgs e)
{
//send a message
GraphSender.SendAsync(
Message.Serialize(
new Message
{
//changed item
Items = new List<Spending> { sender as Spending },
//message type - item value changed
MsgType = MessageType.ItemsValueChanged
}));
}
private void Rectangle_MouseMove(object sender, MouseEventArgs e)
{
if (MouseLeftBtnDown)
{
Rectangle rect = (Rectangle)sender;
if (Dragging == false)
{
Dragging = true;
rect.CaptureMouse();
}
Point CurrentPos = e.GetPosition(sender as Rectangle);
double Moved = CurrentPos.X - PreviousPos.X;
if (rect.Width + Moved >= 0)
{
rect.Width += Moved;
}
PreviousPos = CurrentPos;
}
}
private void Rectangle_MouseLeftButtonDown(object sender,
MouseButtonEventArgs e)
{
MouseLeftBtnDown = true;
PreviousPos = e.GetPosition(sender as Rectangle);
}
private void Rectangle_MouseLeftButtonUp(object sender,
MouseButtonEventArgs e)
{
Rectangle rect = (Rectangle)sender;
if (Dragging)
{
Dragging = false;
rect.ReleaseMouseCapture();
}
MouseLeftBtnDown = false;
}
}
}
|
As before, this application needs to both send and receive messages. As shown in Listing 5, you create instances of LocalMessageSender and LocalMessageReceiver such that this application can receive messages from the worksheet application and send messages to it as well.
In the constructor, after the datasource is bound, you handle the PropertyChanged event for each item in the collection. The PropertyChanged event is raised whenever the user drags a bar within the graph; if you look at the handler for the PropertyChanged event, note that you send a message to the other application indicating thus action.
You also handle the MessageReceived event as before. In the handler, you handle messages of both types—where item values are changed and where items are removed.
If an item value changes, you
check to see if the item that changed already exists or was newly
created in the worksheet application and does not exist in the
datasource for this application. If it is an existing item, you replace
it with the changed item; if it is a new item, you add it to the
collection. You also attach a handler to the Spending item so that you can track changes to it in this application. Finally, you set the Spending.ParentCollection property to the datasource to which it was added to in which it was replaced. If you look at the definition of the Spending type in Listing 5,
you see that this forces a property-change notification for all the
items in the datasource. The bar graph displays the spending as
percentages of the total, and this causes the bar graph's bar widths to
be recalculated based on the new values.
If an item is removed, you first unattach the PropertyChanged
event handler from the item that was removed and then remove it from
the datasource collection. When the removals are complete, you force a
similar recalculation of the bar widths based on the new percentages.