La validation des données sur l'interface utilisateur avec WCF RIA et le motif de MVVM
-
25-09-2019 - |
Question
Y at-il une meilleure façon pratique ou largement acceptée de la structuration et la validation des données en utilisant MVVM conjointement avec les services de RIA en Silverlight?
Voici le point crucial de mon problème. Disons que j'ai un EmployeeView, EmployeeViewModel et une entité de l'employé. Dans les applications régulières RIA j'exposerai cette entité des employés sur la vue et j'obtenir la validation « gratuitement », parce que les entités mettent en œuvre INotifyDataErrorInfo et IDataErrorInfo (correct?).
Maintenant, si je veux exposer certaines propriétés des employés par un ViewModel au lieu de directement par une entité, alors il devient plus compliqué. Je pourrais exposer les bits que j'ai besoin directement et les accrocher dans l'entité sur le backend, comme ceci:
private Employee _employee;
public EmployeeViewModel()
{
_employee = new Employee();
}
public string Name
{
get { return _employee.Name; }
set
{
_employee.Name = value;
// fire property change, etc.
}
}
... mais je perds le savoureux « libre » validation des entités. Dans le cas contraire, je pourrais exposer l'entité directement dans le modèle de vue, comme si
private Employee _employee;
public Employee Employee
{
get { return _employee; }
}
public EmployeeViewModel()
{
_employee = new Employee();
}
Dans ce cas, la vue se lie directement à l'entité des employés et trouver ses propriétés là-dedans, comme ceci:
<StackPanel DataContext="{Binding Employee}">
<TextBox Text="{Binding Name}" />
</StackPanel>
En utilisant cette méthode, nous obtenons la validation « libre », mais ce n'est pas exactement une implémentation propre de MVVM.
Une troisième option serait de mettre en œuvre INotifyDataErrorInfo et IDataErrorInfo moi-même dans les machines virtuelles, mais cela semble être un très grand nombre de code de plomberie, considérant combien il serait facile pour moi d'utiliser la solution ci-dessus et avoir quelque chose un peu moins « propre » mais un diable de beaucoup plus facile à la fin de la journée.
Je suppose que ma question est, qui de ces approches sont appropriées dans quelle situation? Y at-il une meilleure approche que je suis absent?
Dans le cas où il est pertinent que je regarde le cadre Caliburn.Micro MVVM, mais je serais désireux de voir les réponses pertinentes génériquement.
La solution
J'utilise RIA avec Caliburn.Micro et je suis assez satisfait de ma solution pour la validation du côté client.
Ce que je l'ai fait est de mettre un ValidationBaseViewModel
entre Screen
(fourni par Caliburn.Micro) et mon application effective des machines virtuelles (EmployeeViewModel
dans votre cas). ValidationBaseViewModel
implémente INotifyDataErrorInfo
afin que le code de plomberie de votre parler est écrit seulement une fois. Je puis ajouter / supprimer / notify des erreurs par ValidationBaseViewModel
d'un remplacement de la (Caliburn.Micro) PropertyChangedBase.NotifyOfPropertyChange
avec le code suivant:
public override void NotifyOfPropertyChange(string property)
{
if (_editing == null)
return;
if (HasErrors)
RemoveErrorFromPropertyAndNotifyErrorChanges(property, 100);
if (_editing.HasValidationErrors)
{
foreach (var validationError in
_editing.ValidationErrors
.Where(error => error.MemberNames.Contains(property)))
{
AddErrorToPropertyAndNotifyErrorChanges(property, new ValidationErrorInfo() { ErrorCode = 100, ErrorMessage = validationError.ErrorMessage });
}
}
base.NotifyOfPropertyChange(property);
}
Il est en fait dans une autre machine virtuelle (entre ValidationBaseViewModel et EmployeeViewModel) avec la définition suivante:
public abstract class BaseEditViewModel<TEdit> :
ValidationBaseViewModel where TEdit : Entity
où Entity
est RIAs System.ServiceModel.DomainServices.Client.Entity
et l'élément de classe _editing
est une instance de ce type TEdit
qui est en cours de modification par la machine virtuelle en cours.
En combinaison avec coroutines Caliburn cela me permet de faire des trucs cool comme suit:
[Rescue]
public IEnumerable<IResult> Save()
{
if (HasErrors)
{
yield return new GiveFocusByName(PropertyInError);
yield break;
}
...
}
Autres conseils
Si vous ne souhaitez pas utiliser des ressources externes ou des cadres, alors je vous pourriez avoir un ViewModelBase
qui mettent en œuvre INotifyDataErrorInfo
.
Cette classe aura ValidateProperty(string propertyName, object value)
pour valider une propriété speciic et méthode Validate()
pour valider l'objet entier. En interne utiliser le Validator
classe pour retourner les ValidationResult
s.
Si vous utilisez le réflecteur, il peut être assez facile pour atteindre en mimant le processus de validation dans le Entity
classe lui-même à la ViewModelBase
.
Bien qu'il soit pas "libre", est encore relativement tho pas cher.
Voici une implémentation d'IDataErrorInfo
. Bien que non testé, vous donnera l'idée.
public class ViewModelBase : INotifyPropertyChanged, INotifyDataErrorInfo
{
/*
* InotifyPropertyChanged implementation
* Consider using Linq expressions instead of string names
*/
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
public IEnumerable GetErrors(string propertyName)
{
if (implValidationErrors == null) return null;
return ImplValidationErros.Where(ve =>
ve.MemberNames.Any(mn => mn == propertyName));
}
public bool HasErrors
{
get
{
return implValidationErrors == null || ImplValidationErros.Any();
}
}
private List<ValidationResult> implValidationErrors;
private List<ValidationResult> ImplValidationErros
{
get
{
return implValidationErrors ??
(implValidationErrors = new List<ValidationResult>());
}
}
private ReadOnlyCollection<ValidationResult> validationErrors;
[Display(AutoGenerateField = false)]
protected ICollection<ValidationResult> ValidationErrors
{
get
{
return validationErrors ??
(validationErrors =
new ReadOnlyCollection<ValidationResult>(ImplValidationErros));
}
}
protected void ValidateProperty(string propertyName, object value)
{
ValidationContext validationContext =
new ValidationContext(this, null, null);
validationContext.MemberName = propertyName;
List<ValidationResult> validationResults =
new List<ValidationResult>();
Validator.TryValidateProperty(
value,
validationContext,
validationResults);
if (!validationResults.Any()) return;
validationResults
.AddRange(ValidationErrors
.Where(ve =>
!ve.MemberNames.All(mn =>
mn == propertyName)));
implValidationErrors = validationResults;
if (ErrorsChanged != null)
ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
}
}
vous pouvez utiliser une classe partielle pour étendre votre entité et y ajouter la validation des données via IDataErrorInfo.