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
}
}