Introduction
This project will demonstrate the client-server approach using a duplex mechanism, where the server will keep track of each (client) browser and update the client, either individually or all together. But the trick is to use a push (long polling) architecture rather than the client polling the server on a periodical time-range.
Look & Feel
In the image below (Fig 1), you can see the application as it is working. In this project, I am basing the project on a power station scenario, in that the central office (server) will update the client power stations.
Fig. 1
In the application (screenshot above), the server has created two client power stations (browsers), "Greenfield" and "Finnharps". From the server page, I can then click the respective tab (station) and change its power wattage, or if I like, click the "All Browsers" check-box and change all the stations at once (for e.g. send a popup to each station of an impending shutdown).
Project Visual Studio Structure
Fig. 2
Project Modules
Server (Silverlight) Module
The server is really made up of two XAML pages, "MainPage
" and "TabContents
". When the user creates a new station, a new tab is created to maintain that station - thus a new control is inserted into the Microsoft tab control. At the same time, a new browser is opened (passing the name of the station in the URL for displaying purpose in the client browser) and the name will be used as a key to store the browser details in a collection. So, when the user is in a particular tab - the selected tab name is the key that is used to get the browser details from the collection and push the new (class "ClientBrowserInfo
") details (in json format) to the client.
Fig. 3
Client (Silverlight) Module
When the browser is initially opened (from the user clicking the "Add Station" button on the server), it will create a duplex poll between the client and the server and create a callback event to handle communication from the server. Deserializing the json back into a ("ClientBrowserInfo
") class, from which the client can tell what to change and what the new value is to be.
Fig. 4
Server Hosing Module
As well as containing the HTML pages that will host the respective Silverlight projects, the duplex and browser services will be contained within this module. Each browser's state will be maintained with the aid of a static
class\methods and a static
collection. The duplex service will provide the mechanism for pushing the data to each (or all) client(s).
Third Party Control
For the dial, I used Telerik's free Gauge control (http://www.telerik.com/products/free-silverlight-controls.aspx).
Snippets Of Code
The code below will create and open a new client, and insert the "TabControl
" into the Tab.
private void btnSubmitNewPowerStation_Click(object sender, RoutedEventArgs e)
{
tabStations.Visibility = System.Windows.Visibility.Visible;
TabContents newStationControl = new TabContents();
TabItem newStationTabItem = new TabItem();
newStationTabItem.Header = this.txtNewStation.Text;
newStationTabItem.Content = newStationControl;
this.tabStations.Items.Add(newStationTabItem);
newStationTabItem.IsSelected = true;
HtmlPopupWindowOptions options = new HtmlPopupWindowOptions();
options.Menubar = false;
options.Resizeable = false;
options.Status = false;
options.Toolbar = false;
options.Directories = false;
options.Scrollbars = false;
options.Location = false;
options.Height = 500;
options.Width = 500;
HtmlPage.PopupWindow(new Uri
("http://localhost:62115/PowerStationClient.html?name=" +
this.txtNewStation.Text, UriKind.RelativeOrAbsolute), "_blank", options);
}
One of the events in the "TabControl
" is to catch a slider value change and then post it to the respective (or all) clients.
private void sliderWattage_ValueChanged(object sender,
RoutedPropertyChangedEventArgs<double> e)
{
TabItem selectedTab = (TabItem)this.Parent;
string clientKey = selectedTab.Header.ToString();
lblSliderValueSelected.Content = Convert.ToInt32(e.NewValue * 100).ToString();
if (this.imgStatus.Tag == null && e.NewValue == 0)
this.bthStatus.IsEnabled = false;
else if (this.imgStatus.Tag == null && e.NewValue > 0)
this.bthStatus.IsEnabled = true;
else if (this.imgStatus.Tag.ToString() == "off" && e.NewValue == 0)
this.bthStatus.IsEnabled = false;
else this.bthStatus.IsEnabled = true;
DuplexService.DuplexControllerClient client2 =
new DuplexService.DuplexControllerClient();
ClientBrowserInfo browserData = new ClientBrowserInfo();
browserData.BrowserKey = clientKey;
browserData.GaugeValue = int.Parse(lblSliderValueSelected.Content.ToString());
browserData.NotificationMessage = "";
browserData.PopupType = PopupStatus.NoPopup;
browserData.Operation = OperationType.Gauge;
if (!(bool)this.chkAllbrowsers.IsChecked)
client2.SendTriggerAuditDataAsync(SerializeObject(browserData),
browserData.BrowserKey);
else client2.SendTriggerAuditDataAsync(SerializeObject(browserData), "");
}
The following snippet of code will get the respective browser based on the key and perform the push (it also determines if all browsers need to be updated).
public void SendTriggerAuditData(string data, string sessionId)
{
try
{
if (BrowserHelper.GetCallbackChannels().Count() > 0)
{
lock (syncRoot)
{
if (sessionId.Equals(string.Empty))
{
IEnumerable
<IDBNotificationCallbackContract>
allChannels = BrowserHelper.
GetCallbackChannels();
allChannels.ToList().ForEach(c =>
c.SendNotificationToClients(data));
}
else
{
IDBNotificationCallbackContract
channels = BrowserHelper.
GetCallbackChannel(sessionId);
channels.SendNotificationToClients(data);
}
}
}
}
catch (Exception) { }
}
The following code (from the client) will notify the server that it is interested in a callback and that it is registering itself using duplex-polling.
void InitialisePageFunctionality()
{
this.lblStationName.Content = string.Empty;
this.lblStationName.Content = "Station Name: " +
HtmlPage.Document.QueryString["name"].ToString();
client = new DBNotificationClient(new PollingDuplexHttpBinding(),
new EndpointAddress
("http://localhost:62115/BrowserController/DBNotificationService.svc"));
client.SendNotificationToClientsReceived += (sender, e) =>
{
if (e.data != null)
UpdatedAuthorsReceived(e.data); };
this.Subscribe(); }
Gotchas
- I did not implement any validation on the power station name (this is used as a collection key).
- I do not fully clean-up after a browser is closed (calling the "
Unsubscribe
" method) - thus leaving some static
data in the static
collection - that may cause issues later when pushing data to the client.
- High scalability is possible with the use of a background thread (from client) and using Windows 2008 server (not used in this project).
History
- 3rd February, 2011: Initial post