I want to do some simple textbox validation in WPF, but I just realized that IDataErrorInfo relies on raising the PropertyChanged event in order to trigger the validation, which means that the invalid value is applied to my bound object before validation occurs. Is there a way to change this so the validation happens first (and prevents binding on invalid data), or is there another solution that works this way?
Trimmed down code looks like this:
<TextBox>
<TextBox.Text>
<Binding Path="MyProperty" ValidatesOnDataErrors="True" />
</TextBox.Text>
</TextBox>
public class MyViewModel : IDataErrorInfo
{
public string MyProperty
{
get { return _myProperty; }
set
{
if (_myProperty != value)
{
_myProperty = value;
NotifyPropertyChanged(() => MyProperty);
SaveSettings();
}
}
}
public string Error
{
get { return string.Empty; }
}
public string this[string columnName]
{
get
{
if (columnName == "MyProperty")
return "ERROR";
return string.Empty;
}
}
}
The better interface and validation method to use (if using .net 4.5) is INotifyDataErrorInfo. It's main advantage is allowing you to control when and how the validation occurs. One good overview:
http://anthymecaillard.wordpress.com/2012/03/26/wpf-4-5-validation-asynchrone/
I don't think you need to call SaveSettings() method every time property changed. I think it should be called when user click on "Save" button, but not when property changed. However if you still would like to save changes on property changed, you should only do it if there are no validation errors available. For instance:
public class MyViewModel : IDataErrorInfo
{
public string MyProperty
{
get { return _myProperty; }
set
{
if (_myProperty != value)
{
_myProperty = value;
NotifyPropertyChanged(() => MyProperty);
if (string.IsNullOrEmpty(this["MyProperty"]))
{
SaveSettings();
}
}
}
}
public string Error
{
get { return string.Empty; }
}
public string this[string columnName]
{
get
{
if (columnName == "MyProperty")
return "ERROR";
return string.Empty;
}
}
}
Related
I have a DataGrid control bound to an ObservableCollection of a model type that implements INotifyDataErrorInfo. The DataGrid displays the error info correctly on cells in the PhoneNumber column. However, I want to display the number of phone numbers with error to the user before they submit the data to the database (say, a number with a tooltip somewhere on the page). I've scoured the internet for any clue about this, but nothing. Here's the implementation of the model class:
'''
public class ContactModel : ObservableObject, INotifyDataErrorInfo
{
private readonly List<string> _errors = new();
private string _firstName = String.Empty;
public string FirstName
{
get => _firstName;
set
{
_firstName = value;
OnPropertyChanged(nameof(FirstName));
}
}
private string _lastName = String.Empty;
public string LastName
{
get => _lastName;
set
{
_lastName = value;
OnPropertyChanged(nameof(LastName));
}
}
private string _phoneNumber = string.Empty;
public string PhoneNumber
{
get => _phoneNumber;
set
{
_phoneNumber = value;
OnPropertyChanged(nameof(PhoneNumber));
}
}
public bool HasErrors
{
get
{
return _errors.Any();
}
}
public IEnumerable GetErrors(string? propertyName)
{
switch (propertyName)
{
case nameof(PhoneNumber):
if (string.IsNullOrEmpty(nameof(PhoneNumber))
{
_errors.Add("Phone number is blank");
}
break;
case nameof(FirstName):
// do nothing
break;
case nameof(LastName):
// do nothing
break;
default:
break;
}
return _errors;
}
public event EventHandler<DataErrorsChangedEventArgs>? ErrorsChanged;
}
'''
this is a missunderstanding on INotifyDataErrorInfo implementation
on this way
you can validate only one property. if two property have errors, you cannot find error of each property
you cannot find model is valid or not, until GetError called.
add ValidationErrors property
make "_errors" property as public property as ObservableCollection
private ObservableCollection<ValidationResult> _errors;
public ObservableCollection<ValidationResult> Errors
{
get{return _errors;}
set{_errors = value;
OnPropertyChanged(nameof(Errors));
}
}
then construct it at model cunstructor.
then invoke validation procedure on property setter.
private string _phoneNumber = string.Empty;
public string PhoneNumber
{
get => _phoneNumber;
set
{
_errors.RemoveRange(_errors.Where(w=> w.Membernames.Containes(nameof(PhoneNumber))));
_phoneNumber = value;
if (string.IsNullOrEmpty(value)
{
_errors.Add(new ValidationResult("Phone number is blank",new string[]{nameof(PhoneNumber)}));
ErrorsChanged?.invoke(this,new DataErrorsChangedEventArgs(nameof(PhoneNumber)));
}
OnPropertyChanged(nameof(PhoneNumber));
}
}
change GetError to
public IEnumerable GetErrors(string? propertyName)
{
return _errors.Where(w=> w.Membernames.Containes(propertyName);
}
so you can add a listbox (or a complex control called ValidationSummary) to your form and bind it's source to ValidationErrors property of your model.
best practice implementation of InotifyPropertyInfo is to Implement it on basemodel (ObservableObject in your sample) class.
I have a problem using the IDataErrorInfo in combination with IReactiveBinding.Bind(). I hope someone here can help me.
I have a ViewModel that is inherited from ReactiveObject and implements the IDataErrorInfo interface.
public class MainWindowViewModel : ReactiveUI.ReactiveObject, IDataErrorInfo
{
private string username = string.Empty;
public string Username
{
get { return this.username; }
set { this.RaiseAndSetIfChanged(ref this.username, value); }
}
public MainWindowViewModel()
{
this.Validator = new MainWindowViewModelValidator();
}
public AbstractValidator<MainWindowViewModel> Validator { get; set; }
#region IDataErrorInfo Members
string IDataErrorInfo.Error
{
get
{
return Validator != null ? string.Join(Environment.NewLine, Validator.Validate(this).Errors.Select(x => x.ErrorMessage).ToArray())
: string.Empty;
}
}
string IDataErrorInfo.this[string propertyName]
{
get
{
if (Validator != null)
{
var results = Validator.Validate(this, propertyName);
if (results != null
&& results.Errors.Count() > 0)
{
var errors = string.Join(Environment.NewLine, results.Errors.Select(x => x.ErrorMessage).ToArray());
return errors;
}
}
return string.Empty;
}
}
#endregion
}
The MainWindowViewValidator ensures that the Usernameproperty is not empty.
The ViewModel is connected to the View in the code behind of the XAML-File:
public partial class MainWindow : IViewFor<MainWindowViewModel>
{
public MainWindow()
{
InitializeComponent();
this.ViewModel = new MainWindowViewModel();
this.Bind(this.ViewModel, viewmodel => viewmodel.Username, view => view.Username.Text);
}
public MainWindowViewModel ViewModel
{
get { return (MainWindowViewModel)GetValue(ViewModelProperty); }
set { SetValue(ViewModelProperty, value); }
}
public static readonly DependencyProperty ViewModelProperty = DependencyProperty.Register("ViewModel", typeof(MainWindowViewModel), typeof(MainWindow), new PropertyMetadata(null));
object IViewFor.ViewModel
{
get { return ViewModel; }
set { ViewModel = (MainWindowViewModel)value; }
}
}
The problem is now that the model validation is not called, as I don't specify the databinding in the XAML file directly.
Does anybody has a neat solution for this problem?
The problem is now that the model validation is not called, as I don't specify the databinding in the XAML file directly.
ReactiveUI doesn't participate in IDataErrorInfo and friends for binding. It used to, but really if you think about it, Validation itself is a derived property of the form.
ReactiveUI is already really good at describing how properties are related to each other (via WhenAny/ToProperty), so you should just construct an ValidationError property that displays the error message.
I have a property(in viewmodel) bound to a combobox.
When the viewmodel property changes, it uses the Messenger to tell another viewmodel about this.
This other viewmodel then decides if this is ok, if not i want to cancel and send the old value back up to the view.
I guess i can do this by setting the value to the new one first, then set it back. But is there a more elegant soulution?
Failing code
public DeckType SelectedDeckType
{
get { return _selectedDeckType; }
set
{
DeckTypeMessage deckTypeMessage = new DeckTypeMessage(value);
Messenger.Default.Send(deckTypeMessage);
if (deckTypeMessage.IsCancel)
{
//Some background magic finds out the value of this property is still the same?
//So the combobox does not revert!
//I can hack this but is there some way to force this?
RaisePropertyChanged();
return;
}
_selectedDeckType = value;
RaisePropertyChanged();
}
}
I managed to fix it with this workaround, but i dont like it :(
At first glance it seams to be incorrect, but the call stack makes it this way
Using oneway binding on SelectedItem and Interaction Trigger with command
Hacky workaround
public DeckType SelectedDeckType
{
get { return _selectedDeckType; }
set
{
_selectedDeckType = value;
RaisePropertyChanged();
}
}
public ICommand SelectedDeckTypeChangedCommand { get; private set; }
private void ExecuteSelectedItemChangedCommand(DeckType aDeckType)
{
try
{
if (_previousSelectedDeckType == aDeckType)
{
return;
}
_previousSelectedDeckType = aDeckType;
DeckTypeMessage deckTypeMessage = new DeckTypeMessage(this, aDeckType);
Messenger.Default.Send(deckTypeMessage);
if (deckTypeMessage.IsCancel)
{
SelectedDeckType = _selectedDeckType;
_previousSelectedDeckType = _selectedDeckType;
return;
}
SelectedDeckType = aDeckType;
}
catch (Exception ex)
{
NotifyMediator.NotifiyException(new NotifyMediator.NotifyInformation(NotifyMediator.NotificationLevel.Error, ex));
}
}
Kind Regards
You need to use Dispatcher.BeginInvoke to perform the reversal of the user action.
Basically, when the user selects the item on the combo box, any attempt to reject that value will be ignored by WPF. However, if you wait until all the code relating to data binding finishes, then you can basically start a new binding activity. This is what Dispatcher.BeginInvoke does. It allows your reset of the selected item to be postponed until the binding engine has finished its work.
Example:
public class MainViewModel : ViewModelBase
{
private string _selectedItem;
public List<string> Items { get; private set; }
public string SelectedItem
{
get { return _selectedItem; }
set
{
if (value == _selectedItem) return;
var previousItem = _selectedItem;
_selectedItem = value;
var isInvalid = value == "Bus"; // replace w/ your messenger code
if (isInvalid)
{
Application.Current.Dispatcher.BeginInvoke(
new Action(() => ResetSelectedItem(previousItem)),
DispatcherPriority.ContextIdle,
null);
return;
}
RaisePropertyChanged();
}
}
public MainViewModel()
{
Items = new[] { "Car", "Bus", "Train", "Airplane" }.ToList();
_selectedItem = "Airplane";
}
private void ResetSelectedItem(string previousItem)
{
_selectedItem = previousItem;
RaisePropertyChanged(() => SelectedItem);
}
}
I have created a Person MVVM model which needs to be validated. I am using IDataErrorInfo class and validating the user. But when the screen loads the textboxes are already red/validated indicating that the field needs to be filled out. I believe this is because I bind the PersonViewModel in the InitializeComponent. I tried to use LostFocus for updatetriggers but that did not do anything.
Here is my PersonViewModel:
public class PersonViewModel : IDataErrorInfo
{
private string _firstName;
private string _lastName;
public string LastName { get; set; }
public string Error
{
get { throw new NotImplementedException(); }
}
public string FirstName
{
get { return _firstName; }
set { _firstName = value; }
}
public string this[string columnName]
{
get
{
string validationResult = String.Empty;
switch(columnName)
{
case "FirstName":
validationResult = ValidateFirstName();
break;
case "LastName":
validationResult = ValidateLastName();
break;
default:
throw new ApplicationException("Unknown property being validated on the Product");
}
return validationResult;
}
}
private string ValidateLastName()
{
return String.IsNullOrEmpty(LastName) ? "Last Name cannot be empty" : String.Empty;
}
private string ValidateFirstName()
{
return String.IsNullOrEmpty(FirstName) ? "First Name cannot be empty" : String.Empty;
}
}
Here is the XAML:
<StackPanel>
<TextBlock>First Name</TextBlock>
<TextBox Text="{Binding FirstName, ValidatesOnDataErrors=True, UpdateSourceTrigger=LostFocus}" Background="Gray"></TextBox>
<TextBlock>Last Name</TextBlock>
<TextBox Text="{Binding LastName, ValidatesOnDataErrors=True, UpdateSourceTrigger=LostFocus}" Background="Gray"></TextBox>
</StackPanel>
MainWindow.cs:
public MainWindow()
{
InitializeComponent();
_personViewModel = new PersonViewModel();
this.DataContext = _personViewModel;
}
Am I missing something? I do not want the validation to be fired when the screen loads. I only want it to be fired when the user looses the focus of the textboxes.
Rather than fight the tide of how WPF works by default, consider redefining the UI so that the error display 'fits' the scenario of screen load as well as data entry error. Besides, a user should have some hints on a blank form of what is needed.
Create a method to do your validation, and store the validation results in a dictionary:
private Dictionary<string, string> _validationErrors = new Dictionary<string, string>();
public void Validate(string propertyName)
{
string validationResult = null;
switch(propertyName)
{
case "FirstName":
validationResult = ValidateFirstName();
break;
}
//etc.
}
//Clear dictionary properly here instead (You must also handle when a value becomes valid again)
_validationResults[propertyName] = validationResult;
//Note that in order for WPF to catch this update, you may need to raise the PropertyChanged event if you aren't doing so in the constructor (AFTER validating)
}
Then update your ViewModel to:
Have the indexer return a result from the _validationErrors instead, if present.
Call Validate() in your setters.
Optionally, in Validate(), if the propertyName is null, validate all properties.
WPF will call the indexer to display errors, and since you are returning something, it will think that there are errors. It won't unless you explictly call Validate() with this solution.
EDIT: Please also note that there is now a more efficient way of implementing validation in .NET 4.5 called INotifyDataErrorInfo.
I have a View where I bind some deep properties of a Model (using the naming convention of Caliburn.Micro):
View:
<UserControl x:Class="TOP.SomeView"
(...)
<TextBox x:Name="NewFooModel_Foo" .../>
Then I need to catch the firing of the INPC of that property in the ViewModel:
Model:
public class FooModel{
private string _foo;
(...)
public int Foo {
get { return _foo; }
set {
if (_foo != value) {
_foo = value;
NotifyOfPropertyChange(() => Foo);
}
}
}
}
From that point, the property of the model is binded correctly. So, I need that change to be notified to the CanCreateFoo and I don't know how:
ViewModel:
public class SomeViewModel{
(...)
public FooModel NewFooModel {
get { return _newFooModel; }
set {
if (_newFooModel != value) {
_newFooModel = value;
NotifyOfPropertyChange(() => Foo);
//HERE I NEED TO NOTIFY TO CANCREATEFOOMODEL THAT A PROPERTY OF THE MODEL IS CHANGED
}
}
}
public bool CanCreateFooModel{
get{
return NewFooModel.Foo != null;
}
}
}
Please, can someone help me? Thanks in advance.
You could use EventAggregator to publish a message when the Property changes (and also NotifyOfPropertyChange for your current VM).
Each Model which is interested can subscribe to this message and handle it.