This is the eighth in a series, but the numbering is getting confusing, so I'm going to stop counting. The first step I took in creating my pretty WMP remote control UI was to steal design the pretty UI. Once I had the UI, it was a matter of binding the media library entries to the appropriate WPF elements (a listbox). I used some pretty standard dataset creation / binding logic I found somewhere that makes use of the ObjectDataProvider. To implement this, I created a MediaLibrary and Factory class that looked something like this: public class MediaLibrary : ObservableCollection<Song>
{
public MediaLibrary()
{
GetSongs();
}
private void GetSongs()
{
IMediaLibraryService mediaLibrary = new MediaLibraryServiceClient();
int librarySize = mediaLibrary.GetSongCount();
int i = 0;
while (i < (librarySize + _pageSize))
{
List<Guid> currentIds = mediaLibrary.GetSongs(i, _pageSize);
foreach (Guid id in currentIds)
{
Song currentItem = mediaLibrary.GetMediaInfo(id);
if (null != currentItem)
{
this.Add(currentItem);
}
}
i += _pageSize;
}
}
}public class Factory
{
/// <summary>
///
/// </summary>
static Factory()
{
Songs = new MediaLibrary();
}
public static MediaLibrary Songs { get; private set; }
}
And I bound the contents to the UI using syntax like this:
<Grid>
<Grid.Resources>
<ObjectDataProvider x:Key="FactoryDS" ObjectType="{x:Type local:Factory}"/>
<DataTemplate x:Key="SongTemplate">
<StackPanel>
<TextBlock Text="{Binding Title}"/>
</StackPanel>
</DataTemplate>
</Grid.Resources>
<ListBox x:Name="songListBox"
ItemsSource="{Binding Songs, Mode=Default, Source={StaticResource FactoryDS}}"
RenderTransformOrigin="0.5,0.5"
DisplayMemberPath="Title" />
</Grid>
That worked great -- synchronously. But, as we already found out, a synchronous call to the media library webservice isn't the best option (we'd rather page through the results asynchronously, updating the UI while we go). So, I add a background thread, changing the constructor to something like this:
public MediaLibrary()
{
System.Threading.Thread thread =
new System.Threading.Thread(new System.Threading.ThreadStart(GetSongs));
thread.Priority = ThreadPriority.BelowNormal;
thread.Start();
}
Run this, and you get a nasty cross-thread exception: "System.NotSupportedException: This type of CollectionView does not support changes to its SourceCollection from a thread different from the Dispatcher thread." Huh? What does my business layer need to know about threading? I'm just updating a collection. But, not to be intimidated easily, I try other options. How about implementing INotifyCollectionChanged rather than inheriting from ObservableCollection? Same problem. So, Google reveals a nice discussion by ILoggable. He's as frustrated as I am. Really, should my data access layer know whether it's running on the UI thread? I don't think so. Shouldn't the Binding mechanism in WPF assume that I will be updating my business objects in background threads? After all, what's the cost of a 4 core processor nowadays?
I found a bunch of solutions to this problem that relied on explicitly making calls to Application.Current.Dispatcher.BeginInvoke. That seems sleazy to me. One, slightly less sleazy solution I found was to pass in a SynchronizationContext to the MediaLibrary class. This still sounds like the business objects need to know too much about the runtime, but at least SynchronizationContext is in the System.Windows.Threading namespace, so it doesn't completely destroy my hopes of separating UI, business, and data.
The solution I finally came up with (borrowed heavily from here) was a MediaLibrary with these changes:
public classMediaLibrary : List<Song>, INotifyCollectionChanged { private const int _pageSize = 5; privateIMediaLibraryService _mediaLibrary = newMediaLibraryServiceClient(); privateSynchronizationContext _synchronizationContext;
/// <summary> /// /// </summary> publicMediaLibrary(SynchronizationContext sync) { _synchronizationContext = sync; System.Threading.Threadthread = newSystem.Threading.Thread(newSystem.Threading.ThreadStart(GetSongs)); thread.Priority = ThreadPriority.BelowNormal; thread.Start(); } private voidGetSongs() { IMediaLibraryService mediaLibrary = newMediaLibraryServiceClient(); int librarySize = mediaLibrary.GetSongCount(); int i = 0; while(i < (librarySize + _pageSize)) { List<Guid> currentIds = mediaLibrary.GetSongs(i, _pageSize); foreach (Guid id incurrentIds) { SongcurrentItem = mediaLibrary.GetMediaInfo(id); if(null!= currentItem) { this.Add(currentItem); SafeCollectionChangedNotification(currentItem); } } i += _pageSize; } }
private voidSafeCollectionChangedNotification(SongcurrentItem) { if(_synchronizationContext != null) { _synchronizationContext.Post(delegate { OnCollectionChanged(currentItem); }, null); } else { OnCollectionChanged(currentItem); } }
private voidOnCollectionChanged(SongcurrentItem) { if(null!= CollectionChanged) { CollectionChanged(this, newNotifyCollectionChangedEventArgs( NotifyCollectionChangedAction.Add, currentItem)); } }
#regionINotifyCollectionChanged Members
public eventNotifyCollectionChangedEventHandler CollectionChanged;
#endregion
and passing in the context from the factory:
Songs = new MediaLibrary(
new System.Windows.Threading.DispatcherSynchronizationContext(
System.Windows.Application.Current.Dispatcher));
Of course, this means the factory needs to know what context to pass in, but I wasn't convinced the factory needed to be in the service layer anyway. Really, it's just there so WPF has some form of static class to bind to.
I'm still not sure what I think about this approach. While it works, and can even be used within a WinForms application (and probably even in ASP.NET), it seems like we're putting the onus on the event producer rather than the even consumer (who is, after all, the one who knows whether the event should be caught on a particular thread). I guess that is one of the things I like about the event model in the Smart Client Software Factory (SCSF). The producer just raises the even. If the consumer has specific thread constraints on consuming that event, they declare them explicitly. Hmm. Maybe I should start looking at using the SCSF to implement this thing...
This is the seventh in a series. As of Part 5, we had a functional end to end solution (let's call it version 1.0) that I deployed across my enterprise household. So, what's next? As with any product, we know things don't stop here -- the clients have additional expectations for functionality, the developers find new technology that would be cool to use, and the QA team finds bugs. Now that it is deployed, however, I'll have to pay special attention to version handling within our data and operation contracts. Before making many changes, I decided it might be nice to have some unit tests to make sure I didn't break anything when rolling out new versions. A few tests later and I was glad I did. It turns out, I didn't write perfect code during my initial creation (shocking, I know). There were a few off by one errors, some stupid copy and paste errors, and a couple of functions that were just plain wrong. So, even though I didn't do test driven design, per say, I did gain the benefits of a module with reasonable unit test coverage. So far, the biggest complaint observation from the user base has been that the client application sucks. While it's functional, it certainly isn't pretty. Additionally, it isn't the most robust implementation. For example, if you close the server application and start it back up, the client never reconnects. This is because, once a WCF service proxy faults, it stays faulted until you do something about it. So, the next phase will be to write a more robust, prettier version of the desktop client. Let's start with pretty. The first step in creating a prettier application is, as is standard in our industry, to poach someone else's look and feel. While I certainly make my living within the Microsoft camp, the innovative UI solutions fall solidly within the Macintosh camp. So, let's take a look at Front Row. There's a great WPF implementation based on some of the Front Row functionality in a "tutorial" called /backRow. It's been a while since the author updated the article, and the Macintosh camp could do nothing but complain about the features that were missing. But, I'm no artist, so I figure I'll start with the look and feel as presented in his article but with a slightly different animation implementation. More on that later. Source for this article will be available as it becomes ready. The next few segments will cover some of the challenges I ran into while developing the newer, prettier UI. In the mean time, here are a couple of screen shots: 
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.
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).
This is the fourth in a series. With our "design" out of the way, we can start digging in to the actual implementation. Our first draft will be somewhat exploratory in nature. Given my limited knowledge of the Windows Media Player plug-in architecture, I have decided to start by hosting the WMP ActiveX control in a standalone WinForms application, and have this WinForms application host a WCF service. So, here goes: We'll start off by designing the communication contract through which our client controls and queries our media player. For now, we'll concentrate on controlling the media player. For this, we'll use a WCF contract called INowPlayingService: [ServiceContract]
public interface INowPlayingService
{
[OperationContract]
PlayState GetPlayState();
[OperationContract]
Song GetCurrentSong();
[OperationContract]
List<Song> GetCurrentPlaylist();
[OperationContract]
void VolumeUp(int amount);
[OperationContract]
void VolumeDown(int amount);
[OperationContract]
void SetVolume(int volume);
[OperationContract]
void MoveNext();
[OperationContract]
void MovePrevious();
[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]
List<Song> AddToCurrentPlaylist(List<Song> playlist);
[OperationContract]
void PlayInPlaylist(Song song);
[OperationContract]
void ReplaceCurrentPlaylist(List<Song> playlist);
}
That looks like a pretty comprehensive list of operations we would want to perform on a media player. So, let's get down to the business of actually implementing this in Visual Studio 2008. I start a new solution and add three projects, a WinForms host application, a WinForms Client application, and WCF Service Library. I add a project reference from the host application to the service library. Then, I notice I can add a service reference from the client application to the service library. Right click on the project, click add service reference, click discover->services in this solution, and choose the service of interest. Nice! Or, so I thought.
Here's where the first problem arises. Remember, I'm using this as an exercise to get my head around the new technologies. The WCF service library project type does a nice job of housing the service components as well as publishing the appropriate meta data for things like adding service references. However, when I go to debug this application, VS automatically spins up the WcfSvcHost process to host our service library. But, remember, I want to host the WCF endpoint within my host WinForms application. No matter what projects you tell VS to start up (or not start up) on debug, it ALWAYS runs the WcfSvcHost application and starts hosting my service library.
It turns out, I'm not the first person to have this problem: Visual Studio 2008, WCF Service Libraries, and CTRL-F5. It appears that the solution is to "remove the project type GUID" that tells VS that it's a WCF Service Library. What? So, in order to host a WCF service library in my own application, I have to disable all of the helpful aspects of VS regarding WCF endpoints (like, say, updating my service reference as the contract changes over time). Ridiculous.
Here's how I finally resolved it. Leave it as a WCF service library, but change the port values in the app.conf sections so my host uses a different port than the WCF service library. That way, in the course of normal debugging, the client points to the ports opened by my host. When I need to update the reference, I change the port to the VS host port, update the reference, and then change it back. It's a nasty bit of kludgery, but it works for my needs. It seems like I must be making a habit of doing things Microsoft doesn't expect. First, I expected a standard windows application to be simple in WPF, now this.
Anyway, source code (so far) is available here. You'll want to change the URL property of the media player control in the host application. You'll also want to set the startup projects so that the host and the client are both started. It's a pretty basic remote control with standard navigation utilities. Next, we'll flesh out some of the media library functionality so we can change what gets played.
This is the third in a series. Design As with any good development effort, we begin by designing the solution, complete with an assessment of the pros and cons of each option. Of course, as with any good development effort, we begin with every intention of designing the solution, but we usually skip right past this to writing the code since designs are never any fun. Design: There is a server and there are 0 or more clients that want to control what the server spits out the sound card. Right. With the design done, let's talk about implementation options: As I see it, there are two options for implementing the functionality as exhaustively enumerated in Part 0 of our series: 1) A standalone application in which we host and automate a Windows Media Player control, 2) A plug-in to the existing standalone Windows Media Player in which we automate the instance of Windows Media Player in which we are hosted. So, which of these approaches is better? I have no idea. So, we'll design the approach in such a way that the end user (client machine) doesn't care. How? SOA, WCF, and other TLA's. I figure I can hide my media playing component/service/application behind a WCF connection point. Then, if hosting the ActiveX control fails miserably or creating the WMP plug-in doesn't play nicely with .NET, then I'll always be able to call some arcane WIN32 function to dump directly to the sound card or convert to Linux, convert all of my WMA's to raw .au files and just cat [file.au] > /dev/audio. Of course, that's an exaggeration. I'm sure one of the two approaches will work fine. I just don't know which. So, hiding the solution behind a WCF connection point should give me the flexibility to try both approaches without rewriting the clients. At this point, I find that there are a couple of interesting questions to which I do not know the answer. This, of course, will not keep me from plunging headlong into the implementation. They're just questions. - If I host an instance of the WMP ActiveX control, does it still do all of the clever background work of sniffing media information from the Internet, listening for changes in my media library, and downloading album art or is it just a numb playback machine using the existing media library?
- What are the multi-threading characteristics of the WMP library? Will I have to perform the read/write locks, or will WMP take care of that for me?
Build or Buy decision This is the second in a series. The overview is here. State of the industry: So far, I can find a bunch of solutions to stream media to a client. That is, given a server containing a bunch of media, Spit it out to a client machine (PC, cell phone, television, etc.) so you can consume it wherever you are. This doesn't cut it for our purposes. Instead, we want the media to play at the source, but be able to change the behavior of the media player at the host from a client somewhere. Items of interest: So far, that's all I have. So, if I want a remote control to work on a laptop, pocket pc, smartphone, (and maybe a website), it looks like I'm out of luck in the commercial market. So, it looks like the build option is the one for me. Of course, that's what I expected to choose. Otherwise, this would have been a really short series.
This is the beginning in a series of unknown length regarding controlling the Windows Media Player from remote machines. So far, I'm at the requirements phase. Next will come industry research (buy or build), but I'm pretty sure I'm going to build something anyway (even if there are perfectly usable buy options -- I'm just stubborn that way).
Here are the requirements in no particular format or order:
- I have a Server 2003 machine running with its audio output hooked up to the whole house audio system (okay, it's just two speakers in a different room, but there's a volume control in there, so I can call it what I want). The server houses the media collection and runs a uPnP server (TwonkyMedia) for the rest of the machines in the house to stream.
- I can play audio through the server if I remote terminal to the server, run media player, and choose some songs. That, of course, completely fails the Wife Acceptance Factor (WAF).
- What I would like is a service or application running on the server to be ready to play at all times, and have the rest of the machines (including handheld and smartphone) be able to dynamically change the playlist, start, stop, fast forward (most important) and rewind.
- I'd like this to run as part of Media Player. I'm not sure why, since SOA dictates that we're just exposing a service, not a particular technology. I think it's mainly because I'm used to maintaining the media library in WMP and I don't want to change.
- An added bonus is if there were a sidebar gadget for my wife's Vista machine (yes, I'm still on XP until some of the performance issues are worked out), and a 10' UI for the media center PC in the family room.
- Since the uPnP server streams media to all of the important places (laptop and media center), there's no reason to mess around with streaming the actual audio to the remote control client, though the now-playing art would be nice.
That's it. In those few, short paragraphs, I have managed to cut out a lot of work for myself. I'm looking for a media player with remote controls that run on smartphone, pda, xp, and as a sidebar gadget. I'd like it to run on server 2003 every time it starts up, be multi-remote control robust, and extremely fault tolerant.
Now, to the buy or build comparisons...
The Scribble application is back in Visual Studio 2008! Ok, it's not WPF, but at least it exists (http://msdn2.microsoft.com/en-us/library/06s5xsah(VS.80).aspx). It even has a tutorial on extending the application to include a plug-in model (http://msdn2.microsoft.com/en-us/library/11z7687s(VS.80).aspx). It probably pays to start with the WinForms version of the example. Make sure you change the target CPU to Win32 (unless you're on an Itanium) so you'll be able to compile and debug. From there, you can wander through the code and figure out why all of the UI is created via code rather than the clever UI form designer that has been so nicely integrated into VS Orcas (2008). If you figure it out, let me know. Reading UI and UI layout code isn't my idea of a good time. After the WinForms example, take a look at the MFC sample. It's a little involved, but definitely a nice example of integrating native C++/MFC with managed libraries using managed C++ proxy classes to expose managed interfaces to the plug-in libraries.
This segment looks at localization of button toolips and the descriptive text that appears in the statusbar when a menu item or button is active. The code for this segment is available at http://www.cavinconsulting.com/Code/Scribble11.zip. The general idea is that we want to give helpful hints to our user when if comes to deciphering the little icons on the screen. Normally, we attach tooltips to tell the user what each icon does. However, some application go the extra step to make it clear what a command does by placing a brief description of the actions in the status bar. This description appears when the menu item or toolbar button becomes "active" and goes away when the button becomes inactive. For our purposes, Active will mean the mouse is over a button and or that a menu item is the focus item on in the application (the latter lets us display description information even if the menu becomes active via Alt+f,arrow keys). To implement the GotFocus and LostFocus event handlers for the MenuItems, I went down a number of paths including style triggers, control templates, and adding event handlers to each MenuItem individually. The implementation I landed on was to apply an EventSetter to the MenuItem style like this: <Window.Resources>
<Style TargetType="{x:Type MenuItem}" x:Key="{x:Type MenuItem}" >
<EventSetter Event="GotFocus" Handler="OnGotFocus"/>
<EventSetter Event="LostFocus" Handler="OnLostFocus"/>
</Style>
</Window.Resources>
From there, we change the declaration of each MenuItem to look like this:
<MenuItem Header="{x:Static scribble:ScribbleCommands.FileMenuText}">
<MenuItem Command="{x:Static scribble:ScribbleCommands.New}" />
<MenuItem Command="{x:Static scribble:ScribbleCommands.Open}" />
<MenuItem Command="{x:Static scribble:ScribbleCommands.Save}" />
<MenuItem Command="{x:Static scribble:ScribbleCommands.SaveAs}" />
<Separator />
<MenuItem Command="{x:Static scribble:ScribbleCommands.Exit}" />
</MenuItem>
Now, all we have to do is implement the event handlers:
public void OnGotFocus(object sender, RoutedEventArgs e)
{
DisplayCommandDescription(sender);
}
public void OnLostFocus(object sender, RoutedEventArgs e)
{
ClearCommandDescription(sender);
}
Now, the implementation of the Display and Clear functions can look something like this:
private void DisplayCommandDescription(object sender)
{
MenuItem item = sender as MenuItem;
if ((null != item) && (null != item.Command))
{
ScribbleCommand command = item.Command as ScribbleCommand;
if (null != command)
{
StatusText = command.Description;
}
}
ScribbleButton button = sender as ScribbleButton;
if (null != button)
{
StatusText = button.Description;
}
}
private void ClearCommandDescription(object sender)
{
StatusText = ScribbleCommands.StatusbarText;
}
Of course, if you compile and run at this point, you'll get an error about StatusText not existing. That's because it doesn't. Of course, you could just change that to _statusBar.Content, but that would be directly manipulating the UI from code. According to the rumor, it's better to set a property somewhere that notifies the observers that it has changed. So, that's what we'll do, by adding a dependency property to our main window:
static MainWindow()
{
StatusTextProperty =
DependencyProperty.Register(
"StatusText", typeof(string), typeof(MainWindow),
new FrameworkPropertyMetadata(String.Empty, FrameworkPropertyMetadataOptions.AffectsRender));
}
public static DependencyProperty StatusTextProperty;
public string StatusText
{
get
{
return (string)GetValue(StatusTextProperty);
}
set
{
SetValue(StatusTextProperty, value);
}
}
Now, we can databind the status bar's content to the main window's StatusText dependency property (we've added x:Name="main" to the Window tag at the top):
<StatusBar DockPanel.Dock="Bottom"
Visibility="{Binding Path=IsChecked, Converter={StaticResource BooleanToVisibilityConverter}, ElementName=_viewStatusBar}">
<Label x:Name="_statusBarText" Content="{Binding ElementName=main, Path=StatusText}" />
</StatusBar>
Now, a little bit about the ScribbleCommand class. The ScribbleCommands class was getting a little tedious, so I wrote the ScribbleCommand class to encapsulate all of the information we need in order to attach to a menu item or a button. It inherits from RoutedUICommand so it plays nicely with the expectations of WPF. Really, it's just a RoutedUICommand with additional properties for Description and Tooltip.
For the status bar text on the buttons, I tried a different approach. Rather than checking for a command of a particular type in the activate and deactivate handlers, I checked if it was one of my own buttons that had a Description property. This helped in a few ways, one of which was that my inherited class was able to override the Tooltip property with that of the attached command. I'm not sure if this is a good approach or a blatant violation of some of the separation principles we were hoping to instill with the command pattern. I'll lay it out, anyway, and we'll see what we can make of it later.
The ScribbleButton inherits from Button, and has a text dependency property called Description (just like StatusText above). Also, it listens to the OnPropertyChanged event to see when the Command property is changed. If we get attached to a ScribbleCommand, then we set our tooltip and description according to the ScribbleCommand.
static ScribbleButton()
{
DescriptionProperty = DependencyProperty.Register(
"Description", typeof(string), typeof(ScribbleButton),
new FrameworkPropertyMetadata(String.Empty, FrameworkPropertyMetadataOptions.AffectsRender));
}
public static DependencyProperty DescriptionProperty;
public string Description
{
get
{
return (string)GetValue(DescriptionProperty);
}
set
{
SetValue(DescriptionProperty, value);
}
}
protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
{
base.OnPropertyChanged(e);
if(0==string.Compare(e.Property.Name,"Command"))
{
ScribbleCommand newCommand = e.NewValue as ScribbleCommand;
if (null!=newCommand)
{
ToolTip = newCommand.Tooltip;
Description = newCommand.Description;
}
}
}
We do something similar to the GotFocus, LostFocus trick to hook up mouse focus:
<Style TargetType="{x:Type scribble:ScribbleButton}" x:Key="{x:Type scribble:ScribbleButton}" >
<EventSetter Event="MouseEnter" Handler="OnMouseEnter"/>
<EventSetter Event="MouseLeave" Handler="OnMouseLeave"/>
</Style>
And that's it. We finally have an application that looks like an application, localizes nicely, and takes advantage of some of the new features of WPF. I think this is probably the end of the Scribble tutorial, as such. I'm still not entirely convinced I've learned the localization model MS is trying to get across. Every example I see uses that LocBaml example and editing values in Excel. I'm not entirely convinced that's the correct route either.
|