Validación de la interfaz de usuario de WinForm

Necesito implementar la validación de entrada en mi aplicación winform. Hay muchas formas diferentes en las que se pueden ingresar datos y me gustaría no controlarlo por control por formulario y crear valores válidos, etc. por ítem. ¿Cómo han lidiado los demás con esto?

Veo que la mayoría de las publicaciones relacionadas tratan con aplicaciones web y / o mencionan el Bloque de aplicaciones de validación de Enterprise Library . Ahora admito que no he investigado a fondo ELVAB, pero parece que es demasiado para lo que necesito. Mi pensamiento actual es escribir una biblioteca de clases con varios requisitos y pasarle un control como parámetro. Ya tengo una biblioteca de funciones RegEx para cosas como isValidZipCode y tal que ese puede ser un lugar para comenzar.

Lo que me gustaría tener es un botón Validar que onClick recorre todos los controles en esa página de formulario y realiza la validación necesaria. ¿Cómo puedo lograr esto?

En mi propia aplicación, necesito validar las dimensiones a medida que se escriben. La secuencia que utilicé es la siguiente

  1. El usuario selecciona o escribe y luego se aleja del control.
  2. El control pierde el foco y notifica a la Vista que envía su ID y el texto de la entrada.
  3. La Vista verifica qué Shape Program (una clase que implementa una interfaz) creó el Formulario y le pasa el ID y el texto de entrada
  4. El Shape Program devuelve una respuesta.
  5. Si la respuesta es correcta, la vista actualiza la entrada correcta de la clase de forma.
  6. Si la respuesta es correcta, la vista le dice al formulario a través de una interfaz que está bien cambiar el enfoque a la siguiente entrada.
  7. Si la respuesta no es correcta, la vista mira la respuesta y el uso de la interfaz de formulario le dice al formulario qué hacer. Esto generalmente significa que el foco vuelve a la entrada ofensiva con un mensaje que muestra al usuario lo que sucedió.

La ventaja de este enfoque es que la validación se centraliza en una ubicación para un determinado Shape Program. No tengo que ir a modificar cada control o incluso realmente preocuparme por los diferentes tipos de controles en el formulario. Hace mucho tiempo, cuando diseñé el software, decidí cómo iba a funcionar la interfaz de usuario para cuadros de texto, cuadros de lista, cuadros combinados, etc. También se manejan diferentes niveles de severidad de manera diferente.

La Vista se ocupa de que se instruya a la Forma lo que se debe hacer a través de la Interfaz. Cómo se implementa realmente es manejado por el Formulario mismo en su implementación de la Interfaz. A la Vista no le importa si el Formulario se muestra en amarillo para advertencia y rojo para error. Solo que maneja esos dos niveles. Más adelante, si aparece una mejor idea de mostrar los errores de advertencia frente a los errores, puedo hacer que el cambio en el Formulario en sí sea bastante complicado con la lógica de Vista o la validación en el Progtwig de Forma.

Ya estás a la mitad si estás pensando en hacer una clase para mantener tu lógica de validación, esto te ayudará a lograr el rest en tu nuevo diseño.

La validación ya está integrada en la biblioteca WinForms.

Cada objeto derivado del Control tiene dos eventos llamados Validating y Validated . También tiene una propiedad llamada CausesValidation . Cuando esto se establece en verdadero (es cierto de manera predeterminada), el control participa en la validación. De lo contrario, no es así.

La validación ocurre como parte del enfoque. Cuando te enfocas fuera de un control, sus eventos de validación se disparan. De hecho, los eventos de enfoque se disparan en un orden específico. Desde MSDN :

Cuando cambia el foco utilizando el teclado (TAB, MAYÚS + TAB, y así sucesivamente), al llamar a los métodos Select o SelectNextControl, o estableciendo la propiedad ContainerControl .. ::. ActiveControl en el formulario actual, los eventos de foco ocurren en el siguiente orden:

  1. Entrar
  2. Se enfocó
  3. Salir
  4. Validando
  5. Validado
  6. Perdió el enfoque

Cuando cambia el foco utilizando el mouse o llamando al método Focus, los eventos de enfoque ocurren en el siguiente orden:

  1. Entrar
  2. Se enfocó
  3. Perdió el enfoque
  4. Salir
  5. Validando
  6. Validado

Si la propiedad CausesValidation se establece en false, los eventos Validating y Validated se suprimen.

Si la propiedad Cancelar de CancelEventArgs se establece en verdadero en el delegado del evento Validating, se suprimirán todos los eventos que generalmente se producirían después del evento Validating.

También un ContainerControl tiene un método llamado ValidateChildren() que recorrerá los controles contenidos y los validará.

Me doy cuenta de que este hilo es bastante antiguo, pero pensé que publicaría la solución que se me ocurrió.

El mayor problema con la validación en WinForms es que la validación solo se ejecuta cuando el control ha “perdido el foco”. Entonces, el usuario tiene que hacer clic dentro de un cuadro de texto y luego hacer clic en otro lugar para que se ejecute la rutina de validación. Esto está bien si solo le preocupa que los datos ingresados ​​sean correctos. Pero esto no funciona bien si intenta asegurarse de que un usuario no deje un cuadro de texto vacío salteándolo.

En mi solución, cuando el usuario hace clic en el botón de enviar para un formulario, verifico cada control en el formulario (o cualquier contenedor que se especifique) y uso la reflexión para determinar si se define un método de validación para el control. Si lo es, se ejecuta el método de validación. Si alguna de las validaciones falla, la rutina devuelve una falla y permite que el proceso se detenga. Esta solución funciona bien especialmente si tiene varios formularios para validar.

1) Simplemente copie y pegue esta sección de código en su proyecto. Estamos usando Reflection, por lo que necesita agregar System.Reflection a sus instrucciones de uso

 class Validation { public static bool hasValidationErrors(System.Windows.Forms.Control.ControlCollection controls) { bool hasError = false; // Now we need to loop through the controls and deterime if any of them have errors foreach (Control control in controls) { // check the control and see what it returns bool validControl = IsValid(control); // If it's not valid then set the flag and keep going. We want to get through all // the validators so they will display on the screen if errorProviders were used. if (!validControl) hasError = true; // If its a container control then it may have children that need to be checked if (control.HasChildren) { if (hasValidationErrors(control.Controls)) hasError = true; } } return hasError; } // Here, let's determine if the control has a validating method attached to it // and if it does, let's execute it and return the result private static bool IsValid(object eventSource) { string name = "EventValidating"; Type targetType = eventSource.GetType(); do { FieldInfo[] fields = targetType.GetFields( BindingFlags.Static | BindingFlags.Instance | BindingFlags.NonPublic); foreach (FieldInfo field in fields) { if (field.Name == name) { EventHandlerList eventHandlers = ((EventHandlerList)(eventSource.GetType().GetProperty("Events", (BindingFlags.FlattenHierarchy | (BindingFlags.NonPublic | BindingFlags.Instance))).GetValue(eventSource, null))); Delegate d = eventHandlers[field.GetValue(eventSource)]; if ((!(d == null))) { Delegate[] subscribers = d.GetInvocationList(); // ok we found the validation event, let's get the event method and call it foreach (Delegate d1 in subscribers) { // create the parameters object sender = eventSource; CancelEventArgs eventArgs = new CancelEventArgs(); eventArgs.Cancel = false; object[] parameters = new object[2]; parameters[0] = sender; parameters[1] = eventArgs; // call the method d1.DynamicInvoke(parameters); // if the validation failed we need to return that failure if (eventArgs.Cancel) return false; else return true; } } } } targetType = targetType.BaseType; } while (targetType != null); return true; } } 

2) Use el evento de Validación estándar en cualquier control que quiera validar. ¡Asegúrese de utilizar e.Cancelar cuando la validación falla!

 private void txtLastName_Validating(object sender, CancelEventArgs e) { if (txtLastName.Text.Trim() == String.Empty) { errorProvider1.SetError(txtLastName, "Last Name is Required"); e.Cancel = true; } else errorProvider1.SetError(txtLastName, ""); } 

3) ¡No saltees este paso! Establezca la propiedad AutoValidate en el formulario para EnableAllowFocusChange . Esto permitirá tabular a otro control incluso cuando la validación falla.

4) Finalmente, en su método de Botón Enviar, llame al método de Validación y especifique qué contenedor desea verificar. Puede ser el formulario completo o solo un contenedor en el formulario como un Panel o un Grupo.

 private void btnSubmit_Click(object sender, EventArgs e) { // the controls collection can be the whole form or just a panel or group if (Validation.hasValidationErrors(frmMain.Controls)) return; // if we get here the validation passed this.close(); } 

Feliz Codificación!

Me gustaría no tener que controlar el control por formulario y crear isValid, etc. por artículo.

Como nivel tendrá que definir lo que significa ser valid para cada control, a menos que lo único que le importe es que el control tenga un valor de algún tipo.

Dicho esto, hay un componente ErrorProvider que puedes usar que funciona bastante bien.

Hemos tenido buena suerte con Noogen ValidationProvider . Es simple para casos simples (verificaciones de tipos de datos y campos obligatorios) y fácil de agregar validación personalizada para casos más complejos.

En todos mis formularios, implemento el evento isValidating para el control particular en cuestión y si los datos no se validan tengo un errorProvider en el formulario y uso su método SetError (…) para establecer el error en el control en cuestión con información relevante sobre por qué está mal.

edit> Debo señalar que, en general, utilizo el patrón mvc al hacer esto, por lo que la validación específica para ese control / miembro del modelo ocurre en el modelo, por lo que la función de validación isValidating es similar a esto:

 private uicontrol_isValidating(...) { if(!m_Model.MemberNameIsValid()) { errorProvider.SetError(...); } } 

O de esa manera. O puede tener un solo evento de validación asociado con todos o controles que necesitan validaciones similares. Esto eliminará el bucle del código. Supongamos que tiene cuatro cuadros de texto que pueden tener solo números enteros. Lo que puedes hacer es tener un evento único para cada uno de ellos. No tengo ningún IDE, así que el siguiente código es lo mejor que se me ocurre.

 this.textbox1.Validated +=  this.textbox2.Validated +=  this.textbox3.Validated +=  this.textbox4.Validated +=  

En el caso:

  1. Emitir remitente como cuadro de texto.
  2. Compruebe si el valor en el cuadro de texto es numérico.

Y así sucesivamente tienes eventos alineados.

Espero que esto ayude.

Si combina las ideas anteriores con este gestor de eventos de Validación genérico obtendrá un buen “marco” de errores de validación con todos los métodos de validación en sus clases de negocios. Acabo de extender el código de Bruce con la idea danesa. Se hizo para los componentes de Entity Framework y Dev Express, pero esas dependencias se pueden eliminar fácilmente. ¡Disfrutar!

 public class ValidationManager { ///  /// Call this method to validate all controls of the given control list /// Validating event will be called on each one ///  ///  ///  public static bool HasValidationErrors(System.Windows.Forms.Control.ControlCollection controls) { bool hasError = false; // Now we need to loop through the controls and deterime if any of them have errors foreach (Control control in controls) { // check the control and see what it returns bool validControl = IsValid(control); // If it's not valid then set the flag and keep going. We want to get through all // the validators so they will display on the screen if errorProviders were used. if (!validControl) hasError = true; // If its a container control then it may have children that need to be checked if (control.HasChildren) { if (HasValidationErrors(control.Controls)) hasError = true; } } return hasError; } ///  /// Attach all youe Validating events to this event handler (if the controls requieres validation) /// A method with name Validate + PropertyName will be searched on the binded business entity, and if found called /// Throw an exception with the desired message if a validation error is detected in your method logic ///  ///  ///  public static void ValidationHandler(object sender, CancelEventArgs e) { BaseEdit control = sender as BaseEdit; if (control.DataBindings.Count > 0) //control is binded { string bindedFieldName = control.DataBindings[0].BindingMemberInfo.BindingField; object bindedObject = control.BindingManager.Current; if (bindedObject != null) //control is binded to an object instance { //find and call method with name = Validate + PropertyName MethodInfo validationMethod = (from method in bindedObject.GetType().GetMethods() where method.IsPublic && method.Name == String.Format("Validate{0}",bindedFieldName) && method.GetParameters().Count() == 0 select method).FirstOrDefault(); if (validationMethod != null) //has validation method { try { validationMethod.Invoke(bindedObject, null); control.ErrorText = String.Empty; //property value is valid } catch (Exception exp) { control.ErrorText = exp.InnerException.Message; e.Cancel = true; } } } } } // Here, let's determine if the control has a validating method attached to it // and if it does, let's execute it and return the result private static bool IsValid(object eventSource) { string name = "EventValidating"; Type targetType = eventSource.GetType(); do { FieldInfo[] fields = targetType.GetFields( BindingFlags.Static | BindingFlags.Instance | BindingFlags.NonPublic); foreach (FieldInfo field in fields) { if (field.Name == name) { EventHandlerList eventHandlers = ((EventHandlerList)(eventSource.GetType().GetProperty("Events", (BindingFlags.FlattenHierarchy | (BindingFlags.NonPublic | BindingFlags.Instance))).GetValue(eventSource, null))); Delegate d = eventHandlers[field.GetValue(eventSource)]; if ((!(d == null))) { Delegate[] subscribers = d.GetInvocationList(); // ok we found the validation event, let's get the event method and call it foreach (Delegate d1 in subscribers) { // create the parameters object sender = eventSource; CancelEventArgs eventArgs = new CancelEventArgs(); eventArgs.Cancel = false; object[] parameters = new object[2]; parameters[0] = sender; parameters[1] = eventArgs; // call the method d1.DynamicInvoke(parameters); // if the validation failed we need to return that failure if (eventArgs.Cancel) return false; else return true; } } } } targetType = targetType.BaseType; } while (targetType != null); return true; } } 

Método de validación de muestra:

 partial class ClientName { public void ValidateFirstName() { if (String.IsNullOrWhiteSpace(this.FirstName)) throw new Exception("First Name is required."); } public void ValidateLastName() { if (String.IsNullOrWhiteSpace(this.LastName)) throw new Exception("Last Name is required."); } } 

El ciclismo a través de los controles puede funcionar pero es propenso a errores. Trabajé en un proyecto que utilizaba esa técnica (se me concedió que era un proyecto Delphi, no C #) y funcionó como se esperaba, pero era muy difícil actualizarlo si se agregaba o cambiaba un control. Esto puede haber sido correcto. No estoy seguro.

De todos modos, funcionó al crear un único controlador de eventos que luego se adjuntó a cada control. El controlador usaría RTTI para determinar el tipo de control. Luego usaría la propiedad del nombre del control en una gran statement de selección para encontrar el código de validación para ejecutar. Si la validación falló, se envió un mensaje de error al usuario y se le dio el foco al control. Para hacer las cosas más complejas, el formulario se dividió en varias tabs y la pestaña adecuada tenía que estar visible para que el control secundario obtuviera el foco.

Esa es mi experiencia

Prefiero usar un patrón de diseño de Vista pasiva para eliminar todas las reglas comerciales del formulario e insertarlas en una clase de Presentador. Dependiendo del estado de su formulario, puede ser más trabajo que lo que esté dispuesto a invertir.

Solo una idea aproximada:

void btnValidate_Click(object sender, EventArgs e) { foreach( Control c in this.Controls ) { if( c is TextBox ) { TextBox tbToValidate = (TextBox)c; Validate(tbToValidate.Text); } } }
void btnValidate_Click(object sender, EventArgs e) { foreach( Control c in this.Controls ) { if( c is TextBox ) { TextBox tbToValidate = (TextBox)c; Validate(tbToValidate.Text); } } } 

Puede pegar los cuadros de texto dentro de un panel y solo recorrer los controles si desea evitar el bucle a través de otros controles.

¿Por qué no estás usando el evento de Validación? Puede tener un único evento de validación y validar los controles allí. No será necesario utilizar bucles y cada control se validará a medida que se ingresen los datos.