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

发布于 2024-10-31 02:54:46 字数 1495 浏览 0 评论 0原文

假设我有一组这样的模型类:

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>

整个窗口的 DataContext 是人员的 ObservbleCollection。这似乎运作得相当好。我现在想添加一些验证,但我不知道从哪里开始。

我想验证“职位”字段,以便在标题为空或同一个人中的职位重复或日期不按顺序时用户无法选择其他职位(或人员)。此外,如果名称为空,则无法更改人员。

我读过一些有关 WPF 中的验证的内容,但没有找到针对此场景的任何明确的解决方案。做这样的事情的最佳实践是什么?

另外,我的绑定是否可以像我在最后一个面板上设置 DataContext 的方式一样?它可以工作,但感觉有点粗略。另一种方法是为 CollectionDataSources 声明资源,但我也无法真正轻松地弄清楚该方法。

Lets say I have a set of model classes like this:

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

I set up my xaml like this to display a list of people and all details on a single view:

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

The DataContext for the entire Window is an ObservbleCollection of People. This seems to work fairly well. I would now like to add some validation and I have no idea where to start.

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.

I have read a bit about validation in WPF, but have not found any clear solution for this scenario. What is the best practice for doing something like this?

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.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

烏雲後面有陽光 2024-11-07 02:54:46

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

理解本文后,您可以修改您的类 像这样:
例如,让我们为职位名称和职位开始日期添加一个非空职位验证:

Job class:

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

//////////假设我们为您的 xaml 代码使用了一个名为 MainWindow 的窗口,它看起来像这样
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 {};
    }

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

我想验证该职位
使用户无法选择字段
另一个工作(或人),如果标题
为空或重复
同一个人,或者日期已过
的秩序。此外,该人
如果名称是则无法更改
空。

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

为了验证同一个人的头衔是否重复,您可以扩展上面的验证机制,使用一个 ValidationService 类来接收一个 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;
     }
...
 }

另外,我的绑定方式是否正确?
最后一个面板上的 DataContext 就像
那?可以用,但是感觉有点
粗略的。另一种选择是声明
的资源
CollectionDataSources 而我不能
真的很想出这个方法
也很容易。

这不符合 MVVM 精神,如果您正在做的不仅仅是一个小型测试项目,您应该尝试学习 MVVM(您可以从这里开始:

那么您不会直接绑定到人员列表,而是会绑定一些 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 ....
....
}

For the validation part you can start here: http://codeblitz.wordpress.com/2009/05/08/wpf-validation-made-easy-with-idataerrorinfo/

After you understand the article you could modify your classes like this:
For example let's add a non-empty title validation for job title and job start date:

Job class:

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

//////////Supposing we used a window called MainWindow for you xaml code it would look like
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.

One simple solution is to disable the listboxes if the forms has errors (that's what I did in the code above), and enable them when there are no errors. Also you should probably put a nice red border with tooltip on them, explaining the user why he can't select anymore.

For validating if the title is duplicate within the same person, you could extend the validation mechanism above to have a ValidationService class that receives a Person and looks if the person who has the job also has another one with the same name.

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.

It's not in the MVVM spirit, and if what you're doing is going to be something more than a small test project you should try learn MVVM (You could start here: http://fernandomachadopirizen.wordpress.com/2010/06/10/a-simple-introduction-to-the-model-view-viewmodel-pattern-for-building-silverlight-and-windows-presentation-foundation-applications/)

Then you would not bind to a list of persons directly but instead you would have some MainWindowViewModel class to which you bind. This MainWindowViewModel class would contain the list of persons and you would bind to that.
And for the selection you would have a CurrentJob property in your MainWindowViewModel, that is the currently selected job, and you would bind to that:

Basically something like this:

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

And your MainWindowViewModel:

public class MainWindowViewModel : 
{
...
   //Public Properties
   public ObservableCollection<Person> PersonList ....
   public Job CurrentJob ....
....
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文