C#; AvaloniaUI; MVVM; How can i update a views image source when a button on another window is clicked?

305 views Asked by At

I am building an application in Avalonia UI using the MVVM Community Toolkit. In short, it is mainly a tool where the user enters data into certain fields. The user also uses image controls to load images either from file system or captures an image from webcam using the FlashCap library.

I have a MainView Which holds several SKImageViews which is a Skia-Sharp based image control for Avalonia 11.

The MainView also holds several buttons, which when pressed (using a RelayCommand), opens a new Window. This window has two ComboBoxes to get and change the FlashCap device or characteristics, and a CaptureImage Button (using a RelayCommand) to save a snapshot of the current stream to an SKBitmap. Once the SKBitmap has its Bitmap data stored, the ImageCaptureWindow closes.

Now the SKImageViews Source from the MainView should update and show the captured image, which it doesn't.

I am trying to provide all necessary code info.

The MainView.axaml (shortened):

<UserControl x:Class="ArtexControlValveRepCardUI.MainView"
             xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:vm="using:ArtexControlValveRepCardUI.ViewModels"
             xmlns:bh="using:ArtexControlValveRepCardUI.Styles"
             xmlns:i="clr-namespace:Avalonia.Xaml.Interactivity;assembly=Avalonia.Xaml.Interactivity"
             xmlns:siv="clr-namespace:SkiaImageView;assembly=SkiaImageView"
             xmlns:views="clr-namespace:ArtexControlValveRepCardUI.Views"
             Width="1600" Height="1050"
             MinWidth="1600" MinHeight="1050"
             d:DesignHeight="1050" d:DesignWidth="1600"
             x:DataType="vm:MainViewModel"
             x:CompileBindings="False"
             mc:Ignorable="d">
    <Grid>
...
                        <siv:SKImageView Width="600" Height="600"
                                         x:Name="SkImageView1"
                                         HorizontalAlignment="Center" VerticalAlignment="Center"
                                         Source="{Binding CurrentImage11, Mode=TwoWay}"
                                         Stretch="Uniform" />
...

                        <Button Name="CaptureImageButton1"
                                Width="130" Height="35"
                                Content="Bild aufnehmen"
                                Margin="10"
                                HorizontalContentAlignment="Center"
                                FontSize="15"
                                Command="{Binding CaptureImageButtonPressedCommand, FallbackValue=null}"
                                CommandParameter="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type views:MainWindow}}}"/>
...
    </Grid>
</UserControl>

The MainViewModel.cs (shortened):

namespace ArtexControlValveRepCardUI.ViewModels
{
    public partial class MainViewModel : ObservableObject
    {
...
        [ObservableProperty] private SKBitmap? _currentImage11;
...
        public MainViewModel()
        {
...
            var fileStream = new SKFileStream(_baseImagePath);
            CurrentImage11 = SKBitmap.Decode(fileStream);
...
        }
...
        [RelayCommand]
        private async Task CaptureImageButtonPressed(MainWindow ownerWindow)
        {
            await new ImageCaptureWindow().ShowDialog(ownerWindow);
        }
...
    }
}

The ImageCaptureWindow.axaml (shortened):

<Window x:Class="ArtexControlValveRepCardUI.Views.ImageCaptureWindow"
        xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:siv="clr-namespace:SkiaImageView;assembly=SkiaImageView" 
        xmlns:views="clr-namespace:ArtexControlValveRepCardUI.Views"
        xmlns:vm="clr-namespace:ArtexControlValveRepCardUI.ViewModels"
        Title="Kamera"
        Width="800" Height="900"
        MinWidth="800" MinHeight="900"
        d:DesignHeight="900" d:DesignWidth="800"
        x:CompileBindings="False" CanResize="False"
        CornerRadius="5" ExtendClientAreaChromeHints="NoChrome"
        ExtendClientAreaTitleBarHeightHint="0" ExtendClientAreaToDecorationsHint="True"
        WindowStartupLocation="CenterOwner"
        mc:Ignorable="d">
    <Grid ...
...
            <siv:SKImageView Width="750" Height="750"
                             HorizontalAlignment="Center" VerticalAlignment="Center"
                             Source="{Binding Image, Mode=TwoWay}"
                             Stretch="Uniform" />

...
    </Grid>
        <Grid ...
...
            <Button Grid.Row="1" Grid.RowSpan="2"
                    Grid.Column="2"
                    x:Name="CaptureImageButton"
                    Click="CaptureImageButton_OnClick"
                    Width="100"
                    Margin="100,0,100,0" HorizontalAlignment="Center"
                    VerticalAlignment="Center" HorizontalContentAlignment="Center"
                    VerticalContentAlignment="Center"
                    Classes="capture"
                    Command="{Binding CaptureImageButtonPressedCommand, FallbackValue=null}"
                    CommandParameter="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type views:ImageCaptureWindow}}}">
                <Svg Path="/Assets/Icons/CaptureButtonIcon.svg" />
            </Button>
...
        </Grid>
</Window>

The ImageCaptureWindowViewModel.cs (shortened):

namespace ArtexControlValveRepCardUI.ViewModels
{
    public partial class ImageCaptureWindowViewModel : ObservableObject
    {
...
        private long frameCount;

        private ImageCaptureWindow? _currentWindow;
        private MainViewModel _mainViewModel;

        private CaptureDevice? _captureDevice;

        [ObservableProperty] private string? _statistics1;
        [ObservableProperty] private string? _statistics2;
        [ObservableProperty] private string? _statistics3;

        [ObservableProperty] private bool? _isEnabled;
        [ObservableProperty] private bool? _windowIsOpen;

        [ObservableProperty] [NotifyPropertyChangedFor(nameof(DeviceDescriptor))] private ObservableCollection<CaptureDeviceDescriptor>? _devices = new();
        [ObservableProperty] [NotifyPropertyChangedFor(nameof(Characteristic))] private ObservableCollection<VideoCharacteristics>? _characteristics = new();

        [ObservableProperty] private CaptureDeviceDescriptor? _deviceDescriptor;
        [ObservableProperty] private VideoCharacteristics? _characteristic;
        [ObservableProperty] private SKBitmap? _image;

        public ImageCaptureWindowViewModel()
        {
            _mainViewModel = new MainViewModel();

            // Enumerate capture devices:
            var devices = new CaptureDevices();

            // Store device list into combobox:
            Devices?.Clear();

            // Get device descriptor
            foreach (var descriptor in devices.EnumerateDescriptors().
                         Where(d => d.DeviceType == DeviceTypes.DirectShow))
            {
                Devices?.Add(descriptor);
            }

            IsEnabled = true;
        }
        // Partial Methods for auto-generated observables
        partial void OnDeviceDescriptorChanged(CaptureDeviceDescriptor? descriptor)
        {
            OnDeviceListChangedAsync(descriptor);
        }

        partial void OnCharacteristicChanged(VideoCharacteristics? characteristic)
        {
             OnCharacteristicsChangedAsync(characteristic);
        }

        [RelayCommand]
        private Task OnDeviceListChangedAsync(CaptureDeviceDescriptor?  descriptor)
        {
            if (descriptor is {})
            {
                Characteristics?.Clear();

                foreach (var characteristic in descriptor.Characteristics)
                {
                    if (characteristic.PixelFormat != PixelFormats.Unknown)
                    {
                        Characteristics?.Add(characteristic);
                    }
                }

                Characteristic = Characteristics?.FirstOrDefault();
            }
            else
            {
                Characteristics?.Clear();
            }

            DeviceDescriptor = descriptor;
            return default;
        }

        [RelayCommand]
        private async Task OnCharacteristicsChangedAsync(VideoCharacteristics? characteristics)
        {
            IsEnabled = false;

            try
            {
                // Close and dispose when already opened:
                if (_captureDevice is {} captureDevice)
                {
                    captureDevice = null;
                    await captureDevice.StopAsync();
                    await captureDevice.DisposeAsync();
                }

                // Delete preview:
                Image = null;
                Statistics1 = null;
                Statistics2 = null;
                Statistics3 = null;
                frameCount = 0;

                // Descriptor gets assigned and valid characteristics are being set:
                if (DeviceDescriptor is {} descriptor && 
                    characteristics is {})
                {
                    // Open capture device:
                    captureDevice = await descriptor.OpenAsync(
                        characteristics,
                        OnPixelBufferArrivedAsync);

                    // Start capturing:
                    await captureDevice.StartAsync();
                    _captureDevice = captureDevice;
                }
            }
            finally
            {
                IsEnabled = true;
            }
        }

        private async Task OnPixelBufferArrivedAsync(PixelBufferScope bufferScope)
        {
            // Pixel buffer has arrived:
            // <<NOTE: Possibly the thread context is NOT UI thread save>>

            // Get image data binary:
            byte[] image = bufferScope.Buffer.ExtractImage();

            // Decode/convert binary image data to bitmap:
            var bitmap = SKBitmap.Decode(image);

            // Capture statistics:
            var frameCount = Interlocked.Increment(ref this.frameCount);
            var frameIndex = bufferScope.Buffer.FrameIndex;
            var timeStamp = bufferScope.Buffer.Timestamp;

            // PixelBuffer is no longer needed - bitmap has been copied.
            bufferScope.ReleaseNow();

            // Switch to UI Thread:
            Dispatcher.UIThread.InvokeAsync(() =>
            {
                // Update bitmap:
                Image = bitmap;

                // Update statistics:
                var realFps = frameCount / timeStamp.TotalSeconds;
                var fpsByIndex = frameIndex / timeStamp.TotalSeconds;
                Statistics1 = $"Frame={frameCount}/{frameIndex}";
                Statistics1 = $"FPS={realFps:F3}/{fpsByIndex:F3}";
                Statistics1 = $"SKBitmap={bitmap.Width}x{bitmap.Height} [{bitmap.ColorType}]";
            });
        }

        [RelayCommand]
        private Task CloseWindowButtonPressed(ImageCaptureWindow captureWindow)
        {
            _currentWindow = captureWindow;
            _currentWindow.Close();
            return Task.CompletedTask;
        }

        [RelayCommand]
        private async Task CaptureImageButtonPressed(ImageCaptureWindow captureWindow)
        {
            _currentWindow = captureWindow;

            var captureDevice = _captureDevice;
            captureDevice.StopAsync();


            _mainViewModel.CurrentImage11 = Image;
            _currentWindow.Close();
        }
...
    }
}

When i store the ObservableProperty Image of the ImageCaptureViewModel in the ObservableProperty CurrentImage11 from the MainViewModel,...

_mainViewModel.CurrentImage11 = Image;

...the SKBitmap is correctly stored in that variable, but it does not trigger the MainView to update its SKImageView Source to show the changed result.

I have yet alot to learn about MVVM and i am obvioulsy missing something, but i can't find the issue. Could anyone have a look at my code any maybe help me to figure out what is going wrong here?

Any help would be appreciated.

1

There are 1 answers

1
ManuelKetisch On BEST ANSWER

I figured out that the Microsoft MVVM Community Toolkit features messaging (sender, recipients). in order to solve my problem, i implemented this messaging system by creating messenger classes like:

using CommunityToolkit.Mvvm.Messaging.Messages;

namespace ArtexControlValveRepCardUI.Messages
{
    public class UpdateImageMessage : ValueChangedMessage<byte[]>
    {
        public UpdateImageMessage(byte[] value) : base(value)
        {
        }
    }
}

My ViewModels then implement these messenger classes by using the IRecipient interface:

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;

namespace ArtexControlValveRepCardUI.ViewModels
{
    public partial class MainViewModel : ObservableObject, IRecipient<UpdateImageMessage>

}

In the class constructor of the receiving ViewModel i register the Message using the WeakRefereneMessenger:

public MainViewModel()
{
    WeakReferenceMessenger.Default.Register<UpdateImageMessage>(this);
}

Now i can use the Receive methods from the IRecipient interfaces to receive data from another ViewModel and process my code:

public void Receive(UpdateImageMessage message)
{
    _receivedImageData = message.Value;

    DecodeAndUpdateBitmap(_receivedImageData);
    LoadImageBasedOnPressedCaptureButton();
}

To send a message i simply use the WeakReferenceMessengers Send method in the ViewModel i wish to send data from:

using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;

namespace ArtexControlValveRepCardUI.ViewModels
{
    public partial class ImageCaptureWindowViewModel : ObservableObject
    {
        [RelayCommand]
        public async Task CaptureImageButtonPressed(byte[] value)
        {
            if (_captureDevice != null)
            {
                value = _imageData;
                WeakReferenceMessenger.Default.Send(new UpdateImageMessage(value));
            }    
           
            await _captureDevice.StopAsync();
            CurrentImageCaptureWindow?.Close();
        }
    }
}

I hope this helps anyone who might struggle with the same problem i had.

If anyone has some suggestions on how this could be done more efficient or if my explanation wasn't clear enough, please let me know!