Thaxll'ssillyia
Captain
- Registriert
- Dez. 2007
- Beiträge
- 3.532
Hallo Community!
Ich sitze seit ein paar Stunden an einem Problem, welches in meiner C#-WPF Anwendung auftritt. Zur Vereinfachung des Problems hab ich das mal auf nen Beispiel reduziert.
Ausgangslage:
Ich hab ein ViewModel mit INotifyDataErrorInfo-Implementierung. Zwei String-Properties (Firstname und Lastname) sind mit Validator-Attributen ausgestattet (MaxLength und Required). Ich nutze für die Validierung einen (teilweise) selbst geschriebenen ErrorValidationHelper, der auch einwandfrei funktioniert (mehrfach durch UnitTests abgesichert).
Im ViewModel wird beim Konstruktor zwei Werte für die strings in Temp-Felder gelegt, die dann in der (asynchronen) LoadData-Methode den Properties übergeben werden. Außerdem wird in der Load-Data Methode ein Task gestartet (im Original eine WCF-Methode awaited).
Problembeschreibung:
Scheinbar völlig undeterministisch sind die von WPF standardmäßig gezeichneten roten Balken um das Textfeld beim erstmaligen Aufruf der View da oder nicht da. Richtigerweise müssten diese nicht da sein, da die Bedingungen der Validators erfüllt sind (nicht leer und nicht zu lang). Wenn sie rot markiert sind, kann ich die Textboxen leer machen und neu befüllen, dann klappt die Validierung auf der Oberfläche.
Jetzt kommts: Lasse ich den Task mit dem Sleep(0) weg, funktioniert alles bestens. Ebenfalls funktioniert es, wenn ich das Window nicht als Child öffne, sondern als Hauptfenster. Erhöhe ich den Sleep-Wert auf bsp. 20, ist die Validierung nahezu immer fehlerhaft.
Es handelt sich um ein Problem der Oberfläche. Ich vermute hier konkurrierende Tasks, wobei ich den Fehler erstmal nicht blicke. Kann mir jemand helfen? Danke schonmal!
Im Anhang auch nochmal das VS-Projekt zum Testen (enthält auch bereits kompilierte exe).
ViewModel des Childs
ErrorValidationHelper
Anmerkung:
Der ErrorValidationHelper arbeitet nach folgendem Prinzip:
- Bei dessen Erzeugung werden alle Properties mit Validator-Attribut des generic types identifiziert, dann werden Accessoren für die entsprechenden Properties erzeugt und anschließend alle Properties validiert
- Die Validierung funktioniert über Error-Listen, wo die Fehler hinzugefügt oder gelöscht werden
- Bei jedem PropertyChange wird die Validierung für das entsprechende Property neu durchgeführt
Ich sitze seit ein paar Stunden an einem Problem, welches in meiner C#-WPF Anwendung auftritt. Zur Vereinfachung des Problems hab ich das mal auf nen Beispiel reduziert.
Ausgangslage:
Ich hab ein ViewModel mit INotifyDataErrorInfo-Implementierung. Zwei String-Properties (Firstname und Lastname) sind mit Validator-Attributen ausgestattet (MaxLength und Required). Ich nutze für die Validierung einen (teilweise) selbst geschriebenen ErrorValidationHelper, der auch einwandfrei funktioniert (mehrfach durch UnitTests abgesichert).
Im ViewModel wird beim Konstruktor zwei Werte für die strings in Temp-Felder gelegt, die dann in der (asynchronen) LoadData-Methode den Properties übergeben werden. Außerdem wird in der Load-Data Methode ein Task gestartet (im Original eine WCF-Methode awaited).
Problembeschreibung:
Scheinbar völlig undeterministisch sind die von WPF standardmäßig gezeichneten roten Balken um das Textfeld beim erstmaligen Aufruf der View da oder nicht da. Richtigerweise müssten diese nicht da sein, da die Bedingungen der Validators erfüllt sind (nicht leer und nicht zu lang). Wenn sie rot markiert sind, kann ich die Textboxen leer machen und neu befüllen, dann klappt die Validierung auf der Oberfläche.
Jetzt kommts: Lasse ich den Task mit dem Sleep(0) weg, funktioniert alles bestens. Ebenfalls funktioniert es, wenn ich das Window nicht als Child öffne, sondern als Hauptfenster. Erhöhe ich den Sleep-Wert auf bsp. 20, ist die Validierung nahezu immer fehlerhaft.
Es handelt sich um ein Problem der Oberfläche. Ich vermute hier konkurrierende Tasks, wobei ich den Fehler erstmal nicht blicke. Kann mir jemand helfen? Danke schonmal!
Im Anhang auch nochmal das VS-Projekt zum Testen (enthält auch bereits kompilierte exe).
ViewModel des Childs
Code:
namespace InputValidationTesting.ViewModel {
using System;
using System.Collections;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using InputValidationTesting.Helper;
public class CustomerEditViewModel : INotifyPropertyChanged, INotifyDataErrorInfo {
private readonly ErrorValidationHelper<CustomerEditViewModel> _errorValidationHelper;
private readonly string _firstnameTemp;
private readonly string _lastnameTemp;
private string _firstname;
private string _lastname;
public CustomerEditViewModel() {
_firstnameTemp = "Vorname1";
_lastnameTemp = "Vorname2";
_errorValidationHelper = new ErrorValidationHelper<CustomerEditViewModel>(this);
}
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Liefert oder setzt den Vornamen des Kunden.
/// </summary>
[Required(AllowEmptyStrings = false, ErrorMessage = "Vorname wird benötigt!")]
[MaxLength(256, ErrorMessage = "Maximal 256 Zeichen!")]
public string Firstname {
get { return _firstname; }
set {
_firstname = value;
OnPropertyChanged();
}
}
public bool HasErrors {
get { return _errorValidationHelper.HasErrors; }
}
/// <summary>
/// Liefert oder setzt den Nachnamen des Kunden.
/// </summary>
[Required(AllowEmptyStrings = false, ErrorMessage = "Nachname wird benötigt!")]
[MaxLength(256, ErrorMessage = "Maximal 256 Zeichen!")]
public string Lastname {
get { return _lastname; }
set {
_lastname = value;
OnPropertyChanged();
}
}
public IEnumerable GetErrors(string propertyName) {
return _errorValidationHelper.GetErrors(propertyName);
}
public void InvokeErrorsChanged(DataErrorsChangedEventArgs e) {
ErrorsChanged?.Invoke(this, e);
}
public async void LoadData() {
await Task.Run(() => {
Thread.Sleep(0);
});
Firstname = _firstnameTemp;
Lastname = _lastnameTemp;
}
protected void OnPropertyChanged([CallerMemberName] string propertyName = null) {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
ErrorValidationHelper
Code:
namespace InputValidationTesting.Helper {
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using InputValidationTesting.ViewModel;
public class ErrorValidationHelper<T>
where T : CustomerEditViewModel {
private readonly IDictionary<String, IList<String>> _errors = new Dictionary<string, IList<string>>();
private readonly IDictionary<string, KeyValuePair<Func<T, object>, ValidationBaseAttribute[]>> _validators;
private readonly T _viewModel;
public ErrorValidationHelper(T viewModel) {
if (viewModel == null) {
throw new ArgumentNullException(nameof(viewModel));
}
_viewModel = viewModel;
_validators = new Dictionary<string, KeyValuePair<Func<T, object>, ValidationBaseAttribute[]>>();
foreach (PropertyInfo property in typeof(T).GetProperties()) {
ValidationBaseAttribute[] validations = GetValidations(property);
if (validations.Length > 0) {
_validators.Add(property.Name,
new KeyValuePair<Func<T, object>, ValidationBaseAttribute[]>(CreateValueGetter(property), validations));
Validate(_viewModel, property.Name);
}
}
viewModel.PropertyChanged += delegate(object sender, PropertyChangedEventArgs args) {
Validate(viewModel, args.PropertyName);
};
}
public bool HasErrors {
get { return _errors.Count > 0; }
}
public void AddError(string propertyName, string error, bool isWarning) {
if (!_errors.ContainsKey(propertyName))
_errors[propertyName] = new List<string>();
if (!_errors[propertyName].Contains(error)) {
if (isWarning)
_errors[propertyName].Add(error);
else
_errors[propertyName].Insert(0, error);
RaiseErrorsChanged(propertyName);
}
}
public IEnumerable GetErrors(string propertyName) {
if (string.IsNullOrEmpty(propertyName) || !_errors.ContainsKey(propertyName))
return null;
return _errors[propertyName];
}
public void RaiseErrorsChanged(string propertyName) {
_viewModel.InvokeErrorsChanged(new DataErrorsChangedEventArgs(propertyName));
}
public void RemoveError(string propertyName, string error) {
if (_errors.ContainsKey(propertyName) && _errors[propertyName].Contains(error)) {
_errors[propertyName].Remove(error);
if (_errors[propertyName].Count == 0)
_errors.Remove(propertyName);
RaiseErrorsChanged(propertyName);
}
}
public void Validate(T source, string columnName) {
KeyValuePair<Func<T, object>, ValidationBaseAttribute[]> validators;
bool isWatched = _validators.TryGetValue(columnName, out validators);
if (isWatched) {
object value = validators.Key(source);
ValidationBaseAttribute[] validationAttributes = validators.Value;
IEnumerable<ValidationBaseAttribute> invalidAttributes = validationAttributes.Where(v => !v.IsValid(value));
IEnumerable<ValidationBaseAttribute> validAttributes = validationAttributes.Where(v => v.IsValid(value));
foreach (ValidationBaseAttribute validationAttribute in invalidAttributes) {
AddError(columnName, validationAttribute.GetErrorMessageString(), false);
}
foreach (ValidationBaseAttribute validationAttribute in validAttributes) {
RemoveError(columnName, validationAttribute.GetErrorMessageString());
}
}
}
private static Func<T, object> CreateValueGetter(PropertyInfo property) {
ParameterExpression instance = Expression.Parameter(typeof(T), "i");
UnaryExpression cast = Expression.TypeAs(Expression.Property(instance, property), typeof(object));
return (Func<T, object>)Expression.Lambda(cast, instance).Compile();
}
private static ValidationBaseAttribute[] GetValidations(PropertyInfo property) {
return (ValidationBaseAttribute[])property.GetCustomAttributes(typeof(ValidationBaseAttribute), true);
}
}
}
Anmerkung:
Der ErrorValidationHelper arbeitet nach folgendem Prinzip:
- Bei dessen Erzeugung werden alle Properties mit Validator-Attribut des generic types identifiziert, dann werden Accessoren für die entsprechenden Properties erzeugt und anschließend alle Properties validiert
- Die Validierung funktioniert über Error-Listen, wo die Fehler hinzugefügt oder gelöscht werden
- Bei jedem PropertyChange wird die Validierung für das entsprechende Property neu durchgeführt
Anhänge
Zuletzt bearbeitet: