IReactiveBinding doesn't work with IDataErrorInfo - c#

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.

Related

How to get a list of all errors in a column on a WPF DataGrid control?

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.

IDataErrorInfo validate before binding occurs

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;
}
}
}

how to prevent validation when view and viewModel are initialized and displayed for the first time?

I am using Prism+MVVM+C#+WPF for a LoB application.
I've created a ViewModelBase class, which is inherited by all my viewModels.
This base class implements:
IViewModel (one of my base interfaces)
IDataErrorInfo (in order to allow ViewModel validation)
It follows my IDataErrorInfo implementation:
#region IDataErrorInfo members
public string this[string propertyName]
{
get { return this.Validate(propertyName); }
}
public string Error { get; private set; }
#endregion
protected string Validate(string propertyName)
{
this.Error = null;
PropertyInfo property = this.GetType().GetProperty(propertyName);
if (property == null)
throw new ArgumentException("propertyName");
foreach (ValidationAttribute attribute in property.GetCustomAttributes(typeof(ValidationAttribute), true))
{
try
{
object currentValue = property.GetValue(this, null);
attribute.Validate(currentValue, propertyName);
}
catch (ValidationException ex)
{
string errorMessage = (!string.IsNullOrWhiteSpace(attribute.ErrorMessage) ? attribute.ErrorMessage: ex.Message);
if (this.Error == null)
this.Error = errorMessage;
else
this.Error += string.Format("\r\n{0}", errorMessage);
}
}
return this.Error;
}
In a given point of the application, I am building and associating View and ViewModel in this way:
IViewModel viewModel = this.ServiceLocator.GetInstance(typeof(IMyViewModel)) as IMyViewModel;
IView view = this.ServiceLocator.GetInstance(type) as IMyView;
view.ViewModel = viewModel;
this.GlobalRegionManager.Regions[RegionNames.InnerRegion].Add(view);
The problem is occurs when the view starts to read up the viewModel properties. The "this[string propertyName]" is invoked and the validation function gets executed...
In the views, the binding of the properties that need to be validated is defined as:
Text="{Binding SourceFileName, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}"
Could you advice on how to prevent the initial validation?
Thanks in advance,
Gianluca

NavigationService WithParam of Caliburn Micro for WP

As is well known, CM doesn't support passing a object of complex type through NavigationService like MVVM Light. So I searched for a workaround and did it like this.
There are two viewmodels: MainPageViewModel and SubPageViewModel.
I first defined 3 classes, namely GlobalData, SnapshotCache and StockSnapshot. StockSnapshot is the type of which the object I want to pass between the 2 viewmodels.
public class SnapshotCache : Dictionary<string, StockSnapshot>
{
public StockSnapshot GetFromCache(string key)
{
if (ContainsKey(key))
return this[key];
return null;
}
}
public class GlobalData
{
private GlobalData()
{
}
private static GlobalData _current;
public static GlobalData Current
{
get
{
if (_current == null)
_current = new GlobalData();
return _current;
}
set { _current = value; }
}
private SnapshotCache _cachedStops;
public SnapshotCache Snapshots
{
get
{
if (_cachedStops == null)
_cachedStops = new SnapshotCache();
return _cachedStops;
}
}
}
public class StockSnapshot
{
public string Symbol { get; set; }
public string Message { get; set; }
}
Next, I call the navigation service on MainPageViewModel like this:
StockSnapshot snap = new StockSnapshot {Symbol="1", Message = "The SampleText is here again!" };
GlobalData.Current.Snapshots[snap.Symbol] = snap;
NavigationService.UriFor<SubPageViewModel>().WithParam(p=>p.Symbol,snap.Symbol).Navigate();
And on SubPageViewModel I've got this:
private string _symbol;
public string Symbol
{
get { return _symbol; }
set
{
_symbol = value;
NotifyOfPropertyChange(() => Symbol);
}
}
public StockSnapshot Snapshot
{
get { return GlobalData.Current.Snapshots[Symbol]; }
}
And that's where the problem lies. When I run the program, I find out that it always runs to the getter of Snapshot first, when Symbol hasn't been initialized yet. So later I've tried adding some extra code to eliminate the ArgumentNullException so that it can run to the setter of Symbol and then everything goes fine except that the UI doesn't get updated anyway.
Could anyone tell me where I've got wrong?
Thx in advance!!
Why not just use:
private string _symbol;
public string Symbol
{
get { return _symbol;}
set
{
_symbol = value;
NotifyOfPropertyChange(() => Symbol);
NotifyOfPropertyChange(() => Snapshot);
}
}
public StockSnapshot Snapshot
{
get { return Symbol!=null? GlobalData.Current.Snapshots[Symbol]:null; }
}
In this case you don't try and get the data from GlobalData when Symbol is null (sensible approach anyway!) and when "Symbol" is set you call NotifyOfPropertyChange() on Snapshot to force a re-get of the property.

How to show other window from viewmodel?

I have a simple sample that my sample has 2 window :
1-ProductlistView 2-ProductEditView (1-ProductlistViewModel 2-ProductEditViewModel)
I want the user can select a product in my ProductlistView and edit selected product in ProductEditView ...i'm using from this code in my sample:
public Class ProductEditViewModel:ViewModelBase
{
private readonly ProductEditView View;
public ProductModel Model { get; set; }
public ProductEditViewModel(Product myproduct)
{
View = new ProductEditView { DataContext = this };
if(myproduct!= null) Model = myproduct;
}
private bool IsInDialogMode;
public bool? ShowDialog()
{
if (IsInDialogMode) return null;
IsInDialogMode = true;
return View.ShowDialog();
}
}
and write to my editCommant in ProductlistViewModel:
private RelayCommand UpdateProductmdInstance;
public RelayCommand UpdateProductCommand
{
get
{
if (UpdateProductmdInstance!= null) return UpdateProductmdInstance;
UpdateProductmdInstance= new RelayCommand(a => OpenProductDetail(SelectedProduct), p => SelectedProduct!= null);
return UpdateProductmdInstance;
}
}
private void OpenProductDetail(Product product)
{
var ProductEditViewModel= new ProductEditViewModel(product);
var result = personDetailViewModel.ShowDialog();
...
}
I was wondering my sample is wrong?
Can i have an instance from a view in its viewmodel?
If my Sample is wrong how can i do this solution(send an object to other window and after edit get it)?
It is normally recommended to NOT have a ViewModel referencing a View. See this question on how to show a dialog from ViewModel.

Categories

Resources