One of the nifty new features available in Silvering 3 is the Local Connection API. This makes it extremely easy to communicate between two different Silverlight applications. Granted, you could communicate between Silverlight instances before, but you had to use Javascript as a go-between. Now, it is much, much simpler, and as an added bonus, Silverlight apps can communicate from different browser pages! An app loaded in one browser tab can (with the right permissions) communicate to one on a different tab. Pretty cool, eh?
We aren't going to do anything quite so fancy with the example here today - everything is going to be on a single page. Below you can see a Silverlight app which is a table of employees. This table of employees communicates back and forth with a detail view app, which is farther down on the page. When you select an employee in the table, it populates the detail view. In the detail view, you can edit the employee and save it, or you can "add as new" which will add a new employee to the table.
The table view:
And as a separate Silverlight application, the details view:
To start off, we are going to take a look at the class representing a row in the table. This class is more complicated then you might think for two reasons - first, the messages sent between two Silverlight applications through the Local Conection API have to be strings. This means that to send an object, the object has to be serialized somehow. That by itself isn't too bad, except for the fact that Silverlight 3 is still missing all serialization abilities that are built into the regular .NET framework. This means that we have to hand roll some serialization logic.
using System.IO;
using System.Windows;
using System.Runtime.Serialization.Json;
namespace LocalConnectionShared
{
public class Person : DependencyObject
{
public Person()
{ ID = -1; }
public int ID { get; set; }
public static readonly DependencyProperty FirstNameProperty =
DependencyProperty.Register("FirstName", typeof(string),
typeof(Person), new PropertyMetadata(null));
public string FirstName
{
get { return (string)GetValue(FirstNameProperty); }
set { SetValue(FirstNameProperty, value); }
}
public static readonly DependencyProperty LastNameProperty =
DependencyProperty.Register("LastName", typeof(string),
typeof(Person), new PropertyMetadata(null));
public string LastName
{
get { return (string)GetValue(LastNameProperty); }
set { SetValue(LastNameProperty, value); }
}
public static readonly DependencyProperty EmailProperty =
DependencyProperty.Register("Email", typeof(string),
typeof(Person), new PropertyMetadata(null));
public string Email
{
get { return (string)GetValue(EmailProperty); }
set { SetValue(EmailProperty, value); }
}
public static readonly DependencyProperty AddressProperty =
DependencyProperty.Register("Address", typeof(string),
typeof(Person), new PropertyMetadata(null));
public string Address
{
get { return (string)GetValue(AddressProperty); }
set { SetValue(AddressProperty, value); }
}
public static readonly DependencyProperty TitleProperty =
DependencyProperty.Register("Title", typeof(string),
typeof(Person), new PropertyMetadata(null));
public string Title
{
get { return (string)GetValue(TitleProperty); }
set { SetValue(TitleProperty, value); }
}
public static readonly DependencyProperty IsManagerProperty =
DependencyProperty.Register("IsManager", typeof(bool),
typeof(Person), new PropertyMetadata(false));
public bool IsManager
{
get { return (bool)GetValue(IsManagerProperty); }
set { SetValue(IsManagerProperty, value); }
}
public static readonly DependencyProperty VacationDaysProperty =
DependencyProperty.Register("VacationDays", typeof(int),
typeof(Person), new PropertyMetadata(0));
public int VacationDays
{
get { return (int)GetValue(VacationDaysProperty); }
set { SetValue(VacationDaysProperty, value); }
}
public static string Serialize(Person person)
{
if (person == null)
{ return null; }
var serializer = new DataContractJsonSerializer(typeof(Person));
string result = null;
using (var stream = new MemoryStream())
{
serializer.WriteObject(stream, person);
stream.Position = 0;
using (var reader = new StreamReader(stream))
{ result = reader.ReadToEnd(); }
}
return result;
}
public static Person Deserialize(string str)
{
if (String.IsNullOrEmpty(str))
{ return new Person(); }
var serializer = new DataContractJsonSerializer(typeof(Person));
using (var stream = new MemoryStream())
{
using (var writer = new StreamWriter(stream))
{
writer.Write(str);
writer.Flush();
stream.Position = 0;
return serializer.ReadObject(stream) as Person;
}
}
}
}
}
Wow, that is a big chunk of repetitive code. Well, that is what happens when dependency properties get involved. Since we will want to two way bind to the properties of a Person in XAML, we have to make all the properties dependency properties - and that involves a lot of verbose and repetitive code. What we are really interested in are the two static functions at the end of the class: Serialize and Deserialize.
To make our lives slightly easier, instead of hand rolling serialization from scratch, we are using the built in JSON serialization (which is available in Silverlight and we have dealt with before). Not as nice as the standard .NET serialization techniques, but it gets the job done. The special finagling we have to do here is convert from a stream to a string or vice-versa, since the JSON serializer deals in streams, and we need to deal with strings for passing back and forth across Local Connections.
Ok, now that we have our backing data class and a way to serialize it, let's take a look at the table view. First we have the XAML, which is pretty simple:
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">
<data:DataGrid Grid.Row="6" Grid.ColumnSpan="4"
IsReadOnly="True"
AutoGenerateColumns="False"
SelectionChanged="PersonSelectionChanged"
SelectionMode="Single"
Height="200" x:Name="_PersonGrid">
<data:DataGrid.Columns>
<data:DataGridTextColumn Header="First Name"
Binding="{Binding FirstName}"/>
<data:DataGridTextColumn Header="Last Name"
Binding="{Binding LastName}"/>
<data:DataGridTextColumn Header="Email"
Binding="{Binding Email}"/>
<data:DataGridTextColumn Header="Address"
Binding="{Binding Address}"/>
</data:DataGrid.Columns>
</data:DataGrid>
</UserControl>
This is a pretty straightforward use of the Silverlight datagrid. The datagrid's ItemsSource will be a collection of Person objects, and so we set up the columns here to bind to the properties we want to display in the table.
Now for the backing C# code, and our first look at actually using local connections:
using System.Linq;
using System.Windows.Controls;
using System.Windows.Messaging;
using LocalConnectionShared;
namespace LocalConnectionTable
{
public partial class MainPage : UserControl
{
private LocalMessageReceiver _Receiver;
private LocalMessageSender _Sender;
private int _NextId = 0;
public ObservableCollection<Person> People { get; private set; }
public MainPage()
{
People = new ObservableCollection<Person>();
People.Add(new Person()
{
ID = _NextId++,
FirstName = "Manager",
LastName = "Doe",
Email = "manager.doe@email.com",
Address = "5534 Some Other Street City, State 55555",
Title = "Office Manager",
IsManager = true,
VacationDays = 15
});
People.Add(new Person()
{
ID = _NextId++,
FirstName = "Kevin",
LastName = "Doe",
Email = "kevin.doe@email.com",
Address = "6123 Some Other Street City, State 55555",
Title = "",
IsManager = false,
VacationDays = 10
});
_Sender = new LocalMessageSender("DetailsView");
_Receiver = new LocalMessageReceiver("TableView");
_Receiver.MessageReceived += ReceiverMessageReceived;
_Receiver.Listen();
InitializeComponent();
_PersonGrid.ItemsSource = People;
}
private void ReceiverMessageReceived(object sender,
MessageReceivedEventArgs e)
{
var person = Person.Deserialize(e.Message);
if (person.ID == -1)
{
person.ID = _NextId++;
People.Add(person);
_PersonGrid.SelectedItem = person;
}
else
{
var orig = People.First(a => a.ID == person.ID);
orig.IsManager = person.IsManager;
orig.LastName = person.LastName;
orig.Title = person.Title;
orig.VacationDays = person.VacationDays;
orig.FirstName = person.FirstName;
orig.Email = person.Email;
orig.Address = person.Address;
person = orig;
}
e.Response = Person.Serialize(person);
}
private void PersonSelectionChanged(object sender,
SelectionChangedEventArgs e)
{
_Sender.SendAsync(
Person.Serialize(_PersonGrid.SelectedItem as Person));
}
}
}
The keys here are the LocalMessageReceiver and the LocalMessageSender. As you might guess, the receiver listens for messages, and the sender sends messages. If you want your Silverlight application to receive messages, you need to create a LocalMessageReceiver with some name, and tell the receiver to listen. The name has to be unique within the ReceiverNameScope, which by default is the current domain. There is a constructor overload which allows you to specify the scope, as well as what other domains are allowed to talk to this silverlight app (by default, only the current domain is allowed).
In our case, we let the defaults stand, and set up a receiver with the name "TableView". We also set up a sender that tries to connect to a receiver named "DetailsView" (the name of our receiver in our other silverlight app). You don't have to worry if the other silverlight application has already opened its own listener (this way you don't need to worry about loading order issues) - it does not try and connect to the listener right away, but only matters when the time comes to actually send messages.
To actually receive messages, you also need to attach to the MessageReceived event, which will fire whenever a message comes in. As you can see in our case, the method ReceiverMessageReceived is called when this event fires. When that event does fire, we deserialize a Person instance out of the Message property of the MessageReceivedEventArgs instance. If the instance has an ID of -1, we treat it as a new entry, give it an ID and add it to our collection. Otherwise, we find the instance in our collection that matches the ID, and copy all the new data into the existing instance. Finally, whenever you get a message, there is an opportunity to send a response by setting the Response property - and in this case we set it to the person instance. This way the other side gets notified of the new ID if it was an addition.
The only other code here is a very simple function that is called whenever the selection changes on the datagrid. It sends a message through the sender we created in the constructor containing a serialized version of the currently selected person.
That about covers the table Silverlight app - now it is time for the details app. And first, we have the rather repetitive and uninteresting (except for a few small parts) XAML:
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<UserControl.Resources>
<Style TargetType="TextBox" x:Key="BoxStyle">
<Setter Property="Width" Value="200" />
<Setter Property="Margin" Value="3" />
</Style>
<Style TargetType="TextBlock" x:Key="BlockStyle">
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Margin" Value="3" />
</Style>
<Style TargetType="Button" x:Key="BtnStyle">
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="Margin" Value="4 2 4 5" />
<Setter Property="Width" Value="100" />
</Style>
</UserControl.Resources>
<Grid Background="White"
DataContext="{Binding ElementName=This, Path=CurrentPerson}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Border Grid.ColumnSpan="4" Grid.RowSpan="8"
BorderThickness="1" BorderBrush="Black" />
<TextBlock Grid.Row="0" Grid.Column="1"
Style="{StaticResource BlockStyle}">
First Name
</TextBlock>
<TextBox Grid.Row="0" Grid.Column="2"
Style="{StaticResource BoxStyle}">
<TextBox.Text>
<Binding Mode="TwoWay" Path="FirstName" />
</TextBox.Text>
</TextBox>
<TextBlock Grid.Row="1" Grid.Column="1"
Style="{StaticResource BlockStyle}">
Last Name
</TextBlock>
<TextBox Grid.Row="1" Grid.Column="2"
Style="{StaticResource BoxStyle}">
<TextBox.Text>
<Binding Mode="TwoWay" Path="LastName" />
</TextBox.Text>
</TextBox>
<TextBlock Grid.Row="2" Grid.Column="1"
Style="{StaticResource BlockStyle}">
</TextBlock>
<TextBox Grid.Row="2" Grid.Column="2"
Style="{StaticResource BoxStyle}">
<TextBox.Text>
<Binding Mode="TwoWay" Path="Email" />
</TextBox.Text>
</TextBox>
<TextBlock Grid.Row="3" Grid.Column="1"
Style="{StaticResource BlockStyle}">
Address
</TextBlock>
<TextBox Grid.Row="3" Grid.Column="2"
Style="{StaticResource BoxStyle}">
<TextBox.Text>
<Binding Mode="TwoWay" Path="Address" />
</TextBox.Text>
</TextBox>
<TextBlock Grid.Row="4" Grid.Column="1"
Style="{StaticResource BlockStyle}">
Title
</TextBlock>
<TextBox Grid.Row="4" Grid.Column="2"
Style="{StaticResource BoxStyle}">
<TextBox.Text>
<Binding Mode="TwoWay" Path="Title" />
</TextBox.Text>
</TextBox>
<TextBlock Grid.Row="5" Grid.Column="1"
Style="{StaticResource BlockStyle}">
Is Manager
</TextBlock>
<CheckBox Grid.Row="5" Grid.Column="2">
<CheckBox.IsChecked>
<Binding Mode="TwoWay" Path="IsManager" />
</CheckBox.IsChecked>
</CheckBox>
<TextBlock Grid.Row="6" Grid.Column="1"
Style="{StaticResource BlockStyle}">
Vacation Days
</TextBlock>
<TextBox Grid.Row="6" Grid.Column="2"
Style="{StaticResource BoxStyle}">
<TextBox.Text>
<Binding Mode="TwoWay" Path="VacationDays" />
</TextBox.Text>
</TextBox>
<StackPanel Grid.Row="7" Grid.Column="1"
Grid.ColumnSpan="2"
Orientation="Horizontal"
HorizontalAlignment="Center">
<Button Style="{StaticResource BtnStyle}"
Content="Save"
Click="SaveClick"/>
<Button Style="{StaticResource BtnStyle}"
Content="Add As New"
Click="AddAsNewClick"/>
</StackPanel>
</Grid>
</UserControl>
There probably isn't much surprise here - it is just standard XAML. There are two things that you might find interesting - first, in order to make the bindings inside the grid easier (since everything is binding against whatever the current person instance is), we just set the DataContext of the grid to be the current person. This way, we don't have to mention that part of the binding path in every subsequent binding. The other thing (which I thought was really odd) is that unlike in regular .NET, the default binding mode is not two-way in Silverlight. In order to get the bindings to function as two-way bindings, I had to explicitly set the mode to two-way on each binding (which sadly made the binding code much more verbose). If anyone has any information about why that is the case, I'd be interested to hear about it in the comments.
With the XAML out of the way, here is the more exciting C# code:
using System.Windows.Controls;
using System.Windows.Messaging;
using LocalConnectionShared;
namespace LocalConnectionDetails
{
public partial class MainPage : UserControl
{
private LocalMessageReceiver _Receiver;
private LocalMessageSender _Sender;
public static readonly DependencyProperty CurrentPersonProperty =
DependencyProperty.Register("CurrentPerson", typeof(Person),
typeof(MainPage), new PropertyMetadata(new Person()));
public Person CurrentPerson
{
get { return (Person)GetValue(CurrentPersonProperty); }
set { SetValue(CurrentPersonProperty, value); }
}
public MainPage()
{
_Receiver = new LocalMessageReceiver("DetailsView");
_Receiver.MessageReceived += ReceiverMessageReceived;
_Receiver.Listen();
_Sender = new LocalMessageSender("TableView");
_Sender.SendCompleted += SenderSendCompleted;
InitializeComponent();
}
private void ReceiverMessageReceived(object sender,
MessageReceivedEventArgs e)
{ CurrentPerson = Person.Deserialize(e.Message); }
private void SaveClick(object sender, RoutedEventArgs e)
{
if (CurrentPerson.ID == -1)
{ return; }
_Sender.SendAsync(Person.Serialize(CurrentPerson));
}
private void AddAsNewClick(object sender, RoutedEventArgs e)
{
CurrentPerson.ID = -1;
_Sender.SendAsync(Person.Serialize(CurrentPerson));
}
private void SenderSendCompleted(object sender, SendCompletedEventArgs e)
{
if (e.Cancelled || e.Error != null)
{ return; }
CurrentPerson = Person.Deserialize(e.Response);
}
}
}
As with the table application, we create a LocalMessageReceiver and a LocalMessageSender, except that this time the names are reversed - we are listening on "DetailsView" and sending to "TableView". When we receive a message, we simply deserialize it to a Person instance and set it as the CurrentPerson.
Sending is slightly more complicated - because we have two different paths to get to sending, and because we are using a new event that we didn't use in the table app, SendCompleted. If you click "save" and the person is brand new (i.e., ID of -1), we ignore the click. Otherwise, we serialize the current person and send it along. If you click "add as new", we explicitly set the current person ID to -1 (to force the table app to add it as a new entry), and send it along.
If you remember, when the table app receives a message, it sets a response. Attaching to the SendCompleted event is how we get that response. The SendCompletedEventArgs hold a good bit of information, including the response and errors, if any occurred during the sending of the message. If the message was actually sent (not canceled), and there were no errors, we deserialize the response and set it as the current person.
Well, that about covers this introduction to Local Connections in Silverlight 3. The ability to communicate between silverlight apps on different pages and domains is really cool, and I'm hoping to see some neat ideas and apps evolve out of that. As always, the Visual Studio solution for the example is below for you to download, and if you have any questions, leave a comment!
07/16/2009 - 13:52
What is the underlying transport mechanism when communicating between apps?
07/16/2009 - 19:51
I have no idea. I thought at first it might just be wrapping some javascript communication, but the fact that it can go cross page means it is probably something internal to the Silverlight runtime.
Add Comment
[language] [/language]
Examples:
[javascript] [/javascript]
[actionscript] [/actionscript]
[csharp] [/csharp]
See here for supported languages.
Javascript must be enabled to submit anonymous comments - or you can login.