Estou fazendo o código de envio de e-mails do meu site. Comecei naturalmente no modelo, fiz o model binder, e o controlador. Muito simples. Vou mostrar o resultado aqui. Mas o código tem um problema, tente descobrir, no final eu conto.
Montei uma classe de mensagens:
[ModelBinder(typeof(EmailMessageBinder))] public class EmailMessage { public string FromEmailAddress { get; set; } public string Message { get; set; } public string To { get; set; } public string FromName { get; set; } private string _subject; public string Subject { get { return _subject; } set { _subject = "[GIGGIO.NET] " + value; } } }
Nada de mais, certo? Ela tem um atributo “ModelBinder” que aponta para o seu ModelBinder, mas fora isso é uma classe super simples.
Fiz o binder, que está logo abaixo. Aqui cabe algumas explicações. Estou herdando de DefaultModelBinder. Essa classe é bem legal, porque ela já busca todas as propriedades nas coleções de valores (url, posted form, etc) e faz a associação sozinha de acordo com os valores. No meu caso, eu tenho um formulário html com ids iguaizinhos às propriedades da classe EmailMessage, ou seja, “FromEmailAddress”, “Message”, etc… O DefaultModelBinder entende que esses valores devem ser associados às propriedades de mesmo nome e faz isso sozinho (dá-lhe reflection!). Se fosse só isso eu nem precisava herdar, usava o DefaulModelBinder como binder padrão e acabou. Só que eu precisava de algumas coisas a mais. Por exemplo, eu quero setar a propriedade “To”, que não vem no post, para o meu e-mail. Além disso, quero validar se todas as propriedades obrigatórias estão lá, o que o DefaulModelBinder não faz sozinho, e quero validar o e-mail de destino.
Para setar a propriedade “To” eu fiz sobrescrevi o método OnModelUpdating, peguei o modelo já criado pelo DefaulModelBinder, e setei a propriedade. Segundo a documentação do Release Candidate do ASP.Net MVC, este é o método onde preparamos o modelo (alguma inicialização), então achei apropriado fazer as coisas aí.
Para a validação, sobrescrevi o método SetProperty, e lá eu validei os itens obrigatórios e o e-mail, com uma bela regex. Usei o método AddModelError para incluir os erros, e a chave era a própria propriedade.
Pronto, acabou. Tudo bem simples, menos de 50 linha de código.
public class EmailMessageBinder : DefaultModelBinder { private string regexEmail = @"regexgigante"; protected override bool OnModelUpdating(ControllerContext controllerContext, ModelBindingContext bindingContext) { var message = (EmailMessage)bindingContext.Model; message.To = "[email protected]"; return base.OnModelUpdating(controllerContext, bindingContext); } protected override void SetProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, object value) { if ("FromEmailAddress_Message_Subject_To_FromName" .Contains(propertyDescriptor.Name)) { if (string.IsNullOrEmpty((string)value)) bindingContext.ModelState.AddModelError(propertyDescriptor.Name, "Campo(s) obrigatório(s) não preenchido(s)."); else base.SetProperty(controllerContext, bindingContext, propertyDescriptor, value); } if (propertyDescriptor.Name == "FromEmailAddress") { var fromEmailAddress = (string)value; var isValidOriginEmailAddress = Regex.Match(fromEmailAddress, regexEmail).Success; if (!isValidOriginEmailAddress) bindingContext.ModelState.AddModelError(propertyDescriptor.Name, "Endereço de origem inválido."); else { var message = (EmailMessage)bindingContext.Model; message.FromEmailAddress = fromEmailAddress; } } else { base.SetProperty(controllerContext, bindingContext, propertyDescriptor, value); } } }
No controller é tudo limpo e claro:
[HandleError] [AcceptVerbs(HttpVerbs.Post)] public ActionResult EnviarMensagem([Bind(Prefix="")] Models.EmailMessage message) { if (message != null && this.ModelState.IsValid) { this.EnviarMensagemViaSMTP(message); return RedirectToAction("MensagemEnviadaComSucesso"); } else return View("Index"); }
Ele verifica se a mensagem existe, e se é válida. Tudo estando ok ele envia a mensagem e redireciona o usuário à ação de mensagem enviada. Notem que não faço um “return View(“Index”)” para evitar que o usuário atualize a página e acabe enviado o e-mail novamente. Com um redirect, ele recebe a página de retorno através de um GET, não de um POST, e num refresh nada acontece. Esse padrão é conhecido como “Post/Redirect/Get” (PRG).
Se há algo errado eu retorno a mesma View, e os erros são exibidos com chamadas a “Html.ValidationMessage”.
Simples e fácil, não é?
Já pegaram o problema? Em um cenário simples isso funciona perfeitamente, e o código também funciona. Não há erros de compilação ou bugs.
Só que o que eu acabei de demonstrar aqui é um modelo anêmico, onde a entidade existe em uma classe, e o comportamento está em outro lugar. A classe de mensagens (meu Modelo) deveria ser capaz de criar seus próprios validadores e retorná-los de uma forma que a interface gráfica (a View) entenda. Meu Binder deveria apenas construir tudo, e depois perguntar à entidade: você é válida?
Vou refatorar depois posto aqui o resultado.
Giovanni Bassi
Arquiteto e desenvolvedor, agilista, escalador, provocador. É fundador e CSA da Lambda3. Programa porque gosta. Acredita que pessoas autogerenciadas funcionam melhor e por acreditar que heterarquia é mais eficiente que hierarquia. Foi reconhecido Microsoft MVP há mais de dez anos, dos mais de vinte que atua no mercado. Já palestrou sobre .NET, Rust, microsserviços, JavaScript, TypeScript, Ruby, Node.js, Frontend e Backend, Agile, etc, no Brasil, e no exterior. Liderou grupos de usuários em assuntos como arquitetura de software, Docker, e .NET.