WPF。使用 MVVM 了解 UserControl 中的错误验证

WPF. Understanding error validation inside a UserControl with MVVM

我正在尝试验证 Window 中另一个 UserControl 正在使用的 UserControl 元素中的表单。 我正在使用 MVVM 模式,并且在最后一个 UserControl 子级的 ViewModel 中实现 INotifyDataErrorInfo。 问题是,当发生错误时,UserControl 中绑定到 属性 的 TextBox 和 UserControl 本身都被指示错误的红色框包围,我只想要突出显示的文本框。

代码如下:

有MainView(或第一个UserControl)的Window:

<Grid>
    <pages:MainPage>
        <pages:MainPage.DataContext>
            <vm:MainViewModel/>
        </pages:MainPage.DataContext>
    </pages:MainPage>
</Grid>

(它只包含一个 UserControl 作为页面)

"MainPage" 的 UserControl,它包含另一个(也是最后一个)UserControl 作为页面内的页面:

<UserControl.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary>
                <DataTemplate DataType="{x:Type vm:SearchViewModel}">
                    <pages:SearchPage/>
                </DataTemplate>

                ...

        </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
</UserControl.Resources>

...

<ContentControl Content="{Binding CurrentPage}"/>

好吧,现在相信我,"CurrentPage" 有一个从 MainViewModel 属性 获取的 ViewModel 对象,所以假设 "CurrentPage" 是一个 "SearchViewModel" 对象,所以有我们有 SearchPage 用户控件。

现在是最后一个 UserControl,SearchPage:

        <TextBox Grid.Column="1" Grid.Row="0" Text="{Binding CaseNumber}"/>
        <TextBox Grid.Column="1" Grid.Row="1" Text="{Binding PatientNumber}"/>
        <TextBox Grid.Column="1" Grid.Row="2" Text="{Binding PatientName}"/>
        <TextBox Grid.Column="1" Grid.Row="3" Text="{Binding PatientFamilyName}"/>
        <TextBox Grid.Column="1" Grid.Row="4" Text="{Binding PatientMotherMaidenName}"/>
        <TextBox Grid.Column="1" Grid.Row="5" Text="{Binding DoctorName}"/>

为了使 post 尽可能小,我刚刚添加了 UserControl 的 "form" 部分。

现在是最重要的部分,带有 INotifyDataErrorInfo 实现的 SearchViewModel:

public class SearchViewModel : ViewModelBase, INotifyDataErrorInfo, IVMPage
{
    private SearchModel searchModel = new SearchModel();
    public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
    private Dictionary<string, List<string>> errors = new Dictionary<string, List<string>>();


    private string patientNumber;

    public string PatientNumber
    {
        get { return patientNumber; }
        set 
        {
            int number;

            patientNumber = value; 

            if (int.TryParse(value, out number))
            {
                searchModel.PatientNumber = number;
                ClearErrors("PatientNumber");
            }
            else
            {
                AddErrors("PatientNumber", new List<string> { "The value must be a number" });
            }

            RaisePropertyChanged("PatientNumber");
        }
    }
    private string caseNumber;

    public string CaseNumber
    {
        get { return caseNumber; }
        set 
        {
            int number;

            caseNumber = value;

            if (int.TryParse(value, out number))
            {
                searchModel.CaseNumber = number;
                ClearErrors("CaseNumber");
            }
            else
            {
                AddErrors("CaseNumber", new List<string> { "The value must be a number" });
            }

            RaisePropertyChanged("CaseNumber"); 
        }
    }

....

private void ClearErrors(string propertyName)
    {
        errors.Remove(propertyName);

        if (ErrorsChanged != null)
            ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
    }

    private void AddErrors(string propertyName, List<string> newErrors)
    {
        errors.Remove(propertyName);
        errors.Add(propertyName, newErrors);

        if(ErrorsChanged != null)
            ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
    }
public System.Collections.IEnumerable GetErrors(string propertyName)
    {
        if(string.IsNullOrEmpty(propertyName))
        {
            return errors.Values;
        }
        else
        {
            if(errors.ContainsKey(propertyName))
            {
                return errors[propertyName];
            }
            else
            {
                return null;
            }
        }
    }

    public bool HasErrors
    {
        get { return (errors.Count() > 0); }
    }

所以,问题是: 例如,如果我在 "CaseNumber" TextBox 中引入字符,它会被一条指示错误的红线包围,并且所有 SearchPage UserControl 也会被另一条红线包围。我想要的只是用红线标记 TextBox 以指示错误,而不是所有 UserControl。

奇怪的是,如果我在触发 ErrorChanged 事件的 AddError 和 ClearError 方法中注释部分,则 UserControl 不再被红线包围...但我不知道为什么... .

抱歉问了这么长的问题,谢谢。

简要版本

为文本框的背景而不是边框​​着色。

(可选)详细版本

去过那里,运行进入那个问题。文本框的边框相当难以更改,因为它有很多东西。例如,如果您使用的是 DevExpress,则必须覆盖整个文本框样式才能到达边框,然后在选中框时开始失去自然突出显示等。

因此,我建议为文本框的背景着色以指示错误。它对用户来说更明显,看起来很棒,并且在实践中效果很好。

使用非常浅的红色,此页面有助于找到与页面现有配色方案相协调的颜色:

https://color.adobe.com/create/color-wheel/

好的,答案很简单。 问题在于这一行:

<ContentControl Content="{Binding CurrentPage}"/>

因为 WPF 默认情况下将 ValidatesOnNotifyDataErrors 属性 设置为 true,当 "CurrentPage" UserControl 内部发生错误时,在 UserControl 内部产生错误的 TextBox 会用红线表示错误他,正如预期的那样,但 ContentControl 也会检查 "GetErrors" 方法并在所有 "CurrentPage" UserControl 周围绘制另一条红线。

为了避免这种情况并仅在 TextBox 而不是所有 UserControl 中指示错误,只需将其添加到 ContentControl 声明中:

<ContentControl Content="{Binding CurrentPage, ValidatesOnNotifyDataErrors=False}"/>