Cada um no seu quadrado no ASP.NET MVC

Como nos 2 últimos posts eu falei sobre dúvidas que eu tinha recebido, vou continuar na mesma linha, só que agora busquei a dúvida nesta thread do grupo .NetArchitects, entre outras (muitas) coisas que foram discutidas foi levantado a dúvida sobre a responsabilidade da View,  na dúvida o Emmanuel propõe um cenário que está exposto abaixo:

Se você tem que exibir transações de conta corrente, por exemplo, débito e crédito, mas quando o saldo fica negativo você quer colocar a linha em vermelho, é uma regra de apresentação. Mas onde estaria esse código? Acho que não estaria no domínio por que não diz respeito a ele... Seria no Controller?

Esta é uma pergunta mais comum do que se imagina, já me perguntaram diversas vezes sobre a responsabilidade de cada letra do MVC, e é por isso que eu coloquei como título deste post como “Cada um no seu quadrado”.

A resposta simples e direta para a pergunta acima é que essa regra fica na View, mas vamos elaborar um pouco mais, por que não no Controller? Simples, por que assim ele estaria invadindo o quadrado da View, vou exemplificar o meu ponto de vista:

No cenário proposto pelo Emmanuel, o Controller enviaria os dados através da ViewData para a View e ela por sua vez verificaria se o Saldo é negativo, caso fosse exibiria ele em vermelho, vamos ver um pouco de código.

Vamos começar pelo modelo, para representar o cenário criei 3 classes, a Conta que contem os dados da conta e as transações, a Transação que representa cada transação da conta e o Repositório que para o exemplo irá preencher os objetos com dados randômicos.

Conta

public class Conta
{
    public int Numero { get; set; }
    public int Agencia { get; set; }
    public IList<Transacao> Transacoes { get; set; }
    public double Saldo { get; set; }
}

Transação

public class Transacao
{
    public DateTime Data { get; set; }
    public string Descricao { get; set; }
    public double Valor { get; set; }
}

Repositório

public class ContaRepositorio
{
    public Conta get(int Agencia, int Numero) {
        //aqui faria a consulta no banco, vou fazer um
        //faz de conta para facilitar
        Conta conta = new Conta();
        conta.Agencia = Agencia;
        conta.Numero = Numero;
        conta.Transacoes = new List<Transacao>();
        Random rnd = new Random();
        for (int i = 0; i < 15; i++)
        {
            conta.Transacoes.Add(new Transacao{Data = DateTime.Now.AddDays(-i),Descricao = "Transação nº "+i, Valor= rnd.Next(0,358)});
        }
        conta.Saldo = rnd.Next(-1245, 1245);
        return conta;
    }
}

Vamos ver agora o Controller, criei uma única action que mandará através da ViewData os dados da Conta, neste ponto é que surge a dúvida, posso colocar a inteligência de trocar a cor aqui? Bom, tecnicamente pode, poderia criar um objeto intermediário que tem sido chamado de ViewModel para armazenar esta “condição”, mas na minha opinião o Controller estará invadindo o quadrado da View. Na soluçao proposta abaixo eu passo somente os dados para a View.

Controller

public class ContaController : Controller
{
    //
    // GET: /Conta/

    public ActionResult Index()
    {
        ContaRepositorio rep = new ContaRepositorio();
        ViewData.Model = rep.get(1234, 123456);
        return View();
    }

}

Vamos agora para a View, de acordo com o valor do saldo é escolhido a classe que será aplicano na tag que contém o saldo.

View

<h2>Extato de Conta</h2>

<fieldset>
    <p>Numero: <%= Html.Encode(Model.Numero) %></p>
    <p>Agencia: <%= Html.Encode(Model.Agencia) %></p>
    <table>
        <thead>
            <tr>
                <th>Data</th>
                <th>Descrição</th>
                <th>Valor</th>
            </tr>
        </thead>
        <tbody>
            <%foreach (var item in Model.Transacoes){%>
            <tr>
                <td><%= Html.Encode(String.Format("{0:d/M/yyyy}", item.Data))%></td>
                <td><%= Html.Encode( item.Descricao)%></td>
                <td><%= Html.Encode(String.Format("{0:F}", item.Valor)) %></td>
            </tr>

            <%} %>
        </tbody>
    </table>
    <%
    string classe;
    if (Model.Saldo < 0)
    {
        classe = "negativo";
    }
    else {
        classe = "positivo";
    } %>
    <p>Saldo: <span class="<%=classe%>"><%= Html.Encode(String.Format("{0:F}", Model.Saldo)) %></span></p>
</fieldset>

No CSS adicionei as seguintes classes:

.positivo{color:Blue;}
.negativo{color:Red;}

O resultado esta na imagem abaixo:

image

Este código eu fiz com a intenção de demonstrar que a View não é burra (no bom sentido), ela pode e deve conter regras que são de interface, como no exemplo acima.

O Emmanuel quis complicar um pouco mais o cenário e colocou o seguinte:

E se esse núcleo do meu sistema é consumido por diversas interfaces? Mobile, desktop, web; essa regra da interface, não deveria estar em outro local que não a interface?

Quando tive os primeiros contatos com o ASP.NET MVC tive a mesma dúvida, mas com o tempo e várias pesquisas fiquei convencido que isto não é possível e nem interessante, digo isso por 2 motivos:

  1. O primeiro é técnico, como o MVC é um padrão de Interface Gráfica ele depende muito do ambiente, que neste caso é o ASP.NET MVC, ou seja, eu não conseguiria reutilizar o Controller do ASP.NET MVC para uma aplicação Windows Form ou uma aplicação Mobile.
  2. O segundo é sobre os diferente tipos de mídias que poderíamos utilizar para a nossa interface, cada uma dessas mídias tem suas próprias preocupações de acessibilidade e usabilidade e para lidar com essas preocupações cada uma delas deve ter autonomia.

O código deste exemplo já está disponível.

Bom, espero ter respondido a dúvida do Emmanuel e de outras pessoas. Até a próxima.


DropDownList em Cascata com ASP.NET MVC e jQuery

Mais uma vez recebi algumas dúvidas sobre ASP.NET MVC e vou compartilhar com todos a resposta que eu dei, desta vez a dúvida foi de como fazer um conjunto de DropDownList em cascata.

O que me parece recorrente na maioria das dúvidas que eu recebo em relação ao ASP.NET MVC, é que as dúvidas sempre estão no lado do cliente, no WebForms poderíamos configurar a propriedade AutoPostBack para True do DropDowList e tratar o resto no lado do servidor, no MVC é diferente, você vai ter que escrever um pouco de JavaScript para fazer isso, vamos analisar o problema e criar um cenário.

Vamos fazer uma página que terá um DropDownList com uma relação de estados e quando for selecionado o estado listaremos no outro DropDownList a relação de cidades.

Para isso quando a página for renderizada pela primeira vez já iremos trazer o DropDownList de Estados carregado e iremos carregar o de Cidades com jQuery/Ajax.

Para fazer isso vamos criar as nossas classes do modelo, na pasta “models” crie as classes abaixo:

Uf.cs:

public class Uf
{
    public Uf(string siglaUf, string nome)
    {
        this.Nome = nome;
        this.SiglaUf = siglaUf;
    }
    public string SiglaUf { get; set; }
    public string Nome { get; set; }
}

Cidade.cs

public class Cidade
{
    public Cidade(string id,string siglaUf, string nome)
    {
        this.Id = id;
        this.SiglaUf = siglaUf;
        this.Nome = nome;
    }
    public string Id { get; set; }
    public string SiglaUf { get; set; }
    public string Nome { get; set; }

}

Vamos criar agora 2 classes de repositório, para facilitar a consulta no exemplo, crie as 2 classes abaixo na pasta “models”:

UfRepository.cs

public class UfRepository
{
    public static IList<Uf> ListaUf()
    {
        List<Uf> cidades = new List<Uf>();
        cidades.Add(new Uf("SP", "São Paulo"));
        cidades.Add(new Uf("RJ", "Rio de Janeiro"));
        cidades.Add(new Uf("MG", "Minas Gerais"));
        return cidades;

    }

}

CidadeRepository.cs

public class CidadeRepository
{

    public static IList<Cidade> ListaCidade(string SiglaUf)
    {
        List<Cidade> cidades = new List<Cidade>();
        cidades.Add(new Cidade("1","RJ", "Angra dos Reis"));
        cidades.Add(new Cidade("2", "RJ", "Rio de Janeiro"));
        cidades.Add(new Cidade("3", "RJ", "Barra Mansa"));
        cidades.Add(new Cidade("4","SP", "São Paulo"));
        cidades.Add(new Cidade("5", "SP", "Sertãozinho"));
        cidades.Add(new Cidade("6", "SP", "Osasco"));
        cidades.Add(new Cidade("7", "MG", "Belo Horizonte"));
        cidades.Add(new Cidade("8", "MG", "Poços de Caldas"));
        cidades.Add(new Cidade("9", "MG", "Betim"));
        return cidades.Where(x=> x.SiglaUf==SiglaUf).ToList();

    }
}

Agora vamos modificar o arquivo Site.Master que está na pasta \Views\Shared, esta modificação é só para acrescentar a referência para o arquivo do jQuery e criar um ContentPlaceHolder para acrescentarmos o JavaScript necessário na nossa View.

Site.Master (linhas 5 e 6)

<head runat="server">
    <title><asp:ContentPlaceHolder ID="TitleContent" runat="server" /></title>
    <link href="../../Content/Site.css" rel="stylesheet" type="text/css" />
    <script src="../../Scripts/jquery-1.3.2.js" type="text/javascript"></script>
    <asp:ContentPlaceHolder ID="HeadContent" runat="server"></asp:ContentPlaceHolder>
</head>

Como falado acima precisamos que na primeira renderização da página ela carregue a lista de estados, para isso precisamos carregar esta lista para a View, vou fazer isso através da ViewData[“Ufs”], irei fazer isso na Action Index da HomeController (se ainda não foi feito, adicione um using para o namespace Models no começo da sua classe), veja no exemplo abaixo:

public ActionResult Index()
{
    ViewData["Message"] = "Welcome to ASP.NET MVC!";
    ViewData["Ufs"] = new SelectList(UfRepository.ListaUf(),"SiglaUf","Nome");
    return View();
}

Agora poderemos já começar a fazer o HTML, para isso acrescente o código na View Index da pasta Home conforme o exemplo abaixo:

<% =Html.DropDownList("Ufs","Estado") %>
<select id="Cidades">
    <option selected="selected" value="">Cidade</option>
</select>

Como vocês podem perceber na primeira linha, estou criando um DropDownList passando somente 2 parâmetros, o método tentará encontrar uma ViewData com o nome passado no primeiro parâmetro e irá tentar converter esta ViewData para um objeto SelectList, não vou me aprofundar muito para não fugir do escopo, em outro post irei falar mais detalhadamente sobre o DropDownList e SelectList.

Se você testar, verificará que o primeiro DropDownList já está carregando, mas quando mudamos o item selecionado nada acontece, para fazer isso devemos escrever um pouquinho de JavaScript, para facilitar este trabalho iremos usar o jQuery e fazer uma chamada Ajax para uma Action que retorna um JSON.

Vamos começar criando uma Action chamada ListarCidade na HomeController que irá receber no parâmetro id o estado que foi selecionado e irá retornar uma lista de cidades que são daquele estado conforme o código abaixo:

public ActionResult ListaCidade(string id) {
    return Json(CidadeRepository.ListaCidade(id));
}

Voltando na View Index na pasta Home vamos acrescentar o JavaScript necessário para fazer a chamada Ajax via jQuery, segue o código:

<asp:Content ID="Javascript" ContentPlaceHolderID="HeadContent" runat="server">
    <script type="text/javascript">
        $(document).ready(function() {
            $("#Ufs").change(function() {
                listaCidade($(this).val());
            });
        });
        //chamada ajax para a Action ListaCidade 
        //passando como parâmetro a Estado selecionado
        function listaCidade(uf) {
            
            $.getJSON('<%= Url.Action("ListaCidade") %>/' + uf, listaCidadeCallBack);
        }
        //função que irá ser chamada quando terminar
        //a chamada ajax
        function listaCidadeCallBack(json) {
            //Limpar os itens que são maiores que 0
            //Ou seja: não retirar o primeiro item
            $("#Cidades :gt(0)").remove();
            $(json).each(function() {
                //adicionando as opções de acordo com o retorno
                $("#Cidades").append("<option value='" + this.Id + "'>" + this.Nome + "</option>");
            });
        }
    
    </script>
</asp:Content>

Está pronto, é claro que não é mais fácil que o WebForms, mas o controle te traz mais responsabilidades, você vai ter que fazer (e conhecer) mais HTML e Javascript, não tem jeito.

Faça o download da solução.


Upload com ASP.NET MVC e jQuery

Já havia recebido 3 e-mails com dúvidas de como fazer upload no ASP.NET MVC e hoje me chamaram no  messenger com a mesma dúvida, então decidi escrever um post de como fazer isso de forma simples e bonita, é claro que vou usar o jQuery para isso.

No Web Form tínhamos um controle server side chamado FileUpload e no ASP.NET MVC não existe este controle, então temos que escrever o HTML necessário para fazer o formulário da forma correta, eu estou usando o plugin uploadify que faz o envio assíncrono de arquivos para o servidor, ele é muito flexível, mas irei mostrar somente o básico dele.

A primeira coisa a saber é que para enviar um arquivo para o server é necessário que o formulário contenha o parâmetro enctype configurado para "multipart/form-data", sem isso ele não vai conseguir enviar o arquivo, vejam o código HTML abaixo:

<h2>Upload</h2>
<% using (Html.BeginForm("Upload", "Home", FormMethod.Post, new { enctype = "multipart/form-data" }))
{ %>
    <fieldset>
    <label for="photo">Insira a sua foto:</label><input type="file" id="photo" name="photo" /><br />
    <input type="submit" value="Enviar" />
    </fieldset>
<%} %>

Agora  faça o download do plugin uploadify, copie os arquivos jquery.uploadify.v2.1.0.min.js e swfobject.js para a pasta Scripts, copie também os arquivos cancel.png, uploadify.css e uploadify.swf para a pasta Content.

Crie uma pasta com o nome de files na raiz da aplicação, é nesta pasta que será salvo os arquivos enviados.

Inclua o código JavaScript na MasterPage Site.Master (este é apenas um exemplo, eu prefiro colocar este javascript na View):

<script src="../../Scripts/jquery-1.3.2.js" type="text/javascript"></script>
<link href="../../Content/uploadify.css" rel="stylesheet" type="text/css" />
<script src="../../Scripts/swfobject.js" type="text/javascript"></script>
<script src="../../Scripts/jquery.uploadify.v2.1.0.min.js" type="text/javascript"></script>
<script type="text/javascript">
    $(document).ready(function() {
        $('#photo').uploadify({
            'uploader': '/content/uploadify.swf',
            'script': '<%=Url.Action("Upload","Home") %>',
            'cancelImg': '/content/cancel.png',
            'fileDesc':'Aruivos de Imagem',
            'fileExt': '*.jpg;*.gif',
            'sizeLimit':'4000000',
            auto:true
        });
    });
</script>

No código acima eu estou configurando o endereço de onde estão os arquivos necessários para o plugin, defino o caminho da action que irá receber o arquivo, o tamanho máximo do arquivo, habilitei a opção auto que automaticamente irá enviar o arquivo após a seleção do mesmo e também defino a extensão permitida para fazer o upload, isso é bem interessante porque na caixa de dialogo só irá aparecer os arquivos com a extensão que você definir.

Existem mais configurações que podem ser utilizadas, você pode conferir elas na documentação do plugin.

Agora só está faltando o código server side, eu criei uma action chamada upload no HomeController que irá receber e salvar o arquivo:

[AcceptVerbs(HttpVerbs.Post)]
public void Upload(string id)
{
    var file = this.Request.Files[0];
    string savedFileName = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "files");
    savedFileName = System.IO.Path.Combine(savedFileName, System.IO.Path.GetFileName(file.FileName));
    file.SaveAs(savedFileName);
    Response.Write("OK");
}

Com isso você já terá funcionando o jQuery uploadify com o ASP.NET MVC, vejam como fica no final:

jquery_uploadfy_in_action

Baixe a solução completa.