Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / ASP.NET

How to Get Notifications from a .NET Web Service

4.89/5 (42 votes)
17 Sep 2008CPOL5 min read 2   3.6K  
This article describes how to use asynchronous Web Service method calls to implement callback events from a Web Service to its clients.

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:

C#
[WebEvent]
public event ActiveClientsChangedDelegate OnActiveClientsChanged = null;

where the delegate is defined in a Web Service as:

C#
public delegate void ActiveClientsChangedDelegate(int[] clients);

And, in case your service determined that the active client list has changed, it can call:

C#
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:

C#
namespace WebService
{
  /// <summary />
  /// Summary description for WebService
  /// </summary />

  [WebService(Namespace = "http://localhost/webservices/")]
  [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
  public class WebService : System.Web.Services.WebService
  {
    #region private members
    // This static Dictionary keeps track of all currently open sessions
    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))
        {
          // Session found in the list
          m_clientID = s_services[sessionID].ClientID;
        }
        else
        {
          // Add session to the list
          m_clientID = clientID;
          s_services.Add(sessionID, new ClientState(m_clientID));
        }
      }

      lock (s_services)
      {
        // Signal GetActiveClientsCompleted event for each client
        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))
        {
          // Remove session from the list
          s_services.Remove(sessionID);
        }
      }

      lock (s_services)
      {
        // Signal GetActiveClientsCompleted event for each client
        foreach (Guid sID in s_services.Keys)
        {
          s_services[sID].GetActiveClientsCompleted.Set();
        }
      }
    }

    [WebMethod]
    public int[] GetActiveClients(Guid sessionID)
    {
      if (!s_services.ContainsKey(sessionID))
      {
        // Return empty client list
        return new int[] { };
      }

      bool signalled = s_services[sessionID].GetActiveClientsCompleted.WaitOne();
      // wait for GetActiveClientsCompleted event

      if (signalled)
      {
        lock (s_services)
        {
          // Create client list and return it
          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 empty client list
        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:

C#
// Create proxy for WebService
m_service = new WebServiceWindowsClient.localhost.WebService();

// Subscribe for event
m_service.GetActiveClientsCompleted += new 
  WebServiceWindowsClient.localhost.GetActiveClientsCompletedEventHandler(
  m_service_GetActiveClientsCompleted);

And second, we make an asynchronous call to GetActiveUsers to start listening:

C#
// This call activates GetActiveClients event listener
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:

C#
// Signal GetActiveClientsCompleted event for each 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:

C#
bool signalled = GetActiveClientsCompleted.WaitOne();
// wait for GetActiveClientsCompleted event

if (signalled)
{
    lock (s_services)
    {
      // Create client list and return it
      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[] { };
// Return empty client list

As soon as this event fires, each client gets a GetActiveClientsCompleted event, and the corresponding method will be processed:

C#
void m_service_GetActiveClientsCompleted(object sender, 
   WebServiceWindowsClient.localhost.GetActiveClientsCompletedEventArgs e)
{
  // Add current list of active clients to list box
  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));

  // This call reactivates GetActiveClients event listener
  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:

C#
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:

C#
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:

C#
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:

C#
// This call reactivates GetActiveClients event listener only if we are not closing it
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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)