Once I figured out how to update data-bound collections from a background thread, things progressed rather smoothly. But, when it was time to implement the "Now Playing" pane, I wasn't working with a collection of items anymore. No worry, just implement INotifyPropertyChanged the same way we did INotifyCollectionChanged (with the SynchronizationContext "hack") and all was well. Or so I thought.
The NowPlayingService exposes the album art for the media item currently being played as a System.Bitmap. I chose that because it appeared to navigate nicely across WCF services. While that is true, they don't bind very nicely to any property of a WPF Image control. As far as I can tell, there are a few options to solve this problem: implement a System.Bitmap to System.Windows.Media.Imaging.BitmapSource TypeConverter. dig through the documented converter classes to see if there might actually be one implemented for you (still possible, but I couldn't find one), or just do the conversion in the business object and expose the image property as the appropriate System.Windows.Media.Imaging.BitmapSource.
I decided the last option was easiest. So, I exposed a BitmapSource property called AlbumArt and assigned it from the System.Bitmap with something like this:
private System.Windows.Media.Imaging.BitmapSource FromBitmap(System.Drawing.Bitmap bitmap) { return System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap( bitmap.GetHbitmap(), IntPtr.Zero, System.Windows.Int32Rect.Empty, System.Windows.Media.Imaging.BitmapSizeOptions.FromEmptyOptions()); }
That worked great, synchronously. When I started updating it in the background thread (on a periodic timer), I got a "System.Windows.Xaml.XamlParseException: System.Windows.Data.BindingExpression' value cannot be assigned to property 'Source' of object 'System.Windows.Controls.Image'. The calling thread cannot access this object because a different thread owns it. Error at object 'System.Windows.Data.Binding' in markup file ...". What? I'm marshalling across threads correctly. Everything else works. What could be going on here, and why?
Non sequitur - In a previous life, I was a C++, Win32, COM, and ATL developer. I was also an ESRI ArcObjects (a pretty large COM API for automating ESRI ArcMap) developer. As soon as I started working with .NET, I grabbed a copy of Adam Nathan's excellent book on COM Interop. With that, I sought to understand as much as I could regarding the interoperability between COM and .NET for I wanted to take advantage of the .NET UI from within my COM business model.
The inclusion of things like .Interop. in the namespace and GetHbitmap() in the arguments immediately led me to the potential that the returned BitmapSource reference was a simple wrapper around the HBITMAP in the System.Bitmap reference. By the fact that the UI is complaining about accessing an object in a different thread, I surmise that the HBITMAP of the System.Bitmap is in Thread Local Storage rather than global memory. So, I altered the conversion process to something like this:
private System.Windows.Media.Imaging.BitmapSource BitmapSourceFromImage(System.Drawing.Image img) { System.IO.MemoryStream memStream = new System.IO.MemoryStream(); img.Save(memStream, System.Drawing.Imaging.ImageFormat.Png); System.Windows.Media.Imaging.PngBitmapDecoder decoder = new System.Windows.Media.Imaging.PngBitmapDecoder( memStream, System.Windows.Media.Imaging.BitmapCreateOptions.PreservePixelFormat, System.Windows.Media.Imaging.BitmapCacheOption.Default); return decoder.Frames[0]; }
With the business object exposing a copy of the bitmap, all is well. However, I am finding myself, once again, questioning whether it should be the job of the business object to know whether it's being used from a different thread. I guess it's a question of determining who is responsible for ensuring thread safety. Ultimately, it was the business object's fault for exposing a wrapper around thread local storage rather than heap storage.
Disclaimer The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.