Loading Bitmaps: DoEvents and the closure pattern |
Written by Administrator | |||
Wednesday, 16 December 2009 | |||
Page 1 of 2 Sometimes loading a bitmap causes an asynchronous download and you have to wait for it to complete before using it - but how best to wait? The standard solution is to use an event but this breaks what would otherwise be a sequential flow. Can reinventing DoEvents save the day or is the closure pattern a better bet? DoEvents and the closure patternSometimes loading a bitmap causes an asynchronous download and you have to wait for it to complete before using it - but how best to wait? The standard solution is to use an event but this breaks what would otherwise be a sequential flow. Can reinventing DoEvents save the day or is the closure pattern a better bet? For more information on loading bitmaps from local file sources and how to tell if a bitmap is going to be downloaded or not see: BitmapImage and local files BitmapImageAlthough BitmapSource is the base class for most of the bitmap facilities in WPF it is the BitmapImage class that you are likely use most of the time. The BitmapImage class has all of the methods of the BitmapSource class but as the Create method is static and only creates BitmapSource objects you need alternative methods of creating a BitmapImage. In most cases you will use BitmapImage with a bitmap file either stored on the local disk or stored on a web server. Loading a bitmap from a web server usually occurs asynchronously and this creates a problem - how do you wait until the bitmap has finished loading? There are no built-in facilities for synchronous blocking bitmap loads. However, we can make up for this lack by either reinventing the "DoEvents" method or, much better, using the closure pattern. There is an additional problem in that you can't know if the system will use a background download or not because it depends on the file's location. In all cases, however, a file specified as "HTTP" will be downloaded in the background. Loading BitmapsWhen you create a BitmapImage you can specify a URI to the file and it will be loaded as and when it is needed. For example: BitmapImage bmi = new BitmapImage( This sets the BitmapImage source to the specified URI and starts it downloading asynchronously. Until the download is complete its properties are set to default values. For example, if you try to use the PixelHeight property you will discover that it is set to one. If you now use the bitmap, for example: image1.Source = bmi; nothing appears until the download is complete. Detecting when loadedThis raises the question of how to detect when a bitmap has finished downloading. The simple answer is that there are a number of events which are triggered during and after the download completes. In particular the DownloadComplete event occurs when the bitmap is ready to use. To add a handler: bmi.DownloadCompleted += new you need to define a method with the correct signature: void downloadcomplete( In this case you can rest assured that you will get the correct PixelHeight. Synchronous blocking downloadThere is also an IsDownloading property that is true if the bitmap is downloading and false if it isn't. The prime use of this property is to decided if the bitmap is being downloaded in the background or not. In this sense you should test the value of this property before assuming that any of the download events will actually occur, but for simplicity this step is left out of the following examples. However if you think about the IsDownloading property you might be tempted to try to convert the download into a synchronous blocking download using something like: while (bmi.IsDownloading) {} This doesn't work because the thread that you are using is the UI thread and the loop that you have written blocks it from updating the IsDownloading property - or anything else for that matter. You can try to wriggle to make this work but it is very difficult if not impossible as there is no WPF equivalent of DoEvents. For example you might try to be kinder and not hog the processor in the while loop by adding: while (bmi.IsDownloading) { but this does no good because all you have succeeding in doing is putting the UI thread to sleep for 1 second and this again stops it updating anything. DoEvents and the DispatcherOne solution to the problem is to reinvent the DoEvents function but this isn't straightforward as WPF uses a different message system to Windows Forms. It doesn't use a message queue and its associated low level functions but a Dispatcher object which most controls are descended from. The UI thread is associated with a single Dispatcher object which manages the messages on their way to the controls via a prioritised queue. The essential point is that the Dispatcher only gets to run when the UI thread has nothing more urgent to do - it doesn't run when you give it a tight loop to execute and putting the UI thread to sleep doesn't help. The only way to allow the Dispatcher object to process its queue is to somehow tell the UI thread to execute the Dispatcher's processing loop. This doesn't seem possible but you can create and run a new processing loop, called a DispatcherFrame, on the same thread. The details of how this all works are quite tricky so let's examine how it works in a step-by-step manner. The simple-minded approach to the problem simply creates a new DispatcherFrame and uses the PushFrame method to start it running: DispatcherFrame frame = This effectively transfers the attention of the UI thread to the new DispatcherFrame which then processes the Dispatcher's queue - and continues to do this forever. What this means is that after PushFrame any subsequent instructions are ignored as the UI thread is running the Dispatcher's queue and never returns. What we need is an action on the Dispatcher's queue that will stop the new DispatcherFrame at an appropriate time. The solution is to simply use the Dispatcher's Invoke method to add an action to its queue with a suitably low priority. When the new low priority action is executed then you can be sure that a lot of higher priority actions that were ahead of it in the queue have been completed. To stop the DispatcherFrame from processing the queue all we have to do is set its Continue property to false. So we need to arrange to add a delegate something like: delegate(object param) to the queue with a low priority to bring the UI thread back to process the remaining instructions. Putting this all together gives: DispatcherFrame frame = Dispatcher.CurrentDispatcher. Notice that the first parameter of the BeginInvoke specifies the priority and how long the DispatcherFrame gets to run depends on how this is set - it will process all actions with a higher priority before returning. Also notice that the variable "frame" is accessible to the delegate because C# implements a closure which keeps the environment that the delegate was defined in active when it is invoked. You can easily turn this into a DoEvents method: public void DoEvents() And with a DoEvents method you can now write a blocking wait for download as: while (bmi.IsDownloading) { Of course in the real world you would need to add a test to make sure that the loop did actually end at some point after a reasonable time. <ASIN:0321485890> <ASIN:1590599543> <ASIN:0596514824> <ASIN:059651610X> <ASIN:0763723398> |
|||
Last Updated ( Friday, 19 March 2010 ) |