DownloadCompleted do not refresh Image control?

Oct 7, 2008 at 4:56 PM
Edited Oct 7, 2008 at 4:58 PM
Hi,

First, thanks for your great article about custom bitmaps in WPF. I learned lots of things!

But I was surprised by the fact that the DownloadCompleted does not trig the refresh of an Image control. It sounds logical that this is the mechanism used by the BitmapImage class, when a remote image (URL) is used as the source. I mean, when assigning a BitmapImage to the Source property of an Image control, the IsDownloading is True and the size (in pixels) is 1x1. CopyPixels is then called with a very small buffer (4 bytes if bgr/bgra). As far as I know, the only way for the Image control to get notified that it should refresh itself is by listening to the DownloadCompleted event. So, I wondering why, if I raise myself the DownloadCompleted event in a custom class, the Image control is not refreshed?

Here's what I did: I created a new class, CMyBitmapSource, which derives from BitmapSource, and which only acts as a wrapper around a BitmapImage. All overridden stuff delegate to the wrapped object. So, when the "internal" DownloadCompleted occurs, the external "DownloadCompleted" is raised, which should cause the refresh of the listening Image control(s). But the event seems to be ignored when it is not directly raised by a BitmapImage class instance. I tested the event by registering a handler which output a debug line in the console, and this line appeared. Also, a breakpoint on the delegate object (multicast delagate) reveals that two delegates will be called (invocation count = 2).

Here's some observations:

1) As soon as the CMyBitmapSource instance is assigned to the Source property of an Image control, a new event handler is registered with the DownloadCompleted event. The target of the delegate is a System.WeakReference object, which itself targets the Image control.

2) The IsDownloading property is never called. So, I think this enforces the fact that the only way for an Image control to know it should refresh itself is by listening to the DownloadCompleted event.

3) The call order is:

    3a) A new handler is registered for the DownloadCompleted event, which targets the Image control.
    3b) CopyPixels is called with a small buffer (4 bytes).
    3c) DownloadCompleted is raised by the internal BitmapImage. The event is then re-raised to the listening clients.

At this point, isn't logical that CopyPixels should be recalled to get the downloaded data? I suspect this is what happens with the BitmapImage class, but this does not work with derived classes :o(

Here's the class I used. Maybe you will see something I missed?

The markup:

    <StackPanel>
        <Button x:Name="MyButton" Click="MyButton_Click">Click</Button>
        <Image x:Name="MyImage"></Image>
    </StackPanel>

 

 

 


The code-behind:

        private void MyButton_Click(object sender, RoutedEventArgs e)
        {
            BitmapImage i = new BitmapImage(new Uri("http://www.microsoft.com/presspass/presskits/windowsvista/images/icons/Security.jpg"));
            CMyBitmapSource b = new CMyBitmapSource(i);
            MyImage.Source = b;

 

            b.DownloadCompleted += new EventHandler(b_DownloadCompleted);
        }

        void b_DownloadCompleted(object sender, EventArgs e)
        {
            Console.WriteLine("Completed!");
        }



The class:

    class CMyBitmapSource : BitmapSource
    {
        BitmapSource m_bmp;
        EventHandler m_DownloadCompleted;

        public CMyBitmapSource() {
        }
    
        public CMyBitmapSource(BitmapSource pbsSource)
        {
            m_bmp = pbsSource;
            m_bmp.DownloadCompleted += new EventHandler(m_bmp_DownloadCompleted);
        }
    
        protected override Freezable CreateInstanceCore() {
            return new CMyBitmapSource();
        }
    
        void m_bmp_DownloadCompleted(object sender, EventArgs e)
        {
            if (m_DownloadCompleted != null)
                m_DownloadCompleted(sender, e);
        }
    
        public override void CopyPixels(Int32Rect sourceRect, Array pixels, int stride, int offset)
        {
            m_bmp.CopyPixels(sourceRect, pixels, stride, offset);
        }

        public override void CopyPixels(Array pixels, int stride, int offset)
        {
            m_bmp.CopyPixels(pixels, stride, offset);
        }

        public override void CopyPixels(Int32Rect sourceRect, IntPtr buffer, int bufferSize, int stride)
        {
            m_bmp.CopyPixels(sourceRect, buffer, bufferSize, stride);
        }

        public override event EventHandler DownloadCompleted
        {
            add
            {
                m_DownloadCompleted += value;
            }
            remove
            {
                m_DownloadCompleted -= value;
            }
        }
        public override event EventHandler<ExceptionEventArgs> DecodeFailed;
        public override event EventHandler<ExceptionEventArgs> DownloadFailed;
        public override event EventHandler<DownloadProgressEventArgs> DownloadProgress;
    
        public override double DpiX {
            get {
                return m_bmp.DpiX; }}

        public override double DpiY {
            get {
                return m_bmp.DpiY; }}

        public override PixelFormat Format {
            get {
                return m_bmp.Format; }}

        public override double Height {
            get {
                return m_bmp.Height; }}

        public override double Width {
            get {
                return m_bmp.Width; }}

        public override int PixelHeight {
            get {
                return m_bmp.PixelHeight; }}

        public override int PixelWidth {
            get {
                return m_bmp.PixelWidth; }}

        public override BitmapPalette Palette {
            get {
                return m_bmp.Palette; }}

        public override bool IsDownloading {
            get {
                return m_bmp.IsDownloading; }}
    }


Thanks for your help!
Coordinator
Oct 15, 2008 at 7:20 PM
Yes, you are encountering the core issue.  WPF bitmaps are designed primarily for static bitmaps (simple PNG, JPG, etc).  A bitmap is represented as a "resource" and is marshalled to the rendering thread.  Once this is done, WPF generally does not refresh the contents again since they are resumed to be static.  There are a couple of exceptions, including the WriteableBitmap class.  Of course, when you start downloading the bitmap contents on a seperate thread, even "static" bitmaps become effectively "dynamic".  There are a few bugs in WPF that break the scenario where the bitmap download completes after the bitmap has been marshalled to the render thread.  The team is aware of the issue, and is working on a fix.
Oct 15, 2008 at 9:59 PM
In fact, what surprised me is the fact that the IsDownloading property is never read.

Here's what sounds logical to me :

On the first time an Image control needs to be rendered, the first thing the rendering thread should do is to check if the data is available by reading the IsDownloading property. If the data is not available (ie, IsDownloading is True), the DownloadComplete should then be monitored. When the event is fired, the data should be read, and only from this time the bitmap should become "static", so the next DownloadComplete events are ignored.

In other words, the object model allows to build custom asynchronous bitmap source (which may become static when fully downloaded), but it seems that there's some tweaks in WPF to only allow the BitmapImage class to work this way...
Sep 24, 2015 at 9:55 AM
The problem described in your original article about WPF not refresh the rendering after download finishes seem to still exist in WPF4 (.NET 4.5)

Is there going to be a fix to this at all?
Coordinator
Oct 6, 2015 at 9:52 PM
I am not aware of any planned work to address this problem.
Coordinator
Oct 7, 2015 at 4:21 AM
Actually, I talked to the product team and they asked for you to file a connect issue so that it gets on their radar. I don't know the likelihood of this being fixed, but it would be good to get it into the system.

Report Issue in .Net 4.6
Aug 3, 2016 at 11:54 PM
Did you ever log an issue on connect? I want to up-vote it.

I have an (ugly) workaround though. I found that if you set the internal field _needsUpdate to true when download completes before raising DownloadCompleted, things work!

Like this:
var f = typeof(BitmapSource).GetField("_needsUpdate", BindingFlags.NonPublic | BindingFlags.Instance);
f?.SetValue(this, true);