Olá! Depois de alguns anos sem postar nada, resolvi postar sobre esta integração, onde o manual do Bradesco deixa alguns pontos confusos para alguns desenvolvedores. Mas juntos, iremos desvendar os mistérios da geração das chaves para conexão com as APIs do Bradesco.
A forma de comunicação com os serviços é mTLS, você precisa entender que este protocolo diz que ambos os servidores irão utilizar um certificado para garantir que a comunicação é segura, e para garantir que a empresa XYZ é quem está usando o servidores do Bradesco.
O certificado utilizado pelo Bradesco precisa possuir as seguintes características:
Padrão ICP-BRASIL do tipo A1
Tamanho mínimo de 2048 bits
Utilizar algum algoritmo RSA como o RSASHA 256, 384 ou 512
Data de validade/expiração deve ser superior há 4 meses e no máximo 3 anos
E sim, como já deve ter percebido, o certificado que você usa para emitir NF-e, NFC-e entre outros documentos, é válido.
Aqui, vou assumir que você já entrou em contato com o gerente da conta e já tem em mãos as chaves client_id e secret_key. Caso não tenha, envie um e-mail para suporte.api@bradesco.com.br seguindo o template “Cadastro de Client ID”, página 11 do referido manual.
Chega de papo e mãos à obra
Recuperando o token de acesso
Aqui, vou inserir um novo manual, que é o manual de emissão de boleto híbrido ou boleto com QR Code.
Antes de criarmos um boleto, é necessário um token de acesso. Vamos analisar os dados que vamos precisar para criar o token:
Propriedade
Descrição
aud
Valor fixo: 👉🏼 https://proxy.api.prebanco.com.br/auth/server/v1.1/token (audience): É o destinatário do token, quem vai usar este token.
sub
(subject) é o assunto. No caso, aqui é informado o Client_Id retornado pelo Bradesco
iat
(issued at): timestamp de quando o token foi criado; Padrão Unix (EPOCH) ⚠️ Atenção: Guarde este valor, iremos usar o mesmo no futuro
jti
nonce – numérico de no máximo dezoito dígitos, valor sem repetição. o NONCE é uma valor aleatório que só pode ser usado uma única vez 🔗NONCE ⚠️ Atenção: Guarde este valor, iremos usar o mesmo no futuro
exp
(expiration): timestamp de quando o token irá expirar; Padrão Unix (EPOCH)
ver
Valor Fixo: 1.1 É a versão.
E para os cabeçalhos da requisição:
Header
Valor
grant_type
urn:ietf:params:oauth:grant-type:jwt-bearer
assertion
Ah! logo de cara dá medo. Mas após a compreensão da mesma, fica clara que é apenas o JWT assinado com RS256 e o certificado digital enviado para obtenção do Client_Id. E não se preocupe, o código para a geração do “assertion” está logo abaixo, veja a classe “Signer”, método “CreateAssertion”.
A classe “Signer”, abaixo, faz a assinatura e cria o “assertion” de forma simples.
A classe “Signer”
using BoletoHibridoBradesco.Helpers;
using Jose;
using Newtonsoft.Json;
using System.Diagnostics;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
namespace BoletoHibridoBradesco.Security
{
/// <summary>
/// Classe de apoio para geração e assinatura Bradesco
/// </summary>
public abstract class Signer
{
#region Public Methods
public static string ComputeSHA256(string value, X509Certificate2 certificate)
{
using var rsa = certificate.GetRSAPrivateKey();
var data = Encoding.ASCII.GetBytes(value);
var signedData = Convert.ToBase64String(rsa.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1));
return signedData;
}
/// <summary>
/// Cria as afirmações JWT e as assina com base no certificado.
/// </summary>
/// <param name="clientId">Client_Id recebido do Bradesco</param>
/// <param name="certificate">Certificado A1</param>
/// <returns></returns>
public static SignedAssertion CreateAssertion(string clientId, X509Certificate2 certificate)
{
//Gera a Assertion do tópico 3.7 do manual Nov.2022 - Api_Registro_Titulo_Cobranca_QR Code_Bradesco_v1.pdf
var dt = DateTime.UtcNow;
var timestamp = $"{dt:yyyy-MM-ddTHH:mm:ss}-00:00";
var iat = UnixEpochHelper.GetSecondsSince(dt);
var jti = long.Parse($"{UnixEpochHelper.GetSecondsSince(dt)}000");
var exp = UnixEpochHelper.GetSecondsSince(dt.AddHours(1));
var assertion = SignToken(new
{
aud = "https://proxy.api.prebanco.com.br/auth/server/v1.1/token",// (audience): destinatário do token, quem vai usar?
sub = $"{clientId}",//assunto
iat, //(issued at): timestamp de quando o token foi criado;
jti, // nonce - numérico de no máximo dezoito dígitos, valor sem repetição. o NONCE é uma valor aleatório que só pode ser usado uma única vez
exp, // (expiration): timestamp de quando o token irá expirar
ver = "1.1" //versão
}, certificate);
return (assertion, timestamp, iat, jti, exp);
}
/// <summary>
/// Assina o cabeçalho X-Brad-Signature, que deve ser enviado durante as requisições com as APIs Bradesco
/// </summary>
/// <param name="accessToken">Token de acesso obtido anteriormente por serviço de auntenticação/autorização do Bradesco</param>
/// <param name="certificate">Certificado. O mesmo usado para a criação do Client_Id</param>
/// <param name="json">Json do boleto que será enviado para a emissão</param>
/// <param name="nonce">Número único e aleatório. O mesmo usado na criação do assertion. Veja <see cref="CreateAssertion(string, X509Certificate2)"/></param>
/// <param name="timestamp">Timestamp. O mesmo usado na criação do assertion. Veja <see cref="CreateAssertion(string, X509Certificate2)"/></param>
/// <returns></returns>
public static string CreateXBradSignature(string accessToken, string timestamp, string json, long nonce, X509Certificate2 certificate)
{
var sb = new StringBuilder();
sb.Append("POST\n"); // Método HTTP usado na requisição
sb.Append($"/v1/boleto-hibrido/registrar-boleto\n"); // Endpoint da chamada
sb.Append('\n'); // Query Parameters caso existam
sb.Append($"{json}\n"); // Body da requisição ou deixar uma linha em branco nos casos em que não existe body
sb.Append($"{accessToken}\n"); // Token de acesso gerado anteriormente
sb.Append($"{nonce}\n"); // Nonce (Jti) gerado no momento do "assertion"
sb.Append($"{timestamp}\n"); // Timestamp representando o momento da chamada. Foi criado no mesmo instante do "assertion"
sb.Append("SHA256"); // Algoritmo usado para assinar o JWT
// faz a assinatura
var signed = ComputeSHA256(sb.ToString(), certificate);
// remove os =
signed = signed.Replace("=", "");
// troca o + por -
signed = signed.Replace("+", "-");
// troca / por _
signed = signed.Replace("/", "_");
// limpa os espaços das extremidades
signed = signed.Trim();
return signed;
}
/// <summary>
/// Assina a mensagem e retorna o token assinado com o certificado
/// </summary>
/// <param name="message">Mensagem para assinar</param>
/// <param name="certificate">Certificado</param>
/// <returns></returns>
public static string SignToken(object message, X509Certificate2 certificate)
{
var senderPrivateKey = certificate.GetRSAPrivateKey();
var signed = JWT.Encode(message, senderPrivateKey, JwsAlgorithm.RS256, null);
Debug.WriteLine(JsonConvert.SerializeObject(message, Formatting.Indented), "Message");
Debug.WriteLine(signed, "Message Signed");
return signed;
}
#endregion Public Methods
}
public record struct SignedAssertion(string Assertion, string Timestamp, long Iat, long Jti, long Exp)
{
public static implicit operator (string Assertion, string Timestamp, long Iat, long Jti, long Exp)(SignedAssertion value) => (value.Assertion, value.Timestamp, value.Iat, value.Jti, value.Exp);
public static implicit operator SignedAssertion((string Assertion, string Timestamp, long Iat, long Jti, long Exp) value) => new(value.Assertion, value.Timestamp, value.Iat, value.Jti, value.Exp);
}
}
Uma vez que a classe “Signer” foi criada, iremos implementar o método para gerar o token de acesso.
Token de acesso
/// <summary>
/// Gera um token e retorna
/// </summary>
/// <param name="clientId">Client_Id obtido junto ao banco Bradesco</param>
/// <param name="certificate">Certificado enviado ao Bradesco para obtenção do Client_Id</param>
/// <returns></returns>
/// <exception cref="Exception">Exceção genérica</exception>
private static async Task<(SignedAssertion Assertion, string Token)> GetTokenAsync(string clientId, X509Certificate2 certificate)
{
// Assinar e criar o Assertion, necessário para a solicitação do token do Bradesco
var assertion = Signer.CreateAssertion(clientId, certificate);
//Criar o http client para a requisição do token
var serviceProvider = new ServiceCollection().AddHttpClient().BuildServiceProvider();
var httpClientFactory = serviceProvider.GetService<IHttpClientFactory>();
client = httpClientFactory.CreateClient(nameof(BoletoHibridoBradesco));
//prepara os headers
client.DefaultRequestHeaders.Clear();
client.DefaultRequestHeaders.Connection.Add("keep-alive");
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("*/*"));
//conteúdo
var content = new FormUrlEncodedContent(new[]
{
KeyValuePair.Create("grant_type","urn:ietf:params:oauth:grant-type:jwt-bearer"),
KeyValuePair.Create("assertion",assertion.Assertion)
});
//Buscar o token
var response = await client.PostAsync("https://proxy.api.prebanco.com.br/auth/server/v1.2/token", content);
var json = await response.Content.ReadAsStringAsync();
//Se tudo correu bem, é um sucesso e o token foi gerado
if(response.IsSuccessStatusCode)
{
var token = JsonConvert.DeserializeObject<JToken>(json)["access_token"].ToString();
return (assertion, token);
}
//Xii! Deu erro
throw new Exception(json);
}
Emitindo um boleto
Antes de solicitar um boleto, temos que entender os cabeçalhos que serão enviados na requisição:
Lembra da geração do “assertion”? Aqui é o valor do Jti que foi informado na geração do “assertion”
X-Brad-Timestamp
Ao criar o “assertion”, formatamos o “Iat” no padrão yyyy-MM-ddTHH:mm:ss-00:00 aqui, deve ser informado este “timestamp”
X-Brad-Algorithm
SHA256, o mesmo usado para assinar o “assertion”
Authorization
Token recebido no processo de autorização.
O cabeçalho X-Brad-Signature
De acordo com o manual “Manual-Desenvolvedor-V-4.0.pdf”, página 20 tópico 4:
4 – Criando o header X-Brad-Signature para consumo de APIs
O header X-Brad-Signature é um campo obrigatório para todas as chamadas de consumo de nossas APIs. O seu valor é um JWS que deve ser assinado com os seguintes campos:
Método HTTP usado na requisição
Endpoint da chamada
Query Parameters caso existam
Body da requisição ou deixar uma linha em branco nos casos em que não existe body
Token de acesso gerado anteriormente
Nonce (valor numérico de no máximo dezoito dígitos, só pode ser usado uma vez)
Timestamp representando o momento da chamada (o mesmo usado no header X-BradTimestamp)
Algoritmo usado para assinar o JWT, no campo header “X-Brad-Algorithm”
Todas essas informações devem ser escritas em um arquivo texto simples, uma linha abaixo da outra, com o arquivo sendo assinado ao final do preenchimento desses dados. A string gerada nessa assinatura deverá ser usada no campo X-BradSignature.
Isto está uma bagunça.
Esqueça o comando e a geração do arquivo. Atente-se apenas para as regras de geração do mesmo.
Abaixo, o método “CreateXBradSignature” da classe “Signer” que já cria e faz a assinatura do cabeçalho.
/// <summary>
/// Assina o cabeçalho X-Brad-Signature, que deve ser enviado durante as requisições com as APIs Bradesco
/// </summary>
/// <param name="accessToken">Token de acesso obtido anteriormente por serviço de auntenticação/autorização do Bradesco</param>
/// <param name="certificate">Certificado. O mesmo usado para a criação do Client_Id</param>
/// <param name="json">Json do boleto que será enviado para a emissão</param>
/// <param name="nonce">Número único e aleatório. O mesmo usado na criação do assertion. Veja <see cref="CreateAssertion(string, X509Certificate2)"/></param>
/// <param name="timestamp">Timestamp. O mesmo usado na criação do assertion. Veja <see cref="CreateAssertion(string, X509Certificate2)"/></param>
/// <returns></returns>
public static string CreateXBradSignature(string accessToken, string timestamp, string json, long nonce, X509Certificate2 certificate)
{
var sb = new StringBuilder();
sb.Append("POST\n"); // Método HTTP usado na requisição
sb.Append($"/v1/boleto-hibrido/registrar-boleto\n"); // Endpoint da chamada
sb.Append('\n'); // Query Parameters caso existam
sb.Append($"{json}\n"); // Body da requisição ou deixar uma linha em branco nos casos em que não existe body
sb.Append($"{accessToken}\n"); // Token de acesso gerado anteriormente
sb.Append($"{nonce}\n"); // Nonce (Jti) gerado no momento do "assertion"
sb.Append($"{timestamp}\n"); // Timestamp representando o momento da chamada. Foi criado no mesmo instante do "assertion"
sb.Append("SHA256"); // Algoritmo usado para assinar o JWT
// faz a assinatura
var signed = ComputeSHA256(sb.ToString(), certificate);
// remove os =
signed = signed.Replace("=", "");
// troca o + por -
signed = signed.Replace("+", "-");
// troca / por _
signed = signed.Replace("/", "_");
// limpa os espaços das extremidades
signed = signed.Trim();
return signed;
}
Agora sim! O Boleto
Uma vez o método para a geração do cabeçalho X-Brad-Signature criado, vamos emitir nosso boleto.
O json que vou utilizar aqui, é disponibilizado pelo suporte do Bradesco.
Neste artigo vimos como gerar a “assertion”, realizar uma requisição de token. Vimos como gerar a tag x-Brad-Signature e como emitir um boleto. Vimo como usar o certificado para realizar as assinaturas necessárias.
Até que enfim, chegamos ao final da série “S.O.L.I.D“, vamos agora ver a letra D do acróstico que define a sigla DIPDependency Inversion Principle (Princípio da inversão de dependência).
Definição
Classes de alto nível não devem depender de classes de baixo nível. Ambas devem depender de abstrações;
Isso quer dizer que na minha hierarquia de classes, as classes mais genéricas, não devem depender de classes mais especializadas. (Veja: Generalização e Especialização).
Abstrações não devem depender de detalhes. Detalhes devem depender de abstrações.
Com esta definição, eu quero dizer que as minhas classes abstratas devem ser sempre classes estáveis e os detalhes da implementação devem ficar nas classes mais instáveis.
Mas prometo voltar aqui no futuro e corrigir os links para artigos sobre cada assunto que irei escrever.
Não confundir este princípio com Injeção de Dependência, injeção de Dependência é a ideia de injetar código através de algum contêiner ou framework que vai fazer isso para você.
Aqui, você vai inverter, não injetar, a maneira como você depende das coisas.
Problemas ao ferir o princípio
Quando ferimos o princípio da inversão de dependência, temos um código com baixa coesão e alto acoplamento. Dar manutenção em um código escrito desta maneira se torna complexo, uma vez que muitas classes dependem de outras classes concretas e as classes concretas não são estáveis.
Ferimos o princípio da responsabilidade única, uma vez que uma classe pode fazer coisas que não diz respeito à ela.
Em sistemas grandes, onde a customização em clientes se faz necessária, é praticamente impossível de conseguir uma boa customização.
Fica mais difícil escrever testes de unidade, pois não temos como garantir que todas as funcionalidades, ou parte sejam testadas.
Certo ou errado?
Abaixo, iremos ver dois exemplos, começaremos pelo errado e onde erramos e em seguida o correto e o que corrigimos.
public string CNPJ { get; set; }
public int Codigo { get; set; }
public DateTime DataCadastro { get; set; }
public string Email { get; set; }
public string Endereco { get; set; }
public string Nome { get; set; }
#endregion Public Properties
#region Public Methods
public void EnviarEmail()
{
Console.WriteLine($"O e-mail foi enviado para o endereço {Email}");
}
public void Excluir(long id)
{
Console.WriteLine($"O cliente {Nome} foi excluído.");
}
public void Salvar()
{
if(Codigo != 0)
Atualizar();
else
Adicionar();
EnviarEmail();
}
public IList<Cliente> Selecionar(long? id = null)
{
List<Cliente> result = new List<Cliente>();
return result;
}
public bool Validar()
{
if(String.IsNullOrWhiteSpace(Nome))
throw new Exception("O nome é obrigatório");
if(!Email.Contains("@"))
throw new Exception("O e-mail não é válido");
if(CNPJ.Length != 14)
throw new Exception("O CNPJ não é válido");
return true;
}
#endregion Public Methods
}
}[/csharp]
No código acima a classe de cliente é responsável por enviar e-mail, se validar, realizar CRUD, tem acesso ao banco de dados, conhece a implementação do envio de e-mail, enfim. Faz tudo que não se pode fazer.
Realizar a manutenção neste código seria trabalhoso, demorado e totalmente passível de erros.
É demasiado complicado realizar um teste de unidade.
Corrigindo o princípio
Isto está correto.
Aqui, iremos mostrar os fontes e detalhar as classes que foram comentadas no primeiro princípio. (Veja: Princípio da Responsabilidade Única).
Vamos definir aqui a classe de clientes com algumas alterações, para remover a depêndencia com validações, envios de e-mail e estratégias.
Podemos perceber que na classe de clientes, eu não tenho nenhuma chamada para validação e envio de e-mails, mas isto irá acontecer em nossa estratégia.
Devido ao fato de sempre trabalhar com sistemas ERP, onde se exige muitas customizações e mudanças de regras de negócio, é por isso que eu utilizo o Strategy Pattern.
[csharp]/*
* Classe apenas para fins de exemplo e aprendizado não considera nenhum tipo de validação ou regra ou se utiliza de algum framework.
*/
using Solid.Certo.Persistence.Abstract;
using Solid.Certo.Repository;
using System;
namespace Solid.Certo.Persistence
{
public class Cliente: EntityBase
{
#region Public Properties
public string CNPJ { get; set; }
public DateTime DataCadastro { get; set; }
public string Email { get; set; }
public string Endereco { get; set; }
public string Nome { get; set; }
#endregion Public Properties
#region Public Methods
public override void Salvar()
{
//Não estou usando o Repository Pattern
//E vou me permitir fazer isso na classe de cliente e não em um repositório
DummyDBContext<Cliente>.Salvar(this);
}
#endregion Public Methods
}
}[/csharp]
Quem é responsável por chamar as minhas estratégias é o “DummyDBContext“, ele se torna responsável por garantir que a estratégias será executada.
Vamos ver a implementação desta classe a seguir.
[csharp]/*
* Classe apenas para fins de exemplo e aprendizado não considera nenhum tipo de
validação ou regra ou se utiliza de algum framework.
*
* Iremos assumir que nossos objetos serão salvos por um contexto de base de dados.
* Cada linguagem ou ORM irá implementar de forma diferente.
*
*/
using Solid.Certo.Persistence.Contract;
using Solid.Certo.Strategy.Contract;
using System;
using System.Collections.Generic;
namespace Solid.Certo.Repository
{
/*
* Nesta classe a inversão acontece claramente, uma vez que a minha classe de acesso os
dados não sabe quem vai validar e nem como vai validar os dados que serão enviados ao banco de dados.
Ela simplesmente usa a interface de estratégia para achar a estratégia correto e realizar as validações necessárias.
*
*/
public static class DummyDBContext<TEntity>
where TEntity : class, IEntity
{
#region Private Methods
private static void Adicionar(TEntity entity)
{
Console.WriteLine($"A entidade {typeof(TEntity).Name} foi salva na base de dados.");
}
private static void Atualizar(TEntity entity)
{
Console.WriteLine($"A entidade {typeof(TEntity).Name} foi atualizada na base de dados.");
}
private static void EhValida(TEntity entity)
{
foreach(string item in RecuperarEstrategia(entity).Validar())
{
throw new Exception(item);
}
}
public static IEnumerable<TEntity> Selecionar(long? id = null)
{
List<TEntity> result = new List<TEntity>();
foreach(TEntity item in result)
{
yield return item;
}
yield break;
}
#endregion Public Methods
}
}[/csharp]
Podemos perceber que a classe “DummyDBContext” não precisa conhecer as estratégias, nem as validações, nem o serviço de e-mail.
Isso é feito pela classe de serviço “DependencyService.StrategyDependencyManager” no método “RecuperarEstrategiaPersistencia“.
Cada framework ou contêiner de injeção irá fazer de uma forma, aqui estamos fazendo desta forma, pois não é o nosso foco usar qualquer framework de injeção de depêndencia.
Ok. Mas como devo implementar a estratégia para que ela faça a validação da minha classe antes de salvar.
Primeiro temos que implementar nosso serviço de injeção de depêndencia. Veja abaixo a classe “StrategyDependencyManager”.
Esta classe vai registrar as nossas estratégias no método “RegistrarEstrategias”.
[csharp]/*
* Classe apenas para fins de exemplo e aprendizado não considera nenhum tipo de validação ou regra ou se utiliza de algum framework.
*/
using Solid.Certo.Persistence.Contract;
using Solid.Certo.Strategy;
using Solid.Certo.Strategy.Contract;
using System.Collections.Generic;
namespace Solid.Certo.DependencyService
{
public static class StrategyDependencyManager
{
#region Private Fields
private static Dictionary<string, IStrategy> estrategias = new Dictionary<string, IStrategy>();
#endregion Private Fields
#region Public Methods
public static IPersistenceStrategy<TEntity>
RecuperarEstrategiaPersistencia<TEntity>(TEntity entity)
where TEntity : IEntity
{
return (IPersistenceStrategy<TEntity>)estrategias[entity.GetType().Name];
}
public static void RegistrarEstrategias()
{
//aqui eu mantive fixo, mas o correto é manter de forma dinâmica. (desacoplado)
//Estude como o seu framework de injeção de dependência trabalha para que se mantenha esta parte do código dinâmica (desacoplada)
//Não foi intuito deste exemplo de código mostrar nenhum contêiner ou framework de injeção de dependência.
estrategias[typeof(Persistence.Cliente).Name] = new ClienteStrategy();
estrategias[typeof(Persistence.Venda).Name] = new VendaStrategy();
}
#endregion Public Methods
}
}[/csharp]
Abaixo vemos a implementação da classe “ClienteStrategy“, responsável por garantir que os dados de clientes sejam salvos de acordo com a regra de cada customização.
Aqui podemos usar a compilação “On the Fly“; em tempo de execução; Ou usar o “Plugin Pattern” para a definição de novas regras de negócio.
Utilizamos um serviço de e-mail para o envio de e-mail ao cliente.
Utilizamos o “Validator Pattern“, de forma simples, para verificação do CNPJ.
[csharp]/*
* Classe apenas para fins de exemplo e aprendizado não considera nenhum tipo de validação ou regra ou se utiliza de algum framework.
*/
using Solid.Certo.Persistence;
using Solid.Certo.Service;
using Solid.Certo.Strategy.Abstract;
using Solid.Certo.Validator;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Solid.Certo.Strategy
{
/*
* As classes de estratégia não conhecem a implementação do banco de dados,
apenas verificam se está tudo correto para que seja salvo em banco de dados.</pre>
<pre> *
*/
//Sim, ao invés de "Cliente", podemos definir até aqui como sendo uma interface de cliente "ICliente".
public class ClienteStrategy: PersistenceStrategyBase<Cliente>
{
#region Public Constructors
public ClienteStrategy() : base()
{
}
#endregion Public Constructors
#region Public Methods
public override void DepoisDeSalvar(Cliente entity)
{
base.DepoisDeSalvar(entity);
//Podemos também injetar o serviço de envio de emails. Desta forma esta classe não precisaria nem conhecer o EmailService.
EmailService.EnviarEmail(entity.Email);
}
public override IEnumerable<string> Validar()
{
if(String.IsNullOrWhiteSpace(Entidade.Nome))
yield return "O nome é obrigatório";
if(!Entidade.Email.Contains("@"))
yield return "O e-mail não é válido";
//Aqui, para diminuir ainda mais o acoplamento.
//Usando injeção de dependência podemos definir o nosso validador de CNPJ,
//uma vez que o mesmo implementa a interface IValidator<>
//O que poderia ser customizado para cada cliente, por exemplo.
string cnpjEhValido = new CNPJValidator().Validar(Entidade.CNPJ).FirstOrDefault();
Até o momento vimos as classes principais da nossa inversão, sentimos o gostinho da injeção de depêndencia, definimos classes estáveis e instáveis.
Foi muito utilizado o Princípio da Substituição, abusamos de interfaces e abstrações.
Apesar da escrita de código parecer mais demorada, a manutenção em um código bem escrito é muito mais simples.
Escrever testes de unidade, se torna mais simples, pois podemos atacar apenas o ponto que precisamos garantir que tudo ocorra de acordo com as regras.
Abaixo, as demais classes utilizadas no projeto. Não menos importantes que as demais acima.
Interface de validação de campos, CNPJ, CPF entre outros.
[csharp]
/*
* Classe apenas para fins de exemplo e aprendizado
* não considera nenhum tipo de validação ou regra ou se utiliza de algum framework.
*/
using System.Collections.Generic;
namespace Solid.Certo.Validator.Contract
{
public interface IValidator<TValue>
{
#region Public Methods
//Retorna todos os erros ao validar o objeto
IEnumerable<string> Validar(TValue value);
#endregion Public Methods
}
}[/csharp]
Validação do CNPJ
[csharp]
/*
* Classe apenas para fins de exemplo e aprendizado não considera
* nenhum tipo de validação ou regra ou se utiliza de algum framework.
*/
using Solid.Certo.Validator.Contract;
using System.Collections.Generic;
namespace Solid.Certo.Validator
{
public class CNPJValidator: IValidator<string>
{
#region Public Methods
public IEnumerable<string> Validar(string value)
{
if((value?.Length ?? 0) != 14)
yield return "CNPJ não é válido.";
yield break;
}
#endregion Public Methods
}
}[/csharp]
Definição da inerface de estratégia de base
[csharp]/*
* Classe apenas para fins de exemplo e aprendizado não considera nenhum tipo de validação ou regra ou se utiliza de algum framework.
*/
using System.Collections.Generic;
namespace Solid.Certo.Strategy.Contract
{
public interface IStrategy
{
#region Public Methods
IEnumerable<string> Validar();
#endregion Public Methods
}
}[/csharp]
Definição de estrategia de persistência.
[csharp]/*
* Classe apenas para fins de exemplo e aprendizado não considera nenhum tipo de validação ou regra ou se utiliza de algum framework.
*/
using Solid.Certo.Persistence.Contract;
namespace Solid.Certo.Strategy.Contract
{
public interface IPersistenceStrategy<TEntity>: IStrategy
where TEntity : IEntity
{
#region Public Properties
TEntity Entidade { get; set; }
void DepoisDeSalvar(TEntity entity);
void AntesDeSalvar(TEntity entity);
#endregion Public Properties
}
}[/csharp]
Abstração para a estratégia de base.
[csharp]using Solid.Certo.Strategy.Contract;
using System.Collections.Generic;
namespace Solid.Certo.Strategy.Abstract
{
public abstract class StrategyBase: IStrategy
{
#region Public Methods
public abstract IEnumerable<string> Validar();
#endregion Public Methods
}
}[/csharp]
Abstração para definição da estratégia de peristência.
[csharp]/*
* Classe apenas para fins de exemplo e aprendizado não considera nenhum tipo de validação ou regra ou se utiliza de algum framework.
*/
using Solid.Certo.Persistence.Contract;
using Solid.Certo.Strategy.Contract;
namespace Solid.Certo.Strategy.Abstract
{
public abstract class PersistenceStrategyBase<TEntity>: StrategyBase, IPersistenceStrategy<TEntity>
where TEntity : class, IEntity, new()
{
#region Protected Constructors
protected PersistenceStrategyBase()
{
}
#endregion Protected Constructors
#region Public Properties
public TEntity Entidade { get; set; }
#endregion Public Properties
#region Public Methods
public virtual void AntesDeSalvar(TEntity entity)
{
//na classe de base eu não faço nada
}
public virtual void DepoisDeSalvar(TEntity entity)
{
//na classe de base eu não faço nada
}
#endregion Public Methods
}
}[/csharp]
Conclusão
Com isso, terminamos a série de artigos sobre S.O.L.I.D. Claro, que o assunto pode se estender mais. As classes acima podem sofrer modificações dependendo do framework utilizado ou do pattern escolhido para desenvolvimento.
Como tudo em excesso faz mal. O uso excessivo de IoC (Inversão de controles) e DI (Injeção de dependência) pode levar à um código extremamente complexo e de difícil entendimento. Existem programadores que sofrem de “Patternite”, quer dizer que não pode ver um pattern que já quer inserir no desenvolvimento da aplicação.
O uso excessivo pode causar até mesmo lentidão na execução da aplicação. Para se fazer uma simples tela, tem que se desenvolver cinco camadas, dois factories, um listener/ observable.
É! Eu me permito ferir, só um pouco, uma classe, quando tenho certeza de que esta classe não irá se corromper com o tempo. (Eu nunca disse isso).
Continuando a nossa saga de artigos, vamos agora ver a letra I do acróstico que define a sigla ISPInterface Segregation Principle (Princípio de Segregação de Interface).
Definição
Este princípio é o mais simples de entender, ele quer dizer que:
Classes clientes não devem ser forçadas a depender de métodos que elas não usam
Mas eu prefiro dizer que:
As abstrações não dependem de detalhes, mas os detalhes dependem de abstrações.
Evite o uso de interfaces gordas, assim como devemos evitar o uso de classes ou objetos deuses.
A segregação de interfaces assume que suas interfaces devem ser coesas, quando uma interface não é coesa, ele contêm métodos que não dizem respeito ao grupo de comportamento que as definem.
Vamos agora aos nossos exemplos entre certo e errado.
Ferindo o princípio
Isto está errado.
Perceba que a interface “ISaldo” define o método “Atualizar()” e o método “BaixarEstoque()“. A primeira vista está certo, pois podemos assumir que é uma interface para cálculo de saldo de estoques.
[csharp]
/*
* Classe apenas para fins de exemplo e aprendizado não considera nenhum tipo de validação ou regra ou se utiliza de algum framework.
*/
namespace Solid.Errado.Contract
{
public interface ISaldo
{
#region Public Methods
void Atualizar();
void BaixarEstoque();
#endregion Public Methods
}
}
[/csharp]
A classe abaixo, de forma errada e ferindo o princípio da SRP, implementa a interface “ISaldo“, perceba que a classe de saldo é obrigada a atualizar os saldos e realizar a baixa em estoque de um produto qualquer.
[csharp]
/*
* Classe apenas para fins de exemplo e aprendizado não considera nenhum tipo de validação ou regra ou se utiliza de algum framework.
*/
using Solid.Errado.Contract;
using System;
namespace Solid.Errado
{
public class AtualizarSaldoDiario: ISaldo
{
#region Public Methods
public void Atualizar()
{
Console.WriteLine("O saldo DIARIO foi atualizado.");
}
public void BaixarEstoque()
{
Console.WriteLine("O estoque foi baixado em 10 itens");
Atualizar();
}
#endregion Public Methods
}
}
[/csharp]
Classe para cálculo de saldo financeiro.
[csharp]
/*
* Classe apenas para fins de exemplo e aprendizado não considera nenhum tipo de validação ou regra ou se utiliza de algum framework.
*/
using Solid.Errado.Contract;
using System;
namespace Solid.Errado
{
public class SaldoFinanceiro: ISaldo
{
#region Public Methods
public void Atualizar()
{
Console.WriteLine("O saldo financeiro foi atualizado.");
}
public void BaixarEstoque()
{
throw new Exception("OPS! Eu não sei o que fazer aqui. Não sou uma classe de saldo de estoques");
}
#endregion Public Methods
}
}
[/csharp]
Como podemos perceber, à primeira vista nossas definições de interfaces parecem corretas, pois se estamos falando de saldos, podemos baixar o estoque para calcular os saldos. Neste momento já podemos perceber que ferimos o princípio do SRP e do ISP. Apesar de eu estar falando de saldos, eu posso estar calculando saldos financeiros, que nada têm haver com saldos de estoque, e se por alguma razão eu implementar esta definição de saldos, serei obrigado a baixar estoque em uma movimentação financeira.
Então, vamos corrigir o princípio…
Corrigindo o princípio
Isto está correto.
Definição de contratos (Interfaces)
[csharp]
namespace Solid.Certo.ISP.Contract
{
public interface ISaldo
{
#region Public Methods
Continuando a nossa saga de artigos, vamos agora ver a letra L do acróstico que define a sigla LSPLiskov Substitution Principle (Princípio da Substituição de Liskov).
Definido o LSP
Não tem como falar deste princípio sem saber quem o criou, Barbara Liskov, em 1988.
A definição mais usada, e a mais simples, é:
Classes derivadas podem ser substituídas por suas classes de base.
Se q(x) é uma propriedade demonstrável dos objetos x de tipo T. Então q(y) deve ser verdadeiro para objetos y de tipo S onde S é um subtipo de T.
Portanto, a visão de “subtipo” defendida por Liskov é baseada na noção da substituição; isto é, se S é um subtipo de T, então os objetos do tipo T, em um programa, podem ser substituídos pelos objetos de tipo S sem que seja necessário alterar as propriedades deste programa.
[csharp]
/*
* Classe apenas para fins de exemplo e aprendizado não considera nenhum tipo de validação ou regra ou se utiliza de algum framework.
*/
namespace Solid.Errado.LSP.Abstract
{
public abstract class AtualizaSaldoBase
{
#region Public Properties
public double SaldoAtual { get; set; }
#endregion Public Properties
}
}
[/csharp]
Cálculo de saldo diário
[csharp]
/*
* Classe apenas para fins de exemplo e aprendizado não considera nenhum tipo de validação ou regra ou se utiliza de algum framework.
*/
using Solid.Errado.LSP.Abstract;
using System;
namespace Solid.Errado.LSP
{
public class AtualizarSaldoDiario: AtualizaSaldoBase
{
#region Public Methods
public void AtualizarDia()
{
Console.WriteLine("O saldo DIARIO foi atualizado.");
}
#endregion Public Methods
}
}
[/csharp]
Cálculo de saldo mensal
[csharp]
/*
* Classe apenas para fins de exemplo e aprendizado não considera nenhum tipo de validação ou regra ou se utiliza de algum framework.
*/
using Solid.Errado.LSP.Abstract;
using System;
namespace Solid.Errado.LSP
{
public class AtualizarSaldoMensal: AtualizaSaldoBase
{
#region Public Methods
public void AtualizarMes()
{
Console.WriteLine("O saldo MENSAL foi atualizado.");
}
#endregion Public Methods
}
}
[/csharp]
Cálculo de saldo anual
[csharp]
/*
* Classe apenas para fins de exemplo e aprendizado não considera nenhum tipo de validação ou regra ou se utiliza de algum framework.
*/
using Solid.Errado.LSP.Abstract;
using System;
namespace Solid.Errado.LSP
{
public class AtualizarSaldoAnual: AtualizaSaldoBase
{
#region Public Methods
public void AtualizarAno()
{
Console.WriteLine("O saldo ANUAL foi atualizado.");
}
#endregion Public Methods
}
}
[/csharp]
Problemas ao ferir o princípio
Primeiro, já matamos o polimorfismo. Na classe de atualização de saldos “public static class AtualizarSaldo” no método “public static void AtualizarSaldos(params AtualizaSaldoBase[] saldos)” temos que fazer “if/else” para determinar que tipo de classe iremos usar para calcular o saldo.
Na mesma classe citada acima, mesmo que utilizado a herança nas demais classes de atualização de saldo, eu não consigo o reaproveitamento do código de atualização de saldos, uma vez que eu tenho que identificar qual o tipo de classe e determinar o método que é chamado, veja no fragmento abaixo o “if/else”.
[csharp]
if(saldo is AtualizarSaldoAnual)
((AtualizarSaldoAnual)saldo).AtualizarAno();
else if(saldo is AtualizarSaldoMensal)
((AtualizarSaldoMensal)saldo).AtualizarMes();
else if(saldo is AtualizarSaldoDiario)
((AtualizarSaldoDiario)saldo).AtualizarDia();
[/csharp]
Dar manutenção neste código é ter dores de cabeça, uma vez que teremos que lembrar de alterar este método de cálculo de saldos sempre que um novo tipo de saldo for criado.
Corrigindo o princípio
Como o LSP anda de mãos dadas com o OCP, podemos perceber que as classes de saldos definidos no princípio OCP atendem 100% os requisitos deste princípio . Vamos relembrar estas classes:
Isto está correto.
Veja as classes que foram criadas:
Abstração para os saldos, desta forma eu posso herdar minha classe principal e modificar apenas o que eu preciso.
[csharp]
/*
* Classe apenas para fins de exemplo e aprendizado não considera nenhum tipo de validação ou regra ou se utiliza de algum framework.
*/
namespace Solid.Certo.Utility.Abstract
{
public abstract class AtualizaSaldoBase
{
#region Public Methods
public abstract void Atualizar();
#endregion Public Methods
}
}
[/csharp]
Classe concreta de atualização de saldos diários.
[csharp]
/*
* Classe apenas para fins de exemplo e aprendizado não considera nenhum tipo de validação ou regra ou se utiliza de algum framework.
*/
using Solid.Certo.Utility.Abstract;
using System;
namespace Solid.Certo.Utility
{
public class AtualizaSaldoDiario: AtualizaSaldoBase
{
#region Public Methods
public override void Atualizar()
{
Console.WriteLine("O saldo DIÁRIO foi atualizado.");
}
#endregion Public Methods
}
}
[/csharp]
Classe concreta de atualização de saldos mensais.
[csharp]
/*
* Classe apenas para fins de exemplo e aprendizado não considera nenhum tipo de validação ou regra ou se utiliza de algum framework.
*/
using Solid.Certo.Utility.Abstract;
using System;
namespace Solid.Certo.Utility
{
public class AtualizaSaldoMensal: AtualizaSaldoBase
{
#region Public Methods
public override void Atualizar()
{
Console.WriteLine("O saldo MENSAL foi atualizado.");
}
#endregion Public Methods
}
}
[/csharp]
Classe concreta de atualização de saldos anuais.
[csharp]
/*
* Classe apenas para fins de exemplo e aprendizado não considera nenhum tipo de validação ou regra ou se utiliza de algum framework.
*/
using Solid.Certo.Utility.Abstract;
using System;
namespace Solid.Certo.Utility
{
public class AtualizaSaldoAnual: AtualizaSaldoBase
{
#region Public Methods
public override void Atualizar()
{
Console.WriteLine("O saldo ANUAL foi atualizado.");
}
#endregion Public Methods
}
}
[/csharp]
Classe de serviço para atualização de saldos no estoque.
[csharp]
/*
* Classe apenas para fins de exemplo e aprendizado não considera nenhum tipo de validação ou regra ou se utiliza de algum framework.
*/
using Solid.Certo.Utility.Abstract;
using System.Collections.Generic;
namespace Solid.Certo.Service
{
public static class AtualizaSaldoService
{
#region Public Methods
public static void AtualizarSaldos(List<AtualizaSaldoBase> saldos)
{
foreach(AtualizaSaldoBase saldo in saldos)
{
saldo.Atualizar();
}
}
#endregion Public Methods
}
}
[/csharp]
Analisando as classes definidas acima, podemos perceber que:
As classes de saldo herdam diretamente da classe “public abstract class AtualizaSaldoBase”;
Passamos a ter acesso ao método “Atualizar()”, desta forma eu consigo chamar o método “Atualizar()” a partir da classe mais genérica (Veja: Generalização e Especialização), é utilizado o polimorfismo para evitar o uso de “if/ else”, como podemos ver no fragmento abaixo da classe de serviço de atualização de saldo.
[csharp highlight=”9″]
public static class AtualizaSaldoService
{
#region Public Methods
public static void AtualizarSaldos(List<AtualizaSaldoBase> saldos)
{
foreach(AtualizaSaldoBase saldo in saldos)
{
saldo.Atualizar();
}
}
#endregion Public Methods
}
[/csharp]
Se necessário criar um novo tipo de saldo, basta criar a classe, herdar de “AtualizaSaldoBase” e implementar a chamada onde queremos que o novo saldo seja calculado sem termos que nos preocupar em fazer “if” para identificar o tipo de saldo que iremos calcular;
A classe mais especializada pode ser facilmente convertida na classe mais genérica e atende ao princípio em questão;
Pegadinha e cuidados
Que diabos O patinho feio; história infantil; está fazendo aqui?
Ora! É simples. Se nada como um pato, tem as habilidades de um pato, come igual ao pato mas é um marreco cisne, então você tem um problema de abstração.
Vamos ao exemplo mais clássico de erro de abstração Quadrado x Retângulo. Não poderíamos falar de LSP sem comentar sobre o Quadrado x Retângulo.
[csharp]
/*
* Classe apenas para fins de exemplo e aprendizado não considera nenhum tipo de validação ou regra ou se utiliza de algum framework.
*/
namespace Solid.Errado.QuadradoRetangulo
{
public class Retangulo
{
#region Public Properties
public virtual int Altura { get; set; }
public virtual int Largura { get; set; }
#endregion Public Properties
}
}
[/csharp]
Classe do quadrado que herda de retângulo (Já falei sobre herança?)
[csharp]
/*
* Classe apenas para fins de exemplo e aprendizado não considera nenhum tipo de validação ou regra ou se utiliza de algum framework.
*/
namespace Solid.Errado.QuadradoRetangulo
{
public class Quadrado: Retangulo
{
}
}
[/csharp]
Utilitário para cálculo de área.
[csharp]
/*
* Classe apenas para fins de exemplo e aprendizado não considera nenhum tipo de validação ou regra ou se utiliza de algum framework.
*/
using Solid.Errado.QuadradoRetangulo;
namespace Solid.Errado.Utility
{
public static class CalculaArea
{
#region Public Methods
//Sim, este método poderia ser da classe retângulo.
//Mas para efeito de exemplo, vou deixar ele aqui.
public static int CalcularArea(Retangulo retangulo)
{
return retangulo.Altura * retangulo.Largura;
}
#endregion Public Methods
}
}
[/csharp]
Chamada de exemplo para ambos os casos
[csharp]
int altura;
int largura;
Console.WriteLine("\r\nInforme a altura:");
altura = Convert.ToInt32(Console.ReadLine());
Console.WriteLine("Informe a largura:");
largura = Convert.ToInt32(Console.ReadLine());
//aqui, podemos criar um quadrado normalmente.
Quadrado quadrado = new Quadrado
{
Altura = altura,
Largura = largura
};
//aqui, criamos um retângulo com base em um quadrado, atendendo ao princípio do LSP
Retangulo retanguloQuadrado = new Quadrado
{
Altura = altura,
Largura = largura
};
//Aqui criamos um retângulo normalmente.
Retangulo retangulo = new Retangulo
{
Altura = altura,
Largura = largura
};
int quadradoAreaEsperada = altura * altura;
int retanguloAreaEsperada = largura * altura;
//o método CalcularArea() espera um retângulo, perceba que podemos passar tanto um quadrado quanto um retângulo. Atendendo ao princípio LSP.
Console.WriteLine($"A área correta do retângulo é {retanguloAreaEsperada} e a calculada foi {Errado.Utility.CalculaArea.CalcularArea(retangulo)}");
Console.WriteLine($"A área correta do quadrado é {quadradoAreaEsperada} e a calculada foi {Errado.Utility.CalculaArea.CalcularArea(quadrado)}");
[/csharp]
Vamos analisar os fontes e explicar o real problema de não tomarmos cuidado no momento de definirmos nossas abstrações.
As classes definidas, a primeira vista, não ferem o princípio LSP, uma vez que podem ser herdadas, terem seus métodos sobrescritos, são reutilizáveis e eu posso criar a mais genérica pela classe mais especializada, posso utilizar a classe mais genérica como parâmetro e passar a mais especializada, como diz o princípio da substituição.
No caso, como foi mostrado, eu posso criar um retângulo a partir de um quadrado. Veja o fragmento abaixo
[csharp]
//aqui, criamos um retângulo com base em um quadrado, atendendo ao princípio do LSP
Retangulo retanguloQuadrado = new Quadrado
{
Altura = altura,
Largura = largura
};
[/csharp]
Mas isso, tem um erro. Imagina que nossos sistema é usado para calcular as esquadrias em um prédio. E neste sistema eu preciso calcular a área para pedir que se corte uma esquadria quadrada. considerando que o quadrado tem seus lados iguais, e meu sistema aceita que eu passe altura x largura diferentes, mesmo para a classe “Quadrado”, eu tenho um erro de regra de negócio e de abstração.
Se eu crio uma classe retângulo a partir de uma classe “Quadrado” a área calculada ficará errada se eu passar os valores de “Altura” e “Largura” diferentes.
[csharp]
//aqui, criamos um retângulo com base em um quadrado, atendendo ao princípio do LSP
Retangulo retanguloQuadrado = new Quadrado
{
Altura = 10,
Largura = 5
};
//aqui acontece o erro, uma vez que eu passei 10×5=50. O meu calculo de área deveria considerar Altura X Largura igual para o calculo de quadrados.
Console.WriteLine($"A área correta do quadrado é {Errado.Utility.CalculaArea.CalcularArea(retanguloQuadrado)}");
[/csharp]
Pegando um exemplo real, nos artigos anteriores temos tratado as nossas classes de saldos como saldos de estoque. Imaginem que eu queira calcular o saldo financeiro. Em sua essência o cálculo é igual um menos o outro, entradas menos saídas. Só que de um lado eu estou falando de produtos, e de outro de valores monetários, cada qual com suas regras específicas.
Para terminar
Para atender ao princípio de Liskov, temos que garantir que as nossa classes genéricas, sejam substituíveis pelas nossas classes especializadas; Veja: Generalização x Especialização; Desta forma atendemos também o OCP.
Cuidado ao usar o princípio “É UM”, e preste muita atenção que este princípio se aplica ao comportamento da classe e não aos tipos ou subtipos. Valorize o polimorfismo.
Você deve ser capaz de estender um comportamento de uma classe sem modificá-lo.
Esta declaração quer dizer que você tem que escrever suas classes de forma que elas possam ser herdadas e ter seu comportamento estendido ao invés de alterar o código existente. Para fazer isso, iremos usar o que chamamos de herança e abstração.
Para saber mais.
Caso não esteja familiarizado com os termos herança e abstração, dê uma lida no artigo sobre “Herança” e no artigo sobre “Abstração“.
Tenho certeza que suas dúvidas serão sanadas.
Problemas em ferir o princípio OCP
Reaproveitamento de código é praticamente nulo. CTRL+C/ CTRL+V, não é reaproveitamento de código;
As classes podem fazer partes de componentes espalhados pela aplicação;
O que acontece se precisarmos de uma nova implementação?
Colocamos mais um if/else if, um switch…case ?
Criamos uma nova classe e copiamos o código?
Recompilar e fazer deploy em todos os componentes impactados;
Espero que você tenha um bom caso de testes escrito para cada situação.
Certo ou errado?
Abaixo vou colocar os códigos “ERRADO” e “CERTO” e ao final, analisar cada um.
Ferindo o OCP
Isto está errado.
Primeiro, vamos criar nossa classe de venda para atender as vendas da nossa aplicação.
[csharp]/*
* Classe apenas para fins de exemplo e aprendizado não considera nenhum tipo de validação ou regra ou se utiliza de algum framework.
*/
using System;
using System.Collections.Generic;
namespace Solid.Errado
{
public class Venda
{
#region Public Properties
public int Codigo { get; set; }
public int CodigoCliente { get; set; }
public List<string> Itens { get; set; }
public double TotalVenda { get; set; }
#endregion Public Properties
#region Public Methods
public void Salvar()
{
Validar();
Console.WriteLine("A venda foi salva.");
CalcularTotal();
AtualizarSaldo();
}
private void CalcularTotal()
{
Console.WriteLine("O total da venda foi calculado.");
}
public void Validar()
{
if(CodigoCliente == 0)
throw new Exception("O código do cliente é obrigatório.");
if((Itens?.Count ?? 0) == 0)
throw new Exception("A quantidade de itens vendida não pode ser zero.");
}
public void AtualizarSaldo()
{
Utility.AtualizarSaldo.AtualizarSaldos(new AtualizarSaldoAnual(),
new AtualizarSaldoMensal(),
new AtualizarSaldoDiario());
}
#endregion Public Methods
}
}[/csharp]
Com a classe de vendas criada, precisamos atualizar os saldos de estoque, pois a venda faz uma saída em nossos produtos.
Esta classe é um utilitário de atualização de saldos de estoque, e recebe objetos para realizar os cálculos de saldo de estoque.
[csharp]
/*
* Classe apenas para fins de exemplo e aprendizado não considera nenhum tipo de validação ou regra ou se utiliza de algum framework.
*/
namespace Solid.Errado.Utility
{
public static class AtualizarSaldo
{
#region Public Methods
E abaixo, as classes de atualização de saldo diário, mensal e anual.
Saldo Diário
[csharp]
/*
* Classe apenas para fins de exemplo e aprendizado não considera nenhum tipo de validação ou regra ou se utiliza de algum framework.
*/
using System;
namespace Solid.Errado
{
public class AtualizarSaldoDiario
{
#region Public Methods
public void Atualizar()
{
Console.WriteLine("O saldo DIARIO foi atualizado.");
}
#endregion Public Methods
}
}
[/csharp]
Saldo Mensal
[csharp]
/*
* Classe apenas para fins de exemplo e aprendizado não considera nenhum tipo de validação ou regra ou se utiliza de algum framework.
*/
using System;
namespace Solid.Errado
{
public class AtualizarSaldoMensal
{
#region Public Methods
public void Atualizar()
{
Console.WriteLine("O saldo MENSAL foi atualizado.");
}
#endregion Public Methods
}
}
[/csharp]
Saldo Anual
[csharp]
/*
* Classe apenas para fins de exemplo e aprendizado não considera nenhum tipo de validação ou regra ou se utiliza de algum framework.
*/
using System;
namespace Solid.Errado
{
public class AtualizarSaldoAnual
{
#region Public Methods
public void Atualizar()
{
Console.WriteLine("O saldo ANUAL foi atualizado.");
}
#endregion Public Methods
}
}
[/csharp]
Logo de cara já percebemos que a classe de vendas fere completamente o Princípio da Responsabilidade Única, ela se “valida”, ele atualiza os saldos de estoque entre outros problemas que você já é capaz de identificar com a leitura do primeiro artigo.
Mas o que realmente nos interessa aqui, são as classes de saldos. Estas ferem completamente o princípio OCP (Aberto/ Fechado).
Mesmo que eu faça a herança destas classes, por exemplo, para criar um saldo quinzenal, eu não consigo substituir o comportamento do método atualizar;
O utilitário de atualização de saldos, classe “Utility.AtualizarSaldo“, tem que conhecer as classes de saldos e fazer um “if” para identificar qual é o tipo de saldo que deverá ser atualizado;
Se eu precisar implementar mais um tipo de saldo, tenho que lembrar deste “if” e criar um método com o mesmo nome “Atualizar” para manter um mínimo de padrão;
Este utilitário aceita tipos “object“, isso quer dizer que se eu passar uma classe errada para o meu método, posso ter um erro em tempo de execução, ou não ter meus saldos de estoque calculados corretamente;
Percebemos que não houve reaproveitamento de código entre as classes de saldos;
Corrigindo o princípio
Neste momento, iremos corrigir o princípio e ao final analisar as modificações que foram realizadas para atender ao princípio do OCP.
Isto está correto.
Veja as classes que foram criadas:
Classe de venda para realização das vendas em nosso estabelecimento.
[csharp]
/*
* Classe apenas para fins de exemplo e aprendizado não considera nenhum tipo de validação ou regra ou se utiliza de algum framework.
*/
using Solid.Certo.Persistence.Abstract;
using Solid.Certo.Repository;
using System.Collections.Generic;
namespace Solid.Certo.Persistence
{
public class Venda: EntityBase
{
#region Public Properties
public int CodigoCliente { get; set; }
public List<string> Itens { get; set; }
public double TotalVenda { get; set; }
#endregion Public Properties
#region Public Methods
public override void Salvar()
{
DBContext<Venda>.Salvar(this);
}
#endregion Public Methods
}
}
[/csharp]
Abstração para os saldos, desta forma eu posso herdar minha classe principal e modificar apenas o que eu preciso.
[csharp]
/*
* Classe apenas para fins de exemplo e aprendizado não considera nenhum tipo de validação ou regra ou se utiliza de algum framework.
*/
namespace Solid.Certo.Utility.Abstract
{
public abstract class AtualizaSaldoBase
{
#region Public Methods
public abstract void Atualizar();
#endregion Public Methods
}
}
[/csharp]
Classe concreta de atualização de saldos diários.
[csharp]
/*
* Classe apenas para fins de exemplo e aprendizado não considera nenhum tipo de validação ou regra ou se utiliza de algum framework.
*/
using Solid.Certo.Utility.Abstract;
using System;
namespace Solid.Certo.Utility
{
public class AtualizaSaldoDiario: AtualizaSaldoBase
{
#region Public Methods
public override void Atualizar()
{
Console.WriteLine("O saldo DIÁRIO foi atualizado.");
}
#endregion Public Methods
}
}
[/csharp]
Classe concreta de atualização de saldos mensais.
[csharp]
/*
* Classe apenas para fins de exemplo e aprendizado não considera nenhum tipo de validação ou regra ou se utiliza de algum framework.
*/
using Solid.Certo.Utility.Abstract;
using System;
namespace Solid.Certo.Utility
{
public class AtualizaSaldoMensal: AtualizaSaldoBase
{
#region Public Methods
public override void Atualizar()
{
Console.WriteLine("O saldo MENSAL foi atualizado.");
}
#endregion Public Methods
}
}
[/csharp]
Classe concreta de atualização de saldos anuais.
[csharp]
/*
* Classe apenas para fins de exemplo e aprendizado não considera nenhum tipo de validação ou regra ou se utiliza de algum framework.
*/
using Solid.Certo.Utility.Abstract;
using System;
namespace Solid.Certo.Utility
{
public class AtualizaSaldoAnual: AtualizaSaldoBase
{
#region Public Methods
public override void Atualizar()
{
Console.WriteLine("O saldo ANUAL foi atualizado.");
}
#endregion Public Methods
}
}
[/csharp]
Estratégia de venda, responsável pela nossa regra de negócio quando a venda for realizada.
[csharp]
/*
* Classe apenas para fins de exemplo e aprendizado não considera nenhum tipo de validação ou regra ou se utiliza de algum framework.
*/
using Solid.Certo.Persistence;
using Solid.Certo.Strategy.Abstract;
using Solid.Certo.Utility.Abstract;
using System;
using System.Collections.Generic;
namespace Solid.Certo.Strategy
{
public class VendaStrategy: PersistenceStrategyBase<Venda>
{
#region Public Methods
public override void AntesDeSalvar(Venda entity)
{
base.AntesDeSalvar(entity);
//vamos simular uma cálculo de total de venda
Entidade.TotalVenda = 30;
Console.WriteLine("O total da venda foi calculado");
}
public override IEnumerable<string> Validar()
{
if(Entidade.CodigoCliente == 0)
yield return "O código do cliente é obrigatório.";
if((Entidade.Itens?.Count ?? 0) == 0)
yield return "A quantidade de itens vendida não pode ser zero.";
yield break;
}
public override void DepoisDeSalvar(Venda entity)
{
base.DepoisDeSalvar(entity);
Service.AtualizaSaldoService.AtualizarSaldos(new List<AtualizaSaldoBase>
{
new Utility.AtualizaSaldoDiario(),
new Utility.AtualizaSaldoMensal(),
new Utility.AtualizaSaldoAnual()
});
}
#endregion Public Methods
}
}
[/csharp]
Classe de serviço para atualização de saldos no estoque.
[csharp]
/*
* Classe apenas para fins de exemplo e aprendizado não considera nenhum tipo de validação ou regra ou se utiliza de algum framework.
*/
using Solid.Certo.Utility.Abstract;
using System.Collections.Generic;
namespace Solid.Certo.Service
{
public static class AtualizaSaldoService
{
#region Public Methods
public static void AtualizarSaldos(List<AtualizaSaldoBase> saldos)
{
foreach(AtualizaSaldoBase saldo in saldos)
{
saldo.Atualizar();
}
}
#endregion Public Methods
}
}
[/csharp]
Como podemos ver, até a classe de vendas foi corrigida. Não fere mais o primeiro princípio.
Como o foco foram as classes de saldos, iremos listar as vantagens desta abordagem.
Fizemos com que as classes filhas, implementassem o método “Atualizar” para que cada uma saiba como fazer a atualização de saldos em estoque;
As estratégias indicam o momento em que a atualização dos saldos devem ocorrer;
Se for necessário a inclusão do saldo quinzenal, basta apenas criar a classe e inserir na estratégia o momento em que se deseja atualizar o saldo;
Não é necessário fazer o uso de “if/else if” ou “switch…case” no momento da atualização de saldos, isso foi resolvido com o Polimorfismo;
Perceba que ao utilizar a abstração para chamar a atualização de saldos, nos não precisamos mais nos preocupar em fazer “if/else if” e nem se preocupar se o tipo de objeto passado é do tipo saldo. Uma vez que a herança; (clique para saber mais sobre Herança) me permite fazer isto de forma transparente e garante que o tipo passado é do tipo saldo;
E se necessário fazer uma customização para determinado cliente, podemos usar e abusar do padrão “Strategy“.
Dando início a nossa saga de artigos, vamos iniciar com a primeira letra do acróstico, S que define a sigla SRPSingle Responsibility Principle (Princípio da Responsabilidade Única).
Definição
Isto quer dizer que nossas classes devem ter apenas uma, e somente uma, razão para ser modificada. Se a classe possuir mais de um motivo para ser modificada, a mesma não é coesa e isso já fere os princípios da POO (Programação Orientada à Objetos).
Problemas ao ferir o primeiro princípio.
Dificuldade de compreensão, logo dificuldade de manutenção na classe;
Não permite reuso;
Alto acoplamento, a classe depende de conhecer outras e outras e mais outras classes para poder funcionar, o que dificulta a manutenção ao alterar as classes de dependência;
Imaginem um canivete suíço, para quem não sabe, é este ai abaixo, o pai dos canivetes suíços.
Canivete criado por John S. Holler por volta de 1880, na Alemanha e possui 100 funções.
As tampinhas só foram inventadas em 1891. Fonte: Google
Este canivete, se fosse uma aplicação, fere completamente o primeiro princípio. O da responsabilidade única. Em nossas aplicações cada objeto deve fazer apenas o que ele se propõe a fazer.
A ostra ostreia, o gato gateia, o vento venta
(Prof. Clóvis Barros Filho)
Nossas classes devem seguir esta linha de pensamento, se é uma classe de serialização, ela serializa, seja em banco de dados, arquivos, memória. Não importa, ela apenas serializa.
Se nossa classe é de validação, ela apenas valida.
Certo ou Errado?
Nos exemplos abaixo eu sempre colocarei o certo e o errado para compararmos as classes e as soluções propostas. Tentarei usar problemas e soluções do dia-a-dia.
Antes de começarmos vamos ver a imagem abaixo:
Como podemos ver, é muito mais simples criar da forma errada. Mas te trará problemas de manutenção de código, de reusabilidade de código, seu código será macarrônico e anêmico. Alto Acoplamento e baixa coesão. No momento da manutenção deste código, erros simples se tornam complexos de serem resolvidos.
Como ferir o princípio.
Veja o exemplo de código abaixo:
Isto está errado.
[csharp]/*
* Classe apenas para fins de exemplo e aprendizado não considera nenhum tipo de validação ou regra ou se utiliza de algum framework.
*/
using System;
using System.Collections.Generic;
namespace Solid.Errado
{
public class Cliente
{
#region Private Methods
public string CNPJ { get; set; }
public int Codigo { get; set; }
public DateTime DataCadastro { get; set; }
public string Email { get; set; }
public string Endereco { get; set; }
public string Nome { get; set; }
#endregion Public Properties
#region Public Methods
public void EnviarEmail()
{
Console.WriteLine($"O e-mail foi enviado para o endereço {Email}");
}
public void Excluir(long id)
{
Console.WriteLine($"O cliente {Nome} foi excluído.");
}
public void Salvar()
{
if(Codigo != 0)
Atualizar();
else
Adicionar();
EnviarEmail();
}
public IList<Cliente> Selecionar(long? id = null)
{
List<Cliente> result = new List<Cliente>();
return result;
}
public bool Validar()
{
if(String.IsNullOrWhiteSpace(Nome))
throw new Exception("O nome é obrigatório");
if(!Email.Contains("@"))
throw new Exception("O e-mail não é válido");
if(CNPJ.Length != 14)
throw new Exception("O CNPJ não é válido");
return true;
}
#endregion Public Methods
}
}[/csharp]
A classe cliente escrita acima, tem diversos pontos que ferem o princípio da Responsabilidade Única:
Ela é uma classe que se faz sua própria serialização, como pode ver na linha 17 do método “Adicionar()”, linha 23 do método “Atualizar()” e assim nos demais métodos. Logo esta classe tem que conhecer a implementação de sua forma de serialização, seja banco de dados, XML, JSON, memória;
Esta classe é responsável por enviar um e-mail ao cliente, assim que o mesmo é cadastrado na aplicação. Imagina se você criar outras classes de pessoas, como Fornecedor, Usuário, Fabricante, e se fizer necessário o envio de e-mail ao se criar cada pessoa. Cada classe terá sua implementação do método “EnviarEmail()” e você terá que dar manutenção em cada método das classes de pessoas, se por um acaso o servidor for modificado, por exemplo;
Esta classe é responsável por validar seus próprios dados. Em uma customização para clientes, esta abordagem seria trabalhosa, uma vez que cada cliente pode ter sua forma de validação de dados;
Como resolver o problema.
Isto está correto.
[csharp]/*
* Classe apenas para fins de exemplo e aprendizado não considera nenhum tipo de validação ou regra ou se utiliza de algum framework.
*/
using Solid.Certo.Persistence.Abstract;
using Solid.Certo.Repository;
using System;
namespace Solid.Certo.Persistence
{
public class Cliente: EntityBase
{
#region Public Properties
public string CNPJ { get; set; }
public DateTime DataCadastro { get; set; }
public string Email { get; set; }
public string Endereco { get; set; }
public string Nome { get; set; }
#endregion Public Properties
#region Public Methods
public void Excluir()
{
DummyDBContext<Cliente>.Excluir(Codigo);
}
public void Salvar()
{
DummyDBContext<Cliente>.Salvar(this);
}
#endregion Public Methods
}
}[/csharp]
Como podem ver:
Esta classe não se “Valida”;
Para resolver este problema utilizamos o padrão de projeto “Strategy“. Que são:
StrategyDependencyManager: Responsável por fazer a injeção de dependência das estratégias na classe cliente;
IStrategy: Define o contrato para qualquer estratégia que não seja de persistência;
IPersistenceStrategy: Define o contrato para as estratégias de persistência dos dados;
StrategyBase: Abstração de estratégias para os tipos mais básicos;
PersistenceStrategyBase: Abstração para estratégias de persistências;
ClienteStrategy: Estratégia concreta para validação de clientes;
IValidator: Contrato para classes de validação de tipos;
CNPJValidator: Classe concreta para validação do tipo CNPJ;
Estes tópicos, Visitor Pattern e Validators serão discutidos em outros artigos. Aqui iremos falar apenas do conceito S.O.L.I.D.
Ela não conhece a implementação de serialização, no caso, podemos perceber que seria em um banco de dados;
Este problema foi resolvido usando a classe DummyDBContext, mas temos que tomar cuidado com esta abordagem, pois depende do framework de acesso à dados que você vai utilizar, o mais comum, para desenvolvimento .NET é o Entity Framework. Este link do StackOverflow; em Português; tem uma boa explicação sobre com prós e contras;
Ela não manda e-mails;
Quem decide se o e-mail deve ou não ser enviado é a estratégia de cada cliente, para isso a estratégia utiliza-se do serviço de e-mail para o envio do e-mail para o cliente.
EmailService: Classe de serviço de e-mail responsável por enviar e-mail e receber e-mail em toda a aplicação;
Como podemos ver, esta classe apenas faz o que lhe diz respeito. Manter os dados dos clientes.
Não se preocupe com os nomes de classes e conceitos explicados acima como solução para a forma correta. Durante o desenvolvimento dos demais artigos, irei explicar todas as classes e os princípios aplicados.
Use este post como um índice para os demais artigos, basta clicar nos links para ler os artigos.
Olá galera…
Estou aqui mais uma vez com uma série de artigos, desta vez irei falar sobre S.O.L.I.D. Sim, eu sei que existem centenas de artigos sobre o assunto por ai, mas minha intenção aqui é aprender um pouco mais sobre o assunto com alguns exemplos em C#.
Afinal, o que é S.O.L.I.D.?
O padrão S.O.L.I.D. são cinco princípios que nos ajudam a desenvolver aplicações mais robustas e de fácil manutenção. Vamos entender o que é cada um dos princípios nos tópicos abaixo.
Estes princípios foram definidos pelo Robert. C. Martin, ou se preferirem, Uncle Bob, e datam do início do ano 2000.
S.O.L.I.D. é um acróstico para os seguintes acrônimos :
Para ler em detalhes cada uma das siglas, basta clicar na sigla ou em sua definição.
Dependa de uma abstração e não de uma implementação. Depend on abstractions, not on concretions.
Atenção
Durante o desenvolvimento dos exemplos, não vou usar nenhum ORM, Framework ou qualquer outra ferramenta de apoio para não confundir a nossa cabeça, irei apenas aplicar os conceitos de S.O.L.I.D nos exemplos.
Nos artigos anteriores vimos o uso do delegates em eventos, para criar chamadas estilo callback, como invocar métodos e como criar seus delegates.
E continuando nosso assunto sobre delegates, iremos falar sobre três importantes delegates dentro do C#, Func, Action e Predicate.
Func & Action
Nos artigos anteriores criamos uma calculadora usando delegate, aproveitando aquele exemplo, iremos reescrever nossa calculadora usando os delegates Func<> e Action<>.
A única diferença entre estes dois delegates é o retorno, enquanto no Func<> você é obrigado a informar o tipo de retorno esperado, o Action não retorna nada, é um void.
Os dois delegates possuem ao todo 16 parâmetros de entrada cada um e o Func<> mais um de retorno.
Eu nunca precisei usar mais que 3, se começar a usar muitos parâmetros, reveja seu código.
Chega de papo e mãos à obra
Abaixo o código completo da calculadora usando estes dois delegates.
[csharp]using System;
namespace FuncEAction
{
internal class Program
{
private static void Main(string[] args)
{
Console.Title = "Calculadora delegate";
//Criando o ponteiro para o delegate, iniciamos com null, pois não sabemos qual operação iremos realizar.
Func<double, double, double> handler = null;
//Ação que será executada
Action action = null;
//de acordo com a operação utilizada, iremos istanciar o nosso delegate e executar o método associado
switch(operacao)
{
case "+":
handler = Somar;
break;
case "-":
handler = Subtrair;
break;
case "*":
handler = Multiplicar;
break;
case "/":
handler = Dividir;
break;
case "s":
action = Exit;
break;
case "c":
action = Clear;
break;
default:
Console.WriteLine("Operação não implementada.");
continue;
}
if(action != null)
{
//invocar a ação do mal
action();
action = null;
continue;
}
double x = LerNumero("primeiro");
double y = LerNumero("segundo");
//Executa o método associado
Console.WriteLine($"\r\nO resultado da operação {x:N2} {operacao} {y:N2} é {handler(x, y):N2}");
private static void DrawMenu()
{
Console.WriteLine("Informe a operação desejada:");
Console.WriteLine("(+) para Somar.");
Console.WriteLine("(-) para Subtrair.");
Console.WriteLine("(/) para Dividir.");
Console.WriteLine("(*) para Multiplicar.");
Console.WriteLine("(c) para Limpar o log.");
Console.WriteLine("(s) para Sair.");
}
#endregion Métodos de ação
}
}[/csharp]
Predicate
O tipo Predicate é um delegate especial que possui apenas um parâmetro de entrada e retorna um booleano.
Deve ser utilizado quando precisamos informar uma condição para que a ação seja executada. O nosso predicado, pode executar quantas situações forem necessárias para satisfazer a condição.
No código abaixo, iremos ver um exemplo de uso do Predicate e do Action por classes do C#.
[csharp] internal class Program
{
private static void Main(string[] args)
{
//Array com o nome dos países
string[] countries ={
"Afeganistão","África do Sul","Akrotiri","Albânia","Alemanha","Andorra","Angola","Anguila","Antártida","Antígua e Barbuda","Antilhas","Arábia Saudita","Arctic Ocean","Argélia","Argentina","Arménia","Aruba","Ashmore andCartier Islands","Atlantic Ocean","Austrália","Áustria","Azerbaijão","Baamas","Bangladeche","Barbados","Barém","Bélgica","Belize","Benim","Bermudas","Bielorrússia","Birmânia","Bolívia","Bósnia e Herzegovina","Botsuana","Brasil","Brunei","Bulgária","Burquina Faso","Burúndi","Butão","Cabo Verde","Camarões","Camboja","Canadá","Catar","Cazaquistão","Chade","Chile","China","Chipre","ClippertonIsland","Colômbia","Comores","Congo-Brazzaville","Congo-Kinshasa","Coral Sea Islands","Coreia do Norte","Coreia doSul","Costa do Marfim","Costa Rica","Croácia","Cuba","Dhekelia","Dinamarca","Domínica","Egipto","EmiratosÁrabes Unidos","Equador","Eritreia","Eslováquia","Eslovénia","Espanha","Estados Unidos","Estónia","Etiópia","Faroé","Fiji","Filipinas","Finlândia","França","Gabão","Gâmbia","Gana","GazaStrip","Geórgia","Geórgia do Sul e Sandwich do Sul","Gibraltar","Granada","Grécia","Gronelândia","Guame","Guatemala","Guernsey","Guiana","Guiné","Guiné Equatorial","Guiné-Bissau","Haiti","Honduras","Hong Kong","Hungria","Iêmen","Ilha Bouvet","Ilha do Natal","Ilha Norfolk","Ilhas Caimão","Ilhas Cook","Ilhas dos Cocos","Ilhas Falkland","Ilhas Heard e McDonald","Ilhas Marshall","Ilhas Salomão","Ilhas Turcas e Caicos","Ilhas Virgens Americanas","Ilhas VirgensBritânicas","Índia","Indian Ocean","Indonésia","Irão","Iraque","Irlanda","Islândia","Israel","Itália","Jamaica","Jan Mayen","Japão","Jersey","Jibuti","Jordânia","Kuwait","Laos","Lesoto","Letónia","Líbano","Libéria","Líbia","Listenstaine","Lituânia","Luxemburgo","Macau","Macedónia","Madagáscar","Malásia","Malávi","Maldivas","Mali","Malta","Man, Isle of","Marianas do Norte","Marrocos","Maurícia","Mauritânia","Mayotte","México","Micronésia","Moçambique","Moldávia","Mónaco","Mongólia","Monserrate","Montenegro","Mundo","Namíbia","Nauru","Navassa Island","Nepal","Nicarágua","Níger","Nigéria","Niue","Noruega","Nova Caledónia","Nova Zelândia","Omã","Pacific Ocean","Países Baixos","Palau","Panamá","Papua-Nova Guiné","Paquistão","Paracel Islands","Paraguai","Peru","Pitcairn","Polinésia Francesa","Polónia","Porto Rico","Portugal","Quénia","Quirguizistão","Quiribáti","Reino Unido","República Centro-Africana","República Checa","República Dominicana","Roménia","Ruanda","Rússia","Salvador","Samoa","Samoa Americana","Santa Helena","Santa Lúcia","São Cristóvão e Neves","São Marinho","São Pedro e Miquelon","São Tomé e Príncipe","São Vicente e Granadinas","Sara Ocidental","Seicheles","Senegal","Serra Leoa","Sérvia","Singapura","Síria","Somália","Southern Ocean","Spratly Islands","Sri Lanca","Suazilândia","Sudão","Suécia","Suíça","Suriname","Svalbard e Jan Mayen","Tailândia","Taiwan","Tajiquistão","Tanzânia","Território Britânico do Oceano Índico","Territórios Austrais Franceses","Timor Leste","Togo","Tokelau","Tonga","Trindade e Tobago","Tunísia","Turquemenistão","Turquia","Tuvalu","Ucrânia","Uganda","União Europeia","Uruguai","Usbequistão","Vanuatu","Vaticano","Venezuela","Vietname","Wake Island","Wallis e Futuna","West Bank","Zâmbia","Zimbabué"
};
//encontrar um último da letra M
Console.WriteLine("\r\nÚltimo com a letra M\r\n");
Predicate<string> findLastM = (item) =>
{
return item.StartsWith("M", StringComparison.InvariantCultureIgnoreCase);
};
Console.WriteLine($"O último com a letra M é {Array.FindLast(countries, findLastM)}");
//imprimir todos que tem a letra k
Console.WriteLine("\r\nTodos que tem a letra K\r\n");
Predicate<string> hasK = (item) =>
{
return item.Contains("k");
};
Continuando nossa série sobre delegates, desta vez iremos falar sobre eventos, e quase impossível não entrar no assunto quando estamos falando de delegates.
Neste artigo iremos aprender a escrever os eventos de forma complexa e simples, com o uso da palavra reservada “event“.
Eventos, de forma geral, é um meio da classe, ou componente enviar mensagens aos seus ouvintes quando algo interessante acontece. Os eventos em C#, utilizam-se de delegates para a troca de mensagens.
Como por exemplo:
O Clique do botão;
O movimento do cursor;
O pressionamento de uma tecla;
Uma ação ocorrida;
Entenda eventos como mudanças ou ações que precisam ser notificadas para seus ouvintes, listenners, através de seus objetos, senders.
Além do uso tradicional, iremos ver neste artigo:
Eventos em herança;
Eventos em interfaces;
Criando eventos
Os eventos em C# são definidos em três passos:
Primeiro, criamos o delegate que irá tratar os métodos que serão chamados pelo evento. Opcionalmente podemos usar o delegate “System.EventHandler“
Segundo, criamos a classe de argumentos para o evento;
Terceiro, criamos o evento com o uso da palavra reservada “event“.
Esta declaração segue o padrão de criação de eventos em C#.
Abaixo, iremos ver um exemplo simples da criação de eventos, completo e com o delegate genérico “System.EventHandler“. Preste atenção aos comentários no código.
[csharp]using System;
//Criando eventos seguindo o padrão de criação definido pelo C#
namespace DelegatesEEventos
{
//declara o tratador do evento com dois parâmetros
//Esta declaração pode ser ocultada com o uso do delegate genérico System.EventHandler&lt;&gt;
//como iremos ver mais abaixo
public delegate void CustomHandler(object sender, CustomEventArgs args);
//Cria a classe de argumentos do evento
public class CustomEventArgs: EventArgs
{
public string Mensagem { get; private set; }
public CustomEventArgs(string mensagem)
{
Mensagem = mensagem;
}
}
//define uma classe com eventos
public class ClasseQualquerComEvento
{
//Define um evento na classe, usando o nosso delegate
public event CustomHandler OnFazendoAlgo;
//Podemos declarar um evento genérico usando o System.EventHandler,
//neste caso não é necessário criar o nosso delegate
public event EventHandler&lt;CustomEventArgs&gt; OnFazendoEventoGenerico;
}
}[/csharp]
Invocando um evento
Para chamar um evento em C#, basta chamar o mesmo como se fosse um método, passando os parâmetros pedido pelo delegate, seja ele genérico ou não.
[csharp] public void FacaAlgo()
{
//Diferente do VB, em C# temos que verificar se o evento não é nulo antes de chamar.
if(OnFazendoAlgo != null)
//Basta chamar o evento como se fosse um método da classe, passando os parâmetros definidos no delegate
OnFazendoAlgo(this, new CustomEventArgs("Olá, estou fazendo algo pelo handler customizado"));
if(OnFazendoEventoGenerico != null)
//O mesmo acontece com o delegate genérico
OnFazendoEventoGenerico(this, new CustomEventArgs("Estou fazendo algo pelo delegate genérico"));
}[/csharp]
O que acontece por baixo?
O que acontece por baixo?
Aprendemos a criar o evento usando a palavra reservada “event” para diminuir o tamanho de código digitado. Mas, o que realmente acontece por baixo dos panos?
Quando o compilador encontra a palavra reservada “event“, é criado dois novos métodos pelo C# um “add” e um “remove” seguidos de “_NomeDoEvento“, mas nós também podemos usar esta técnica com as palavras reservadas “add” e “remove“. Veja como ficaria a nossa declaração de eventos no exemplo abaixo:
[csharp]using System;
//Criando eventos seguindo o padrão de criação definido pelo C#
namespace DelegatesEEventos.AddRemove
{
//declara o tratador do evento com dois parâmetros
//Esta declaração pode ser ocultada com o uso do delegate genérico System.EventHandler<>
//como iremos ver mais abaixo
public delegate void CustomHandler(object sender, CustomEventArgs args);
//Cria a classe de argumentos do evento
public class CustomEventArgs: EventArgs
{
public string Mensagem { get; private set; }
public CustomEventArgs(string mensagem)
{
Mensagem = mensagem;
}
}
//define uma classe com eventos
public class ClasseQualquerComEvento
{
//Definimos a instancia do nosso delegate para tratar os eventos
private CustomHandler customHandler;
//Define um evento na classe, usando o nosso delegate
public event CustomHandler OnFazendoAlgo
{
add
{
lock (customHandler)
{
//adiciona um evento à lista de chamadas
customHandler += value;
}
}
remove
{
lock (customHandler)
{
//remove um evento da lista de chamadas
customHandler -= value;
}
}
}
public void FacaAlgo()
{
//Diferente do VB, em C# temos que verificar se o evento não é nulo antes de chamar.
if(customHandler != null)
//Basta chamar o evento como se fosse um método da classe, passando os parâmetros definidos no delegate
customHandler.Invoke(this, new CustomEventArgs("Olá, estou fazendo algo pelo add/remove"));
}
}
}[/csharp]
Esta abordagem foi apenas para mostrar que podemos criar os métodos de acesso ao evento. Na maioria dos casos a forma tradicional atende.
Eventos em Herança
Vou mostrar duas formas de realizar a chamada de eventos por herança em C#, depois iremos comentar as duas formas e mostrar qual a melhor.
Quando criamos classes, temos que ter em mente que os eventos são um tipo especial de delegates que só podem ser invocados dentro da própria classe, as classes filhas não podem executar estes eventos diretamente.
Mas como posso fazer com que as minhas classes filhas utilizem os eventos da classe pai?
Para ilustrar a chamada, vamos analisar o código abaixo.
[csharp]using System;
//Criando eventos seguindo o padrão de criação definido pelo C#
namespace DelegatesEEventos.Heranca
{
//declara o tratador do evento com dois parâmetros
//Esta declaração pode ser ocultada com o uso do delegate genérico System.EventHandler<>
//como iremos ver mais abaixo
public delegate void CustomHandler(object sender, CustomEventArgs args);
//Cria a classe de argumentos do evento
public class CustomEventArgs: EventArgs
{
public string Mensagem { get; private set; }
public CustomEventArgs(string mensagem)
{
Mensagem = mensagem;
}
}
//define uma classe com eventos
public class ClasseQualquerComEvento
{
//Define um evento na classe, usando o nosso delegate
//Veja, marcamos como "virtual" o nosso evento
public virtual event CustomHandler OnFazendoAlgo;
public void FacaAlgo()
{
//Diferente do VB, em C# temos que verificar se o evento não é nulo antes de chamar.
if(OnFazendoAlgo != null)
//Basta chamar o evento como se fosse um método da classe, passando os parâmetros definidos no delegate
OnFazendoAlgo(this, new CustomEventArgs("Olá, estou fazendo algo pelo método FacaAlgo"));
}
}
//classe herdada
public class ClasseQualquerComEventoHeranca: ClasseQualquerComEvento
{
//Sobrescrevemos o evento que queremos chamar, pois como já foi dito, só podemos
//chamar eventos dentro da classe
public override event CustomHandler OnFazendoAlgo;
//método qualquer que utiliza o evento
public void FacaAlgoFilho()
{
//Como sobrescrevemos o evento, podemos chamá-lo aqui
if(OnFazendoAlgo != null)
OnFazendoAlgo(this, new CustomEventArgs("Olá, estou fazendo algo pelo método filho"));
}
}
}[/csharp]
Isto está errado.
A abordagem acima está errada, pois ao chamar o método “FacaAlgo()” da classe pai o evento não será lançado.
[csharp]ClasseQualquerComEventoHeranca classeQualquerComEventoHeranca = new ClasseQualquerComEventoHeranca();
classeQualquerComEventoHeranca.OnFazendoAlgo += (s, a) =>
{
Console.WriteLine(a.Mensagem);
};
classeQualquerComEventoHeranca.FacaAlgo();
classeQualquerComEventoHeranca.FacaAlgoFilho();
/* Output:
Olá, estou fazendo algo pelo método filho
*/
[/csharp]
Não marque seus eventos como virtuais, o compilador do C# não irá definir a chamada corretamente, e ao sobrescrever o evento apenas a classe filha atende ao delegate. A ação definida no evento para o método pai não será invocada.
O padrão correto
Agora, vamos utilizar o padrão correto para este tipo de caso.
Crie seu evento como já foi mostrado;
Crie um método protegido e virtual para notificar que o evento deverá ocorrer;
[csharp]using System;
//Criando eventos seguindo o padrão de criação definido pelo C#
namespace DelegatesEEventos.Heranca
{
//declara o tratador do evento com dois parâmetros
//Esta declaração pode ser ocultada com o uso do delegate genérico System.EventHandler<>
//como iremos ver mais abaixo
public delegate void CustomHandler(object sender, CustomEventArgs args);
//Cria a classe de argumentos do evento
public class CustomEventArgs: EventArgs
{
public string Mensagem { get; private set; }
public CustomEventArgs(string mensagem)
{
Mensagem = mensagem;
}
}
//define uma classe com eventos
public class ClasseQualquerComEvento
{
//Define um evento na classe, usando o nosso delegate
//Veja, marcamos como "virtual" o nosso evento
public event CustomHandler OnFazendoAlgo;
public void FacaAlgo()
{
FazendoAlgoNotify(new CustomEventArgs("Olá, estou fazendo algo pelo método FacaAlgo"));
}
//Define o método de notificãção do evento OnFazendoAlgo
protected virtual void FazendoAlgoNotify(CustomEventArgs args)
{
//Diferente do VB, em C# temos que verificar se o evento não é nulo antes de chamar.
if(OnFazendoAlgo != null)
//Basta chamar o evento como se fosse um método da classe, passando os parâmetros definidos no delegate
OnFazendoAlgo(this, args);
}
}
//classe herdada
public class ClasseQualquerComEventoHeranca: ClasseQualquerComEvento
{
//método qualquer que utiliza o evento
public void FacaAlgoFilho()
{
//Como sobrescrevemos o evento, podemos chamá-lo aqui
FazendoAlgoNotify(new CustomEventArgs("Olá, estou fazendo algo pelo método filho"));
}
//se precisar, pode sobrescrever o método
protected override void FazendoAlgoNotify(CustomEventArgs args)
{
//Se necessário. Faça alguma ação antes aqui
//depois chama o método de base
base.FazendoAlgoNotify(args);
}
}
}[/csharp]
Isto está correto.
Como podem ver, ao chamar os dois métodos, o evento será executado duas vezes, pois cada método quer notificar que fez algo.
[csharp]ClasseQualquerComEventoHeranca classeQualquerComEventoHeranca = new ClasseQualquerComEventoHeranca();
classeQualquerComEventoHeranca.OnFazendoAlgo += (s, a) =>
{
Console.WriteLine(a.Mensagem);
};
classeQualquerComEventoHeranca.FacaAlgo();
classeQualquerComEventoHeranca.FacaAlgoFilho();
/* Output:
Olá, estou fazendo algo pelo método FacaAlgo
Olá, estou fazendo algo pelo método filho
*/[/csharp]
Eventos em interfaces
Interfaces permitem definir a assinatura de seus eventos. Para isso, declare-os normalmente na interface.
Veja o exemplo:
[csharp]using System;
namespace DelegatesEEventos.Interfaces
{
//declara o tratador do evento com dois parâmetros
public delegate void CustomHandler(object sender, CustomEventArgs args);
//Cria a classe de argumentos do evento
public class CustomEventArgs: EventArgs
{
public string Mensagem { get; private set; }
public CustomEventArgs(string mensagem)
{
Mensagem = mensagem;
}
}
//Define a interface
public interface IImplementotoEvento
{
//Define o evento na interface
event CustomHandler OnFazendoAlgo;
//método faça algo
void FacaAlgo();
}
//define uma classe com eventos, e implementa a interface IImplentoEvento
public class ClasseQualquerComEventoEInterface: IImplementotoEvento
{
//Define o evento implementado pela interface
public event CustomHandler OnFazendoAlgo;
//define o método implementado na interface
public void FacaAlgo()
{
//Diferente do VB, em C# temos que verificar se o evento não é nulo antes de chamar.
if(OnFazendoAlgo != null)
//Basta chamar o evento como se fosse um método da classe, passando os parâmetros definidos no delegate
OnFazendoAlgo(this, new CustomEventArgs("Olá, estou fazendo algo."));
}
}
}[/csharp]
Para chamar, podemos criar uma instância da interface ou a classe diretamente, mas para o exemplo, iremos criar uma instância do tipo da interface.
[csharp]IImplementotoEvento implentoEvento = new ClasseQualquerComEventoEInterface();
implentoEvento.OnFazendoAlgo += (s, a) =>
{
Console.WriteLine(a.Mensagem);
};
implentoEvento.FacaAlgo();
Neste tópico estarei falando sobre delegates, sobre sua utilização em eventos, em funções, em ações, em lambda expressions, sobre multicasting e a construção de expressões lambdas e Linq.
Iremos começar da base, para entendermos o que realmente é um delegate e como ele pode nos ser útil. Iremos começar com exemplo simples e rápidos, ao modo que cada artigo vai sendo escrito, iremos nos aprofundando mais neste tópico, até chegarmos ao ponto de escrevermos nossas próprias expressões lambdas.
Índice
Vou apontar o índice para os demais artigos conforme forem sendo escritos
Podemos dizer que um delegate é uma função de retorno (callback function), um ponteiro para uma função que será chamada em outro momento. Com esta peculiaridade, podemos tratar o clique de um botão, o movimento do mouse, a seleção de um menu. O que chamamos de eventos.
Um delegate suporta chamadas assíncronas, síncronas e multicasting.
Iremos falar disso em outros tópicos
Neste artigo, iremos analisar sobre os diversos aspectos de um delegate, como criá-lo e como instanciá-lo e como utilizar o delegate.
Compreendendo o Delegate
Diferente dos ponteiros programados usando C que são apontamentos direto para um endereço de memória, os delegates em C# são mais seguros e fortemente tipados, mais orientado a objetos, um delegate é um type-safe
Type-Safe é a capacidade do C# de garantir que um objeto tem suas características preservadas. Está também ligado à forma de acesso em memória deste objeto.
Exemplo:
Em C, podemos declarar algo assim, de forma insegura;
Você declara um inteiro, faz um cast (conversão) para um tipo char e consegue acessar fora dos limites do int.
[c]int i = 10;
char *s = (char*)i;
print(*(s+10));
[/c]
Em c# isso não é possível, a não ser que você use a palavra chave “unsafe (inseguro)”
[csharp]
int i = 10;
char *s >> //isto não será permitido fora de um contexto unsafe (inseguro)
[/csharp]
Um delegate mantem 3 informações importantes:
O endereço do método para o qual deverá ser chamadoOs parâmetros, se existiremO retorno, se existir
Delegates podem apontar para métodos estáticos ou de instância.
Quando um objeto do tipo delegate é criado ele já vem adaptado para ser chamado de forma síncrona ou assíncrona, isto é muito útil quando iremos programar pensando em multi-tarefas, sem ter que criar um objeto de tarefas (Thread).
Iremos falar sobre este assunto,Multi-Tasking, em outro momento.
Definindo um delegate
A definição do delegate em C# segue esta regra: modificador de acessodelegate retorno (ou void)nome (parâmetros (opcionais))
[csharp]public delegate int FacaAlgoERetorne(int x, int y);
public delegate void SoFacaAlgo(int x, int y);
public delegate void FacaAlgo();[/csharp]
Quando o compilador C# interpretar o nosso objeto delegate, ele automaticamente irá criar uma classe que herda de “System.MulticastDelegate” esta classe terá os métodos que iremos utilizar para executar nosso delegate quando necessário. Iremos nos ater apenas ao método “Invoke()”
Chega de papo e mãos à obra
Mãos à obra
Para reforçar o entendimento, iremos escrever uma calculadora simples, que fará as quatro operações básicas. Abaixo o código completo em C#
[csharp]using System;
namespace CalculadoraDelegate
{
internal class Program
{
//========================================================
//Criação do nosso delegate
private delegate double Operacao(double x, double y);
//========================================================
/* veja que nosso delegate tem:
* Modificador de acesso (private)
* palavra reservada (delegate)
* Tipo de retorno (double)
* Parâmetros (x e y)
* Isso determina como deverá ser a assinatura do método que será utilizado para o nosso delegate.
* Caso o método não tenha esta assinatura o erro "Error CS0123 No overload for ‘Nome do Método’ matches delegate ‘namespace.NomeDoDelegate’" será lançado
*/
private static void Main(string[] args)
{
Console.Title = "Calculador delegate (CTRL + C para sair)";
double x = LerNumero();
string operacao = LerOperacao();
double y = LerNumero();
//========================================================
//Criando o ponteiro para o delegate, iniciamos com null, pois não sabemos qual operação iremos realizar.
Operacao handler = null;
//========================================================
//de acordo com a operação utilizada, iremos istanciar o nosso delegate e executar o método associado
switch(operacao)
{
case "+":
handler = new Operacao(Somar);
break;
case "-":
handler = new Operacao(Subtrair);
break;
case "*":
handler = new Operacao(Multiplicar);
break;
case "/":
handler = new Operacao(Dividir);
break;
default:
Console.WriteLine("Operação não implementada.");
break;
}
//Executa o método associado
Console.WriteLine($"\r\nO resultado da operação {x:N2}{operacao}{y:N2} é {handler.Invoke(x, y):N2}");
do
{
Console.Clear();
Console.WriteLine("Informe a operação desejada:");
Console.WriteLine("(+) para Somar.");
Console.WriteLine("(-) para Subtrair.");
Console.WriteLine("(/) para Dividir.");
Console.WriteLine("(*) para Multiplicar.");
result = Console.ReadKey().KeyChar.ToString();
if(!validas.Contains(result))
{
Console.WriteLine("\r\nInforme uma operação válida.");
result = "";
Console.ReadKey();
}
} while(String.IsNullOrWhiteSpace(result));
return result;
}
private static double LerNumero()
{
double result = 0;
do
{
Console.Clear();
Console.WriteLine("Informe um número.");
string numero = Console.ReadLine();
if(!double.TryParse(numero, out result))
{
Console.WriteLine("\r\nInforme um número válido.");
Console.ReadKey();
}
} while(result == 0);
Conforma a operação é definida o delegate é instanciado passando os parâmetros para o mesmo:
[csharp]//de acordo com a operação utilizada, iremos istanciar o nosso delegate e executar o método associado
switch(operacao)
{
case "+":
handler = new Operacao(Somar);
break;
case "-":
handler = new Operacao(Subtrair);
break;
case "*":
handler = new Operacao(Multiplicar);
break;
case "/":
handler = new Operacao(Dividir);
break;
default:
Console.WriteLine("Operação não implementada.");
break;
}[/csharp]
E ao final, é invocado e recuperado o resultado da operação.
[csharp]//Executa o método associado
Console.WriteLine($"\r\nO resultado da operação {x:N2}{operacao}{y:N2} é {handler.Invoke(x, y):N2}");[/csharp]
Ao desenvolvermos um componente utilizando como base um componente já criado no Ext.NET, ganhamos de brinde toda a reutilização deste, por exemplo: – Nosso componente já pode ser utilizado em WebForm, em ASP MVC, com a engine Razor, no código markup, etc.
Outra vantagem desta abordagem é que quando estudamos o código do Ext.NET, você poderá entender como eles criam seus componentes com base nos componentes Ext JS,e não necessariamente você precisa ser um conhecedor profundo do Ext JS.
Como vantagem também, podemos citar que você pode desenvolver seu componente em qualquer linguagem .NET que você tenha familiaridade e não precisa aprender a ser um ninja em javascript, pois todo o javascript será gerado para o lado cliente.
Neste tópico, iremos focar na criação do componente do lado servidor, na utilização do lado cliente, e como criar seus próprios métodos que podem ser acessados do lado cliente e lado servidor.
Antes de começarmos…
Vamos definir nossas abstrações e nossos objetos de validação do componente. Como neste artigo eu irei criar dois componentes para inserção de texto. Iremos criar uma abstração para tipos texto e uma classe de resultado da validação.
Classe ValidateResult
[csharp]/// <summary>
/// Classe que determina o resultado da validação feita e retorna.
/// </summary>
public sealed class ValidateResult
{
#region locais
bool valid = true;
string invalidMessage = "";
#endregion
#region propriedades
/// <summary>
/// Se true, o valor inserido no componente é valido.
/// </summary>
public bool Valid
{
get { return valid; }
private set { valid = value; }
}
/// <summary>
/// Se não for válido é necessário informar uma mensagem de erro
/// </summary>
public string InvalidMessage
{
get { return invalidMessage; }
private set { invalidMessage = value; }
}
#endregion
#region Construtores
/// <summary>
/// Inicia o resultado da validação
/// </summary>
/// <param name="valid">true/false para válido ou não.</param>
/// <param name="invalidMessage">Se "valid" for verdadeiro, é obrigatório informar a mensagem.</param>
/// <exception cref="ArgumentException">Se "valid" for verdadeiro, e o parâmetro "invalidMessage"
/// não for informado. Esta exceção será lançada.</exception>
public ValidateResult(bool valid, string invalidMessage = "")
{
this.valid = valid;
if(!valid && String.IsNullOrEmpty(invalidMessage))
throw new ArgumentException("Se \"valid\" for definido como false. É obrigatório informar uma mensagem de erro em \"invalidMessage\".", "invalidMessage");
/// <summary>
/// Faz a validação do componente e retorna true se o texto do componente for válido, caso contrário false
/// </summary>
/// <param name="value">valor que deverá ser validado</param>
/// <returns></returns>
protected abstract ValidateResult Validate(string value);
#endregion
}[/csharp]
No código em destaque, vejam que eu fiz uma herança da classe “Ext.Net.TextField” e da interface “IValidator”.
Desta forma eu já herdo todas as funcionalidades de um componente do tipo “TextField” e apenas ajusto para a minha necessidade. a herança da interface “IValidator” é apenas para a implementação dos métodos de validação.
Ok! Definimos a nossa abstração. Vamos agora criar o nosso componente.
Meu primeiro componente
Vamos criar um componente bem simples. Um campo de CEP, que irá fazer uma validação e será utilizado em uma página.
CEPField
[csharp]public class CEPField: TextFieldBase
{
#region Contrutores
public CEPField()
: base()
{
DirectEvents.Blur.Event += new ComponentDirectEvent.DirectEventHandler(Blur_Event);
AllowBlank = false;
this.IsRemoteValidation = true;
this.RemoteValidation.Validation += new RemoteValidationDirectEvent.RemoteValidationEventHandler(RemoteValidation_Validation);
FieldLabel = "CEP";
}
#endregion
public override int MaxLength
{
get { return 9; }
set { base.MaxLength = 9; }
}
#endregion
#region IValidator Members
protected override ValidateResult Validate(string value)
{
if(AllowBlank && String.IsNullOrEmpty(value))
return new ValidateResult(true);
value = Utilities.OnlyNumbers(value);
if(String.IsNullOrEmpty(value))
return new ValidateResult(false, "O CEP não pode ser vazio ou não foi digitado corretamente.");
//permite somente números
if(value.Count(w => !Char.IsNumber(w)) > 0)
return new ValidateResult(false, "O CEP não é válido.");
//para ser válido tem que ter 8 caracteres
if(value.Length != 8)
return new ValidateResult(false, "O CEP não é válido.");
return new ValidateResult(true);
}
#endregion
}[/csharp]
Hm! OK! Eu criei meu componente. Mas como eu faço para chamá-lo em uma página asp? É possível utilizar a mesma marcação que temos para o Ext.NET, utilizando a tag “<ext>“?
R: Sim, é possível, com um pequeno ajuste no nosso arquivo “web.config”.
Dentro da tag “” devemos inserir a linha em destaque, logo abaixo
[xml highlight=”5″]
<pages>
<controls>
<!–Indica que toda página criada irá ter a diretiva "ext" como padrão para ser usada como marcação na criação.–>
<add assembly="Ext.Net" namespace="Ext.Net" tagPrefix="ext"/>
<add assembly="Ext.Net.Tutorial" namespace="Ext.Net.Tutorial.ComponentModel" tagPrefix="tut"/>
</controls>
</pages>
[/xml]
Esta linha indica que um assembly deverá ser adicionado; “add assembly“; para todas as páginas criadas;
Os controles que estão no “namespace” …
Deverão ser associados com o prefixo;”tagPrefix“; “tut“;
Pronto. Agora podemos utilizar o nosso componente apenas escrevendo a tag
[html]<tut:CEPField ID="CEPField1" runat="server" FieldLabel="Informe seu CEP">
</tut:CEPField>[/html]
O próximo componente que será criado é um “CurrencyField“, um campo para inserir valores monetários. Vamos ver o código do mesmo, e explicar logo em seguida.
[csharp highlight=”31,32-97”]public class CurrencyField: TextFieldBase
{
#region Construtores
public CurrencyField()
: base()
{
//————————————————————————-
// Define que só podemos digitar números de 0-9 e a vírgula.
//————————————————————————-
MaskRe = "/[0-9\\,]/";
//————————————————————————-
// Define o método javascript que irá ser chamado ao receber o foco
//————————————————————————-
Listeners.Focus.Handler = "this.setRawValue(this.getNumber());this.selectText();";
//————————————————————————-
// Define o método que será chamado ao perder o foco
//————————————————————————-
Listeners.Blur.Handler = "this.setValue(this._getValue(this.getValue()));";
//————————————————————————-
// Define que o valor deverá ser alinhado a direta do componente
//————————————————————————-
FieldStyle = "text-align:right;";
}
#endregion
#region Overrides
public override ConfigItemCollection CustomConfig
{
get
{
ConfigItemCollection result = base.CustomConfig;
string curSbl = "R$";
#region _getValue
//————————————————————————-
// Criando o método _getValue para o lado cliente.
//————————————————————————-
ConfigItem it = new ConfigItem("_getValue", @"function(v){
return MyApp.Util.Format.currency(this.cleanValue(v), 2,’" + curSbl + @"’);
}
");
result.Add(it);
#endregion
#region getNumber
//————————————————————————-
// Criando o método getNumber para o lado cliente.
//————————————————————————-
it = new ConfigItem("getNumber", @"function(){
if(this.number === undefined || this.number == 0)
this.number = MyApp.Convert.toNumber(this.getValue());
return this.number;
}
");
result.Add(it);
#endregion
#region setValue
//————————————————————————-
// Substituindo o método setValue, com a nossa definição
//————————————————————————-
it = new ConfigItem("setValue", @"function(v){
v = this._getValue(v);
this.setRawValue(v);
this.number = MyApp.Convert.toNumber(this.getValue());
this.text = v;
}
");
result.Add(it);
#endregion
#region cleanValue
//————————————————————————-
// Criando o método cleanValue para o lado cliente.
//————————————————————————-
it = new ConfigItem("cleanValue", @"function(v){
if(v){
return MyApp.Util.Format.number(v, 2);
}else
return 0;
}
");
result.Add(it);
#endregion
return result;
}
}
public override void SetValue(object value)
{
this.SetValue(value.ToString());
base.SetValue(Number);
}
protected void SetValue(string text)
{
Text = text;
Number = Convert.ToDouble(text);
}
#endregion
#region Propriedades
public virtual double Number
{
get { return Utilities.Convert.ToDouble(base.Text); }
set { base.Text = value.ToString(); }
}
#endregion
#region IValidator members
protected override ValidateResult Validate(string value)
{
double d = Utilities.Convert.ToDouble(value);
if(!AllowBlank && d == 0)
return new ValidateResult(false, "É obrigatório informar o valor monetário.");
return new ValidateResult(true);
}
#endregion
}[/csharp]
Neste código foi apresentado um meio de você criar seus próprios métodos javascript e associá-los ao seus componentes.
Para isto, utilizamos o que chamamos de “CustomConfig“, veja nas linhas em destaque.
O objeto “Ext.Net.ConfigItem“, permite que você defina métodos e propriedades para seus controles. Feito isso, você pode acessar em um código javascript.
[js]var v = App.MyComponent._getValue();[/js]
Entendendo o objeto “Ext.Net.ConfigItem“
Este objeto tem a finalidade de vincular seus métodos javascript aos componentes Ext.NET, isso se torna útil quando estamos criando nossos próprios componentes.
Possui 3 propriedades importantes:
Name: Define o nome do método ou propriedade que será exposto;
Value: Define o valor que será representado pelo sue método ou propriedade;
Mode: O mode pode ser:
Auto: O Ext.NET tentará detectar o tipo e se não for capaz, A propriedade “Value” será encapsulada como uma string, ou como um número, de acordo com seu tipo;
Raw: A propriedade “Value” será exposta como está;
Value: Valor da propriedade ou código do método;
Por convenção, nomeie seus métodos utilizando “camelCase“. Se você escrever o nome do método em “UpperCase” o Ext.NET irá converter para “camelCase“, mantendo a convenção de nomes definida pelo javascript.