: C# 2008 Programmer

Implementing WCF Callbacks

Implementing WCF Callbacks

One of the limitations of a traditional ASMX Web Service call lies in its request/response communication model. ASMX Web Services calls are passive and return results only when called upon. For instance, say that a particular cinema operator deploys a Web Service to allow online purchasing of tickets. The cinema's branches have systems that are connected to the Web Service to obtain the latest status of seat allocation and that sell tickets to cinema goers. In this case, the systems have to keep polling the Web Service at regular intervals to obtain the latest seats status. Moreover, it is very likely that a few branches may be booking the same seat(s) at the same time.

A better approach would be for the Web Service to notify all the branches about the changes in seat status as and when a seat has been reserved. This way, all branches have the latest seat information, and there is no need to poll the Web Service at regular intervals, thereby relieving the Web Service of the additional load. To accomplish this, you need a communication model in which the client is always connected to the service and is notified when an event occurs. Using WCF, this communication model can be implemented by using callbacks. A callback allows a service to call back its clients. The roles of the service and the client are now duplicated the client is also the service, and the service is also the client.

This section of the chapter leads you through building a WCF ticketing service that allows clients to book tickets. When multiple clients are connected to the service, a seat booked by one client is broadcast to all the connected clients. Figure 20-37 illustrates the flow of the system. It shows four cinema branches using the client to connect to the WCF ticketing service. Once seats are selected (represented by the yellow buttons), a client will click on the Book Seats button to send the reservation to the WCF service. The WCF service will then broadcast the booked seats to all connected clients, which will then set the booked seats in red.


Figure 20-37

Building the Service

The WCF service that allows clients to book cinema tickets needs to come first. Launch Visual Studio 2008 and create a new WCF Service Library project. Name the project WcfTicketingService (see Figure 20-38).


Figure 20-38

In this example, the WCF service will be hosted by the WCF Service Host, a utility provided by Visual Studio 2008.

In the IService1.cs file, define the following service and data contracts:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
namespace WcfTicketingService {
[ServiceContract(
Name = "TicketingService",
Namespace = "http://www.learn2develop.net/",
CallbackContract = typeof(ITicketCallBack),
SessionMode = SessionMode.Required)]
public interface ITicketService {
[OperationContract(IsOneWay = true)]
void SetSeatStatus(string strSeats);
[OperationContract(IsOneWay = true)]
void RegisterClient(Guid id);
[OperationContract(IsOneWay = true)]
void UnRegisterClient(Guid id);
}
public interface ITicketCallBack {
[OperationContract(IsOneWay = true)]
void SeatStatus(string message);
}
//---each client connected to the service has a GUID---
[DataContract]
public class Client {
[DataMember]
public Guid id { get; set; }
}
}

The ITicketService interface defines three operations, which are described in the following table.

Operation Description
SetSeatStatus Allows clients to book seats. Takes in a string containing the seats to be booked.
RegisterClient Registers a client when it connects to the service. Takes in a GUID so that the service can uniquely identify a client.
UnRegisterClient Unregisters a client when it disconnects from the service. Takes in the client's GUID.

The ITicketService interface is also prefixed with the [ServiceContract] attribute. Specifically, note the CallbackContract property, which specifies the interface that defines the callback operation. The SessionMode property is set to Required, indicating that state must be maintained between the service and client.

The ITicketCallBack interface contains one operation SeatStatus, which allows the service to initiate a callback to the client, thereby updating the client about the latest seat status (that is, which seats have been booked by other clients).

The Client class defines the data contract. It contains the GUID of a client connecting to the service.

All the operations in these two interfaces are defined as one-way operations. To understand why this is so, assume that all the operations use the default request/response model. When the SetSeatStatus() method is called to book seats, it waits for a response from the service. However, the service now invokes the SeatStatus callback on the client (the service informs all clients about the seats booked) and waits for a reply from the client. A deadlock occurs because the client is waiting for a response from the service while the service is waiting for a response from the client after invoking the callback. By defining the operations as one-way, the service can invoke the callback on the client without waiting for a reply from the client, preventing a deadlock from happening.

In the Service1.cs file, define the SeatStatus class:

using System;
using System.Text;
using System.Timers;
namespace WcfTicketingService {
//...
}
public class SeatStatus {
//---a string representing the seats booked by a client---
public string Seats { get; set; }
}

The SeatStatus class contains Seats, a property for storing the seats booked by a client.

In the Service1.cs file, define the Ticketing class that implements the ITicketingService service contract:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
using System.Collections;
namespace WcfTicketingService {
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single,
ConcurrencyMode = ConcurrencyMode.Multiple)]
public class Ticketing : ITicketService {
//---used for locking---
private object locker = new object();
private SeatStatus _seatStatus = null;
//---for storing all the clients connected to the service---
private Dictionary<Client, ITicketCallBack> clients =
new Dictionary<Client, ITicketCallBack>();
public Ticketing() { }
//---add a newly connected client to the dictionary---
public void RegisterClient(Guid guid) {
ITicketCallBack callback =
OperationContext.Current.GetCallbackChannel<ITicketCallBack>();
//---prevent multiple clients adding at the same time---
lock (locker) {
clients.Add(new Client { id = guid }, callback);
}
}
//---unregister a client by removing its GUID from
// dictionary---
public void UnRegisterClient(Guid guid) {
var query = from c in clients.Keys
where c.id == guid select c;
clients.Remove(query.First());
}
//---called by clients when they want to book seats---
public void SetSeatStatus(string strSeats) {
_seatStatus = new SeatStatus {
//---stores the seats to be booked by a client---
Seats = strSeats
};
//---get all the clients in dictionary---
var query = (from c in clients
select c.Value).ToList();
//---create the callback action delegate---
Action<ITicketCallBack> action =
delegate(ITicketCallBack callback) {
//---callback to pass the seats booked
// by a client to all other clients---
callback.SeatStatus(_seatStatus.Seats);
};
//---for each connected client, invoke the callback---
query.ForEach(action);
}
}
}

Within the Ticketing class are the implementations for the three operations defined in the ITicketService interface:

?RegisterClient() Called when clients are connected to the service for the first time. Clients are stored in a generic Dictionary<K,V> object. The key used for storing a client is its GUID, and its callback handler is stored as the value.

?UnRegisterClient() Called when a client is disconnected from the service; its entry in the Dictionary object is removed.

?SetSeatStatus() Called when clients want to book seats. The seats to be booked are stored in a SeatStatus object and then you create an Action delegate to invoke the callback of a client to pass the seats that have been booked by a client. Because all connected clients need to be notified, you invoke the callback for each client.

The [ServiceBehavior] attribute specifies the InstanceContextMode to be Single and the ConcurrencyMode property to be Multiple.

Service Behaviors ConcurrencyMode

When messages are received by a WCF service, you can set how threads are used to manage all received messages:

?One thread can be used to access the receiver object(s) at a time, or

?Multiple threads can be used to access the receiver object(s) concurrently.

How you handle all incoming messages is specified using the ConcurrencyMode property of the [ServiceBehavior] attribute, which can assume one of the following values:

?Single (default) Only one thread can access the receiver object at a time.

?Multiple Multiple threads can access the receiver object(s) concurrently.

?Reentrant Only one thread can access the receiver object at a time, but callbacks can reenter that object on another thread.

When you use the Multiple mode on the service, take special care to make sure that threads are synchronized properly and that critical regions are locked when a threading is accessing it.

For simplicity of demonstration, the following shortcuts are made:

?The seats booked by a client are simply broadcast to all connected clients. In real life, they would also be saved in database or array.

?When new clients connect to the server, the current seat allocation status (which seats are booked and which are not) is not sent to them.

Next, double-click on the App.config file in Solution Explorer. Change the following highlighted attributes values:

<system.serviceModel>: <services>
<service name="WcfTicketingService.Ticketing"
behaviorConfiguration="WcfTicketingService.Service1Behavior">
<host>
<baseAddresses>
<add
baseAddress="http://localhost:8731/Design_Time_Addresses/WcfTicketingService/Service1/" />
</baseAddresses>
</host>
<!-- Service Endpoints -->
<!-- Unless fully qualified, address is relative to base address
supplied above -->
<endpoint address="" binding="wsHttpBinding"
contract="WcfTicketingService.ITicketService">
...

Right-click on the App.config file, and select Edit WCF Configuration. Expand the EndPoints node (see Figure 20-39), and select the first [Empty Name] node. Set its properties as follows:

Property Value
Address net.tcp://localhost:5000/TicketingService
Binding NetTcpBinding

Figure 20-39

TCP is the transport protocol.

Save the app.config file and close the configuration window. Press F5 to debug the service. In the WCF Test Client, you will see something like Figure 20-40. The error icons (represented by the exclamation symbols) are normal.


Figure 20-40

Building the Client

The WCF service is complete, so it's time to build the client to consume the service. Add a new Windows Forms Application project to the current solution. Name the project Client.

Add a service reference to the ticketing WCF service. In the Add Service Reference dialog, click the Discover button and locate the Ticketing WCF service (see Figure 20-41). Click OK.


Figure 20-41

Populate Form1 with the controls shown in Figure 20-42. Set the Size property of Form1 to 477, 387.


Figure 20-42

In the code-behind of Form1, import the following namespace:

using System.ServiceModel;

Declare the following constants and objects:

namespace Client {
public partial class Form1 : Form {
int ROWS = 10;
int COLUMNS = 10;
const int SEAT_WIDTH = 45;
const int SEAT_HEIGHT = 25;
const int START_X = 10;
const int START_Y = 40;
static Button[,] seatsArray;
private ServiceReference1.TicketingServiceClient _client;
private Guid _guid = Guid.NewGuid();

Define the SeatsOccupied() static function within the Form1 class as follows:

public partial class Form1 : Form {
...
...
...
//---set all occupied seats in red---
public static void SeatsOccupied(string strSeatsOccupied) {
string[] seats = strSeatsOccupied.Split(',');
for (int i = 0; i < seats.Length - 1; i++) {
string[] xy = seats[i].Split('-');
Button btn = seatsArray[int.Parse(xy[0]) - 1,
int.Parse(xy[1]) - 1];
btn.BackColor = Color.Red;
}
}
}

This function accepts a string containing the seats that are occupied. The format of the string is:

<column>-<row>,<column>-<row>,...

For each seat (represented by the Button control) that is booked, the background color is changed to red.

Define the SeatStatusCallback class and implement the SeatStatus() method as defined in the TicketingServiceCallback interface (defined in the service):

namespace Client {
public partial class Form1 : Form {
//...
}
public class SeatStatusCallback :
ServiceReference1.TicketingServiceCallback {
public void SeatStatus(string message) {
Form1.SeatsOccupied(message);
}
}
}

The SeatStatus() method is invoked when the service calls the client's callback. Here, you call the static SeatsOccupied() function to update the seats status.

Code the Form1_Load event handler as follows:

private void Form1_Load(object sender, EventArgs e) {
InstanceContext context =
new InstanceContext(new SeatStatusCallback());
_client = new
ServiceReference1.TicketingServiceClient(context);
_client.RegisterClient(_guid);
//---display the seats---
seatsArray = new Button[COLUMNS, ROWS];
for (int r = 0; r < ROWS; r++) {
for (int c = 0; c < ROWS; c++) {
Button btn = new Button();
btn.Location = new Point(
START_X + (SEAT_WIDTH * c),
START_Y + (SEAT_HEIGHT * r));
btn.Size = new Size(SEAT_WIDTH, SEAT_HEIGHT);
btn.Text =
(c + 1).ToString() + "-" + (r + 1).ToString();
btn.BackColor = Color.White;
seatsArray[c, r] = btn;
btn.Click += new EventHandler(btn_Click);
this.Controls.Add(btn);
}
}
}

These statements basically create an instance of the InstanceContext class by passing it an instance of the SeatStatusCallback class. Then an instance of the WCF client is created using the constructor that requires an InstanceContext object. In addition, the form is dynamically populated with Button controls representing the seats in a cinema. Each Button control's Click event is wired to the btn_Click event handler.

Define the btn_Click event handler as follows:

void btn_Click(object sender, EventArgs e) {
if (((Button)sender).BackColor == Color.White) {
((Button)sender).BackColor = Color.Yellow;
} else if (((Button)sender).BackColor == Color.Yellow) {
((Button)sender).BackColor = Color.White;
}
}

This event handler toggles the color of the seats as users click on the Button controls. White indicates that the seat is available; yellow indicates that the seat has been selected for booking.

Code the Book Seats button as follows:

private void btnBookSeats_Click(object sender, EventArgs e) {
string seatsToBook = string.Empty;
for (int r = 0; r < ROWS; r++) {
for (int c = 0; c < ROWS; c++) {
if (seatsArray[c, r].BackColor == Color.Yellow) {
seatsToBook += seatsArray[c, r].Text + ",";
}
}
}
//---send to WCF service---
_client.SetSeatStatus(seatsToBook);
}

To specify the seats that are selected for booking, a string is created to containing the seats to be booked in the following format:

<column>-<row>,<column>-<row>,...

Finally, code the Form1_FormClosing event as follows:

private void Form1_FormClosing(object sender, FormClosingEventArgs e) {
_client.UnRegisterClient(_guid);
}

Testing the Application

To test the application, press F5 to debug and launch the service. Once this is done, you can debug the client. Right-click the Client project in Solution Explorer, and select Debug?Start New Instance (see Figure 20-43).


Figure 20-43

Run a few instances of the client and you can start to book cinema tickets. As one client books the seats, the other clients are automatically updated.


: 1.147. /Cache: 3 / 1