Validação da interface do WinForm

Eu preciso implementar a validação de input em todo o meu aplicativo winform. Existem muitas formas diferentes em que os dados podem ser inseridos e eu gostaria de não controlar o controle pelo formulário e criar isValid etc por item. Como os outros lidaram com isso?

Vejo que as postagens mais relacionadas lidam com Web Apps e / ou mencionam o Bloqueio de Aplicativos de Validação da Biblioteca Corporativa . Agora eu admito que não pesquisei completamente o ELVAB, mas parece um exagero para o que eu preciso. Meu pensamento atual é escrever uma biblioteca de classs com os vários requisitos e passar um controle como um parâmetro. Eu já tenho uma biblioteca de funções RegEx para coisas como isValidZipCode e tal que pode ser um lugar para eu começar.

O que eu gostaria de ter é um botão Validate que o onClick percorra todos os controles nessa Página de Formulário e realize a validação necessária. Como posso fazer isso?

No meu próprio aplicativo, preciso validar as dimensões conforme elas são digitadas. A sequência que usei é a seguinte

  1. O usuário seleciona ou digita e então se afasta do controle.
  2. O controle perde o foco e notifica a Visualização enviando seu ID e o texto da input.
  3. A View verifica qual Shape Program (uma class que implementa uma interface) criou o Form e passa o ID e o texto da input.
  4. O programa de forma retorna uma resposta.
  5. Se a resposta for OK, a canvas atualiza a input correta da class Shape.
  6. Se a Resposta estiver OK, a Visualização informará ao Formulário através de uma Interface que está OK mudar o foco para a próxima input.
  7. Se a Resposta não estiver OK, a Visualização examinará a resposta e, usando a Interface do Formulário, informará ao formulário o que fazer. Isso geralmente significa que o foco volta para a input incorreta com uma mensagem exibida informando ao usuário o que aconteceu.

A vantagem dessa abordagem é que a validação é centralizada em um local para um determinado Shape Program. Eu não tenho que ir modificar cada controle ou mesmo realmente me preocupar com os diferentes tipos de controles no formulário. Lá atrás, quando desenhei o software, decidi como a interface do usuário funcionaria para checkboxs de texto, checkboxs de listview, checkboxs de combinação, etc. Também diferentes níveis de gravidade são tratados de maneira diferente.

A View cuida de instruir o Form sobre o que fazer através da Interface. Como isso realmente é implementado é tratado pelo próprio formulário em sua implementação da interface. O modo de exibição não importa se o formulário está exibindo amarelo para aviso e vermelho para erro. Só que lida com esses dois níveis. Mais tarde, se surgir uma ideia melhor de exibir os erros de aviso vs, posso fazer a alteração no próprio formulário, evitando a lógica de exibição ou a validação no programa de forma.

Você já está no meio do caminho se estiver pensando em fazer uma aula para manter sua lógica de validação, isso fará com que você descubra o seu novo design.

A validação já está incorporada na biblioteca do WinForms.

Cada object derivado de Control possui dois events denominados Validating e Validated . Também tem uma propriedade chamada CausesValidation . Quando isso é definido como true (é verdadeiro por padrão), o controle participa da validação. Caso contrário, isso não acontece.

A validação ocorre como parte do foco. Quando você se concentra em um controle, seus events de validação são triggersdos. Na verdade, os events de foco são triggersdos em uma ordem específica. Do MSDN :

Quando você altera o foco usando o teclado (TAB, SHIFT + TAB e assim por diante), chamando os methods Select ou SelectNextControl, ou definindo a propriedade ContainerControl .. ::. ActiveControl para o formulário atual, ocorrem events de foco em a seguinte ordem:

  1. Entrar
  2. Ter foco
  3. Sair
  4. Validando
  5. Validado
  6. LostFocus

Quando você altera o foco usando o mouse ou chamando o método Focus, os events de foco ocorrem na seguinte ordem:

  1. Entrar
  2. Ter foco
  3. LostFocus
  4. Sair
  5. Validando
  6. Validado

Se a propriedade CausesValidation estiver definida como false, os events Validating e Validated serão suprimidos.

Se a propriedade Cancel do CancelEventArgs estiver definida como true no delegado do evento Validating, todos os events que normalmente ocorreriam após o evento Validating serão suprimidos.

Além disso, um ContainerControl possui um método chamado ValidateChildren() que passará pelos controles contidos e os validará.

Eu percebo que este tópico é bem antigo, mas eu pensei em postar a solução que eu criei.

O maior problema com a validação no WinForms é a validação só é executada quando o controle “perdeu o foco”. Assim, o usuário precisa clicar dentro de uma checkbox de texto e clicar em outro lugar para a rotina de validação ser executada. Isso é bom se você está preocupado apenas com os dados inseridos que estão corretos. Mas isso não funciona bem se você estiver tentando garantir que um usuário não deixe uma checkbox de texto vazia, pulando por cima dela.

Na minha solução, quando o usuário clica no botão de envio de um formulário, eu verifico cada controle no formulário (ou qualquer recipiente especificado) e uso a reflection para determinar se um método de validação está definido para o controle. Se for, o método de validação é executado. Se alguma das validações falhar, a rotina retornará uma falha e permitirá que o processo seja interrompido. Essa solução funciona bem, especialmente se você tiver vários formulários para validar.

1) Basta copiar e colar esta seção do código no seu projeto. Estamos usando o Reflection, então você precisa adicionar System.Reflection às suas declarações usando

 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 o evento padrão de Validação em qualquer controle que você queira validar. Certifique-se de usar e.Cancel quando a validação falhar!

 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) Não pule este passo! Defina a propriedade AutoValidate no formulário para EnableAllowFocusChange . Isso permitirá tabular para outro controle, mesmo quando a validação falhar.

4) Finalmente, em seu método Submit Button, chame o método Validation e especifique qual contêiner você deseja verificar. Pode ser o formulário inteiro ou apenas um contêiner no formulário, como um painel ou um 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(); } 

Codificação Feliz!

Eu gostaria de não ter que ir controlar pelo controle pelo formulário e criar isValid etc por item.

Como algum nível, você terá que definir o que significa ser valid para cada controle, a menos que tudo de que você goste seja que o controle tenha algum valor.

Dito isso, há um componente ErrorProvider que você pode usar e funciona muito bem.

Tivemos boa sorte com o Noogen ValidationProvider . É simples para casos simples (verificações de tipo de dados e campos obrigatórios) e fácil de adicionar validação personalizada para casos mais complexos.

Em todos os meus formulários, eu implemento o evento isValidating para o controle específico em questão e se os dados não valida eu tenho um errorProvider no formulário e eu uso seu método SetError (…) para definir o erro para o controle em questão com informações relevantes sobre o motivo de estar errado.

edit> Devo observar que geralmente uso o padrão mvc ao fazer isso, então a validação específica para esse controle / membro do modelo acontece no modelo, portanto, o isValidating se parece com isso:

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

De qualquer maneira. Ou você pode ter um único evento de validação associado a todos ou controles que precisam de validações semelhantes. Isso removerá o loop do código. Digamos que você tenha quatro checkboxs de texto que podem ter somente inteiros. O que você pode fazer é ter um único evento para cada um deles. Eu não estou tendo nenhum IDE, então o código abaixo é o melhor que eu posso criar.

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

No evento:

  1. Transmitir remetente como checkbox de texto.
  2. Verifique se o valor na checkbox de texto é numérico.

E assim por diante você tem events alinhados.

Espero que isto ajude.

Se você combinar as idéias acima com um manipulador de events genérico Validating, obterá uma boa “estrutura” de erros de validação com todos os methods de validação em suas classs de negócios. Eu apenas estendi o código de Bruce com a ideia dinamarquesa. O foi feito para os componentes Entity Framework e Dev Express, mas essas dependencies podem ser facilmente removidas. Apreciar!

 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 validação de amostra:

 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."); } } 

Andar de bicicleta através de controles pode funcionar, mas é propenso a erros. Eu trabalhei em um projeto que usava essa técnica (já que era um projeto Delphi não C #) e funcionava como esperado, mas era muito difícil de atualizar se um controle fosse adicionado ou alterado. Isso pode ter sido corrigível. Não tenho certeza.

De qualquer forma, funcionou criando um único manipulador de events que foi então anexado a cada controle. O manipulador usaria o RTTI para determinar o tipo de controle. Em seguida, ele usaria a propriedade name do controle em uma instrução select grande para localizar o código de validação a ser executado. Se a validação falhou, uma mensagem de erro foi enviada ao usuário e o controle recebeu o foco. Para tornar as coisas mais complexas, o formulário foi dividido em várias guias e a guia adequada teve que ser visível para seu controle de criança para obter o foco.

Então essa é a minha experiência.

Eu preferiria usar um padrão de design do Passive View para remover todas as regras de negócios do formulário e enviá-las para uma class do Presenter. Dependendo do estado do formulário, pode ser mais trabalho do que você querer investir.

Apenas uma ideia 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); } } } 

Você pode colocar as checkboxs de texto dentro de um painel e apenas percorrer os controles se desejar evitar o loop através de outros controles.

Por que você não está usando o evento Validating? Você pode ter um único evento de validação e validar os controles lá. Não haverá necessidade de usar loops e cada controle será validado à medida que os dados forem inseridos.