具有IDataErrorInfo的ViewModel总是在设置属性后调用getter

本文关键字:属性 设置 调用 getter IDataErrorInfo ViewModel 具有 | 更新日期: 2023-09-27 18:22:43

我想给这个问题尽可能多的上下文,但总的来说,我基本上问了两个问题:

  • 当setter没有抛出异常时,WPF是否总是在设置绑定属性后调用getter
  • 当ViewModel实现IDataErrorInfo时,是否可以防止在setter中发生错误后调用绑定属性的getter

我目前有一个Model类,它通过从属性setter抛出异常来实现验证。此外,许多属性是耦合的,因此修改其中一个属性的值可能会导致重新计算其他几个属性。它实现INotifyPropertyChanged,以便在重新计算发生时向外部侦听器发出警报。它看起来像这样:

public class Model : INotifyPropertyChanged
{
    private double property1;
    public double Property1
    {
        get { return property1; }
        set
        {
            if (value < 0.0)
                throw new Exception("Property1 cannot be less than 0.0.");
            property1 = value;
            OnPropertyChanged(new PropertyChangedEventArgs("Property1"));
        }
    }
    // ...Everything needed to support INotifyPropertyChanged...
}

最初,我为这个类实现了ViewModel,以充当模型属性的精简包装器,每当发生错误时(标记无效数据、禁用按钮等),都会提供额外的视图特定行为:

public class ViewModel : INotifyPropertyChanged
{
    private readonly Model model;
    public ViewModel()
    {
        model = new Model();
        model.PropertyChanged += (sender, args) => OnPropertyChanged(args);
    }
    public string Property1
    {
        get { return model.Property1.ToString(); }
        set
        {
            try
            {
                model.Property1 = Double.Parse(value);
            }
            catch (Exception)
            {
                // Perform any view-specific actions
                throw;
            }
        }
    }
    // ...Everything needed to support INotifyPropertyChanged
}

值得注意的是,ViewModel没有任何额外的支持字段;它的所有属性都直接链接到模型中的相应属性,并且来自模型的任何PropertyChanged通知都由ViewModel传递。

但是,我经常听说使用异常进行验证可能会受到限制,我也开始意识到这一点,特别是在这个应用程序中对交叉耦合验证规则的需求增加的时候。

我不想改变模型的行为,因为它已经在其他几个地方使用了,但我开始改变ViewModel以实现IDataErrorInfo:

public class ViewModel : INotifyPropertyChanged, IDataErrorInfo
{
    private readonly Model model;
    public ViewModel()
    {
        model = new Model();
        model.PropertyChanged += (sender, args) => 
        {
            errorList.Remove(args.PropertyName);
            OnPropertyChanged(args);
        };
    }
    public string Property1
    {
        get { return model.Property1.ToString(); }
        set
        {
            try
            {
                model.Property1 = Double.Parse(value);
            }
            catch(Exception ex)
            {
                // Perform any view-specific actions                    
                errorList["Property1"] = ex.Message;
            }
        }
    }
    private readonly Dictionary<string, string> errorList = new Dictionary<string, string>();
    public string this[string propertyName]
    {
        get
        {
            string errorMessage;
            if (errorList.TryGetValue(propertyName, out errorMessage))
                return errorMessage;
            return String.Empty;
        }
    }
    public string Error { get { return String.Empty; } }
    // ...Everything needed to support INotifyPropertyChanged
}

然而,这导致了行为上的剧烈变化。当使用异常时,在用户输入无效值后,将显示控件的Validation.ErrorTemplate,并且无效值保留在控件中,从而为用户提供纠正错误的机会。当使用IDataErrorInfo时,WPF似乎在setter完成后调用属性getter。由于每当发生错误时,模型都不会更改,因此无效值将替换为以前的值。所以现在我有一个显示Validation.ErrorTemplate的控件,但显示了一个VALID值!

WPF在没有收到PropertyChanged通知的情况下(在窗口初始化后)自动调用属性getter,这对我来说似乎很疯狂。而且它不会在抛出异常后尝试调用getter,那么为什么在使用IDataErrorInfo时会这样做呢?

我是不是做错了什么导致了这种行为?当ViewModel实现IDataErrorInfo时,在setter中发生错误后,是否有方法防止WPF调用ViewModel中的属性getter?

我曾尝试在ViewModel中添加后备字段来存储无效值,但由于Model属性可以在ViewModel之外进行修改(这就是它最初实现INotifyPropertyChanged的原因),因此该解决方案最终非常复杂,并且在具有不同技能水平的程序员的环境中基本上是不可持续的。

具有IDataErrorInfo的ViewModel总是在设置属性后调用getter

以下是您希望在MVVM 中进行表单验证的方法

您的型号

public class Product:IDataErrorInfo
{
    public string ProductName {get;set;}
    public string this[string propertyName]
    {
       get 
       {
           string validationResult = null;
           switch (propertyName)
           {
               case "ProductName":
               validationResult = ValidateName();
           }
       }
     }
}

然后在您的ViewModel 中

public string ProductName
{
  get { return currentProduct.ProductName; }
  set 
  {
    if (currentProduct.ProductName != value)
    {
      currentProduct.ProductName = value;
      base.OnPropertyChanged("ProductName");
    }
  }  
}

另一个考虑因素是,当我想验证数字时(例如您的双重验证),请将模型保持为具有双重而不是字符串

public double? Property1 {get;set;}

那么你可以做这个

<Textbox Name="myDoubleTextbox" >
    <Binding ValidatesOnDataErrors="true" Path="Property1" TargetValueNull="" />
/>

因此,当他们在双框中键入不正确的内容时,它会向您的模型发送null,您可以对此进行检查。

虽然它不能回答我提出的问题,但我的同事建议,只要出现错误,就可以通过向errorList添加无效值并修改ViewModel中的getter来返回无效值,从而实现所需的行为。

因此ViewModel中的属性如下所示:

public string Property1
{
    get
    {
        ErrorInfo error;
        if (errorList.TryGetValue("Property1", out error))
            return error.Value;
        return model.Property1.ToString();
    }
    set
    {
        try
        {
            model.Property1 = Double.Parse(value);
        }
        catch (Exception ex)
        {
            // Perform any view-specific actions
            errorList["Property1"] = new ErrorInfo(value, ex.Message);
        }
    }
}

IDataErrorInfo方法进行以下更新:

private struct ErrorInfo
{
    public readonly string Value;
    public readonly string Message;
    public ErrorInfo(string value, string message)
    {
        Value = value;
        Message = message;
    }
}
private readonly Dictionary<string, ErrorInfo> errorList = new Dictionary<string, ErrorInfo>();
public string this[string propertyName]
{
    get
    {
        ErrorInfo error;
        if (errorList.TryGetValue(propertyName, out error))
            return error.Message;
        return String.Empty;
    }
}
public string Error { get { return String.Empty; } }

这允许PropertyChanged事件处理程序保持不变,并且只需要对属性getter和setter进行小的更改。