首页 > 解决方案 > 用户控件中的自定义依赖属性不绑定

问题描述

我有一个图像屏幕用户控件,它包含一个 WPF 图像控件和一些输入控件,它在一些用户控制的数据处理后显示图像数据。我正在使用 MVVM 设计模式,因此它由一个视图和一个视图模型组成 - 控件是旨在可用于多个项目的库的一部分,因此该模型将特定于消费项目。ImageScreen 看起来像这样:

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>

ViewModel 是我实现 INotifyPropertyChanged 接口的子类:

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

视图模型本身主要由一组视图能够绑定的属性组成,如下所示:

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
            }
        }
        (...)
    }
}

在特定项目中,此控件是主窗口视图的一部分,而主窗口视图又具有自己的 ViewModel。由于 ImageScreen 应该根据自己视图中的选择来处理和显示数据,因此它只应该公开一个属性,即图像数据(顺便说一句,这是一个 2D ushort 数组),其余的应该由它的视图模型。但是,使用该控件的主窗口 ViewModel 对 ImageScreen ViewModel 没有直接的了解,因此我不能简单地访问它并直接传递数据。因此,我已将 ImageData 定义为后面的 ImageScreen 代码中的依赖属性(我尽可能避免使用后面的代码,但我认为我不能仅在 XAML 中定义依赖属性)并带有转发的 PropertyChanged 回调数据到 ViewModel。然后,ImageData 依赖属性旨在通过 MainWindowView 中的 XAML 数据绑定绑定到 MainWindowViewModel。所以后面的 ImageScreenView 代码是这样的:

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

MainWindow XAML 如下所示:

主窗口视图 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>

然后在主窗口 ViewModel 中,绑定属性 LiveFrame 和 SavedFrame 应更新为相应的值。ViewModel 的相关部分如下所示:

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
            }
        }
        (...)
    }
}

每当有新的帧数据到达时,就会更新 MainWindowViewModel 的 LiveFrame 属性。我不会质疑相机代码,因为我在其他地方使用它没有问题,并且在调试模式下我可以看到数据正确到达。但是,由于某种原因,当设置 LiveFrame 属性时,它不会触发 ImageScreen 的 ImageData 依赖属性的更新,即使它绑定到 LiveFrame 属性也是如此。我在依赖属性的设置器和 PropertyChanged 回调中设置了断点,但它们只是没有被执行。就像绑定没有' 尽管我在 MainWindowView XAML 中明确设置了它,并且我用于 ViewModels 属性更改的 NotifyPropertyChanged 基类应该(实际上在其他任何地方)引发刷新数据绑定的 PropertyChanged 事件,但它仍然存在。我尝试进行双向绑定,并将 UpdateSourceTrigger 设置为 OnPropertyChanged,但均无效。最让我困惑的是 MainWindowView 中的其他数据绑定到其 ViewModel 中的属性工作正常,其中之一也是我自己定义的自定义依赖属性,尽管在这种情况下它是自定义控件,而不是用户控件。

有谁知道我在哪里搞砸了数据绑定?


编辑:应 Ed 的要求,我重新发布完整的 ImageScreenView.xaml 和 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
    }
}

标签: c#wpfxamlmvvmdependency-properties

解决方案


这会破坏与 的LiveFrameSavedFrame属性的绑定,MainWindowViewModel因为它将 DataContext 设置为ImageScreenViewModel没有这些属性的:

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

我不知道的目的,ImageScreenViewModel但你可能应该用类本身的依赖属性替换它,并在使用 aUserControl的 XAML 标记中绑定到这些:UserControlRelativeSource

{Binding UcProperty, RelativeSource={RelativeSource AncestorType=UserControl}}

推荐阅读