当模式设置为 OneWay 时绑定对象被清除 - 使用 TwoWay 工作正常

问题描述 投票:0回答:2

我有一个简单的 wpf Popup 控件。当用户在文本框中输入错误的年龄时,我想显示弹出窗口。

我的代码片段

<TextBox Name="TxtAge"  LostFocus="TxtAge_OnLostFocus" ></TextBox>
<Popup Name="InvalidAgePopup" IsOpen="{Binding IsAgeInvalid, Mode=OneWay}"/>

代码隐藏

private void TxtAge_OnLostFocus(object sender, RoutedEventArgs e)
{
        var text = TxtAge.Text;
        double value = 0;
        if (Double.TryParse(text, out value))
        {
            vm.IsAgeInvalid = false;
        }
        else
        {
            vm.IsAgeInvalid = true;
        }
}

视图模型

public class AgeViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    private bool _isAgeInvalid;
    public bool IsAgeInvalid
    {
        get { return _isAgeInvalid; }
        set
        {
            _isAgeInvalid = value;
            OnPropertyChanged();
        }
    }
}

IsAgeInvalid 属性在输入无效年龄的文本框时设置。那时我想显示弹出窗口。而且我不想在弹出控件关闭时设置 IsAgeInvalid = false。 为此,我设置 Mode=OneWay

IsOpen="{Binding IsAgeInvalid, Mode=OneWay}

当我输入错误的数据时,弹出窗口显示正常。当我关闭弹出窗口时,我的绑定对象正在被清除。 以下截图来自snoop工具。

绑定对象第一次出现。

弹窗关闭后,绑定对象清空。

绑定部分在 TwoWay 模式下工作正常,我不希望将 IsAgeInvalid 属性设置为 false,因为 IsOpen 设置为 false。 我试过设置 UpdateSourceTriger 和其他几种方法,弹出窗口关闭后绑定对象仍然被清除。

c# wpf mvvm data-binding binding
2个回答
1
投票

假设发生的事情是 WPF 错误或只是一个不值得失眠的怪癖,这里有一个(可能是显而易见的)解决方法:

视图模型:

    private bool _isAgeInvalid;
    public bool IsAgeInvalid
    {
        get 
        {
            return _isAgeInvalid; 
        }
        set
        {
            _isAgeInvalid = value;
            this.IsAgeValidationPopupOpen = valid;
            OnPropertyChanged();
        }
    }

    private bool _isAgeValidationPopupOpen;
    public bool IsAgeValidationPopupOpen
    {
        get => _isAgeValidationPopupOpen;
        set
        {
            _isAgeValidationPopupOpen = value;
            OnPropertyChanged();
        }
    }

XAML:

<Popup Name="InvalidAgePopup" IsOpen="{Binding IsAgeValidationPopupOpen, Mode=TwoWay}"/>

即使弹出窗口的关闭不应该破坏你的绑定,至少使用这种方法你有一个变量总是跟踪弹出窗口打开状态,另一个变量总是跟踪年龄有效性状态,所以可以说它是你的 UI 状态的更好表示.


0
投票

你的代码有一些问题。您没有理由不将

TextBox
绑定到您的视图模型类。验证视图中的输入只是为了在视图模型上设置一个属性以指示验证失败是没有意义的。这样的属性必须是只读的,以确保不能随意设置。如果您选择在视图的代码隐藏中进行验证,那么只需从那里切换
Popup

但是,我建议实施
INotifyDataErrorInfo
(见下文)。

解释你的观察

根据您的陈述

“当我关闭弹出窗口时,我的绑定对象正在被清除。”

“我不希望 IsAgeInvalid 属性设置为 false,因为 IsOpen 设置为 false”

我得出结论,您不会通过将

Popup
设置为
AgeViewModel.IsAgeInvalid
来关闭
false
。相反,您直接设置
Popup.IsOpen
以关闭
Popup
.
重要的一点是,如果
Binding.Mode
设置为
BindingMode.OneWay
并且直接(本地)设置依赖属性,则新值将清除/覆盖以前的值和值源,在您的情况下是一个
Binding
对象定义了
IsAgeInvalid
作为源属性。
这就是为什么您在关闭
Binding
(您的方式)时观察到
Popup
被删除的原因。
在本地设置依赖属性将始终清除以前的值和值源(
Binding
和本地值具有相同的优先级)。

因为

IsOpen
绑定到
IsAgeInvalid
,你必须通过绑定来设置它(这是你明确不想做的)。

解决方案 1

使用

Popup.IsOpen
方法设置
DependencyObject.SetCurrentValue
属性。
这允许在不清除值源的情况下在本地分配新值(
Binding
):

this.Popup.SetCurrentValue(Popup.IsOpenProperty, false);

方案二(推荐)

另外解决设计问题的正确解决方案是:

  1. 让您的视图模型类
    AgeViewModel
    实现
    INotifyDataErrorInfo
    以启用属性验证。
  2. 覆盖默认的验证错误反馈。当验证失败时,WPF 将自动在绑定目标周围绘制一个红色边框(在您的例子中为
    TextBox
    )。
    您可以通过定义分配给附加的
    ControlTemplate
    属性的
    Validation.ErrorTemplate
    来非常轻松地自定义错误反馈。

以下示例使用解决方案 2) “使用 lambda 表达式和委托进行数据验证” 来自 如何添加验证以查看模型属性或如何实现

INotifyDataErrorInfo
。只有属性
UserInput
被重命名为
Age
并且方法
IsUserInputValid
被重命名为
IsAgevalid
以使其解决您给定的场景。

AgeViewModel.cs

class AgeViewModel : INotifyPropertyChanged, INotifyDataErrorInfo
{
  // This property is validated using a lambda expression
  private string age;
  public string Age
  { 
    get => this.age; 
    set 
    {       
      // Use Method Group
      if (IsPropertyValid(value, IsAgeValid))
      {
        this.age = value; 
        OnPropertyChanged();
      }
    }
  }

  // Constructor
  public AgeViewModel()
  {
    this.Errors = new Dictionary<string, IList<object>>();
  }

  // The validation method for the UserInput property
  private (bool IsValid, IEnumerable<object> ErrorMessages) IsAgeValid(string value)
  {
    return double.TryParse(value, out _) 
      ? (true, Enumerable.Empty<object>()) 
      : (false, new[] { "Age must be numeric." });
  }

/***** Implementation of INotifyDataErrorInfo *****/

  // Example uses System.ValueTuple
  public bool IsPropertyValid<TValue>(
    TValue value, 
    Func<TValue, (bool IsValid, IEnumerable<object> ErrorMessages)> validationDelegate, 
    [CallerMemberName] string propertyName = null)  
  {  
    // Clear previous errors of the current property to be validated 
    _ = ClearErrors(propertyName); 

    // Validate using the delegate
    (bool IsValid, IEnumerable<object> ErrorMessages) validationResult = validationDelegate?.Invoke(value) ?? (true, Enumerable.Empty<object>());

    if (!validationResult.IsValid)
    {
      AddErrorRange(propertyName, validationResult.ErrorMessages);
    } 

    return validationResult.IsValid;
  }  

  // Adds the specified errors to the errors collection if it is not 
  // already present, inserting it in the first position if 'isWarning' is 
  // false. Raises the ErrorsChanged event if the Errors collection changes. 
  // A property can have multiple errors.
  private void AddErrorRange(string propertyName, IEnumerable<object> newErrors, bool isWarning = false)
  {
    if (!newErrors.Any())
    {
      return;
    }

    if (!this.Errors.TryGetValue(propertyName, out IList<object> propertyErrors))
    {
      propertyErrors = new List<object>();
      this.Errors.Add(propertyName, propertyErrors);
    }

    if (isWarning)
    {
      foreach (object error in newErrors)
      {
        propertyErrors.Add(error);
      }
    }
    else
    {
      foreach (object error in newErrors)
      {
        propertyErrors.Insert(0, error);
      }
    }

    OnErrorsChanged(propertyName);
  }

  // Removes all errors of the specified property. 
  // Raises the ErrorsChanged event if the Errors collection changes. 
  public bool ClearErrors(string propertyName)
  {
    this.ValidatedAttributedProperties.Remove(propertyName);
    if (this.Errors.Remove(propertyName))
    { 
      OnErrorsChanged(propertyName); 
      return true;
    }
    return false;
  }

  // Optional method to check if a particular property has validation errors
  public bool PropertyHasErrors(string propertyName) => this.Errors.TryGetValue(propertyName, out IList<object> propertyErrors) && propertyErrors.Any();

  #region INotifyDataErrorInfo implementation

  // The WPF binding engine will listen to this event
  public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

  // This implementation of GetErrors returns all errors of the specified property. 
  // If the argument is 'null' instead of the property's name, 
  // then the method will return all errors of all properties.
  // This method is called by the WPF binding engine when ErrorsChanged event was raised and HasErrors return true
  public System.Collections.IEnumerable GetErrors(string propertyName) 
    => string.IsNullOrWhiteSpace(propertyName) 
      ? this.Errors.SelectMany(entry => entry.Value) 
      : this.Errors.TryGetValue(propertyName, out IList<object> errors) 
        ? (IEnumerable<object>)errors 
        : new List<object>();

  // Returns 'true' if the view model has any invalid property
  public bool HasErrors => this.Errors.Any(); 

  #endregion

  #region INotifyPropertyChanged implementation

  public event PropertyChangedEventHandler PropertyChanged;

  #endregion

  protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)        
  {
    this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
  }

  protected virtual void OnErrorsChanged(string propertyName)
  {
    this.ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
  }

  // Maps a property name to a list of errors that belong to this property
  private Dictionary<string, IList<object>> Errors { get; }
}

MainWindow.xaml

AgeViewModel
中生成的错误消息由绑定引擎自动包装到
ValidationError
对象中。
我们可以通过引用
ValidationError.ErrorContent
属性来获取消息。

<Window>
  <Window.Resources>
      <ControlTemplate x:Key="ValidationErrorTemplate">
      <StackPanel>
        <Border BorderBrush="Red"
                BorderThickness="1"
                HorizontalAlignment="Left">

          <!-- Placeholder for the TextBox itself -->
          <AdornedElementPlaceholder x:Name="AdornedElement" />
        </Border>

        <!-- Your Popup goes here.
             The Popup will close automatically 
             once the error template is disabled by the WPF binding engine.
             Because this content of the ControlTemplate is already part of a Popup,
             you could skip your Popup and only add the TextBlock 
             or whatever you want to customize the look -->
        <Popup IsOpen="True">

          <!-- The error message from the view model is automatically 
               wrapped into a System.Windows.Controls.ValidationError object.
               Our view moel returns a collection of messages. 
               So we bind to the first message -->
          <TextBlock Text="{Binding [0].ErrorContent}" 
                     Foreground="Red" />
        </Popup>
      </StackPanel>
    </ControlTemplate>
  </Window.Resources>

  <!-- Enable validation and override the error template 
       using the attached property 'Validation.ErrorTemplate' -->
  <TextBox Text="{Binding Age, ValidatesOnNotifyDataErrors=True}"
           Validation.ErrorTemplate="{StaticResource ValidationErrorTemplate}" />
</Window>
最新问题
© www.soinside.com 2019 - 2024. All rights reserved.