Introduction
Web Service is a very popular tool which makes it possible to share useful Internet resources among Internet communities. Using the .NET Framework and Visual Studio 2005 or later as a development tool, anybody can develop a Web Service in minutes. There are some tricks which a novice usually struggles with, such as publishing, web referencing, etc., but finally, it's an easy way to create a Web Service. The only thing I found inconvenient is that there is no way to send events from a Web Service to a client if you are using as a template, say an ASP.NET Web Service Application template which is available in Visual Studio 2005. This article describes a way to fix this inconvenience.
Background
When you describe a Web Service interface, you have to use attributes to make Web Service methods visible from clients. For example, you have to put a [WebMethod]
attribute before each method which you want to call from remote web clients. I would like to have something similar for events. Say, hypothetically, it would be nice if the following is possible:
[WebEvent]
public event ActiveClientsChangedDelegate OnActiveClientsChanged = null;
where the delegate is defined in a Web Service as:
public delegate void ActiveClientsChangedDelegate(int[] clients);
And, in case your service determined that the active client list has changed, it can call:
if (OnActiveClientsChanged != null) OnActiveClientsChanged(clients);
where clients
is array of IDs of all the currently active clients of this service. Unfortunately, this is not possible if you are using the current .NET Framework templates for Web Service creation. One way to get around this is to poll the service from the client. This is not good because it's not real time and it overloads the client PC. Here is another way to do this using asynchronous calls.
Implementation of Callbacks from a Web Service Using Asynchronous Calls
Let's consider an example. We have a Web Service and we want it to report its clients the number of currently active clients. If any client logs in, then all other active clients should receive notification about this. Here is the Web Server part of the code:
namespace WebService
{
[WebService(Namespace = "http://localhost/webservices/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
public class WebService : System.Web.Services.WebService
{
#region private members
private static Dictionary<Guid, ClientState> s_services =
new Dictionary<Guid, ClientState>();
private int m_clientID;
#endregion private members
#region WebService interface
[WebMethod]
public void StartSession(Guid sessionID, int clientID)
{
lock (s_services)
{
if (s_services.ContainsKey(sessionID))
{
m_clientID = s_services[sessionID].ClientID;
}
else
{
m_clientID = clientID;
s_services.Add(sessionID, new ClientState(m_clientID));
}
}
lock (s_services)
{
foreach (Guid sID in s_services.Keys)
{
s_services[sID].GetActiveClientsCompleted.Set();
}
}
}
[WebMethod]
public void StopSession(Guid sessionID)
{
lock (s_services)
{
if (s_services.ContainsKey(sessionID))
{
s_services.Remove(sessionID);
}
}
lock (s_services)
{
foreach (Guid sID in s_services.Keys)
{
s_services[sID].GetActiveClientsCompleted.Set();
}
}
}
[WebMethod]
public int[] GetActiveClients(Guid sessionID)
{
if (!s_services.ContainsKey(sessionID))
{
return new int[] { };
}
bool signalled = s_services[sessionID].GetActiveClientsCompleted.WaitOne();
if (signalled)
{
lock (s_services)
{
List<int> clients = new List<int>();
foreach (Guid sID in s_services.Keys)
{
if (sID == sessionID) continue;
clients.Add(s_services[sID].ClientID);
}
return clients.ToArray();
}
}
else
{
return new int[] { };
}
}
#endregion
private class ClientState
{
public int ClientID;
public AutoResetEvent GetActiveClientsCompleted = new AutoResetEvent(false);
public ClientState(int clientID)
{
ClientID = clientID;
}
}
}
}
As you can find out from the proxy part of the WebService (you can find it in the auto-generated module, Reverence.cs), for each declared [WebMethod]
, there is an additional asynchronous call and an additional completion event. In our case, for the declared method GetActiveUsers
, there is additional auto-generated method GetActiveUsersAsync
and an additional auto-generated event GetActiveUsersCompleted
. On the client-side, when we want to create a listener for the GetActiveUsersCompleted
event, we do two things. First, in the constructor, we setup a handler to process the GetActiveUsersCompleted
event:
m_service = new WebServiceWindowsClient.localhost.WebService();
m_service.GetActiveClientsCompleted += new
WebServiceWindowsClient.localhost.GetActiveClientsCompletedEventHandler(
m_service_GetActiveClientsCompleted);
And second, we make an asynchronous call to GetActiveUsers
to start listening:
m_service.GetActiveClientsAsync(m_sessionID);
This asynchronous operation is indefinitely long, and it ends when an "active clients list changed" event occurs in the Web Service part (say, if a new client is added to or removed from the s_services
list) and the GetActiveClientsCompleted
event is signaled in this case for each active client:
foreach (Guid sID in s_services.Keys)
{
s_services[sID].GetActiveClientsCompleted.Set();
}
When this occurs, this part of the GetActiveClients
method of the Web Service works:
bool signalled = GetActiveClientsCompleted.WaitOne();
if (signalled)
{
lock (s_services)
{
List<int> clients = new List<int>();
foreach (Guid sID in s_services.Keys)
{
if (sID == sessionID) continue;
clients.Add(s_services[sID].m_clientID);
}
return clients.ToArray();
}
}
else return new int[] { };
As soon as this event fires, each client gets a GetActiveClientsCompleted
event, and the corresponding method will be processed:
void m_service_GetActiveClientsCompleted(object sender,
WebServiceWindowsClient.localhost.GetActiveClientsCompletedEventArgs e)
{
int[] clients = e.Result;
string client_list = "";
foreach (int client in clients) client_list += client + " ";
listBoxEvents.Items.Add(string.Format("GetActiveClients " +
"completed with result: {0}", client_list));
m_service.GetActiveClientsAsync(m_sessionID);
}
You can notice that at the end of this handler, we call the GetActiveUsersAsync
method to activate the listener again. This is a trick.
Instructions to Setup the Demo Project
You can download the sources of this demo solution to check that this approach works and that there is no need to have any polling methods on the client side. In the sources, the Web Service is published on the localhost. You can try to publish it on any web host you can access, and check how it works. In this case, you will have to change all the lines of the client's source code containing localhost. When you are done with the setup, just run some number of WebServiceWindowsClient.exes and push the Start Session buttons on each.
Conclusion
This article described how to use asynchronous Web Service method calls to implement callback events from a Web Service to its clients. Every asynchronous call from a client to a Web Service invokes the corresponding synchronous call which just waits for some AutoResetEvent
to fire. As soon as this event occurs, the corresponding <method name>Completed
event on the client side gets the control.
First Revision
After this article was published I found and fixed two bugs:
- if you stop the session for one of the clients it does not stop event listener
- if you restart the session it will crash
The problem with stopping event listener is this: to stop event listener I use AutoResetEvent
:
s_services[sessionID].GetActiveClientsCompleted.Set();
and there is no simple way to notify the method WebServiceClientForm.m_service_GetActiveClientsCompleted
that it does not have to reactivate event listener. To do that I introduced a new member, GetActiveClientsDone
, of ClientState
. Now, when I need to close listener I can set this member to true
:
public void StopGetActiveClients(Guid sessionID)
{
s_services[sessionID].GetActiveClientsDone = true;
s_services[sessionID].GetActiveClientsCompleted.Set();
}
This value will be passed along with the result of the GetActiveClients
method in the object of type GetActiveClientsResult
:
public class GetActiveClientsResult
{
public bool _Done = false;
public int[] _Clients = new int[] { };
public GetActiveClientsResult() { }
public GetActiveClientsResult(int[] clients, bool done)
{
_Clients = clients;
_Done = done;
}
}
Then I can use member _Done
of this class GetActiveClientsResult
in WebServiceClientForm.m_service_GetActiveClientsCompleted
to prevent event listener reactivation:
if (!e.Result._Done) m_service.GetActiveClientsAsync(m_sessionID, new object());
Also pay attention on the second parameter of call to the GetActiveClientsAsync
method: new object()
added there! This fixes the second critical crash bag listed above because it creates new context for every new invocation of the asynchronous method.
History
- First published on September 1, 2008.
- First revision & bug fixes on September 15, 2008. Solution for download also was updated.