我有一个简单的 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 和其他几种方法,弹出窗口关闭后绑定对象仍然被清除。
假设发生的事情是 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 状态的更好表示.
你的代码有一些问题。您没有理由不将
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
,你必须通过绑定来设置它(这是你明确不想做的)。
使用
Popup.IsOpen
方法设置DependencyObject.SetCurrentValue
属性。Binding
):
this.Popup.SetCurrentValue(Popup.IsOpenProperty, false);
另外解决设计问题的正确解决方案是:
AgeViewModel
实现 INotifyDataErrorInfo
以启用属性验证。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>