Connecting a WCF Dual Channel service to ASP.NET 

My favority hobby project is an MP3 music jukebox. The first incarnation of this project was written for my previous company 3 years ago. It has the following features:

    • Runs as a service on a PC with at least Windows XP
    • Can play MP3 music files
    • Builds a database with tags of songs from a folder structure loaded with MP3s
    • Multiple users can register to request songs using a web client or a windows client (Windows XP or Vista)
    • When a user requests a song it will be placed in a requests list
    • The jukebox plays the songs from the request list, when idle the jukebox plays random songs
    • Users can pause, skip songs and the volume can be changed
    • Users can rate songs
    • The database tracks all requests and all skip operations for all users iindividually to be able to play an overall top N on a friday afternoon (for instance).

The Juke

In the last version the clients were communicating with the service by reading and writing in the database. Crude but effective. This time I put my spare time in turning the service into a WCF service. To allow the Windows clients to function very efficively I chose to use WCF dual channel contracts. This resulted in the following JukeBox interfaces:

   1: [ServiceContract(CallbackContract = typeof(IAvSolJukeCallBack), SessionMode = SessionMode.Required)]
   2: public interface IAvSolJukeService 
   3: {
   4:     [OperationContract(IsOneWay = true)] 
   5:     void ScanForNewSongs(string path); 
   6:  
   7:     [OperationContract(IsOneWay = true)] 
   8:     void FindSongs(string searchText); 
   9:  
  10:     [OperationContract(IsOneWay = true)] 
  11:     void AddRequest(int playerID, int userID, int songID); 
  12:  
  13:     [OperationContract(IsOneWay = true)] 
  14:     void AddRating(int userID, int songID, int value); 
  15:  
  16:     [OperationContract(IsOneWay = true)] 
  17:     void SkipPlayerSong(int playerID, int userID); 
  18:  
  19:     [OperationContract(IsOneWay = true)] 
  20:     void PausePlayer(int playerID, int userID); 
  21:  
  22:     [OperationContract(IsOneWay = true)] 
  23:     void VolumeUp(int playerID, int userID); 
  24:  
  25:     [OperationContract(IsOneWay = true)] 
  26:     void VolumeDown(int playerID, int userID); 
  27:  
  28:     [OperationContract(IsOneWay = true)] 
  29:     void Visit(int playerID, string guid, string name); 
  30:  
  31:     [OperationContract(IsOneWay = true)] 
  32:     void Join(int playerID, string guid, string name); 
  33:  
  34:     [OperationContract(IsOneWay = true)] 
  35:     void Leave(int playerID, int userID); 
  36:  
  37: } 
  38:  
  39: [ServiceContract(SessionMode = SessionMode.Required)]
  40: public interface IAvSolJukeCallBack
  41: {
  42:     [OperationContract(IsOneWay = true)]
  43:     void Identified(int userID, string playerName); 
  44:  
  45:     [OperationContract(IsOneWay = true)]
  46:     void NowPlaying(string title, string artist, string album); 
  47:  
  48:     [OperationContract(IsOneWay = true)]
  49:     void StatusChange(Status status); 
  50:  
  51:     [OperationContract(IsOneWay = true)]
  52:     void PlayListChange(Song[] list); 
  53:  
  54:     [OperationContract(IsOneWay = true)]
  55:     void FindSongResults(Song[] list);
  56: } 
  57:  

The basics come from the well known Chat Service sample. Opening a TCP channel with MEX support from a System.ServiceProcess.ServiceBase class based windows service is not very difficult.

   1: /// <summary>
   2: /// Windows service OnStart override 
   3:  
   4: /// </summary> 
   5:  
   6: protected override void OnStart(string[] args)
   7: { 
   8:  
   9:     if (_serviceHost != null)
  10:     {
  11:         _serviceHost.Close();
  12:     }
  13:     // Create a ServiceHost for the AvSolJukeService type and 
  14:     // provide the base address.
  15:     _serviceHost = new ServiceHost(typeof(AvSolJukeService));
  16:     // Open the ServiceHostBase to create listeners and start 
  17:     // listening for messages.
  18:     _serviceHost.Open(); 
  19:  
  20:     // Initialize the player.... 
  21:  
  22: ... 
  23:  
  24: } 
  25:  
  26: /// <summary>
  27: /// Windows service OnStop override 
  28:  
  29: /// </summary> 
  30:  
  31: protected override void OnStop()
  32: {
  33:     // Stop the player... 
  34:  
  35:       ... 
  36:  
  37:     if (_serviceHost != null)
  38:     {
  39:         _serviceHost.Close();
  40:         _serviceHost = null;
  41:     }
  42: } 
  43:  

This is how the AvSolJukeCallBack service implementation retrieves the CallBack channel:

   1: /// <summary>
   2: /// This property gives access to the callback channel. The callback channel notifies clients 
   3: /// about the current status and status changes of the player service.
   4: /// </summary>
   5: public IAvSolJukeCallBack ClientCallBack
   6: {
   7:     get
   8:     {
   9:         if (_clientCallback == null)
  10:             _clientCallback = OperationContext.Current.GetCallbackChannel<IAvSolJukeCallBack>();
  11:         return _clientCallback;
  12:     }
  13: } 

And this in the app.config:

   1: <system.serviceModel>
   2:   <services>
   3:     <service name="AvSol.Juke.Service.AvSolJukeService" behaviorConfiguration="MEX">
   4:       <host>
   5:         <baseAddresses>
   6:           <add baseAddress = "net.tcp://localhost:1040/AvSolJukeService"/>
   7:         </baseAddresses>
   8:       </host>          <!-- Service Endpoints -->
   9:       <endpoint address="MEX" binding="mexTcpBinding" contract="IMetadataExchange"/>
  10:       <endpoint address="net.tcp://localhost:1040/AvSolJukeService" binding="netTcpBinding" contract="AvSol.Juke.Service.IAvSolJukeService"/>
  11:     </service>
  12:   </services>
  13:   <behaviors>
  14:     <serviceBehaviors>
  15:       <behavior name="MEX">
  16:         <serviceMetadata/>
  17:       </behavior>
  18:     </serviceBehaviors>
  19:   </behaviors>
  20: </system.serviceModel> 

Writing the windows client is a breeze. First the windows client should call Join. When successful it will be followed by an initial callback Identified and subsequent callbacks StatusChange, NowPlaying, PlayListChange every time something happens in the jukebox. Calling FindSongs will eventually result in the FindSongResults callback. When the windows client closes it should call Leave. This will stop the callbacks and allows the client to stop the connection gracefully.

However, when working on this I forgot about the Web client. How do you get this callback mechanism to work in the web request/response world? Can you use AJAX to callback into your web page? Fortunately I found an article that showed how to use WaitHandle to turn the asynchronous callbacks into single synchronous calls. When callbacks trigger subsequent AutoResetEvent objects, an initiating call can wait for one or more callbacks to signify completion.

The jukebox WCF service now needed a little rewrite. For the purpose of the web client I introduced the Visit call. The Visit call replaces the Join operation and will always in sequence send the callbacks Identified, StatusChange, NowPlaying, PlayListChange. Visit needs no to call Leave to stop broadcasts, because it simply doesn't start the broadcasting process at all. FindSongs and FindSongResults also do not need a broadcast session. A wrapper around the client can call Visit or FindSongs and wait for the callbacks. The wrapper should also correctly handle the threading of the WCF calls. Here is the implementation of the Visit in the web client wrapper class, the AvSolJukeServiceClient has been generated by the Add Services option of Visual Studio 2008:

   1: /// <summary>
   2: /// This request Joins the service and Leaves it again in a synchronous operation to retrieve only the current status.
   3: /// </summary>
   4: /// <param name="userGuid"></param>
   5: /// <param name="name"></param>
   6: /// <returns></returns>
   7: public int Visit(string userGuid, string name)
   8: {
   9:     WaitHandle[] waitHandles = { _waitIdentifiedHandle, _waitNowPlayingHandle, _waitStatusChangeHandle, _waitPlayListChangedHandle }; 
  10:  
  11:     using (AvSolJukeServiceClient client = new AvSolJukeServiceClient(new InstanceContext(this)))
  12:     {
  13:         client.Open();
  14:         client.Visit(1, userGuid, name);
  15:         WaitHandle.WaitAll(waitHandles, MAXWAITIDENTIFIED, false);
  16:         client.Close();
  17:     }
  18:     return _userID;
  19: } 

And snippet to show how to handle one of the events:

   1: /// <summary>
   2: /// Implementation of the Identified callback method
   3: /// </summary>
   4: /// <param name="userID"></param>
   5: /// <param name="playerName"></param>
   6: void IAvSolJukeServiceCallback.Identified(int userID, string playerName)
   7: {
   8:     _userID = userID;
   9:     PlayerName = playerName;
  10:     _waitIdentifiedHandle.Set();
  11: } 
  12:  

Now the ASP.NET page can call the wrapped Visit in the Page_Load. It then has all the information it needs to show the status and current requests. A search button on the page calls a wrapped FindSongs and waits for the callback to show the results. The web client uses the WCF service in a real stateless way. To my knowledge this results in the best achievable way to use the WCF dual channel service.

Now the jukebox is a dynamic service, people request songs, press pause and play and skip songs. The web page now only shows the results after a reload of the page. How to get the very dynamic service into the not so dynamic request/response jacket? I chose AJAX and polling.

I moved the controls showing the dynamic stuff like the status and the current requests in an UpdatePanel and hooked it up to an AJAX Timer. Next step was to put the FindSongs in an AJAX Web script service proxy and call it from script using the ScriptManager on the aspx page.

I hope this gives you some grip on getting WCF working in your ASP.NET web apps. If you have any other input on this ever ongoing hobby project, you're welcome.

Posted on 04-11-2008 by Wim The
0 Comments  |  Trackback Url  |  Link to this post
Tags: .NET

Links to this post

Comments

Name:
URL:
Email:
Comments:

CAPTCHA Image Validation