我有一个接受用户输入的用户控件。该用户控件通知用户输入值是否在允许范围内。
在主窗口中,我将使用其中的几个用户控件从用户那里获取不同参数的值。
最后,将有一个提交按钮。
现在,如果用户控件中的任何输入值超出范围,我想禁用该按钮。我怎么做?我被困在这里。
如果这样做是错误的方法,请也纠正我。我希望遵循最佳实践方法。
非常感谢。
用户控件“ UserInputFieldUC”的XAML代码:
<UserControl x:Class="TestUserInput.UserInputFieldUC"
...
xmlns:local="clr-namespace:TestUserInput"
x:Name="parent">
<StackPanel Orientation="Horizontal" DataContext="{Binding ElementName=parent}" >
<TextBlock Name="UserLabel" Width="50" Text="{Binding Path=Label}"/>
<TextBox Name="MetricValue" Width="50" Text="{Binding Path=Value}" TextChanged="MetricValue_TextChanged"/>
<TextBlock Name="MetricUnits" Width="50" Text="{Binding Path=Units}" VerticalAlignment="Center"/>
<TextBlock Name="ErrorDisplay" Width="50" VerticalAlignment="Center"/>
</StackPanel>
</UserControl>
“ UserInputFieldUC”的代码隐藏:
namespace TestUserInput
{
/// <summary>
/// Interaction logic for UserInputFieldUC.xaml
/// </summary>
public partial class UserInputFieldUC : UserControl
{
public UserInputFieldUC()
{
InitializeComponent();
}
#region Label DP
/// <summary>
/// Label dependency property
/// </summary>
public static readonly DependencyProperty LPShowFieldUCPercentCheck =
DependencyProperty.Register("Label",
typeof(string),
typeof(UserInputFieldUC),
new PropertyMetadata(""));
/// <summary>
/// Gets or sets the Label which is displayed to the field
/// </summary>
public string Label
{
get { return GetValue(LPShowFieldUCPercentCheck) as String; }
set { SetValue(LPShowFieldUCPercentCheck, value); }
}
#endregion // Label DP
#region Value DP
/// <summary>
/// Value dependency property.
/// </summary>
public static readonly DependencyProperty ValueProp =
DependencyProperty.Register("Value",
typeof(string),
typeof(UserInputFieldUC),
new PropertyMetadata(""));
/// <summary>
/// Gets or sets the value being displayed
/// </summary>
public string Value
{
get { return GetValue(ValueProp) as String; }
set { SetValue(ValueProp, value); }
}
#endregion // Value DP
#region Units DP
/// <summary>
/// Units dependency property
/// </summary>
public static readonly DependencyProperty UnitsProperty =
DependencyProperty.Register("Units",
typeof(string),
typeof(UserInputFieldUC),
new PropertyMetadata(""));
/// <summary>
/// Gets or sets the Units which is displayed to the field
/// </summary>
public string Units
{
get { return GetValue(UnitsProperty) as String; }
set { SetValue(UnitsProperty, value); }
}
#endregion // Units DP
#region Maximum Allowable Input Value DP
public static readonly DependencyProperty MaxInputProperty =
DependencyProperty.Register("UpperLimit",
typeof(string),
typeof(UserInputFieldUC), new PropertyMetadata(""));
public string UpperLimit
{
get { return GetValue(MaxInputProperty) as String; }
set { SetValue(MaxInputProperty, value); }
}
#endregion // Max Value DP
#region Minimum Allowable Input DP
public static readonly DependencyProperty MinInputProperty =
DependencyProperty.Register("LowerLimit",
typeof(string),
typeof(UserInputFieldUC), new PropertyMetadata(""));
public string LowerLimit
{
get { return GetValue(MinInputProperty) as String; }
set { SetValue(MinInputProperty, value); }
}
#endregion // Max Value DP
#region Display Error DP
public static readonly DependencyProperty ErrorProperty =
DependencyProperty.Register("ShowErr",
typeof(string),
typeof(UserInputFieldUC),
new PropertyMetadata(""));
public string ShowErr
{
get { return GetValue(ErrorProperty) as String; }
set { SetValue(ErrorProperty, value); }
}
#endregion // Display Error DP
/// <summary>
/// Check user input
/// </summary>
private void MetricValue_TextChanged(object sender, TextChangedEventArgs e)
{
string inputText = MetricValue.Text;
if (inputText == "")
inputText = "0";
double inputValue = Convert.ToDouble(inputText);
double maxValue = Convert.ToDouble(UpperLimit);
double minValue = Convert.ToDouble(LowerLimit);
ErrorDisplay.Text = "OK";
if (inputValue <= minValue)
{
ErrorDisplay.Text = "Err";
}
if (inputValue >= maxValue)
{
ErrorDisplay.Text = "Err";
}
}
}
}
“ MainWindow”的XAML代码显示用户控件和提交按钮:
<Window x:Class="TestUserInput.MainWindow"
...
xmlns:local="clr-namespace:TestUserInput"
Title="MainWindow" Height="450" Width="350">
<Grid>
<StackPanel Margin="5">
<local:UserInputFieldUC Margin="5" Label="Param1" Value="{Binding Path=Param1}" Units="m" LowerLimit="5" UpperLimit="10"/>
<local:UserInputFieldUC Margin="5" Label="Param2" Value="{Binding Path=Param2}" Units="m" LowerLimit="50" UpperLimit="100"/>
<Button Content="Submit"/>
</StackPanel>
</Grid>
</Window>
主窗口的视图模型实现了通常的INotifyPropertyChangedview接口
public class MainWindowViewModel : INotifyPropertyChanged
{
public double Param1 { get; set; }
public double Param2 { get; set; }
...
...
}
答案 0 :(得分:2)
通常的方法是将验证与可以执行的命令结合在一起。
您将在窗口viewmodel上将ICommand的实现公开为公共属性。调用该SubmitCommand。通常是中继命令或委托命令,但这可能与主题略有出入。
icommand提供的功能之一就是可以执行。这是一个返回true或false的方法。如果返回false,将禁用带有此命令绑定的按钮。
您的mainwindowview模型还将实现验证接口之一。推荐使用InotifyDataErrorinfo。
一个完整的完整工作示例将很复杂。
本质上,您需要两件事。
您的视图需要告诉视图模型是否存在从视图到绑定属性的转换失败。这将在绑定到双精度字符(例如)的文本框中键入文本。 否则,视图模型将不知道这些失败。
视图模型还需要在属性值更改时检查其值。在这种特定情况下,似乎可以使用range属性,但通常还需要一系列函子。
我看看是否可以找到一些代码。
在视图中,您要处理绑定传递错误事件,该事件将冒泡。添加错误以及删除错误时,您都会收到通知。
我发现的示例是使用mvvmlight的.Net旧版本,并且可能不是直接粘贴到.net核心上。仍然说明了原理。
在父面板中(例如,窗口的主网格)。您需要一些事件处理程序。我的示例使用了交互(现在是xaml behaviors nuget)。
<i:Interaction.Triggers>
<UIlib:RoutedEventTrigger RoutedEvent="{x:Static Validation.ErrorEvent}">
<e2c:EventToCommand
Command="{Binding ConversionErrorCommand, Mode=OneWay}"
EventArgsConverter="{StaticResource BindingErrorEventArgsConverter}"
PassEventArgsToCommand="True" />
</UIlib:RoutedEventTrigger>
<UIlib:RoutedEventTrigger RoutedEvent="{x:Static Binding.SourceUpdatedEvent}">
<e2c:EventToCommand
Command="{Binding SourceUpdatedCommand, Mode=OneWay}"
EventArgsConverter="{StaticResource BindingSourcePropertyConverter}"
PassEventArgsToCommand="True" />
</UIlib:RoutedEventTrigger>
</i:Interaction.Triggers>
那些转换器:
public object Convert(object value, object parameter)
{
ValidationErrorEventArgs e = (ValidationErrorEventArgs)value;
PropertyError err = new PropertyError();
err.PropertyName = ((System.Windows.Data.BindingExpression)(e.Error.BindingInError)).ResolvedSourcePropertyName;
err.Error = e.Error.ErrorContent.ToString();
// Validation.ErrorEvent fires both when an error is added AND removed
if (e.Action == ValidationErrorEventAction.Added)
{
err.Added = true;
}
else
{
err.Added = false;
}
return err;
}
正如我所提到的,您将获得一个事件,该事件添加了一个错误,而另一个则被删除。因此,如果还有。
另一个转换器正在告诉viewmodel哪个属性已更改。还有其他选项,例如以一种通用的基本方法行事,用于从所有属性设置器发出属性更改通知。
public class BindingSourcePropertyConverter : IEventArgsConverter
{
public object Convert(object value, object parameter)
{
DataTransferEventArgs e = (DataTransferEventArgs)value;
Type type = e.TargetObject.GetType();
BindingExpression binding = ((FrameworkElement)e.TargetObject).GetBindingExpression(e.Property);
return binding.ParentBinding.Path.Path ?? "";
//return binding.ResolvedSourcePropertyName ?? "";
}
}
请注意,这可以直接使用具有值类型属性的viewmodel框。您的viewmodel属性是一个复杂的对象,并且具有绑定属性,而另一层向下则需要更多的复杂性。
在视图中,您还需要告诉它从您要验证以下数据的每个绑定中进行通知:
<TextBox Text="{Binding FirstName
, UpdateSourceTrigger=PropertyChanged
, NotifyOnSourceUpdated=True}"
基本视图模型有点复杂。如您所见,它实现了INotifyDataErrorInfo。在视图模型上使用Validator.Validate,该模型将检查其上的数据注释。还有一个谓词列表,用于处理不适合注释的复杂验证。
所有这些都用于驱动属性IsValid。 如果那是假的,那么您可以在submitcommand上执行的操作应该返回假。
public class BaseValidVM : BaseNotifyUI, INotifyDataErrorInfo, INotifyPropertyChanged
{
// From Validation Error Event
private RelayCommand<PropertyError> conversionErrorCommand;
public RelayCommand<PropertyError> ConversionErrorCommand
{
get
{
return conversionErrorCommand
?? (conversionErrorCommand = new RelayCommand<PropertyError>
(PropertyError =>
{
if (PropertyError.Added)
{
AddError(PropertyError.PropertyName, PropertyError.Error, ErrorSource.Conversion);
}
FlattenErrorList();
}));
}
}
// From Binding SourceUpdate Event
private RelayCommand<string> sourceUpdatedCommand;
public RelayCommand<string> SourceUpdatedCommand
{
get
{
return sourceUpdatedCommand
?? (sourceUpdatedCommand = new RelayCommand<string>
(Property =>
{
ValidateProperty(Property);
}));
}
}
private RelayCommand validateCommand;
public RelayCommand ValidateCommand
{
get
{
return validateCommand
?? (validateCommand = new RelayCommand
(() =>
{
bool isOk = IsValid;
RaisePropertyChanged("IsValid");
}));
}
}
private ObservableCollection<PropertyError> errorList = new ObservableCollection<PropertyError>();
public ObservableCollection<PropertyError> ErrorList
{
get
{
return errorList;
}
set
{
errorList = value;
RaisePropertyChanged();
}
}
protected Dictionary<string, List<AnError>> errors = new Dictionary<string, List<AnError>>();
protected bool isBusy = false;
public bool IsBusy
{
get { return isBusy; }
set { isBusy = value; RaisePropertyChanged("IsBusy"); }
}
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
public IEnumerable GetErrors(string property)
{
if (string.IsNullOrEmpty(property))
{
return null;
}
if (errors.ContainsKey(property) && errors[property] != null && errors[property].Count > 0)
{
return errors[property].Select(x => x.Text).ToList();
}
return null;
}
public bool HasErrors
{
get { return errors.Count > 0; }
}
public void NotifyErrorsChanged(string propertyName)
{
if (ErrorsChanged != null)
{
ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
}
}
public virtual Dictionary<string, List<PredicateRule>> ValiditionRules { get; set; }
private List<string> lastListFailures = new List<string>();
public bool IsValid {
get
{
// Clear only the errors which are from object Validation
// Conversion errors won't be detected here
RemoveValidationErrorsOnly();
var vContext = new ValidationContext(this, null, null);
List<ValidationResult> vResults = new List<ValidationResult>();
Validator.TryValidateObject(this, vContext, vResults, true);
TransformErrors(vResults);
// Iterating the dictionary allows you to check the rules for each property which has any rules
if(ValiditionRules != null)
{
foreach (KeyValuePair<string, List<PredicateRule>> ppty in ValiditionRules)
{
ppty.Value.Where(x => x.IsOK(this) == false)
.ToList()
.ForEach(x =>
AddError(ppty.Key, x.Message, ErrorSource.Validation)
);
}
}
var propNames = errors.Keys.ToList();
propNames.Concat(lastListFailures)
.Distinct()
.ToList()
.ForEach(pn => NotifyErrorsChanged(pn));
lastListFailures = propNames;
FlattenErrorList();
//foreach (var item in errors)
//{
// Debug.WriteLine($"Errors on {item.Key}");
// foreach (var err in item.Value)
// {
// Debug.WriteLine(err.Text);
// }
//}
if (propNames.Count > 0)
{
return false;
}
return true;
}
}
private void RemoveValidationErrorsOnly()
{
foreach (KeyValuePair<string, List<AnError>> pair in errors)
{
List<AnError> _list = pair.Value;
_list.RemoveAll(x => x.Source == ErrorSource.Validation);
}
var removeprops = errors.Where(x => x.Value.Count == 0)
.Select(x => x.Key)
.ToList();
foreach (string key in removeprops)
{
errors.Remove(key);
}
}
public void ValidateProperty(string propertyName)
{
errors.Remove(propertyName);
lastListFailures.Add(propertyName);
if(!propertyName.Contains("."))
{
var vContext = new ValidationContext(this, null, null);
vContext.MemberName = propertyName;
List<ValidationResult> vResults = new List<ValidationResult>();
Validator.TryValidateProperty(this.GetType().GetProperty(propertyName).GetValue(this, null), vContext, vResults);
TransformErrors(vResults);
}
// Apply Predicates
// ****************
if (ValiditionRules !=null && ValiditionRules.ContainsKey(propertyName))
{
ValiditionRules[propertyName].Where(x => x.IsOK(this) == false)
.ToList()
.ForEach(x =>
AddError(propertyName, x.Message, ErrorSource.Validation)
);
}
FlattenErrorList();
NotifyErrorsChanged(propertyName);
RaisePropertyChanged("IsValid");
}
private void TransformErrors(List<ValidationResult> results)
{
foreach (ValidationResult r in results)
{
foreach (string ppty in r.MemberNames)
{
AddError(ppty, r.ErrorMessage, ErrorSource.Validation);
}
}
}
private void AddError(string ppty, string err, ErrorSource source)
{
List<AnError> _list;
if (!errors.TryGetValue(ppty, out _list))
{
errors.Add(ppty, _list = new List<AnError>());
}
if (!_list.Any(x => x.Text == err))
{
_list.Add(new AnError { Text = err, Source = source });
}
}
private void FlattenErrorList()
{
ObservableCollection<PropertyError> _errorList = new ObservableCollection<PropertyError>();
foreach (var prop in errors.Keys)
{
List<AnError> _errs = errors[prop];
foreach (AnError err in _errs)
{
_errorList.Add(new PropertyError { PropertyName = prop, Error = err.Text });
}
}
ErrorList = _errorList;
}
public void ClearErrors()
{
List<string> oldErrorProperties = errors.Select(x => x.Key.ToString()).ToList();
errors.Clear();
ErrorList.Clear();
foreach (var p in oldErrorProperties)
{
NotifyErrorsChanged(p);
}
NotifyErrorsChanged("");
}
}
一个示例视图模型:
public class UserControl1ViewModel : BaseValidVM
{
private string firstName;
[Required]
[StringLength(20, MinimumLength = 2, ErrorMessage = "Invalid length for first name")]
public string FirstName
{
get { return firstName; }
set { firstName = value; RaisePropertyChanged(); }
}
private string surName;
[Required]
[StringLength(40, MinimumLength = 4, ErrorMessage = "Invalid length for last name")]
public string SurName
{
get { return surName; }
set { surName = value; RaisePropertyChanged(); }
}
private Decimal amount = 1;
[Required]
[MaxDecimalPlaces(MantissaDigits = 1)]
public Decimal Amount
{
get { return amount; }
set { amount = value; RaisePropertyChanged(); }
}
private Decimal amount2 = 2;
[Required]
[MaxDecimalPlaces(ErrorMessage = "Amount 2 is money, it can have up to 2 numbers after the decimal place.")]
public Decimal Amount2
{
get { return amount2; }
set { amount2 = value; RaisePropertyChanged(); }
}
private DateTime orderDate = DateTime.Now.Date;
[Required]
public DateTime OrderDate
{
get { return orderDate; }
set { orderDate = value; RaisePropertyChanged(); }
}
private RelayCommand saveCommand;
// If IsValid is false then a button bound to savecommand will be disabled.
public RelayCommand SaveCommand
{
get
{
return saveCommand
?? (saveCommand = new RelayCommand
(async () =>
{
if (IsBusy)
{
return;
}
IsBusy = true;
Debug.WriteLine("Would have saved");
await Task.Delay(2000); // Simulates a delay doing something DO NOT USE LIVE
IsBusy = false;
SaveCommand.RaiseCanExecuteChanged(); // Force UI to requery canexecute
},
() => IsValid && !IsBusy // CanExecute when valid and not busy
));
}
}
// Empty string is validation which is not at all property specific
// Otherwise.
// Add an entry per property you need validation on with the list of PredicateRule containing the validation(s)
// to apply.
public override Dictionary<string, List<PredicateRule>> ValiditionRules { get; set; }
public UserControl1ViewModel()
{
// Constructor of the inheriting viewmodel adds any rules which do not suit annotations
// Note
// Two alternative styles of writing rules:
ValiditionRules = new Dictionary<string, List<PredicateRule>>
{
{"Amount2",
new List<PredicateRule>
{
new PredicateRule
{
Message ="Amount2 must be greater than Amount",
IsOK = x => Amount2 > Amount
}
}
},
{"OrderDate",
new List<PredicateRule>
{
new PredicateRule
{
Message ="Amount must be greater than 1 if Order Date is in future",
IsOK = x =>
{
if(OrderDate.Date > DateTime.Now.Date)
{
if(Amount <= 1)
{
return false;
}
}
return true;
}
},
new PredicateRule
{
Message ="Order Date may only be a maximum of 31 days in the future",
IsOK = x => (OrderDate - DateTime.Now.Date).Days < 31
}
}
}
};
}
}
此代码基于我编写的示例:
https://gallery.technet.microsoft.com/WPF-Entity-Framework-MVVM-78cdc204
该示例通过viewmodels公开EF模型类型。
我计划了系列中的三分之一,但没有时间。
我建议不要将模型属性直接绑定到“伙伴”类中,而是将验证属性放置在“伙伴”类中,而不是直接将其复制到视图模型中,然后再次提交以进行提交。