如何将验证错误传递给可重用 UserControl 中的子控件

How to pass validation error to child control in reusable UserControl

我创建了自己的 UserControl,名为 PersonNameControl,旨在重复使用。 该控件具有三个 TextBox 字段,并且在其 class 文件中具有三个依赖属性。

每个依赖项 属性 值都绑定到一个字段,因此依赖项 属性 Firstname 绑定到 Firstname TextBox, 依此类推

我有意没有明确设置 UserControl 的 DataContext。 控制应尽可能松散。它应该只通过其依赖属性获取它的值(对于字段)。它甚至不应该寻找像 DataContext 这样的东西。

    <UserControl x:Class="WpfApplication1.PersonNameControl">
        <StackPanel>

            <Label>Firstname:</Label>
            <TextBox Text="{Binding Firstname, Mode=TwoWay,
                RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type UserControl}}}">
            </TextBox>

            <Label>Insertion:</Label>
            <TextBox Text="{Binding Insertion, Mode=TwoWay,
                RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type UserControl}}}">
            </TextBox>

            <Label>Lastname:</Label>
            <TextBox Text="{Binding Lastname, Mode=TwoWay,
                RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type UserControl}}}">
            </TextBox>

        </StackPanel>
    </UserControl>

与对照class:

public partial class PersonNameControl : UserControl
{
    public PersonNameControl()
    {
        InitializeComponent();
    }

    public string Firstname
    {
        get { return (string)GetValue(FirstnameProperty); }
        set { SetValue(FirstnameProperty, value); }
    }
    public static readonly DependencyProperty FirstnameProperty =
        DependencyProperty.Register("Firstname", typeof(string), typeof(PersonNameControl), 
            new FrameworkPropertyMetadata("", FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));


    public string Insertion
    {
        get { return (string)GetValue(InsertionProperty); }
        set { SetValue(InsertionProperty, value); }
    }
    public static readonly DependencyProperty InsertionProperty =
        DependencyProperty.Register("Insertion", typeof(string), typeof(PersonNameControl), 
            new FrameworkPropertyMetadata("", FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));


    public string Lastname
    {
        get { return (string)GetValue(LastnameProperty); }
        set { SetValue(LastnameProperty, value); }
    }
    public static readonly DependencyProperty LastnameProperty =
        DependencyProperty.Register("Lastname", typeof(string), typeof(PersonNameControl), 
            new FrameworkPropertyMetadata("", FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
}

该控件应该在另一个视图中使用,如下所示:

<!-- 
Here we are inside a view or some other control.
The bindings here provide the dependency properties of the UserControl with a value.
The DataContext of the view where my UserControl is used, is a ViewModel that implements INotifyDataErrorInfo 
-->

<myControls:PersonNameControl 
    Firstname="{Binding SomeFirstnameFromVM, Mode=TwoWay}"
    Insertion="{Binding SomeInsertionFromVM, Mode=TwoWay}"
    Lastname="{Binding SomeLastnameFromVM, Mode=TwoWay}">
</myControls:PersonNameControl>

当 ViewModel(实现 INotifyDataErrorInfo)创建验证错误时,我的 PersonNameControl UserControl 没有任何反应。 我设法制作了一个独立的控件,因为它不依赖于特定的 DataContext,不在其代码隐藏文件中设置自己的 DataContext,而只是通过依赖属性获取其值。这些值通过绑定交换并显示,但不显示验证错误。 我想要的是将验证错误传递给 UserControl。

互联网上的一些解决方案利用了 ValidationAdornerSite,我试过了。但这只适用于一个 TextBox.

如果不让我的控件依赖于外部世界或引入丑陋的额外属性来解决它很麻烦,我看不到任何解决方案。我认为错误 'tunneled' 就像一条信息,通过所有绑定到达值到达的最后一层。但这似乎不是正确的考虑。

编辑:

我添加了我的 ViewModel class。

public class CustomerFormViewModel : ViewModelBase, INotifyDataErrorInfo
{
    protected string _clientNumber;
    protected DateTime _date;
    protected string _firstname;
    protected string _insertion;
    protected string _lastname;
    protected Address _address;
    protected ObservableCollection<Email> _emails;
    protected ObservableCollection<PhoneNumber> _phoneNumbers;
    protected string _note;

    protected bool _hasErrors;
    protected IList<ValidationFailure> _validationErrors;

    public IList<ValidationFailure> ValidationErrors
    {
        get { return _validationErrors; }
        set { _validationErrors = value; OnPropertyChanged("ValidationErrors"); }
    }

    public string ClientNumber
    {
        get { return _clientNumber; }
        set { _clientNumber = value; OnPropertyChanged("ClientNumber"); }
    }
    public DateTime Date
    {
        get { return _date; }
        set { _date = value; OnPropertyChanged("Date"); }
    }
    public string Firstname
    {
        get { return _firstname; }
        set { _firstname = value; OnPropertyChanged("Firstname"); }
    }
    public string Insertion
    {
        get { return _insertion; }
        set { _insertion = value; OnPropertyChanged("Insertion"); }
    }
    public string Lastname
    {
        get { return _lastname; }
        set { _lastname = value; OnPropertyChanged("Lastname"); }
    }
    public Address Address
    {
        get { return _address; }
        set { _address = value; OnPropertyChanged("Address"); }
    }
    public ObservableCollection<Email> Emails
    {
        get { return _emails; }
        set { _emails = value; OnPropertyChanged("Emails"); }
    }
    public ObservableCollection<PhoneNumber> PhoneNumbers
    {
        get { return _phoneNumbers; }
        set { _phoneNumbers = value; OnPropertyChanged("PhoneNumbers"); }
    }
    public string Note
    {
        get { return _note; }
        set { _note = value; OnPropertyChanged("Note"); }
    }

    private DelegateCommand _saveCustomerCommand;

    public DelegateCommand SaveCustomerCommand
    {
        get { return _saveCustomerCommand; }
        private set { _saveCustomerCommand = value; OnPropertyChanged("SaveCustomerCommand"); }
    }

    public CustomerFormViewModel()
    {
        ValidationErrors = new List<ValidationFailure>();
        SaveCustomerCommand = new DelegateCommand(SaveCustomer, CanSaveCustomer);
    }

    protected void ValidateInput()
    {
        ValidationErrors.Clear();

        CustomerFormValidator validator = new CustomerFormValidator();
        FluentValidation.Results.ValidationResult result = validator.Validate(this);

        ValidationErrors = result.Errors;

        foreach (ValidationFailure f in ValidationErrors)
        {
            Console.WriteLine(f.ErrorMessage);
        }

        _hasErrors = result.Errors.Count != 0;

        List<string> vmProperties = new List<string>() { "Firstname", "Lastname", "Address", "ClientNumber", "Date" };

        foreach (string propertyName in vmProperties)
        {
            OnErrorsChanged(propertyName);
        }
    }

    public bool HasErrors
    {
        get { return _hasErrors; }
    }

    public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

    protected void OnErrorsChanged(string name)
    {
        ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(name));
    }

    public IEnumerable GetErrors(string propertyName)
    {
        return ValidationErrors.Where<ValidationFailure>(x => x.PropertyName == propertyName);
    }

    public void SaveCustomer(object parameter)
    {
        this.ValidateInput();

        if( ! HasErrors)
        {
            Customer customer = new Customer(-1, ClientNumber, Date, Firstname, Insertion, Lastname, Address);

            ICustomerRepository repo = new CustomerRepository();
            bool res = repo.SaveCustomer(customer);

            if(res) {
                // ...
            }
            // ...

        } else
        {
            MessageBox.Show("One or more fields are not filled in correctly.", "Invalid input", MessageBoxButton.OK, MessageBoxImage.Error);
        }
    }

    public bool CanSaveCustomer(object parameter)
    {
        return true;
    }
}

所以,我准备了一个演示用户控件。它是一个子用户控件,从其 MainViewModel

获取所有验证信息

主窗口

<Window
    x:Class="ValidationSubUI.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:local="clr-namespace:ValidationSubUI"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Name="MyWindow"
    Title="MainWindow"
    Width="800"
    Height="450"
    mc:Ignorable="d">
    <Window.DataContext>
        <local:MainViewModel />
    </Window.DataContext>
    <Grid>
        <local:SubUserControl
            FirstName="{Binding FirstName, Mode=TwoWay}"
            LastName="{Binding LastName, Mode=TwoWay}"
            ValidationSource="{Binding ElementName=MyWindow, Path=DataContext}" />
    </Grid>
</Window>

MainViewModel

using GalaSoft.MvvmLight;
using System.ComponentModel;

namespace ValidationSubUI
{
    public class MainViewModel : ViewModelBase, IDataErrorInfo
    {
        public string Error
        {
            get
            {
                return string.Empty;
            }
        }

        private string m_FirstName;
        public string FirstName
        {
            get { return m_FirstName; }
            set
            {
                m_FirstName = value;
                RaisePropertyChanged();
            }
        }

        private string m_LastName;
        public string LastName
        {
            get { return m_LastName; }
            set
            {
                m_LastName = value;
                RaisePropertyChanged();
            }
        }


        public string this[string columnName]
        {
            get
            {
                if (columnName == nameof(FirstName))
                {
                    return GetFirstNameError();
                }
                else if (columnName == nameof(LastName))
                {
                    return GetLastNameError();
                }

                return null;
            }
        }

        private string GetFirstNameError()
        {
            string result = string.Empty;

            if (string.IsNullOrEmpty(FirstName))
            {
                result = "First name required";
            }

            return result;
        }

        private string GetLastNameError()
        {
            string result = string.Empty;

            if (string.IsNullOrEmpty(LastName))
            {
                result = "Last name required";
            }

            return result;
        }
    }
}

SubUserControl 从 MainViewModel 获取所有验证逻辑

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

namespace ValidationSubUI
{
    /// <summary>
    /// Interaction logic for SubUserControl.xaml
    /// </summary>
    public partial class SubUserControl : UserControl, IDataErrorInfo
    {
        public SubUserControl()
        {
            InitializeComponent();
        }

        public IDataErrorInfo ValidationSource
        {
            get { return (IDataErrorInfo)GetValue(ValidationSourceProperty); }
            set { SetValue(ValidationSourceProperty, value); }
        }

        // Using a DependencyProperty as the backing store for ValidationSource.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty ValidationSourceProperty =
            DependencyProperty.Register("ValidationSource", typeof(IDataErrorInfo), typeof(SubUserControl), new PropertyMetadata(null));



        public string FirstName
        {
            get { return (string)GetValue(FirstNameProperty); }
            set { SetValue(FirstNameProperty, value); }
        }

        // Using a DependencyProperty as the backing store for FirstName.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty FirstNameProperty =
            DependencyProperty.Register("FirstName", typeof(string), typeof(SubUserControl), new PropertyMetadata(string.Empty));



        public string LastName
        {
            get { return (string)GetValue(LastNameProperty); }
            set { SetValue(LastNameProperty, value); }
        }

        // Using a DependencyProperty as the backing store for LastName.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty LastNameProperty =
            DependencyProperty.Register("LastName", typeof(string), typeof(SubUserControl), new PropertyMetadata(string.Empty));


        public string Error
        {
            get
            {
                return string.Empty;
            }
        }

        public string this[string columnName]
        {
            get
            {
                if (ValidationSource != null)
                {
                    return ValidationSource[columnName];
                }

                return null;
            }
        }
    }
}

和子用户控件

<UserControl
    x:Class="ValidationSubUI.SubUserControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    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"
    x:Name="CustomControl"
    d:DesignHeight="450"
    d:DesignWidth="800"
    mc:Ignorable="d">

    <UserControl.Resources>

        <Style TargetType="{x:Type TextBox}">
            <Setter Property="Validation.ErrorTemplate">
                <Setter.Value>
                    <ControlTemplate>
                        <DockPanel>
                            <Border BorderBrush="Red" BorderThickness="1">
                                <AdornedElementPlaceholder x:Name="controlWithError" />
                            </Border>
                            <TextBlock
                                Margin="5,0,0,0"
                                HorizontalAlignment="Center"
                                VerticalAlignment="Center"
                                FontSize="12"
                                FontWeight="DemiBold"
                                Foreground="Red"
                                Text="{Binding ElementName=controlWithError, Path=AdornedElement.ToolTip, Mode=OneWay}" />
                        </DockPanel>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
            <Style.Triggers>
                <Trigger Property="Validation.HasError" Value="True">
                    <Setter Property="ToolTip" Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}" />
                </Trigger>
            </Style.Triggers>
        </Style>

    </UserControl.Resources>

    <Grid DataContext="{x:Reference Name=CustomControl}">
        <StackPanel>
            <TextBox
                Width="120"
                Height="30"
                Margin="5"
                HorizontalAlignment="Left"
                VerticalAlignment="Top"
                Text="{Binding FirstName, Mode=TwoWay, ValidatesOnDataErrors=True}"
                TextWrapping="Wrap" />

            <TextBox
                Width="120"
                Height="30"
                Margin="5"
                HorizontalAlignment="Left"
                VerticalAlignment="Top"
                Text="{Binding LastName, Mode=TwoWay, ValidatesOnDataErrors=True}"
                TextWrapping="Wrap" />
        </StackPanel>
    </Grid>
</UserControl>