0

I have an image screen user control, which contains a WPF image control and a few input controls, it displays image data after some user controlled data processing. I am using the MVVM design pattern so it consists of a view and a viewmodel - the control is part of a library that is intended to be usable from multiple projects, so the model would be specific to the consuming project. The ImageScreen looks like this:

ImageScreenView XAML:

<UserControl (...)
             xmlns:local="clr-namespace:MyCompany.WPFUserControls" x:Class="MyCompany.WPFUserControls.ImageScreenView"
             (...) >
    <UserControl.DataContext>
        <local:ImageScreenViewModel/>
    </UserControl.DataContext>
    <Border>
    (...)    // control layout, should not be relevant to the problem
    </Border>
</UserControl>

The ViewModel is a child class of my implementation of the INotifyPropertyChanged interface:

NotifyPropertyChanged.cs:

using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace MyCompany
{
    public abstract class NotifyPropertyChanged : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        private readonly Dictionary<string, PropertyChangedEventArgs> argumentsCache = new Dictionary<string, PropertyChangedEventArgs>();

        protected bool SetProperty<T>(ref T field, T newValue, [CallerMemberName]string propertyName = null)
        {
            if (!EqualityComparer<T>.Default.Equals(field, newValue))
            {
                field = newValue;
                if (argumentsCache != null)
                {
                    if (!argumentsCache.ContainsKey(propertyName))
                        argumentsCache[propertyName] = new PropertyChangedEventArgs(propertyName);

                    PropertyChanged?.Invoke(this, argumentsCache[propertyName]);
                    return true;
                }
                else
                    return false;
            }
            else
                return false;
        }
    }
}

The viewmodel itself consists mainly of a set of properties that the view is able to bind to, like this:

ImageScreenViewModel.cs:

using System;
using System.ComponentModel;
using System.Windows.Media.Imaging;

namespace MyCompany.WPFUserControls
{
    public class ImageScreenViewModel : NotifyPropertyChanged
    {
        public ushort[,] ImageData
        {
            get => imageData;
            set => SetProperty(ref imageData, value);
        }
        (...)

        private ushort[,] imageData;
        (...)

        public ImageScreenViewModel()
        {
            PropertyChanged += OnPropertyChanged;
        }

        protected void OnPropertyChanged(object sender, PropertyChangedEventArgs eventArgs)
        {
            switch (eventArgs.PropertyName)
            {
              (...) // additional property changed event logic
            }
        }
        (...)
    }
}

Within the particular project, this control is part of a main window View, which in turn has its own ViewModel. As the ImageScreen is supposed to process and display data based on the selections in its own view, it is only supposed to expose one property, the image data (this is a 2D ushort array btw) and the rest should be taken care of by its viewmodel. However, the main window ViewModel where the control is used has no direct knowledge of the ImageScreen ViewModel so I can't simply access it and pass the data directly. Therefore, I have defined the ImageData as a dependency property in the ImageScreen code behind (I avoided the code behind as much as I could but I don't think I can define a dependency property in just the XAML) with a PropertyChanged callback that forwards the data to the ViewModel. The ImageData dependency property is then intended to be bound via XAML data binding in the MainWindowView to the MainWindowViewModel. So the ImageScreenView code behind looks like this:

ImageScreenView.cs:

using System.Windows;
using System.Windows.Controls;

namespace MyCompany.WPFUserControls
{
    public partial class ImageScreenView : UserControl
    {
        public ushort[,] ImageData
        {
            get => (ushort[,])GetValue(ImageDataProperty);
            set => SetValue(ImageDataProperty, value);
        }

        public static readonly DependencyProperty ImageDataProperty = DependencyProperty.Register("ImageData", typeof(ushort[,]), typeof(ImageScreenV),
            new PropertyMetadata(new PropertyChangedCallback(OnImageDataChanged)));

        public ImageScreenView()
        {
            InitializeComponent();
        }

        private static void OnImageDataChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs eventArgs)
        {
            ImageScreenViewModel imageScreenViewModel = (dependencyObject as ImageScreenView).DataContext as ImageScreenViewModel;
            if (imageScreenViewModel != null)
                imageScreenViewModel.ImageData = (ushort[,])eventArgs.NewValue;
        }
    }
}

The MainWindow XAML looks like this:

MainWindowView XAML:

<Window (...)
        xmlns:local="clr-namespace:MyCompany.MyProject" x:Class="MyCompany.MyProject.MainWindowView"
        xmlns:uc="clr-namespace:MyCompany.WPFUserControls;assembly=WPFUserControls"
        (...) >
    <Window.DataContext>
        <local:MainWindowViewModel/>
    </Window.DataContext>
    <Grid Width="Auto" Height="Auto">
        <uc:ImageScreenView (...) ImageData="{Binding LiveFrame}"/>
        (...)
        <uc:ImageScreenView (...) ImageData="{Binding SavedFrame}"/>
        (...)
    </Grid>
</Window>

Then in the main window ViewModel the bound properties LiveFrame and SavedFrame shall be updated with their appropriate values. The relevant part of the ViewModel looks like this:

MainWindowViewModel.cs:

using System;
using System.ComponentModel;
using System.Threading.Tasks;
using System.Windows.Input;

namespace MyCompany.MyProject
{
    public class MainWindowViewModel : NotifyPropertyChanged
    {
        (...)
        public ushort[,] LiveFrame
        {
            get => liveFrame;
            set => SetProperty(ref liveFrame, value);
        }

        public ushort[,] ResultFrame
        {
            get => resultFrame;
            set => SetProperty(ref resultFrame, value);
        }
        (...)
        private ushort[,] liveFrame;
        private ushort[,] resultFrame;
        (...)

        public MainWindowViewModel()
        {
            PropertyChanged += OnPropertyChanged;
            InitCamera();
        }

        #region methods
        protected void OnPropertyChanged(object sender, PropertyChangedEventArgs eventArgs)
        {
            switch (eventArgs.PropertyName)
            {
               (...) // additional property changed event logic
            }
        }

        private bool InitCamera()
        {
           (...) // camera device initialization, turn streaming on
        }

        (...)

        private void OnNewFrame(ushort[,] thermalImg, ushort width, ushort height, ...)
        {
            LiveFrame = thermalImg;  // arrival of new frame data
        }

        private void Stream()
        {
            while (streaming)
            {
               (...) // query new frame data
            }
        }
        (...)
    }
}

Whenever new frame data arrives, the MainWindowViewModel's LiveFrame property is updated. I do not question the camera code because I used it somewhere else without problems and in debug mode I can see the data properly arriving. However, for some reason, when the LiveFrame property is being set it doesn't trigger an update of the ImageScreen's ImageData dependency property even though it is bound to the LiveFrame property. I set breakpoints in the dependency property's setter and the PropertyChanged callback but they just aren't being executed. It's like the binding doesn't exist despite me having explicitly set it in the MainWindowView XAML and with the NotifyPropertyChanged base class I used for the ViewModels property changes should be (and everywhere else in fact are) raising the PropertyChanged event that refresh data bindings. I tried making the binding two-way, and setting UpdateSourceTrigger to OnPropertyChanged, both without effect. What puzzles me most is that other data bindings in the MainWindowView to properties within its ViewModel work fine, one of which is also with a custom dependency property I defined myself, although in that case it's a custom control, not a user control.

Does anyone know where I screwed up the data binding?


EDIT: on Ed's request, I'm re-posting the full ImageScreenView.xaml and ImageScreenViewModel.cs:

ImageScreenV.xaml:

<UserControl xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:MyCompany.WPFUserControls" x:Class="MyCompany.WPFUserControls.ImageScreenV"
             xmlns:cc="clr-namespace:MyCompany.WPFCustomControls;assembly=WPFCustomControls"
             mc:Ignorable="d" d:DesignWidth="401" d:DesignHeight="300">
    <Border BorderBrush="{Binding BorderBrush, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:ImageScreenV}}}"
            Background="{Binding Background, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:ImageScreenV}}}"
            BorderThickness="{Binding BorderThickness, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:ImageScreenV}}}">
        <Border.DataContext>
            <local:ImageScreenVM x:Name="viewModel"/>
        </Border.DataContext>
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition MinWidth="{Binding ImageWidth}"/>
                <ColumnDefinition Width="Auto"/>
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition MinHeight="{Binding ImageHeight}"/>
            </Grid.RowDefinitions>
            <Image MinWidth="{Binding ImageWidth}" MinHeight="{Binding ImageHeight}"
                   Source="{Binding ScreenImageSource}"/>
            <StackPanel Grid.Column="1" Margin="3">
                <Label Content="Scaling Method"/>
                <ComboBox MinWidth="95" SelectedIndex="{Binding ScalingMethodIndex}">
                    <ComboBoxItem Content="Manual"/>
                    <ComboBoxItem Content="MinMax"/>
                    <ComboBoxItem Content="Sigma 1"/>
                    <ComboBoxItem Content="Sigma 3"/>
                </ComboBox>
                <Label Content="Minimum" Margin="0,5,0,0"/>
                <cc:DoubleSpinBox DecimalPlaces="2" Prefix="T = " Suffix=" °C" Maximum="65535" Minimum="0" MinHeight="22"
                                  Value="{Binding ScalingMinimum, Mode=TwoWay}"  VerticalContentAlignment="Center"
                                  IsEnabled="{Binding ScalingManually}"/>
                <Label Content="Maximum"/>
                <cc:DoubleSpinBox DecimalPlaces="2" Prefix="T = " Suffix=" °C" Maximum="65535" Minimum="0" MinHeight="22"
                                  Value="{Binding ScalingMaximum, Mode=TwoWay}"  VerticalContentAlignment="Center"
                                  IsEnabled="{Binding ScalingManually}"/>
                <Label Content="Color Palette" Margin="0,5,0,0"/>
                <ComboBox MinWidth="95" SelectedIndex="{Binding ColorPaletteIndex}">
                    <ComboBoxItem Content="Alarm Blue"/>
                    <ComboBoxItem Content="Alarm Blue Hi"/>
                    <ComboBoxItem Content="Alarm Green"/>
                    <ComboBoxItem Content="Alarm Red"/>
                    <ComboBoxItem Content="Fire"/>
                    <ComboBoxItem Content="Gray BW"/>
                    <ComboBoxItem Content="Gray WB"/>
                    <ComboBoxItem Content="Ice32"/>
                    <ComboBoxItem Content="Iron"/>
                    <ComboBoxItem Content="Iron Hi"/>
                    <ComboBoxItem Content="Medical 10"/>
                    <ComboBoxItem Content="Rainbow"/>
                    <ComboBoxItem Content="Rainbow Hi"/>
                    <ComboBoxItem Content="Temperature"/>
                </ComboBox>
            </StackPanel>
        </Grid>
    </Border>
</UserControl>

ImageScreenVM.cs:

using MyCompany.Vision;
using System;
using System.ComponentModel;
using System.Windows.Media.Imaging;

namespace MyCompany.WPFUserControls
{
    public class ImageScreenVM : NotifyPropertyChanged
    {
        #region properties
        public BitmapSource ScreenImageSource
        {
            get => screenImageSource;
            set => SetProperty(ref screenImageSource, value);
        }

        public int ScalingMethodIndex
        {
            get => scalingMethodIndex;
            set => SetProperty(ref scalingMethodIndex, value);
        }

        public int ColorPaletteIndex
        {
            get => colorPaletteIndex;
            set => SetProperty(ref colorPaletteIndex, value);
        }

        public double ScalingMinimum
        {
            get => scalingMinimum;
            set => SetProperty(ref scalingMinimum, value);
        }

        public double ScalingMaximum
        {
            get => scalingMaximum;
            set => SetProperty(ref scalingMaximum, value);
        }

        public bool ScalingManually
        {
            get => scalingManually;
            set => SetProperty(ref scalingManually, value);
        }

        public uint ImageWidth
        {
            get => imageWidth;
            set => SetProperty(ref imageWidth, value);
        }

        public uint ImageHeight
        {
            get => imageHeight;
            set => SetProperty(ref imageHeight, value);
        }

        public MyCompany.Vision.Resolution Resolution
        {
            get => resolution;
            set => SetProperty(ref resolution, value);
        }

        public ushort[,] ImageData
        {
            get => imageData;
            set => SetProperty(ref imageData, value);
        }

        public Action<ushort[,], byte[,], (double, double), MyCompany.Vision.Processing.Scaling> ScalingAction
        {
            get => scalingAction;
            set => SetProperty(ref scalingAction, value);
        }

        public Action<byte[,], byte[], MyCompany.Vision.Processing.ColorPalette> ColoringAction
        {
            get => coloringAction;
            set => SetProperty(ref coloringAction, value);
        }
        #endregion

        #region fields
        private BitmapSource screenImageSource;
        private int scalingMethodIndex;
        private int colorPaletteIndex;
        private double scalingMinimum;
        private double scalingMaximum;
        private bool scalingManually;
        private uint imageWidth;
        private uint imageHeight;
        private MyCompany.Vision.Resolution resolution;
        private ushort[,] imageData;
        private byte[,] scaledImage;
        private byte[] coloredImage;
        private Action<ushort[,], byte[,], (double, double), MyCompany.Vision.Processing.Scaling> scalingAction;
        private Action<byte[,], byte[], MyCompany.Vision.Processing.ColorPalette> coloringAction;
        #endregion

        public ImageScreenVM()
        {
            PropertyChanged += OnPropertyChanged;
        }

        #region methods
        protected void OnPropertyChanged(object sender, PropertyChangedEventArgs eventArgs)
        {
            switch (eventArgs.PropertyName)
            {
                case nameof(ImageData):
                    if (imageData.GetLength(0) != resolution.Height || imageData.GetLength(1) != resolution.Width)
                        Resolution = new MyCompany.Vision.Resolution(checked((uint)imageData.GetLength(1)), checked((uint)imageData.GetLength(0)));
                    goto case nameof(ScalingMaximum);
                case nameof(ScalingMethodIndex):
                    ScalingManually = scalingMethodIndex == (int)MyCompany.Vision.Processing.Scaling.Manual;
                    goto case nameof(ScalingMaximum);
                case nameof(ColorPaletteIndex):
                case nameof(ScalingMinimum):
                case nameof(ScalingMaximum):
                    ProcessImage();
                    break;
                case nameof(Resolution):
                    scaledImage = new byte[resolution.Height, resolution.Width];
                    coloredImage = new byte[resolution.Pixels() * 3];
                    ImageWidth = resolution.Width;
                    ImageHeight = resolution.Height;
                    break;
            }
        }

        private void ProcessImage()
        {
            // not sure yet if I stick to this approach
            if (scalingAction != null && coloringAction != null)
            {
                scalingAction(imageData, scaledImage, (scalingMinimum, scalingMaximum), (Processing.Scaling)scalingMethodIndex);
                coloringAction(scaledImage, coloredImage, (Processing.ColorPalette)colorPaletteIndex);
            }
        }
        #endregion
    }
}
flibbo
  • 125
  • 10
  • 1
    When you clobber the UserControl's inherited DataContext, you break all the bindings on it. Remove this: ` `. A usercontrol should inherit its datacontext. It should not create its own viewmodel; that's called "view-first design" and it's an anti-pattern for more reasons than just the one you discovered. If you don't want to redesign the thing, make the viewmodel the DataContext of the outermost Border in the UserControl XAML. That won't break bindings on the UserControl itself. – 15ee8f99-57ff-4f92-890c-b56153 Oct 25 '19 at 14:06
  • 1
    If I had written it, and speaking without having contemplated your code at length, I think my UserControl would have no dependency properties. It would get ImageData from its viewmodel, and it would inherit its viewmodel from context. – 15ee8f99-57ff-4f92-890c-b56153 Oct 25 '19 at 14:07
  • Thanks for your input @EdPlunkett . I did what I did because most of the details of my ImageScreenViewModel's functionality are not context sensitive, it's really just supposed to receive raw data from wherever it is used and I'd rather not have to re-write the screen specific viewmodel code every time I use the control. Do you happen to have a quick idea how this could sensibly be done? – flibbo Oct 25 '19 at 14:51
  • 1
    OK, now I've looked it over and I'm going to totally change my story, and hope you don't notice. So: If the only property in ImageScreenViewModel is ImageData, I would consider just getting rid of that viewmodel, and continue to bind the usercontrol's ImageData property in MainWindow.xaml just like you're already doing. Can you show the full XAML for the UserControl, or at least enough to illustrate how its own ImageData property, and the ImageData property of its viewmodel, are being used internally? – 15ee8f99-57ff-4f92-890c-b56153 Oct 25 '19 at 14:58
  • 1
    If there *are* more properties in the viewmodel, get rid of the dependency property. Then you would write an implicit datatemplate ``. Then for each image thing, I would give the mainviewmodel a property of type `ImageScreenViewModel`, with all properties set as desired. Then in MainWindow.xaml: ``. – 15ee8f99-57ff-4f92-890c-b56153 Oct 25 '19 at 15:01
  • Ok thanks. The ViewModel does in fact have a bunch of properties so I'll try the datatemplate approach. I also updated the questions with the full ImageScreen view XAML and viewmodel code, if you're still interested. – flibbo Oct 25 '19 at 15:22
  • 1
    Sounds good. Make sure you get rid of `Border.DataContext` in the XAML, since now the control will be inheriting its viewmodel from context in a DataTemplate. Another thing: That big switch statement in the image viewmodel's PropertyChanged method, I would do that stuff in the respective property setters. Call `ProcessImage();` in the setter for `ScalingMaximum`, for example. – 15ee8f99-57ff-4f92-890c-b56153 Oct 25 '19 at 15:25
  • 1
    And now `LiveFrame` in MainViewModel becomes another property which holds a `ImageScreenVM` whose ImageData is whatever `LiveFrame` is now, but plus all the other stuff as well. And with the implicit datatemplate, you'll display it like ``. – 15ee8f99-57ff-4f92-890c-b56153 Oct 25 '19 at 15:27
  • Hm, I remember reading that putting that stuff in the property setters should be avoided. Here it would bypass the PropertyChanged event and just always be executed regardless of whether the property was actually changed or not (it might have been set with the same value it already had). In case of dependency properties, any logic in the code setters is usually ignored iirc (i know in this case they're normal properties but maybe it's best not to pick up that habit). Is it still better than the OnPropertyChanged method here? – flibbo Oct 25 '19 at 15:35
  • 1
    Property setters ordinarily have a redundancy guard: `if (_backingField != value) { _backingField = value; /* other stuff */ OnPropertyChanged(); }`. However, you're using SetProperty(). If that returns a bool saying whether the property was actually changed or not, use `if (SetProperty(...)) { /* do stuff */ }`, or fall back on `if (_backingField != value)` for those properties. Doing extra stuff in the setter is much more idiomatic than a switch statement in OnPropertyChanged(). – 15ee8f99-57ff-4f92-890c-b56153 Oct 25 '19 at 15:37
  • Right, ofc. Ok thanks a bunch for your help! – flibbo Oct 25 '19 at 15:40
  • 1
    In the case of dependency properties, the setters are not called when a property is set via XAMl or a Binding. Those cases always call `obj.SetValue(YourClass.FooProperty, someValue)` rather than `obj.Foo = someValue;`. You can omit the regular get/set property entirely and XAML will never even notice. That's just for your own convenience. The way to hook into a property change on a dependency property is to give it a PropertyChanged handler in its PropertyMetadata() that you pass to RegisterProperty. – 15ee8f99-57ff-4f92-890c-b56153 Oct 25 '19 at 15:40
  • 1
    BTW your switch statement is very far from a great sin, it's just not idiomatic. – 15ee8f99-57ff-4f92-890c-b56153 Oct 25 '19 at 15:41

1 Answers1

1

This breaks the binding to the LiveFrame and SavedFrame properties of the MainWindowViewModel as it sets the DataContext to an ImageScreenViewModel which doesn't have these properties:

<UserControl.DataContext>
    <local:ImageScreenViewModel/>
</UserControl.DataContext>

I don't know the purpose of the ImageScreenViewModel but you should probably replace it with dependency properties of the UserControl class itself and bind to these in the XAML markup of the UserControl using a RelativeSource:

{Binding UcProperty, RelativeSource={RelativeSource AncestorType=UserControl}}
mm8
  • 163,881
  • 10
  • 57
  • 88
  • Simple problem but I had no idea since I'm still new to all this. Thanks a lot for the solution. It works if I do what Ed suggested in his comment - setting the data context at the border instead of the user control - but it looks like I have some redesigning to do if I want a sensible design pattern. – flibbo Oct 25 '19 at 14:30