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

问题描述 投票:1回答:1

我已经创建了自己的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。

[Internet上的某些解决方案使用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;
    }
}
c# wpf validation data-binding wpf-controls
1个回答
0
投票

因此,我准备了一个演示用户控件。它是一个子用户控件,从其MainViewModel获取所有验证信息

enter image description here

MainWindow

<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, ValidatesOnNotifyDataErrors=True}"
            LastName="{Binding LastName, Mode=TwoWay, ValidatesOnNotifyDataErrors=True}"
            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;
            }
        }
    }
}

和SubUserControl

<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>
                        <Grid>
                            <Border
                                x:Name="errorBorder"
                                Background="#11FF0000"
                                BorderBrush="#FFCB2E2E"
                                BorderThickness="1"
                                IsHitTestVisible="False" />
                            <AdornedElementPlaceholder x:Name="placeholder" />
                            <Popup
                                HorizontalAlignment="Right"
                                AllowsTransparency="True"
                                HorizontalOffset="0"
                                IsOpen="{Binding ElementName=placeholder, Path=AdornedElement.IsFocused, Mode=OneWay}"
                                Placement="Right"
                                PlacementTarget="{Binding ElementName=errorBorder}"
                                PopupAnimation="Fade"
                                VerticalOffset="0">
                                <StackPanel Orientation="Horizontal">
                                    <Polygon
                                        VerticalAlignment="Center"
                                        Fill="#FFCB2E2E"
                                        Points="0,4 4,0 4,8"
                                        Stretch="Fill"
                                        Stroke="#FFCB2E2E"
                                        StrokeThickness="2" />
                                    <Border
                                        Padding="4"
                                        Background="#FFCB2E2E"
                                        CornerRadius="4">
                                        <TextBlock
                                            Margin="2,0,0,0"
                                            HorizontalAlignment="Center"
                                            FontWeight="Bold"
                                            Foreground="White"
                                            Text="{Binding ElementName=placeholder, Path=AdornedElement.ToolTip, Mode=OneWay}" />
                                    </Border>
                                </StackPanel>
                            </Popup>
                        </Grid>
                    </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>
© www.soinside.com 2019 - 2024. All rights reserved.