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; } }
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...
Disclaimer The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.