【问题标题】:How to pass validation error to child control in reusable UserControl如何将验证错误传递给可重用用户控件中的子控件
【发布时间】:2020-06-27 22:03:34
【问题描述】:

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

  • 名字
  • 插入
  • 姓氏

每个依赖属性值都绑定到一个字段,因此依赖属性 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>

还有控制类:

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

如果不使我的控制依赖于外部世界或引入丑陋的额外属性来解决它很麻烦,我看不到任何解决方案。我认为错误像一条信息一样通过所有绑定“隧道化”到值到达的最后一个级别。但这似乎不是正确的考虑。

编辑:

我添加了我的 ViewModel 类。

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

【问题讨论】:

  • 我也一直在处理这个问题。没有太多选项,您可能正在考虑将 INotifyDataErrorInfo 与 UserControl 一起使用。您可以将您的视图模型注册为 INotifyDataErrorInfo 以从 VM 获取逻辑。
  • @Ugur 你按照你说的方式解决了吗?如果是这样,你能告诉我更多关于它的信息吗?如果这是最好的选择,甚至可以给出答案。
  • 你能发布你的视图模型与INotifyDataErrorInfo实现吗?
  • 我在问题中添加了我的 ViewModel 类。
  • ValidateInput 在哪里被调用?

标签: c# wpf validation data-binding wpf-controls


【解决方案1】:

所以,我准备了一个演示用户控件。它是一个子用户控件,从它的 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>

主视图模型

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>

【讨论】:

  • 是一种解决方案,但是父控件的视图模型上的属性名称,必须与子用户控件中的依赖属性名称相同。
  • 是的,应该是。但如果不是,它也不会产生问题
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-10-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-12-15
  • 2012-01-22
相关资源
最近更新 更多