c# - Validación adecuada con MVVM
wpf validation (4)
Advertencia: publicación muy larga y detallada.
De acuerdo, validación en WPF cuando se usa MVVM. He leído muchas cosas ahora, analicé muchas preguntas de SO, y probé muchos enfoques, pero en algún momento todo parece algo hacky y realmente no estoy seguro de cómo hacerlo de la manera correcta ™.
Idealmente, quiero que todas las validaciones sucedan en el modelo de vista usando IDataErrorInfo
; así que eso es lo que hice. Sin embargo, existen diferentes aspectos que hacen que esta solución no sea una solución completa para todo el tema de validación.
La situación
Tomemos la siguiente forma simple. Como puede ver, no es nada lujoso. Solo tenemos dos cuadros de texto que se unen a una string
y una propiedad int
en el modelo de vista cada uno. Además, tenemos un botón que está vinculado a un ICommand
.
Entonces, para la validación, ahora tenemos dos opciones:
- Podemos ejecutar la validación automáticamente cada vez que cambie el valor de un cuadro de texto. Como tal, el usuario obtiene una respuesta instantánea cuando ingresa algo inválido.
- Podemos ir un paso más allá para deshabilitar el botón cuando hay algún error.
- O podemos ejecutar la validación solo de forma explícita cuando se presiona el botón, y luego mostrar todos los errores, si corresponde. Obviamente no podemos deshabilitar el botón de errores aquí.
Idealmente, quiero implementar la opción 1. Para enlaces de datos normales con ValidatesOnDataErrors
activado, este es el comportamiento predeterminado. Entonces, cuando el texto cambia, el enlace actualiza el origen y desencadena la validación IDataErrorInfo
para esa propiedad; los errores se informan en la vista. Hasta aquí todo bien.
Estado de validación en el modelo de vista
Lo interesante es dejar que el modelo de vista, o el botón en este caso, sepa si hay algún error. La forma en que funciona IDataErrorInfo
, está principalmente allí para informar errores a la vista. Por lo tanto, la vista puede ver fácilmente si hay errores, mostrarlos e incluso mostrar anotaciones usando Validation.Errors
. Además, la validación siempre ocurre al mirar una sola propiedad.
Así que tener el modelo de vista saber cuando hay algún error, o si la validación tuvo éxito, es complicado. Una solución común es simplemente activar la validación IDataErrorInfo
para todas las propiedades en el propio modelo de vista. Esto a menudo se hace usando una propiedad IsValid
separada. El beneficio es que esto también se puede usar fácilmente para deshabilitar el comando. El inconveniente es que esto podría ejecutar la validación en todas las propiedades con demasiada frecuencia, pero la mayoría de las validaciones deberían ser lo suficientemente simples como para no perjudicar el rendimiento. Otra solución sería recordar qué propiedades producen errores usando la validación y solo verificarlas, pero eso parece un poco complicado e innecesario para la mayoría de las veces.
La conclusión es que esto podría funcionar bien. IDataErrorInfo
proporciona la validación para todas las propiedades, y podemos simplemente usar esa interfaz en el propio modelo de vista para ejecutar la validación allí también para el objeto completo. Presentando el problema:
Excepciones vinculantes
El modelo de vista usa tipos reales para sus propiedades. Entonces, en nuestro ejemplo, la propiedad entera es una int
real. Sin embargo, el cuadro de texto utilizado en la vista de forma nativa solo admite texto . Por lo tanto, cuando se vincula al int
en el modelo de vista, el motor de enlace de datos realizará automáticamente conversiones de tipo, o al menos lo intentará. Si puede ingresar texto en un cuadro de texto destinado a números, es muy probable que no siempre haya números válidos: por lo tanto, el motor de enlace de datos no podrá convertir y lanzar una FormatException
.
En el lado de la vista, podemos ver eso fácilmente. Las excepciones del motor de enlace son automáticamente detectadas por WPF y se muestran como errores; ni siquiera es necesario habilitar Binding.ValidatesOnExceptions
que serían necesarios para las excepciones lanzadas en el setter. Sin embargo, los mensajes de error tienen un texto genérico, por lo que podría ser un problema. He resuelto esto por mí mismo mediante el uso de un controlador Binding.UpdateSourceExceptionFilter
, inspeccionando la excepción que se lanza y mirando la propiedad de origen y luego generando un mensaje de error menos genérico en su lugar. Todo eso encapsulado en mi propia extensión de marcado de encuadernación, para que pueda tener todos los valores predeterminados que necesito.
Entonces la vista esta bien. El usuario comete un error, ve algunos comentarios de error y puede corregirlo. El modelo de vista sin embargo se pierde . Como el motor vinculante lanzó la excepción, la fuente nunca se actualizó. Entonces, el modelo de vista todavía está en el valor anterior, que no es lo que se muestra al usuario, y la validación de IDataErrorInfo
obviamente no se aplica.
Lo que es peor, no hay una buena manera para que el modelo de vista lo sepa. Al menos, todavía no he encontrado una buena solución para esto. Lo que sería posible es hacer que la vista reporte al modelo de vista que hubo un error. Esto podría hacerse mediante el enlace de datos de la propiedad Validation.HasError
al modelo de vista (que no es posible directamente), por lo que el modelo de vista podría verificar primero el estado de la vista.
Otra opción sería transmitir la excepción manejada en Binding.UpdateSourceExceptionFilter
al modelo de vista, por lo que se le notificaría también. El modelo de vista podría incluso proporcionar alguna interfaz para que el enlace informe estas cosas, permitiendo mensajes de error personalizados en lugar de los de tipo genérico. Pero eso crearía un acoplamiento más fuerte desde la vista hacia el modelo de vista, que generalmente quiero evitar.
Otra "solución" sería deshacerse de todas las propiedades tipadas, usar propiedades de string
simples y realizar la conversión en el modelo de vista. Obviamente, esto movería toda la validación al modelo de vista, pero también significaría una cantidad increíble de duplicación de las cosas que el motor de enlace de datos normalmente se ocupa. Además, cambiaría la semántica del modelo de vista. Para mí, una vista se construye para el modelo de vista y no a la inversa; por supuesto, el diseño del modelo de vista depende de lo que imaginemos que la vista hacer, pero todavía hay libertad general de cómo lo hace la vista. Entonces, el modelo de vista define una propiedad int
porque hay un número; la vista ahora puede usar un cuadro de texto (que permite todos estos problemas) o usar algo que funcione de forma nativa con números. Entonces no, cambiar los tipos de propiedades a string
no es una opción para mí.
Al final, este es un problema de la vista. La vista (y su motor de enlace de datos) es responsable de dar a la vista los valores adecuados del modelo para trabajar. Pero en este caso, parece que no hay una buena forma de decirle al modelo de vista que debe invalidar el antiguo valor de la propiedad.
BindingGroups
Los grupos de encuadernación son una forma en que traté de abordar esto. Los grupos de enlace tienen la capacidad de agrupar todas las validaciones, incluidas IDataErrorInfo
y excepciones lanzadas. Si están disponibles para el modelo de vista, incluso tienen un medio para verificar el estado de validación de todas esas fuentes de validación, por ejemplo, utilizando CommitEdit
.
Por defecto, los grupos de enlace implementan la opción 2 desde arriba. Hacen que los enlaces se actualicen de manera explícita, lo que básicamente agrega un estado no confirmado adicional. Por lo tanto, al hacer clic en el botón, el comando puede confirmar esos cambios, activar las actualizaciones de origen y todas las validaciones y obtener un único resultado si tiene éxito. Entonces la acción del comando podría ser esta:
if (bindingGroup.CommitEdit())
SaveEverything();
CommitEdit
solo devolverá true si todas las validaciones tuvieron éxito. Tomará IDataErrorInfo
en cuenta y también verificará las excepciones vinculantes. Esta parece ser una solución perfecta para la opción 2. Lo único que es un poco complicado es administrar el grupo vinculante con los enlaces, pero me he construido algo que en su mayoría se ocupa de esto ( related ).
Si un grupo vinculante está presente para un enlace, el enlace se establecerá de forma predeterminada en un UpdateSourceTrigger
explícito. Para implementar la opción 1 desde arriba utilizando grupos de enlace, básicamente tenemos que cambiar el desencadenador. Como tengo una extensión de encuadernación personalizada de todos modos, esto es bastante simple, solo configuré LostFocus
para todos.
Así que ahora, los enlaces se actualizarán siempre que cambie un campo de texto. Si la fuente puede actualizarse (el motor vinculante no arroja ninguna excepción) entonces IDataErrorInfo
se ejecutará como de costumbre. Si no se pudo actualizar, la vista aún puede verlo. Y si hacemos clic en nuestro botón, el comando subyacente puede llamar a CommitEdit
(aunque no es necesario CommitEdit
nada) y obtener el resultado de la validación total para ver si puede continuar.
Es posible que no podamos desactivar el botón fácilmente de esta manera. Al menos no desde el modelo de vista. Comprobar la validación una y otra vez no es realmente una buena idea simplemente para actualizar el estado del comando, y el modelo de vista no se notifica cuando se lanza una excepción de motor de enlace de todos modos (que luego debería deshabilitar el botón) o cuando se va a habilita el botón nuevamente Todavía podríamos agregar un disparador para deshabilitar el botón en la vista usando Validation.HasError
así que no es imposible.
¿Solución?
Entonces, en general, esta parece ser la solución perfecta. ¿Cuál es mi problema con eso? Para ser honesto, no estoy del todo seguro. Los grupos de vinculación son una cosa compleja que parece usarse generalmente en grupos más pequeños, que posiblemente tengan múltiples grupos de unión en una sola vista. Al usar un gran grupo de enlace para toda la vista solo para asegurar mi validación, parece como si estuviera abusando de ella. Y sigo pensando, que debe haber una mejor manera de resolver toda esta situación, porque seguramente no puedo ser el único que tenga estos problemas. Y hasta ahora, realmente no he visto a mucha gente usar grupos vinculantes para la validación con MVVM en absoluto, por lo que se siente extraño.
Entonces, ¿cuál es la forma correcta de hacer la validación en WPF con MVVM mientras se pueden verificar las excepciones vinculantes del motor?
Mi solución (/ hack)
Antes que nada, ¡gracias por tu aporte! Como he escrito anteriormente, ya estoy usando IDataErrorInfo
para hacer mi validación de datos y personalmente creo que es la utilidad más cómoda para hacer el trabajo de validación. Estoy usando utilidades similares a las sugeridas por Sheridan en su respuesta a continuación, por lo que también funciona bien.
Al final, mi problema se redujo al problema de excepción vinculante, donde el modelo de vista simplemente no sabría cuándo sucedió. Si bien pude manejar esto con grupos vinculantes como se detalló anteriormente, aún así decidí no hacerlo, ya que no me sentía tan cómodo con eso. Entonces, ¿qué hice en su lugar?
Como mencioné anteriormente, detecto excepciones vinculantes en el lado de la vista al escuchar el UpdateSourceExceptionFilter
un enlace. Allí, puedo obtener una referencia al modelo de vista del DataItem
la expresión vinculante. Luego tengo una interfaz IReceivesBindingErrorInformation
que registra el modelo de vista como un posible receptor para obtener información sobre los errores de enlace. Luego lo uso para pasar la ruta de enlace y la excepción al modelo de vista:
object OnUpdateSourceExceptionFilter(object bindExpression, Exception exception)
{
BindingExpression expr = (bindExpression as BindingExpression);
if (expr.DataItem is IReceivesBindingErrorInformation)
{
((IReceivesBindingErrorInformation)expr.DataItem).ReceiveBindingErrorInformation(expr.ParentBinding.Path.Path, exception);
}
// check for FormatException and produce a nicer error
// ...
}
En el modelo de vista, recuerdo cada vez que recibo una notificación sobre la expresión vinculante de una ruta:
HashSet<string> bindingErrors = new HashSet<string>();
void IReceivesBindingErrorInformation.ReceiveBindingErrorInformation(string path, Exception exception)
{
bindingErrors.Add(path);
}
Y cada vez que IDataErrorInfo
revalida una propiedad, sé que el enlace funcionó, y puedo borrar la propiedad del conjunto de hash.
En el modelo de vista, puedo verificar si el conjunto de hash contiene algún elemento y cancelar cualquier acción que requiera la validación completa de los datos. Puede que no sea la mejor solución debido al acoplamiento de la vista al modelo de vista, pero usar esa interfaz es al menos un problema menor.
El inconveniente es que esto podría ejecutar la validación en todas las propiedades con demasiada frecuencia, pero la mayoría de las validaciones deberían ser lo suficientemente simples como para no perjudicar el rendimiento. Otra solución sería recordar qué propiedades producen errores usando la validación y solo verificarlas, pero eso parece un poco complicado e innecesario para la mayoría de las veces.
No necesita rastrear qué propiedades tienen errores; solo necesitas saber que existen errores. El modelo de vista puede mantener una lista de errores (también es útil para mostrar un resumen de error), y la propiedad IsValid
simplemente puede ser un reflejo de si la lista tiene algo. No necesita verificar todo cada vez que se IsValid
, siempre que se asegure de que el resumen del error sea actual y de que IsValid
se actualice cada vez que cambie.
Al final, este es un problema de la vista. La vista (y su motor de enlace de datos) es responsable de dar a la vista los valores adecuados del modelo para trabajar. Pero en este caso, parece que no hay una buena forma de decirle al modelo de vista que debe invalidar el antiguo valor de la propiedad.
Puede escuchar los errores dentro del contenedor que está vinculado al modelo de vista:
container.AddHandler(Validation.ErrorEvent, Container_Error);
...
void Container_Error(object sender, ValidationErrorEventArgs e) {
...
}
Esto le notifica cuando se agregan o eliminan errores, y puede identificar las excepciones vinculantes según si e.Error.Exception
existe, por lo que su vista puede mantener una lista de excepciones obligatorias e informar al modelo de vista de la misma.
Pero cualquier solución a este problema siempre será un truco, porque la vista no cumple su función correctamente, lo que le brinda al usuario un medio para leer y actualizar la estructura del modelo de vista. Esto debe verse como una solución temporal hasta que presente correctamente al usuario con algún tipo de "cuadro de entero " en lugar de un cuadro de texto .
Advertencia: respuesta larga también
Uso la interfaz IDataErrorInfo
para la validación, pero la he personalizado según mis necesidades. Creo que encontrarás que también resuelve algunos de tus problemas. Una diferencia para su pregunta es que la implemento en mi clase de tipo de datos base.
Como señaló, esta interfaz solo trata con una propiedad a la vez, pero claramente en este día y edad, eso no es bueno. Así que acabo de agregar una propiedad de colección para usar en su lugar:
protected ObservableCollection<string> errors = new ObservableCollection<string>();
public virtual ObservableCollection<string> Errors
{
get { return errors; }
}
Para abordar su problema de no poder mostrar errores externos (en su caso desde la vista, pero desde el modelo de vista), simplemente agregué otra propiedad de colección:
protected ObservableCollection<string> externalErrors = new ObservableCollection<string>();
public ObservableCollection<string> ExternalErrors
{
get { return externalErrors; }
}
Tengo una propiedad HasError
que mira mi colección:
public virtual bool HasError
{
get { return Errors != null && Errors.Count > 0; }
}
Esto me permite vincular esto a Grid.Visibility
usando un BoolToVisibilityConverter
personalizado, ej. para mostrar una Grid
con un control de colección dentro que muestra los errores cuando los hay. También me permite cambiar un Brush
a Red
para resaltar un error (usando otro Converter
), pero supongo que entiendes la idea.
Luego, en cada tipo de datos, o clase de modelo, anulo la propiedad Errors
e implemento el indexador de Item
(simplificado en este ejemplo):
public override ObservableCollection<string> Errors
{
get
{
errors = new ObservableCollection<string>();
errors.AddUniqueIfNotEmpty(this["Name"]);
errors.AddUniqueIfNotEmpty(this["EmailAddresses"]);
errors.AddUniqueIfNotEmpty(this["SomeOtherProperty"]);
errors.AddRange(ExternalErrors);
return errors;
}
}
public override string this[string propertyName]
{
get
{
string error = string.Empty;
if (propertyName == "Name" && Name.IsNullOrEmpty()) error = "You must enter the Name field.";
else if (propertyName == "EmailAddresses" && EmailAddresses.Count == 0) error = "You must enter at least one e-mail address into the Email address(es) field.";
else if (propertyName == "SomeOtherProperty" && SomeOtherProperty.IsNullOrEmpty()) error = "You must enter the SomeOtherProperty field.";
return error;
}
}
El método AddUniqueIfNotEmpty
es un método de extension
personalizado y ''hace lo que dice en la lata''. Tenga en cuenta cómo llamará cada propiedad que quiero validar por turno y compilará una colección de ellas, ignorando los errores duplicados.
Usando la colección ExternalErrors
, puedo validar cosas que no puedo validar en la clase de datos:
private void ValidateUniqueName(Genre genre)
{
string errorMessage = "The genre name must be unique";
if (!IsGenreNameUnique(genre))
{
if (!genre.ExternalErrors.Contains(errorMessage)) genre.ExternalErrors.Add(errorMessage);
}
else genre.ExternalErrors.Remove(errorMessage);
}
Para abordar su punto con respecto a la situación en la que un usuario ingresa un carácter alfabético en un campo int
, tiendo a utilizar una IsNumeric AttachedProperty
personalizada para el TextBox
, por ejemplo. No les dejo cometer estos tipos de errores. Siempre siento que es mejor detenerlo, que dejar que ocurra y luego arreglarlo.
En general, estoy muy contento con mi capacidad de validación en WPF y no me faltan nada.
Para finalizar y para completar, sentí que debería alertarlo sobre el hecho de que ahora existe una interfaz INotifyDataErrorInfo
que incluye parte de esta funcionalidad adicional. Puede encontrar más información en la página Interfaz INotifyDataErrorInfo
en MSDN.
ACTUALIZACIÓN >>>
Sí, la propiedad ExternalErrors
simplemente permite que agregue errores relacionados con un objeto de datos desde fuera de ese objeto ... lo siento, mi ejemplo no fue completo ... si le hubiera mostrado el método IsGenreNameUnique
, lo habría visto utiliza LinQ
en todos los elementos de datos de Genre
en la colección para determinar si el nombre del objeto es único o no:
private bool IsGenreNameUnique(Genre genre)
{
return Genres.Where(d => d.Name != string.Empty && d.Name == genre.Name).Count() == 1;
}
En cuanto a su problema de string
/ int
, la única forma en que puedo ver que obtenga esos errores en su clase de datos es si declara todas sus propiedades como object
, pero entonces tendría que hacer una gran cantidad de fundición. Quizás podría duplicar sus propiedades de esta manera:
public object FooObject { get; set; } // Implement INotifyPropertyChanged
public int Foo
{
get { return FooObject.GetType() == typeof(int) ? int.Parse(FooObject) : -1; }
}
Entonces, si se usó Foo
en el código y se usó FooObject
en el FooObject
, podrías hacer esto:
public override string this[string propertyName]
{
get
{
string error = string.Empty;
if (propertyName == "FooObject" && FooObject.GetType() != typeof(int))
error = "Please enter a whole number for the Foo field.";
...
return error;
}
}
De esta forma, podría cumplir sus requisitos, pero tendrá que agregar un gran número de código adicional.
En mi opinión, el problema radica en que la validación ocurre en demasiados lugares. También deseaba escribir todos mis ViewModel
sesión de validación en ViewModel
pero todos esos enlaces numéricos estaban ViewModel
loco mi ViewModel
.
Resolví este problema creando un enlace que nunca falla. Obviamente, si un enlace siempre es exitoso, entonces el tipo en sí tiene que manejar las condiciones de error correctamente.
Tipo de valor de Failable
Comencé creando un tipo genérico que admitiría con gracia las conversiones fallidas:
public struct Failable<T>
{
public T Value { get; private set; }
public string Text { get; private set; }
public bool IsValid { get; private set; }
public Failable(T value)
{
Value = value;
try
{
var converter = TypeDescriptor.GetConverter(typeof(T));
Text = converter.ConvertToString(value);
IsValid = true;
}
catch
{
Text = String.Empty;
IsValid = false;
}
}
public Failable(string text)
{
Text = text;
try
{
var converter = TypeDescriptor.GetConverter(typeof(T));
Value = (T)converter.ConvertFromString(text);
IsValid = true;
}
catch
{
Value = default(T);
IsValid = false;
}
}
}
Tenga en cuenta que incluso si el tipo no se inicializa debido a la cadena de entrada no válida (segundo constructor), también almacena silenciosamente el estado no válido junto con el texto no válido . Esto es necesario para admitir el enlace de ida y vuelta, incluso en el caso de una entrada incorrecta .
Convertidor de valor genérico
Un convertidor de valor genérico podría escribirse utilizando el tipo anterior:
public class StringToFailableConverter<T> : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value.GetType() != typeof(Failable<T>))
throw new InvalidOperationException("Invalid value type.");
if (targetType != typeof(string))
throw new InvalidOperationException("Invalid target type.");
var rawValue = (Failable<T>)value;
return rawValue.Text;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value.GetType() != typeof(string))
throw new InvalidOperationException("Invalid value type.");
if (targetType != typeof(Failable<T>))
throw new InvalidOperationException("Invalid target type.");
return new Failable<T>(value as string);
}
}
XAML Handy Converters
Debido a que crear y usar las instancias de los genéricos es dolor en XAML, hagamos instancias estáticas de los convertidores comunes:
public static class Failable
{
public static StringToFailableConverter<Int32> Int32Converter { get; private set; }
public static StringToFailableConverter<double> DoubleConverter { get; private set; }
static Failable()
{
Int32Converter = new StringToFailableConverter<Int32>();
DoubleConverter = new StringToFailableConverter<Double>();
}
}
Otros tipos de valores se pueden extender fácilmente.
Uso
El uso es bastante simple, solo necesita cambiar el tipo de int
a Failable<int>
:
ViewModel
public Failable<int> NumberValue
{
//Custom logic along with validation
//using IsValid property
}
XAML
<TextBox Text="{Binding NumberValue,Converter={x:Static local:Failable.Int32Converter}}"/>
De esta forma, puede usar el mismo mecanismo de validación ( IDataErrorInfo
o INotifyDataErrorInfo
o cualquier otra cosa) en ViewModel
al marcar la propiedad IsValid
. Si IsValid
es verdadero, puede usar directamente el Value
.
Ok, creo que encontré la respuesta que estabas buscando ...
No será fácil de explicar, pero ...
Muy fácil de entender una vez explicado ...
Creo que es más preciso / "certificado" para MVVM visto como "estándar" o al menos el intento de estándar.
Pero antes de comenzar ... necesitas cambiar un concepto al que te acostumbraste con respecto a MVVM:
"Además, cambiaría la semántica del modelo de vista. Para mí, una vista está construida para el modelo de vista y no a la inversa, por supuesto, el diseño del modelo de vista depende de lo que imaginamos que la vista hacer, pero todavía hay una general libertad cómo la vista hace eso "
Ese párrafo es la fuente de tu problema. ¿Por qué?
Porque está diciendo que View-Model no tiene ningún rol para ajustarse a la Vista ...
Eso está mal de muchas maneras, como te demostraré de manera muy simple.
Si tiene una propiedad como:
public Visibility MyPresenter { get...
¿Qué es Visibility
si no es algo que sirve a la vista?
El tipo en sí y el nombre que se le dará a la propiedad definitivamente está hecho para la vista.
Hay dos categorías de modelos de vista distinguibles en MVVM según mi experiencia:
- Modelo de vista de presentador: que se enganchará a botones, menús, elementos de pestaña, etc.
- Modelo de vista de la entidad: que se debe bloquear en los controles que llevan los datos de la entidad a la pantalla.
Estas son dos cosas diferentes, preocupaciones completamente diferentes.
Y ahora a la solución:
public abstract class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged([CallerMemberName] string propertyName = null)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public class VmSomeEntity : ViewModelBase, INotifyDataErrorInfo
{
//This one is part of INotifyDataErrorInfo interface which I will not use,
//perhaps in more complicated scenarios it could be used to let some other VM know validation changed.
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
//will hold the errors found in validation.
public Dictionary<string, string> ValidationErrors = new Dictionary<string, string>();
//the actual value - notice it is ''int'' and not ''string''..
private int storageCapacityInBytes;
//this is just to keep things sane - otherwise the view will not be able to send whatever the user throw at it.
//we want to consume what the user throw at us and validate it - right? :)
private string storageCapacityInBytesWrapper;
//This is a property to be served by the View.. important to understand the tactic used inside!
public string StorageCapacityInBytes
{
get { return storageCapacityInBytesWrapper ?? storageCapacityInBytes.ToString(); }
set
{
int result;
var isValid = int.TryParse(value, out result);
if (isValid)
{
storageCapacityInBytes = result;
storageCapacityInBytesWrapper = null;
RaisePropertyChanged();
}
else
storageCapacityInBytesWrapper = value;
HandleValidationError(isValid, "StorageCapacityInBytes", "Not a number.");
}
}
//Manager for the dictionary
private void HandleValidationError(bool isValid, string propertyName, string validationErrorDescription)
{
if (!string.IsNullOrEmpty(propertyName))
{
if (isValid)
{
if (ValidationErrors.ContainsKey(propertyName))
ValidationErrors.Remove(propertyName);
}
else
{
if (!ValidationErrors.ContainsKey(propertyName))
ValidationErrors.Add(propertyName, validationErrorDescription);
else
ValidationErrors[propertyName] = validationErrorDescription;
}
}
}
// this is another part of the interface - will be called automatically
public IEnumerable GetErrors(string propertyName)
{
return ValidationErrors.ContainsKey(propertyName)
? ValidationErrors[propertyName]
: null;
}
// same here, another part of the interface - will be called automatically
public bool HasErrors
{
get
{
return ValidationErrors.Count > 0;
}
}
}
Y ahora, en algún lugar de su código, su método de comando de comando ''CanExecute'' puede agregar a su implementación una llamada a VmEntity.HasErrors.
Y que la paz sea con su código con respecto a la validación a partir de ahora :)