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 = "meuemail@meudominio.net";
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.
cb4c16d1-8226-4f91-92e3-ef9cd54f03cb|0|.0