This is the sixth in a series.
With the completion of the previous article, I finally have a media player remote control that allows a client to do most of the things set out in the original requirements specification. So, what happened when I deployed it to my production server? Clicking on the Playlist button causes all sorts of problems. Why? Unlike my test environment, my production environment has several thousand songs in its media library. So, the call to GetMediaLibrary() fails spectacularly. Even if it didn't fail completely, an individual call to GetMediaInfo for each song would take prohibitively long if I had a sizeable media library.
Chris Sells wrote a wonderful article series on the benefits of multithreading in WinForms version 1 (here, here, and here). While these methods are still applicable, multithreading has gotten easier in version 2 by using the BackgroundWorker component and a few other approaches outlined in Chris' excellent book Windows Forms 2.0 Programming. Additionally, the service reference I added to our client application has asynchronous calls built-in, so I may be able to make use of those to keep from freezing the UI.
As far as I can tell, there are two popular ways to solve our problem: 1) take care of paging in the UI, 2) take care of paging at the service level. There is a nice discussion of some reasons for each here.
So, what's the answer? Our original goal was to allow client applications to run on a variety of platforms, including via a web page. So, not all of our clients will be able to solve the problem by multithreading the UI or using asynchronous calls. I guess I have to figure out how to implement a service which will allow paging to the client. That will allow us to solve the client responsiveness issue in whatever way is most apparent for the client. Our WinForms application can call a huge get-all type of solution in a background thread while showing a please wait message, our web application can page the current n records to the display, and our WPF front-end can change animations and button themes by directly databinding to the service connection state by injecting dependency objects into its visualization tree (or whatever it is that the WPF kids are doing these days).
That means changing our MediaLibraryService contract to look something like this:
[ServiceContract(Namespace="http://www.cavinconsulting.com/MediaPlayerRemote/")] public interface IMediaLibraryService { [OperationContract] List<Guid> GetMediaLibrary(); [OperationContract] int GetSongCount(); [OperationContract] List<Guid> GetSongs(int startRow, int rowCount); [OperationContract] List<Guid> GetSongsByAlbum(string albumID, int startRow, int rowCount); [OperationContract] List<Guid> GetSongsByGenre(string genre, int startRow, int rowCount); [OperationContract] List<Guid> GetSongsByArtist(string artist, int startRow, int rowCount); [OperationContract] Song GetMediaInfo(Guid songID); }
I've left the GetMediaLibrary() method to support the non-paged version of getting all of the songs from our library. Also, I switched the song ID type from string to Guid since they will pass nicely across a WCF ServiceContract and DataContract. Lastly, I have omitted some potentially useful methods such as GetGenres and GetArtists. That is because the underlying media library doesn't really support this easily. We may add this later, depending on the requirements of our client applications, but it will be less than trivial.
The implementation of the paged back-end was relatively straight forward, though the implementation I came up with may not be the fastest. Now, we can take a look at how make use of them from the BackgroundWorker control. Here's what I did: drop a BackgroundWorker control on the form, add a DoWork event handler, add a DoWorkAsync call to the form load. Here's what I ended up with for the DoWork event handler:
private void _mediaLibraryBackgroundWorker_DoWork(object sender, DoWorkEventArgs e) { // Populate the library int librarySize = _mediaLibraryService.GetSongCount(); int i = 0; while (i < (librarySize + _pageSize)) { List<Guid> currentIds = _mediaLibraryService.GetSongs(i, _pageSize); foreach (Guid id in currentIds) { Song currentItem = _mediaLibraryService.GetMediaInfo(id); if (null != currentItem) { AddSong(currentItem); } } i += _pageSize; } }
Where the AddSong() method looks something like this:
private delegate void AddSongDelegate(Song song); private void AddSong(Song song) { if (InvokeRequired) { // Invoke AddSong on the UI thread (since this gets called from the worker thread) this.BeginInvoke(new AddSongDelegate(AddSong), new object[] { song }); } else { if (null != song) { _mediaLibrary.Add(song); // {... Add the appropriate TreeNodes to the TreeView ...} } } }
That's it. Now, the playlist UI comes up immediately and the library pane is updated a few songs at a time, while still remaining responsive to the user. One thing to note -- the only time we access our member collection (_mediaLibrary) is after we know we're on the UI thread. So, we shouldn't have any problems with concurrent access to the collection (reader / writer problem). As a result, we don't have to mess around with locking the collection or using the SynchronizedCollection<Song> class. Of course, I could be wrong, and often am.
The source for this version is available here. Now that things are functional (i.e., we've proven the concept), it's probably time to revisit the design and implementation with an eye for error handling, testing, usability, etc. Since the service layer is the foundation for a potentially large number of clients, we should probably start there.
Disclaimer The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.