c# – 在多级wpf视图中验证子记录

可以说我有一组这样的模型类:

public class Person
{
    public string Name { get; set; }
    public ObservableCollection<Job> Jobs { get; private set; }
}
public class Job
{
    public string Title { get; set; }
    public DateTime? Start { get; set; }
    public DateTime? End { get; set; }
}

我像这样设置我的xaml来显示一个人的列表和一个视图的所有细节:

<StackPanel Orientation="Horizontal">
    <ListView ItemsSource="{Binding}" Width="200" DisplayMemberPath="Name" IsSynchronizedWithCurrentItem="True" />
    <DockPanel Width="200" Margin="10,0">
        <TextBox Text="{Binding Name}" DockPanel.Dock="Top" Margin="0,0,0,10"/>
        <ListView ItemsSource="{Binding Jobs}" Name="_jobList" DisplayMemberPath="Title" IsSynchronizedWithCurrentItem="True"/>
    </DockPanel>
    <StackPanel Width="200" DataContext="{Binding ElementName=_jobList, Path=SelectedItem}">
        <TextBox Text="{Binding Title}"/>
        <DatePicker SelectedDate="{Binding Start}"/>
        <DatePicker SelectedDate="{Binding End}"/>
    </StackPanel>
</StackPanel>

整个Window的DataContext是People的ObservbleCollection.这似乎运作得相当好.我现在想添加一些验证,我不知道从哪里开始.

我想验证作业字段,以便用户无法选择另一个作业(或人员),如果标题为空或同一个人内的副本,或者日期不按顺序.此外,如果名称为空,则无法更改Person.

我已经阅读了一些关于WPF验证的内容,但是没有找到任何明确的解决方案.做这样的事情的最佳做法是什么?

另外,我的绑定是否正如我在最后一个面板上设置DataContext那样?它有效,但感觉有点粗略.另一种方法是为CollectionDataSources声明资源,我也不能很容易地想出这个方法.

最佳答案 对于验证部分,您可以从这里开始:
http://codeblitz.wordpress.com/2009/05/08/wpf-validation-made-easy-with-idataerrorinfo/

理解了文章后,您可以像这样修改类:
例如,让我们为职位和工作开始日期添加非空标题验证:

工作类:

 public class Job : IDataErrorInfo
    {
        public string Title { get; set; }
        public DateTime? Start { get; set; }
        public DateTime? End { get; set; }

        public string Error
        {
            get { throw new NotImplementedException(); }
        }

        public string this[string columnName]
        {
            get
        {
            string result = null;
            if (columnName == "Title")
            {
                if (string.IsNullOrEmpty(Title))
                    result = "Please enter a Title ";
            }
            if (columnName == "Start")
            {
                if (Start == null)
                    result = "Please enter a Start Date";
                else if (Start > End)
                {
                    result = "Start Date must be less than end date";
                }
            }
            return result;
        }
        }
    }

//////////假设我们使用一个名为MainWindow的窗口为你的xaml代码看起来像
MainWindow.xaml

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525"
        x:Name="mainWindow">
    <Window.Resources>
        <ControlTemplate x:Key="errorTemplate">
            <DockPanel LastChildFill="true">
                <Border Background="Red" DockPanel.Dock="right" Margin="5,0,0,0" Width="20" Height="20" CornerRadius="10"
                                    ToolTip="{Binding ElementName=customAdorner, Path=AdornedElement.(Validation.Errors)[0].ErrorContent}">
                    <TextBlock Text="!" VerticalAlignment="center" HorizontalAlignment="center" FontWeight="Bold" Foreground="white">
                    </TextBlock>
                </Border>
                <AdornedElementPlaceholder Name="customAdorner" VerticalAlignment="Center" >
                    <Border BorderBrush="red" BorderThickness="1" />
                </AdornedElementPlaceholder>
            </DockPanel>
        </ControlTemplate>

    </Window.Resources>
        <Grid>
        <StackPanel Orientation="Horizontal">
            <ListView ItemsSource="{Binding}" Width="200" DisplayMemberPath="Name" IsSynchronizedWithCurrentItem="True" IsEnabled="{Binding ElementName=mainWindow, Path=FormHasNoNoErrors}"/>
            <DockPanel Width="200" Margin="10,0">
                <TextBox Text="{Binding Name}" DockPanel.Dock="Top" Margin="0,0,0,10"/>
                <ListView ItemsSource="{Binding Jobs}" Name="_jobList" DisplayMemberPath="Title" IsSynchronizedWithCurrentItem="True" IsEnabled="{Binding ElementName=mainWindow, Path=FormHasNoNoErrors}" />
            </DockPanel>
            <StackPanel Width="200" DataContext="{Binding ElementName=_jobList, Path=SelectedItem}">
                <TextBox Text="{Binding Title, ValidatesOnDataErrors=true, NotifyOnValidationError=true}" Validation.Error="Validation_OnError" Validation.ErrorTemplate="{StaticResource errorTemplate}"/>
                <DatePicker SelectedDate="{Binding Start, ValidatesOnDataErrors=true, NotifyOnValidationError=true}" Validation.Error="Validation_OnError" Validation.ErrorTemplate="{StaticResource errorTemplate}"/>
                <DatePicker SelectedDate="{Binding End}"/>
            </StackPanel>
        </StackPanel>
    </Grid>
</Window>

////////MainWindow.xaml.cs

   public partial class MainWindow : Window, INotifyPropertyChanged
    {


        public MainWindow()
        {
            var jobs1 = new ObservableCollection<Job>()
                            {
                                new Job() {Start = DateTime.Now, End = DateTime.Now.AddDays(1), Title = "Physical Enginer"},
                                new Job() {Start = DateTime.Now, End = DateTime.Now.AddDays(1), Title = "Mechanic"}

                            };
            var jobs2 = new ObservableCollection<Job>()
                            {
                                new Job() {Start = DateTime.Now, End = DateTime.Now.AddDays(1), Title = "Doctor"},
                                new Job() {Start = DateTime.Now, End = DateTime.Now.AddDays(1), Title = "Programmer"}

                            };
            var personList = new ObservableCollection<Person>()
                                   {
                                       new Person() {Name = "john", Jobs = jobs1},
                                       new Person() {Name="alan", Jobs=jobs2}
                                   };

            this.DataContext = personList;
            InitializeComponent();
        }

        private void Validation_OnError(object sender, ValidationErrorEventArgs e)
        {
            if (e.Action == ValidationErrorEventAction.Added)
                NoOfErrorsOnScreen++;
            else
                NoOfErrorsOnScreen--;
        }

        public bool FormHasNoNoErrors
        {
            get { return _formHasNoErrors; }
            set 
            { 
                if (_formHasNoErrors != value)
                {
                    _formHasNoErrors = value;
                     PropertyChanged(this, new PropertyChangedEventArgs("FormHasNoErrors")); 
                }
            }
        }

        public int NoOfErrorsOnScreen
        {
            get { return _noOfErrorsOnScreen; }
            set 
            { 
                _noOfErrorsOnScreen = value;
                FormHasNoNoErrors = _noOfErrorsOnScreen == 0 ? true : false;
            }
        }

        private int _noOfErrorsOnScreen = 0;
        private bool _formHasNoErrors = true;

        public event PropertyChangedEventHandler PropertyChanged = delegate {};
    }

//////////////////////

I would like to validate the Job
fields so that the user cannot select
another Job (or person) if the title
is empty or a duplicate within the
same person, or if the dates are out
of order. Additionally, the Person
cannot be changed if the name is
empty.

一个简单的解决方案是,如果表单有错误(这就是我在上面的代码中所做的那样),则禁用列表框,并在没有错误时启用它们.你也应该在它们上面放一个带有工具提示的漂亮红色边框,向用户解释为什么他不能再选择了.

为了验证标题是否在同一个人中是重复的,您可以将上面的验证机制扩展为具有接收Person的ValidationService类,并查看具有该作业的人是否还有另一个具有相同名称的人.

public class Person:IDataErrorInfo
    {

    public string this[string columnName]
    {
        get
    {
        //look inside person to see if it has two jobs with the same title
        string result = ValidationService.GetPersonErrors(this);
        return result;
     }
...
 }

Also, is my binding ok the way I set
the DataContext on the last panel like
that? It works, but it feels a little
sketchy. The alternative is declaring
resources for the
CollectionDataSources and I couldn’t
really figure that method out very
easily either.

它不是MVVM的精神,如果你所做的不仅仅是一个小测试项目,你应该尝试学习MVVM(你可以从这里开始:http://fernandomachadopirizen.wordpress.com/2010/06/10/a-simple-introduction-to-the-model-view-viewmodel-pattern-for-building-silverlight-and-windows-presentation-foundation-applications/)

然后你不会直接绑定到一个人列表,而是你会绑定一些MainWindowViewModel类.此MainWindowViewModel类将包含人员列表,您将绑定到该列表.
对于选择,您将在MainWindowViewModel中拥有一个CurrentJob属性,即当前选定的作业,您将绑定到该:

基本上是这样的:

<StackPanel Orientation="Horizontal">
    <ListView ItemsSource="{Binding PersonList}" SelectedItem="{Binding CurrentPerson, Mode=TwoWay}" Width="200" DisplayMemberPath="Name" IsSynchronizedWithCurrentItem="True" IsEnabled="{Binding ElementName=mainWindow, Path=FormHasNoNoErrors}"/>
    <DockPanel Width="200" Margin="10,0">
        <TextBox Text="{Binding Name}" DockPanel.Dock="Top" Margin="0,0,0,10"/>
        <ListView ItemsSource="{Binding Jobs}" SelectedItem="{Binding CurrentJob, Mode=TwoWay}", DisplayMemberPath="Title" IsSynchronizedWithCurrentItem="True" />
    </DockPanel>
    <StackPanel Width="200">
        <TextBox Text="{Binding CurrentJob.Title}"/>
       .....
    </StackPanel>
</StackPanel> 

和你的MainWindowViewModel:

public class MainWindowViewModel : 
{
...
   //Public Properties
   public ObservableCollection<Person> PersonList ....
   public Job CurrentJob ....
....
}
点赞