Search code examples
c#wpfmvvmdispatcherwriteablebitmap

C# passing property of viewmodel into dispatcher


I have a method which updates the contents of a WriteableBitmap (FrameDataNew) which is a property in my viewmodel (VM):

    public WriteableBitmap FrameDataNew
    {
        get { return frameDataNew; }
        set
        {
            frameDataNew = value;
            OnProptertyChanged("FrameDataNew");
        }
    }
    private WriteableBitmap frameDataNew = null; 

When I receive a new Bitmap from a gstreamer class I have written I update FrameDataNew in order to show the latest frame onscreen. the window is a simple Image control which has its source bound to FrameDataNew.

The following code works fine to do this in my event handler:

    /// <summary>
    /// BitmapCaptured event handler for when a new video frame is received from the IP source
    /// </summary>
    /// <param name="sender">The originating source of the event</param>
    /// <param name="NewBitmap">The new frame in Bitmap form</param>
    private void PipeLine_BitmapCaptured(object sender, Bitmap NewBitmap)
    {
        // render to the screen
        Dispatcher.Invoke(() =>
        {
            // check the existence of the WriteableBitmap and also the dimensions, create a new one if required
            if ((VM.FrameDataNew == null) || (VM.FrameDataNew.Width != NewBitmap.Width) || (VM.FrameDataNew.Height != NewBitmap.Height))
                VM.FrameDataNew = new WriteableBitmap(NewBitmap.Width, NewBitmap.Height, NewBitmap.HorizontalResolution, NewBitmap.VerticalResolution, PixelFormats.Bgr24, null);

            // lock the bitmap data so we can use it
            BitmapData data = NewBitmap.LockBits(new Rectangle(0, 0, NewBitmap.Width, NewBitmap.Height), ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format24bppRgb);

            // lock the backbuffer of the WritebaleBitmap so we can modify it
            VM.FrameDataNew.Lock();

            // Copy the bitmap's data directly to the on-screen buffers
            CopyMemory(VM.FrameDataNew.BackBuffer, data.Scan0, (data.Stride * data.Height));

            // Moves the back buffer to the front.
            VM.FrameDataNew.AddDirtyRect(new Int32Rect(0, 0, data.Width, data.Height));

            // unlock the back buffer again
            VM.FrameDataNew.Unlock();

            // 
            //NewBitmap.UnlockBits(data);
        });
    }

    [DllImport("Kernel32.dll", EntryPoint = "RtlMoveMemory")]
    public static extern void CopyMemory(IntPtr Destination, IntPtr Source, int Length);

Now I want to update my program to handle multiple pipelines and WriteableBitmaps so I can show several video feeds. The first thing I did was create a static utilities class so I can pass in the new bitmap (NewBitmap) and the WriteableBitmap (VM.FrameDataNew) I wish to update. This I then called as required when the new frame arrives:

The utilities class (just the code concerned with my question):

    public static class Utils
{
    /// <summary>
    /// Inject the source Bitmap into the Destination WriteableBitmap
    /// </summary>
    /// <param name="Src">The source Bitmap</param>
    /// <param name="Dest">The destination WriteableBitmap</param>
    public static void InjectBitmap(Bitmap Src, WriteableBitmap Dest)
    {
        if ((Dest == null) || (Dest.Width != Src.Width) || (Dest.Height != Src.Height))
            Dest = new WriteableBitmap(Src.Width, Src.Height, Src.HorizontalResolution, Src.VerticalResolution, PixelFormats.Bgr24, null);

        BitmapData data = Src.LockBits(new Rectangle(0, 0, Src.Width, Src.Height), ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format24bppRgb);
        Dest.Lock();

        // Copy the bitmap's data directly to the on-screen buffers
        CopyMemory(Dest.BackBuffer, data.Scan0, (data.Stride * data.Height));

        // Moves the back buffer to the front.
        Dest.AddDirtyRect(new Int32Rect(0, 0, data.Width, data.Height));
        Dest.Unlock();

        // 
        Src.UnlockBits(data);
    }

    [DllImport("Kernel32.dll", EntryPoint = "RtlMoveMemory")]
    public static extern void CopyMemory(IntPtr Destination, IntPtr Source, int Length);
}

...and the way I call it:

/// <summary>
    /// BitmapCaptured event handler for when a new video frame is received from the IP source
    /// </summary>
    /// <param name="sender">The originating source of the event</param>
    /// <param name="NewBitmap">The new frame in Bitmap form</param>
    private void PipeLine_BitmapCaptured(object sender, Bitmap NewBitmap)
    {
        // render to the screen
        Dispatcher.Invoke(() =>
        {
            Utils.InjectBitmap(NewBitmap, VM.FrameDataNew);
        });
    }

The image no longer appears onscreen. Stepping through the code it looks like every time InjectBitmap() is called the destination WriteableBitmap is null? The code creates a new one in the first if statement but VM.FrameDataNew remains null?

I'm definitely at the edge of my experience with this so any help would be much appreciated.

Edit: The aim is to have an observable collection of WriteableBitmaps so I can handle several of these efficiently.


Solution

  • The reason is simple: you don't assign the newly created object to the FrameDataNew property, so its value remains null.

    Don't forget that in C# the reference type instances are passed by reference.

    So in your method you're doing the following:

    void InjectBitmap(Bitmap Src, WriteableBitmap Dest)
    {
        if (Dest == null)
            Dest = new WriteableBitmap(/* ... */);
    }
    

    But Dest is just a local variable inside of your method - the method's arguments can be seen as method's local variables.

    So you assign a newly created instance to a local variable that will of course be lost when the method returns.

    What you need here is a ref parameter:

    void InjectBitmap(Bitmap src, ref WriteableBitmap dest)
    {
        if (dest == null)
            dest = new WriteableBitmap(/* ... */);
    }
    

    Note that I changed the parameter name casing to match the C# style guide.

    However, this won't work for properties, so InjectBitmap(NewBitmap, VM.FrameDataNew) won't compile if FrameDataNew is a property.

    You could make the FrameDataNew to a field, but having a non-private mutable field is a bad idea.

    You could use a temporary local variable, but it's somehow ugly:

    var copy = VM.FrameDataNew;
    InjectBitmap(NewBitmap, ref copy);
    if (copy != VM.FrameDataNew)
        VM.FrameDataNew = copy;
    

    I would rethink your design so that you don't need all that stuff.

    By the way, having multiple calls to Dispatcher.Invoke in a multithreaded environment (especially if the invoked methods are slow, and yours are) will drop your app's performance and make the UI unresponsive.