SilverLight - Fazendo a validação no databinding (C#)


Eu já tratei do assunto databinding em aplicações SilverLight em diversos antigos anteriores e hoje vou falar um pouco sobre um tópico muito importante relacionado ao assunto: a validação de dados.

A validação de dados permite que as informações gravadas no seu banco de dados tenham integridade e confiabilidade e dessa forma não pode ser desprezada. Se você não realiza validação de dados em suas aplicações elas podem aceitar como informações qualquer entrada que o usuário informar e isso não é aconselhável.

Neste artigo eu mostro como você pode implementar a validação e como capturar erros de validação que ocorrem em suas aplicações SilverLight e como fornecer ao usuário mensagens indicativas destes erros.

Para começar um pouco de teoria

Um dos caminhos que podemos seguir para realizar a validação em aplicações que usam o databinding é anexar manipuladores ao evento BindingValidationError do controle SilverLight a ser validado e assim ter certeza de que a vinculação está definida para levantar eventos em exceções ocorridas nas validações.

A classe Binding esta no namespace System.Windows.Data e é responsável por manter a comunicação entre a origem e o destino, expondo uma série de propriedades que nos permite
customizar o comportamento dessa comunicação. Entre as principais propriedades, temos:
  • ElementName: define o nome do elemento que servirá como fonte. Utilize esta propriedade quando desejar preencher uma outra propriedade com o valor de um controle do WPF.
  • Mode: determina a direção das informações.
  • NotifyOnSourceUpdated: valor boleano indicando se o evento SourceUpdated é disparado quando alguma atualização na fonte das informações ocorrer.
  • NotifyOnTargetUpdated: valor boleano indicando se o evento SourceUpdated é disparado quando alguma atualização no destino das informações ocorrer.
  • Path: espefica o nome da propriedade que será exibida.
  • RelativeSource: especifica uma fonte de forma relativa à posição do objeto atual.
  • Source: define o nome do objeto que servirá como fonte. Utilize esta propriedade quando desejar preencher com uma instância de um objeto.
  • XPath: a mesma finalidade da propriedade Path, mas define uma expressão XPath quando a fonte de informações for um arquivo Xml.

O subsistema de vinculação de dados do SilverLight oferece suporte embutido para a notificação de erros de validação. Para ativar este suporte a propriedade Binding.ValidatesOnExceptions precisa ser definida como true na vinculação de maneira a permitir que o framework possa capturar qualquer exceção disparada durante a criação/definição de uma propriedade em uma fonte de dados ou durante uma conversão entre tipos e possa propagá-la para o seu código de validação de erros.

A maioria dos controles na class library que podem ser utilizados em vinculações no modo two-way fornecem um interface embutida para exibir erros de validações de vinculação ao usuário. Essa interface geralmente fornece um pequeno ícone de erro exibido no controle, que por sua vez exibe uma mensagem de erro em uma tooltip ao lado do controle.

A mensagem de erro exibida é o valor da propriedade Exception.Message da exceção disparada sendo que uma vez que o erro é corrigido o controle remove de forma automática a interface de erros.

Obtendo a informação do Erro

Em alguns casos pode não ser necessário simplesmente exibir a mensagem de erro e você pode querer acessar via código a informação sobre o erro. Para permitir este recurso a classe FrameworkElement (e por herança cada controle) pode disparar o evento BindingValidationError sempre que uma exceção se propagar como um erro de validação ou quando um erro existente de validação for removido.

Para instruir o subsistema de vinculação a levantar este evento, você precisa definir a propriedade Binding.NotifyOnValidation como true na vinculação.

Se você tratar o evento BindingValidationError poderá acessar a informação detalhada do erro através do argumento do evento de tipo ValidationErrorEventArgs.

A propriedade ValidationErrorEventArgs.Action, do tipo ValidationErrorEventAction, possui dois valores possíveis:

A propriedade ValidationErrorEventArgs.Exception lhe dá acesso a exceção atual que casou o erro de validação.

Obtendo um sumário dos erros durante a validação

Em muitas aplicações é comum exibir um sumário de erros que o usuário pode ter feito na realização da entrada dos dados. Um sumário exibe os campos onde os erros foram feitos, a natureza do erro, e em alguns casos, ele também inclui uma navegação automática para navegar para o campo. Esta característica também existe no mecanismo de validação de vinculação do SilverLight e esta habilitada através do controle System.Windows.Controls.ValidationSummary e esta relacionado em classes no assembly System.Windows.Controls.Data.Input.

Uma vez que você coloca um controle ValidationSummary em sua página , o subsistema de vinculação automaticamente sabe como preenchê-lo com as entradas de erros e erros de vinculação que ocorrerem. A seguir temos um exemplo de declaração XAML que define este recurso:

Ex: <input:ValidationSummary />

Observe também que os erros de validação preenchem a árvore visual por porte do subsistema de vinculação. Isto significa que o controle ValidationSummary pode ser colocado em qualquer lugar da sua página.

Nota: As interfaces de usuário para o sumário de validação e o erro padrão podem ser customizada usando o controle de templates.

Antes de partir para a parte prática que falar de dois conceitos importantes que eu estou usando na aplicação e que você deve conhecer:

1-
Quando você tem elementos vinculados a dados em sua interface em uma aplicação SilverLight como podemos ativar as notificações de alteração e atualização automáticas da interface quando o aplicativo altera dados vinculados ?

Basta implementar a interface System.ComponentModel.INotifyPropertyChanged em seus tipos de dados e a interface
System.Collections.Specialized.INotifyCollectionChanged nos tipos da sua coleção. Depois basta disparar os eventos definidos nestas interface a partir da implementação dos tipos para fornecer as notificações de mudança.

Obs: Garanta que a propriedade Mode para cada vinculação de dados esteja definida como  BindingMode.OneWay ou BindingMode.TwoWay para permitir atualização automática da interface do usuário

2- Como fazer para converter um valor de origem para um tipo qualquer diferente ou valor diferente adequado que deverá ser exibido na interface do usuário em uma aplicação SilverLight em dados vinculados a uma fonte de dados ?

Basta implementar a interface System.Windows.Data.IValueConverter  para criar um tipo de conversor de valor e associá-lo a vinculação de dados para converter o valor de forma correta.

Este exemplo implementa a interface INotifyPropertyChanged  para fornecer as notificações de mudanças ocorridas na aplicação e interface IValueConverter para realizar a conversão de tipos.

Da teoria a prática

Depois de toda essa teoria que tal criar algo prático para mostrar como tudo isso funciona ?

Vamos criar um exemplo bem simples onde iremos definir um modelo de dados nas classes Contato e Endereco para exibir o nome, email, telefone, e endereço.

A exibição do nome e email será feita no controle ListBox e os detalhes em grid usando os controles TextBox e TextBlock.

Abaixo temos o leiaute da aplicação SilverLight em tempo de projeto:

Executando a aplicação teremos :

Então vamos construir o projeto...

Eu vou usar como ferramenta o Visual Web Developer 2010 Express Edition (VWD 2010) que é gratuito , assim todos poderão acompanhar o exemplo.

Abra o VWD 2010 Express e clique em File -> New Project;

Selecione Visual C# -> SilverLight e o template SilverLight Application e informe o nome Validacao_SilverLight e clique em OK;

A seguir desmarque a opção Host the SilverLight application in a new Web Site e selecione a versão SilverLight 4;

Dessa forma teremos um projeto bem simples que será executado pelo SilverLight Developer Runtime;

Será criado o projeto SilverLight com a estrutura conforme mostra a figura da janela Solution Explorer:

No menu Project clique em Add Reference e na janela Add Reference selecione a aba .NET e escolha System.Windows.Controls.Data.Input e clique em OK;

Vamos agora criar uma novo arquivo do tipo Class chamado DataClasses.cs (o nome fica seu critério) onde iremos definir duas classes :

Nestas classe teremos a implementação da interface INotifyPropertyChanged onde disparamos os eventos definidos nestas interface a partir da implementação dos tipos para fornecer as notificações de mudança conforme explicado na teoria.

Nestas classe definimos as validações que desejamos realizar. Para o exemplo temos definidos:

No menu Project clique em Add Class e informe o nome DataClasses.cs e clique em Add;

A seguir inclua o seguinte código neste arquivo:

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;

namespace Validacao_SilverLight
{
    public class Contato : INotifyPropertyChanged
    {
        // implementação da interface INotifyPropertyChanged
        public event PropertyChangedEventHandler PropertyChanged;

        private void RaisePropertyChanged(PropertyChangedEventArgs e)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, e);
        }

        public Contato()
        {
        }
        private string _Nome;
        public string Nome
        {
            get { return _Nome; }
            set
            {
                string ValorAnterior = _Nome;
                if (ValorAnterior != value)
                {
                    _Nome = value;
                    RaisePropertyChanged(new PropertyChangedEventArgs("Nome"));
                }
            }
        }//_Nome

        private string _Email;
        public string Email
        {
            get { return _Email; }
            set
            {
                string ValorAnterior = _Email;
                if (ValorAnterior != value)
                {
                    _Email = value;
                    RaisePropertyChanged(new PropertyChangedEventArgs("Email"));
                }
            }
        }//_Email
        private string _Telefone;
        public string Telefone
        {
            get { return _Telefone; }
            set
            {
                string ValorAnterior = _Telefone;
                if (value.Length != 10)
                    throw new Exception("O númreo do telefone deve ter 10 dígitos");
                try
                {
                    Convert.ToInt64(value);
                }
                catch
                {
                    throw new Exception("O número do telefone deve ter 10 dígitos");
                }
                if (ValorAnterior != value)
                {
                    _Telefone = value;
                    RaisePropertyChanged(new PropertyChangedEventArgs("Telefone"));
                }
            }
        }//_Telefone

        private Endereco _Endereco;
        public Endereco Endereco
        {
            get { return _Endereco; }
            set
            {
                Endereco ValorAnterior = _Endereco;
                if (ValorAnterior != value)
                {
                    _Endereco = value;
                    RaisePropertyChanged(new PropertyChangedEventArgs("Endereco"));
                }
            }
        }//_Endereco

        private bool _InError = default(bool);
        public bool InError
        {
            get
            {
                return _InError;
            }
            set
            {
                if (value != _InError)
                {
                    _InError = value;
                    if (PropertyChanged != null)
                        PropertyChanged(this, new PropertyChangedEventArgs("InError"));
                }
            }
        }
    }//Contato

    public class Endereco : INotifyPropertyChanged
    {
        private static List<string> EstadoLista =
          new List<string>(){ "AC","AL","AP","AM","BA","CE","DF","ES","GO","MA","MT",
        "MS","MG","PA","PB","PR","PE","PI","RJ","RN","RS","RO","RR","SC","SP","SE",
        "TO"};
        public event PropertyChangedEventHandler PropertyChanged;
        private void RaisePropertyChanged(PropertyChangedEventArgs e)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, e);
        }

        private string _Rua;
        public string Rua
        {
            get { return _Rua; }
            set
            {
                string ValorAnterior = _Rua;
                if (ValorAnterior != value)
                {
                    _Rua = value;
                    RaisePropertyChanged(new PropertyChangedEventArgs("Rua"));
                }
            }
        }//_Rua

        private string _Cidade;
        public string Cidade
        {
            get { return _Cidade; }
            set
            {
                string ValorAnterior = _Cidade;

                if (ValorAnterior != value)
                {
                    _Cidade = value;
                    RaisePropertyChanged(new PropertyChangedEventArgs("Cidade"));
                }
            }
        }//_Cidade

        private string _Estado;
        public string Estado
        {
            get { return _Estado; }
            set
            {
                string ValorAnterior = _Estado;
                //estado deve ter 2 caracteres
                if (EstadoLista.Contains(value) == false)
                    throw new Exception(
                      "A sigla do estado deve possuir duas Letras"
                      );
                if (ValorAnterior != value)
                {
                    _Estado = value;
                    RaisePropertyChanged(new PropertyChangedEventArgs("Estado"));
                }
            }
        }//_Estado

        private string _Cep;
        public string Cep
        {
            get { return _Cep; }
            set
            {
                string ValorAnterior = _Cep;
                //cep deve ter 8 caracteres
                if (value.Length != 8)
                {
                    throw new Exception("O Cep deve conter 8 dígitos numéricos");
                }

                try
                {
                    Convert.ToInt32(value);
                }
                catch
                {
                    throw new Exception("O Cep deve conter 8 dígitos numéricos");
                }
                if (ValorAnterior != value)
                {
                    _Cep = value;
                    RaisePropertyChanged(new PropertyChangedEventArgs("Cep"));
                }
            }
        }//_Cep
    }//Endereco   
}

Conforme o código acima já temos as nossas classes Contato e Endereco que representam o nosso modelo de dados e onde definimos os membros e a validação que iremos realizar na aplicação.

Precisamos criar também o arquivo a classe BoolToVisibilityConverter.cs onde iremos implementar a interface IValueConverter para fornecer as notificações de mudanças ocorridas na aplicação e interface IValueConverter para realizar a conversão de tipos.

No menu Project clique em Add Class e a seguir informe o nome BoolToVisibilityConverter.cs e clique no botão Add;

Obs: Não vou entrar em detalhes nos conversores, irei apenas usá-los no projeto. Se você quiser saber mais sobre eles consulte o meu artigo:  http://www.macoratti.net/10/03/wpf_cvd1.htm

Inclua o seguinte código este arquivo:

using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Windows.Data;

namespace Validacao_SilverLight
{
    public class BoolToVisibilityConverter : IValueConverter
    {
        public object Convert(object value, Type targetType,
          object parameter, System.Globalization.CultureInfo culture)
        {
            //verifica se os tipos de parametros estão em conformidade
            if (!(value is bool) || targetType != typeof(Visibility))
                return null;
            return (bool)value == true ? Visibility.Visible : Visibility.Collapsed;
        }
        public object ConvertBack(object value, Type targetType,
          object parameter, System.Globalization.CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}

Agora podemos definir o código XAML referente a interface com o usuário no arquivo MainPage.xaml conforme abaixo:

<UserControl x:Class="Validacao_SilverLight.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:local="clr-namespace:Validacao_SilverLight"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
   xmlns:input=
"clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data.Input"
             mc:Ignorable="d"             
             Width="405"
             Height="462">
    <UserControl.Resources>

        <local:BoolToVisibilityConverter x:Key="REF_BoolToVisibilityConverter" />
        <DataTemplate x:Key="dtContato">
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto" />
                    <ColumnDefinition Width="Auto" />
                    <ColumnDefinition Width="Auto" />
                </Grid.ColumnDefinitions>
                <TextBlock Text="{Binding Nome}"/>
                <TextBlock Text="{Binding Email}"
                   Grid.Column="1"
                   Grid.Row="0"
                   Margin="5,0,0,0" />
                <TextBlock Text=" -> Error!!" Foreground="Red"
                   Visibility=
      "{Binding InError, Converter={StaticResource REF_BoolToVisibilityConverter}}"
                   Grid.Column="2" />
            </Grid>
        </DataTemplate>
    </UserControl.Resources>
    <Grid x:Name="LayoutRoot"
        Background="White"
        Margin="10,10,10,10">
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <ListBox Grid.Row="0"
             x:Name="lbx_Contatos"
             ItemTemplate="{StaticResource dtContato}"
             SelectionChanged="lbx_Contatos_SelectionChanged" />

        <Grid x:Name="grid_NewButton"
          Margin="0,2,0,0"
          Grid.Row="1"
          HorizontalAlignment="Right">
            <Button  x:Name="btn_Novo"
               Click="btn_Novo_Click"
               Content="Novo Contato" />
        </Grid>

        <input:ValidationSummary Grid.Row="2" Margin="0,10,0,5"/>
 
        <Border Grid.Row="3"
            Visibility="Collapsed"
            x:Name="border_ContatoForm"
            Margin="0,2,0,0"
            BorderBrush="Black"
            BorderThickness="1"
            Padding="1,1,1,1">
            <Grid x:Name="grid_ContatoForm">

                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="0.142*" />
                    <ColumnDefinition Width="0.379*" />
                    <ColumnDefinition Width="0.1*" />
                    <ColumnDefinition Width="0.097*" />
                    <ColumnDefinition Width="0.082*" />
                    <ColumnDefinition Width="0.2*" />
                </Grid.ColumnDefinitions>
                <Grid.RowDefinitions>
                    <RowDefinition Height="0.10*" />
                    <RowDefinition Height="0.15*" />
                    <RowDefinition Height="0.15*" />
                    <RowDefinition Height="0.15*" />
                    <RowDefinition Height="0.45*" />
                </Grid.RowDefinitions>

                <TextBox HorizontalAlignment="Stretch"
                 Margin="1,1,1,1"
                 x:Name="tbxNome"
                 VerticalAlignment="Stretch"
                 Text="{Binding Nome, Mode=TwoWay}"
                 Grid.Row="1"
                 Width="Auto"
                 Grid.RowSpan="1"
                 Grid.ColumnSpan="2"
                 Grid.Column="1" />
                <TextBox HorizontalAlignment="Stretch"
                 Margin="1,1,1,1"
                 x:Name="tbxEmail"
                 VerticalAlignment="Stretch"
                 Text="{Binding Email, Mode=TwoWay}"
                 Grid.Row="1"
                 Grid.Column="3"
                 Width="Auto"
                 Grid.RowSpan="1"
                 Grid.ColumnSpan="3" />
                <TextBlock HorizontalAlignment="Stretch"
                   Margin="1,1,1,1"
                   VerticalAlignment="Stretch"
                   Text="Email"
                   TextWrapping="Wrap"
                   Grid.RowSpan="1"
                   Grid.Column="4"
                   Grid.ColumnSpan="2"
                   Height="Auto"
                   Width="Auto" />
                <TextBlock HorizontalAlignment="Center"
                   Margin="1,1,1,1"
                   VerticalAlignment="Stretch"
                   Text="Nome"
                   TextWrapping="Wrap"
                   Grid.RowSpan="1"
                   Grid.Row="1"
                   Height="Auto"
                   Width="Auto" />
                <TextBlock HorizontalAlignment="Center"
                   Margin="1,1,1,1"
                   VerticalAlignment="Stretch"
                   Text="Rua"
                   TextWrapping="Wrap"
                   Grid.Row="2"
                   Width="Auto" />
                <TextBox HorizontalAlignment="Stretch"
                 x:Name="tbxRua"
                 VerticalAlignment="Stretch"
                 Text="{Binding Endereco.Rua, Mode=TwoWay}"
                 Grid.Row="2"
                 Margin="1,1,1,1"
                 Grid.Column="1"
                 Grid.ColumnSpan="5"
                 Width="Auto" />
                <TextBlock HorizontalAlignment="Center"
                   VerticalAlignment="Stretch"
                   Text="Cidade"
                   TextWrapping="Wrap"
                   Margin="1,1,1,1"
                   Grid.Row="3" />
                <TextBlock Text="UF"
                   Margin="1,1,1,1"
                   TextWrapping="Wrap"
                   Grid.Column="2"
                   Grid.Row="3"
                   HorizontalAlignment="Center" />
                <TextBlock Text="Cep"
                   Margin="1,1,1,1"
                   TextWrapping="Wrap"
                   Grid.Column="4"
                   Grid.Row="3"
                   HorizontalAlignment="Center" />
                <TextBox HorizontalAlignment="Stretch"
                 x:Name="tbxCidade"
                 Margin="1,1,1,1"
                 VerticalAlignment="Stretch"
                 Text="{Binding Endereco.Cidade, Mode=TwoWay}"
                 Grid.Row="3"
                 Grid.Column="1" />
                <TextBox Background="Transparent"
                 Grid.Column="3"
                 Margin="1,1,1,1"
                 Grid.Row="3"
                 Text="{Binding Endereco.Estado, Mode=TwoWay,
                ValidatesOnExceptions=True,NotifyOnValidationError=True}"
                 x:Name="tbxEstado">
                </TextBox>
                <TextBox Background="Transparent"
                 Grid.Column="5"
                 Grid.Row="3"
                 Margin="1,1,1,1"
                 Text="{Binding Endereco.Cep, Mode=TwoWay ,
                  ValidatesOnExceptions=True,NotifyOnValidationError=True}"
                 x:Name="tbxCep" />

                <TextBlock HorizontalAlignment="Center"
                   VerticalAlignment="Stretch"
                   Text="Fone"
                   Margin="1,1,1,1"
                   TextWrapping="Wrap"
                   Grid.Row="4" />
                <TextBox Grid.Column="1"
                 Grid.Row="4"
                 Margin="1,1,1,1"
                 Text="{Binding Telefone, Mode=TwoWay ,
          ValidatesOnExceptions=True,NotifyOnValidationError=True}"
                 x:Name="tbxTelefone" />
                <Button  Grid.Column="5"
                 Margin="1,1,1,1"
                 Grid.Row="4"
                 Height="30.911"
                 VerticalAlignment="Top"
                 Content="Fechar"
                 x:Name="btnFechar"
                 Click="btnFechar_Click" />
            </Grid>
        </Border>
    </Grid>
</UserControl>

 

Neste código estamos usando o controle  ValidationSummary  e definindo nas vinculações onde estamos efetuando a propriedade Binding.ValidatesOnExceptions precisa ser definida como true na vinculação de maneira a permitir que o framework possa capturar qualquer exceção disparada durante a criação/definição de uma propriedade em uma fonte de dados ou durante uma conversão entre tipos e possa propagá-la para o seu código de validação de erros.

Executando o projeto ao tentar informar valores não válidos nos campos que estamos validando iremos obter:

No exemplo podemos ver o controle ValidationSummary preenchido com os erros definidos na validação.

Eu sei que a primeira vista pode parecer um tanto complexo efetuar a validação no SilverLight mas com o tempo e a prática o processo passa a ser incorporado e quem sabe também nas próximas versões o mesmo seja mais simplificado.

Lembrando que existem outras maneiras de realizar a validação como usar as interfaces IDataError e INotifyDataErrorInfo mas isso é assunto para outro artigo.

Nota: Muitos dos conceitos abordados neste artigos também são validos para aplicações WPF.

Pegue o projeto completo aqui: Validacao_SilverLight.zip

Eu sei é apenas SilverLight, mas eu gosto...

Referências:

José Carlos Macoratti