This is the fifth in a series.
This time, let's concentrate on the manipulations a user will want to do on the media library (retrieving songs, genres, artists, etc.). For this, we'll add another WCF Service to the MediaPlayerRemoteListener project called MediaLibraryService. The service contract for this will looks something like:
[ServiceContract]
public interface IMediaLibraryService
{
[OperationContract]
List<Song> GetMediaLibrary();
}
With that, all we need to do is plug in the implementation on the server side and start making use of it on the client side. Of course, this is were things get difficult.
Once again, I find myself at odds with the tools. I've labeled both service contracts with the "ServiceContract" attribute. I've also been very careful to use the "DataContract" attribute for any data items returned from our service (in this case, the Song data contract). However, when adding references from the client via the Add Service Reference option, I am faced with two copies of the Song data contract on the client -- one in the MediaLibrary namespace, and one in the NowPlaying namespace. So, if I get a List<Song> from my MediaLibrary service, and I want to send them to the Play() method of the NowPlaying service, I have to convert the MediaLibrary.Song to a NowPlaying.Song. This leads to a large number of mapper classes (or one cleverly designed generic mapper that uses reflection). I recall similar difficulties with the old .NET 2.0 webservices, and I had hoped this challenge had been overcome in the new tool set. Apparently not.
I supposed it's prudent to talk about the idea of a data contract. Should separate services need to share the exact same data definition as an input or an output? One could argue that if they're so similar, they should be a part of the same service. However, the simple truth is that it happens quite often in the real world. For example I'd like to host one interface for read only access and one interface for read/write access. This allows separate levels of permissions / authentication / protocol for each interface.
One solution is to revert to command line tools with the new /shareTypes parameter to wsdl.exe (via http://www.theserverside.net/tt/blogs/showblog.tss?id=WSStrikesBackP6). But, this makes the already tedious task of updating our service reference all the more tedious. Every time I change my contract (which has happened several times so far), I have to run the hosted version of my service library, escape to the command line and run a batch file refreshing the service reference in our client application. Of course, now that I write it down, it's not that much more difficult than changing the port numbers in my app.config and running the built-in refresh command.
Here's another option. Given our current implementation, we may be able to change the interfaces to remove any need to share data contracts. Does the Play method really need the entire song, or just some form of unique identifier to allow the media player to add it to the play list? It seems like we may be able to get by with a single GetMediaInfo method returning all of the Song information, and simply passing around some form of unique identifier for all of the rest of the methods. So we end up with interfaces like this:
[ServiceContract(Namespace = "http://www.cavinconsulting.com/MediaPlayerRemote/")]
public interface INowPlayingService
{
[OperationContract]
PlayState GetPlayState();
[OperationContract]
string GetCurrentSong();
[OperationContract]
List<string> GetCurrentPlaylist();
[OperationContract]
void VolumeUp(int amount);
[OperationContract]
void VolumeDown(int amount);
[OperationContract]
void SetVolume(int volume);
[OperationContract]
void MoveNext();
[OperationContract]
void MovePrevious();
[OperationContract]
void MoveToSong(string song);
[OperationContract]
void Play();
[OperationContract]
void Pause();
[OperationContract]
void Stop();
[OperationContract]
void SetRandom(bool random);
[OperationContract]
void SetRepeat(bool repeat);
[OperationContract]
void SetMute(bool mute);
[OperationContract]
void PlayPlaylist(List<string> playlist, bool appendToCurrentPlaylist);
}[ServiceContract(Namespace="http://www.cavinconsulting.com/MediaPlayerRemote/")]
public interface IMediaLibraryService
{
[OperationContract]
List<string> GetMediaLibrary();
[OperationContract]
Song GetMediaInfo(string song);
}
This has the advantage of sending around significantly less data than our previous version. It has the downside of passing primary keys around as strings (I think WMP uses a 128 bit GUID). This isn't exactly what I had in mind for a solution, but it seems to be the direction the tools are pushing me. I'm still not sure this makes sense from a practical standpoint, but we'll keep on following this path for a while longer.
The source for this post is available here. There is a really bad implementation of a ListBox that supports being a DragDrop source as well as destination, and a horrible implementation of displaying a media library in a TreeView (BTW, Drag and Drop on playlists only works for songs in this example).