MVVM com KnockoutJs e ASP.NET MVC (com código fonte)

O objetivo desse artigo é demonstrar a utilização do KnockoutJs como alternativa a criação de Classes MVVM(Model-View-View-Model) no servidor, o KnockoutJs simplifica a criação de interfaces dinâmicas utilizando o padrão MVVM no cliente. O KnockoutJS é calcado em quatro principais conceitos:
  • Bindings declarativos: com ele criamos associação entre as tags html com o modelo de dados.
  • Refresh automático da interface, quando você muda sua estrutura de dados, a tela é atualizada "automagicamente"
  • Rastreamento de dependência : implicitamente criar cadeias de relacionamento entre os dados do modelo, para transformar e combiná-lo.
  • Templates: você não precisa se preocupar em criar manualmente as tags html, a partir de um template ele replica a mesma estrutura n-vezes.
Mais detalhes, documentação e exemplos recomento uma visita ao site oficial do projeto: http://knockoutjs.com/
Um projeto semelhante é o AngularJS, também vale a visita.
Nesse tutorial utilizarei o Visual Studio 2013 (vc pode baixar de graça a versão express aqui: http://www.visualstudio.com/pt-br/downloads/download-visual-studio-vs#DownloadFamilies_2), os meus print screen podem ser levemente diferentes das versões 2012 e 2010. Mas não se preocupe o KnockoutJS é independente da versão do Visual Studio que você esteja utilizando. Eu considero o conteúdo de intermediário a avançado, então se você tiver alguma dúvida fique a vontade para perguntar. Lá no final tem o link para o código fonte do projeto para o Visual Studio 2013.
Vamos por a mão na massa! A começar pela criação do projeto:



Como não vamos utilizar autenticação, que tal tirar essa carga extra?

Observe na pasta Scripts do seu projeto que não deve existe nenhuma referencia ao KnockoutJs, vamos instalar via Nuget para agilizar a história.

Projeto sem Knockout
Para isso vamos no menu Tools -> Library Package Manager -> Package Manager Console



Um console ter aberto no rodapé do Visual Studio, então digite o comando abaixo e dê enter o Nuget já instalar a biblioteca na parta Scripts, molezinha. Para saber quais outras bibliotecas podemos instalar no nosso projeto visite https://www.nuget.org/

PM> Install-Package knockoutjs


Olha como sua pasta Scripts fica depois de instalar o Knockoutjs pelo Nuget.


Agora já na pasta Contollers apague o HomeController, vá até a pasta Views e apague a pasta Home, desse jeito ficamos em nenhum. Vamos também reconfigurar a rota padrão, para isso abra classe RouteConfig que fica na pasta App_Start.


Altere a classe de forma que ela fique assim:

using System.Web.Mvc;
using System.Web.Routing;

namespace ExemploMultiploModels
{
    public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.MapRoute(
                name: "Default",
                url: "{controller}/{action}/{id}",
                defaults: new { controller = "Pessoa", action = "Index", id = UrlParameter.Optional }
            );
        }
    }
}

Agora vamos criar o controller Pessoa, ele será o mais simples possível, mas também não precisa ser vazio. Escolha a opção MVC 5 Controller with read/write actions.


Depois de criado deixe o código do seu controller desse jeito.

using ExemploMultiploModels.Models;
using System.Web.Mvc;

namespace ExemploMultiploModels.Controllers
{
    public class PessoaController : Controller
    {
        public ActionResult Index()
        {
            return View();
        }

        public ActionResult Create()
        {
            return View();
        }

        [HttpPost]
        public ActionResult Create(Pessoa pessoa)
        {
            if (ModelState.IsValid)
            {
                return RedirectToAction("Index");
            }
            else
            {
                return View(pessoa);
            }
        }
    }
}

Perceba que temos duas actions Create, a action Create simples representa o request GET, ela exibe a tela, e a outra action que está marcada com [HttpPost] captura o request POST, que normalmente é o submit do form, mas aqui vamos usar o $.ajax do jQuery.
Ah! Deve ter um erro para você por conta do parâmetro Pessoa pessoa, isso por que não criamos a classe pessoa, vamos fazer isso agora. Na verdade vamos criar três classes: Pessoa, Telefone e Endereco para nosso exemplo ficar rico. Olha o diagrama de classe.

Diagrama de classes feito no próprio Visual Studio, utilizei a versão Ultimate. Não lembro agora se a versão Express também faz.
Crie as classes dentro da pasta Models mesmo para simplificar nossa história aqui. Olha como fica o código delas.

Classe Endereco

using System;

namespace ExemploMultiploModels.Models
{
    public class Endereco
    {
        public Guid Id { get; set; }
        public String Principal { get; set; }
        public String Complemento { get; set; }
    }
}

Classe Telefone

using System;

namespace ExemploMultiploModels.Models
{
    public class Telefone
    {
        public Guid Id { get; set; }
        public int DDD { get; set; }
        public int Numero { get; set; }
    }
}

Classe Pessoa

using System;
using System.Collections.Generic;

namespace ExemploMultiploModels.Models
{
    public class Pessoa
    {
        public Guid Id { get; set; }
        public String Nome { get; set; }
        public List<Telefone> Telefones { get; set; }
        public List<Endereco> Enderecos { get; set; }
    }
}

Já temos nosso controller, nossos modelos, vamos para views. Cria uma view Index utilizando o wizard de views, nesse tutorial nem vamos utilizar ela, quem sabe no próximo. Vá no controller Pessoa, cliquei com o botão direito dentro da action Index e no menu de contexto escolha a opção Add View.


No wizard coloque essas opções e veja a mágica acontecer. Risos. Isso me poupa muito tempo por não criar a view do zero. Se você utiliza o VS2012 a classe Pessoa só aparece depois de compilar o projeto.

Veja que a pasta Pessoa foi criada dentro da pasta Views e um código que você pode alterar a vontade.
Agora vamos para a view mais legal, a view da action Create. Vamos fazer o mesmo processo de antes. O wizard criará uma view como essa para você.

View do Create

@model ExemploMultiploModels.Models.Pessoa

@{
    ViewBag.Title = "Create";
}

<h2>Create</h2>


@using (Html.BeginForm()) 
{
    @Html.AntiForgeryToken()
    
    <div class="form-horizontal">
        <h4>Pessoa</h4>
        <hr />
        @Html.ValidationSummary(true)

        <div class="form-group">
            @Html.LabelFor(model => model.Nome, new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Nome)
                @Html.ValidationMessageFor(model => model.Nome)
            </div>
        </div>

        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Create" class="btn btn-default" />
            </div>
        </div>
    </div>
}

<div>
    @Html.ActionLink("Back to List", "Index")
</div>

@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
}

Um dos recursos legais do Visual Studio 2013 é que ele já cria as views utilizando o css do Twitter Bootstrap, para mim o bootstrap (para os íntimos) é muito prático, pois pessoas como eu conseguem fazer uma interface legal sem dominar a criação de CSS na munheca. Como a View que segue abaixo, é a View do Create alterada para contemplar as classes Telefone e Endereco.


@model ExemploMultiploModels.Models.Pessoa

@{
    ViewBag.Title = "Create";
}

<h2>Create</h2>


@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()

    <div class="form-horizontal" >
        <h4>Pessoa</h4>
        <hr />
        @Html.ValidationSummary(true)

        <div class="form-group">
            <label class="control-label col-md-2">Nome</label>
            <div class="col-md-10">
                <input type="text" name="Nome" class="form-control" />
            </div>
        </div>

        <div class="row">
            <div class="col-md-6">
                <fieldset>
                    <legend></legend>
                    <h3>Endereços <button type="button" class="btn btn-primary btn-xs pull-right" >Add</button></h3>
                    <table class="table" style="margin-bottom:0">
                        <thead>
                            <tr>
                                <th width="340">Principal</th>
                                <th width="340">Complemento</th>
                                <th>#</th>
                            </tr>
                        </thead>
                    </table>
                    <div style="height: 8em; overflow-y: auto">
                        <table class="table table-striped" style="font-size: 85%">
                            <tbody >
                                <tr>
                                    <td width="340"><input class="form-control input-sm" maxlength="80"  /></td>
                                    <td width="340"><input class="form-control input-sm" maxlength="80"  /></td>

                                    <td><button type="button" class="btn btn-danger btn-xs" >Del</button></td>
                                </tr>
                            </tbody>
                        </table>
                    </div>
                </fieldset>
            </div>
            <div class="col-md-3">
                <fieldset>
                    <legend></legend>
                    <h4>Telefones <button type="button" class="btn btn-primary btn-xs pull-right" >Add</button></h4>
                    
                    <table class="table" style="margin-bottom:0">
                        <thead>
                            <tr>
                                <th width="170">DDD</th>
                                <th width="280">Numero</th>
                                <th>#</th>
                            </tr>
                        </thead>
                    </table>
                    <div style="height: 8em; overflow-y: auto">
                        <table class="table table-striped" style="font-size: 85%">
                            <tbody >
                                <tr>
                                    <td width="50"><input class="form-control input-sm" maxlength="2"  /></td>
                                    <td width="100"><input class="form-control input-sm" maxlength="9"  /></td>
                                    <td><button type="button" class="btn btn-danger btn-xs" >Del</button></td>
                                </tr>
                            </tbody>
                        </table>
                    </div>
                </fieldset>
            </div>
        </div>

        <div class="form-group">
            <input type="button" value="Create" class="btn btn-default"  />
        </div>

    </div>
}

<div>
    @Html.ActionLink("Back to List", "Index")
</div>

Percebam que é uma View muito mais legal (cof, cof). Fica meio torta, mas cada um "desintorte" depois hehehe. Mas, porém, contudo, todavia, não obstante, ATÉ AGORA NADA DE MAIS! (Achilles, você está me enrolando.)
Então tá, agora que vem a parte boa. No final dessa view vamos colocar uma section scripts.

@section scripts{
    @Scripts.Render("~/Scripts/knockout-3.2.0.js")

    <script type="text/javascript">

        function TelefoneCM() {
            this.DDD = ko.observable();
            this.Numero = ko.observable();
        }

        function EnderecoCM() {
            this.Principal = ko.observable();
            this.Complemento = ko.observable();
        }

        function PessoaCM() {
            this.Nome = ko.observable();
            
            this.Telefones = ko.observableArray([]);
            this.Enderecos = ko.observableArray([]);
        }

    </script>
}

Vejam que eu criei três estruturas de dados em Javascript, utilizei a sigla CM para Client Model. Vamos em frente. Agora é hora do ViewModel, todo o controle da tela fica nesse componente.

function PessoaViewModel() {

            var self = this;

            self.pessoa = new PessoaCM();

            self.adicionarEndereco = function () {

                self.pessoa.Enderecos.push(new EnderecoCM());
            }

            self.removerEndereco = function () {
                self.pessoa.Enderecos.remove(this);
            }

            self.adicionarTelefone = function () {

                self.pessoa.Telefones.push(new TelefoneCM());
            }

            self.removerTelefone = function () {
                self.pessoa.Telefones.remove(this);
            }

            self.Salvar = function () {
                $.ajax("@Url.Action("Create", "Pessoa")", {
                    data: ko.toJSON(self.pessoa),
                    type: "post", contentType: "application/json",
                });
            }
        }

Perceba que aqui tenho uma referencia a PessoaCM e por consequência aos arrays de TelefoneCM e EnderecoCM. Além disso, quatros funções básicas para incluir e remover itens do array e por fim a função de Salvar que serializa em JSON o "objeto" PessoaCM através da referencia self.pessoa e submete via POST ajax para action Create do controller Pessoa.
Para relacionarmos todo esse controle de com as tags HTML precisaremos fazer os binds do Knockout.

@section scripts{
    @Scripts.Render("~/Scripts/knockout-3.2.0.js")

    <script type="text/javascript">

        function TelefoneCM() {
            this.DDD = ko.observable();
            this.Numero = ko.observable();
        }

        function EnderecoCM() {
            this.Principal = ko.observable();
            this.Complemento = ko.observable();
        }

        function PessoaCM() {
            this.Nome = ko.observable();
            
            this.Telefones = ko.observableArray([]);
            this.Enderecos = ko.observableArray([]);
        }

        function PessoaViewModel() {

            var self = this;

            self.pessoa = new PessoaCM();

            self.adicionarEndereco = function () {

                self.pessoa.Enderecos.push(new EnderecoCM());
            }

            self.removerEndereco = function () {
                self.pessoa.Enderecos.remove(this);
            }

            self.adicionarTelefone = function () {

                self.pessoa.Telefones.push(new TelefoneCM());
            }

            self.removerTelefone = function () {
                self.pessoa.Telefones.remove(this);
            }

            self.Salvar = function () {
                $.ajax("@Url.Action("Create", "Pessoa")", {
                    data: ko.toJSON(self.pessoa),
                    type: "post", contentType: "application/json",
                });
            }
        }

        ko.applyBindings(new PessoaViewModel());
    </script>
}

Essa ultima instrução inserida ativa o knockout e faz com ele procure por atributos data-bind dentro das tags HTML. Vamos adicionar esses data-binds na nossa view agora.

@model ExemploMultiploModels.Models.Pessoa

@{
    ViewBag.Title = "Create";
}

<h2>Create</h2>


@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()

    <div class="form-horizontal" data-bind="with:pessoa">
        <h4>Pessoa</h4>
        <hr />
        @Html.ValidationSummary(true)

        <div class="form-group">
            <label class="control-label col-md-2">Nome</label>
            <div class="col-md-10">
                <input type="text" name="Nome" class="form-control" data-bind="value: Nome" />
            </div>
        </div>

        <div class="row">
            <div class="col-md-6">
                <fieldset>
                    <legend></legend>
                    <h3>Endereços <button type="button" class="btn btn-primary btn-xs pull-right" data-bind="click: $root.adicionarEndereco">Add</button></h3>
                    <table class="table" style="margin-bottom:0">
                        <thead>
                            <tr>
                                <th width="340">Principal</th>
                                <th width="340">Complemento</th>
                                <th>#</th>
                            </tr>
                        </thead>
                    </table>
                    <div style="height: 8em; overflow-y: auto">
                        <table class="table table-striped" style="font-size: 85%">
                            <tbody data-bind="foreach: Enderecos">
                                <tr>
                                    <td width="340"><input class="form-control input-sm" maxlength="80" data-bind="value: Principal" /></td>
                                    <td width="340"><input class="form-control input-sm" maxlength="80" data-bind="value: Complemento" /></td>

                                    <td><button type="button" class="btn btn-danger btn-xs" data-bind="click: $root.removerEndereco">Del</button></td>
                                </tr>
                            </tbody>
                        </table>
                    </div>
                </fieldset>
            </div>
            <div class="col-md-3">
                <fieldset>
                    <legend></legend>
                    <h4>Telefones <button type="button" class="btn btn-primary btn-xs pull-right" data-bind="click: $root.adicionarTelefone">Add</button></h4>
                    
                    <table class="table" style="margin-bottom:0">
                        <thead>
                            <tr>
                                <th width="170">DDD</th>
                                <th width="280">Numero</th>
                                <th>#</th>
                            </tr>
                        </thead>
                    </table>
                    <div style="height: 8em; overflow-y: auto">
                        <table class="table table-striped" style="font-size: 85%">
                            <tbody data-bind="foreach: Telefones">
                                <tr>
                                    <td width="50"><input class="form-control input-sm" maxlength="2" data-bind="value: DDD" /></td>
                                    <td width="100"><input class="form-control input-sm" maxlength="9" data-bind="value: Numero" /></td>
                                    <td><button type="button" class="btn btn-danger btn-xs" data-bind="click: $root.removerTelefone">Del</button></td>
                                </tr>
                            </tbody>
                        </table>
                    </div>
                </fieldset>
            </div>
        </div>

        <div class="form-group">
            <input type="button" value="Create" class="btn btn-default" data-bind="click: $parent.Salvar" />
        </div>

    </div>
}

<div>
    @Html.ActionLink("Back to List", "Index")
</div>

@section scripts{
    @Scripts.Render("~/Scripts/knockout-3.2.0.js")

    <script type="text/javascript">

        function TelefoneCM() {
            this.DDD = ko.observable();
            this.Numero = ko.observable();
        }

        function EnderecoCM() {
            this.Principal = ko.observable();
            this.Complemento = ko.observable();
        }

        function PessoaCM() {
            this.Nome = ko.observable();
            
            this.Telefones = ko.observableArray([]);
            this.Enderecos = ko.observableArray([]);
        }

        function PessoaViewModel() {

            var self = this;

            self.pessoa = new PessoaCM();

            self.adicionarEndereco = function () {

                self.pessoa.Enderecos.push(new EnderecoCM());
            }

            self.removerEndereco = function () {
                self.pessoa.Enderecos.remove(this);
            }

            self.adicionarTelefone = function () {

                self.pessoa.Telefones.push(new TelefoneCM());
            }

            self.removerTelefone = function () {
                self.pessoa.Telefones.remove(this);
            }

            self.Salvar = function () {
                $.ajax("@Url.Action("Create", "Pessoa")", {
                    data: ko.toJSON(self.pessoa),
                    type: "post", contentType: "application/json",
                });
            }
        }

        ko.applyBindings(new PessoaViewModel());
    </script>
}

Percebam como os data-binds servem para ligar as tags HTML a cada parte da PessoaViewModel.
Vamos executar e ver a tela como ficou?

Experimente brincar com os botões Add e Del e pense quanto tempo você levaria para fazer com ou sem jQuery.
Agora a cereja do bolo, preencha o campo nome, coloque alguns endereços e alguns telefone e antes de clicar no botão Create lembre de colocar o breakpoint no if da action Create no controller Pessoa.
Você verá todo o seu modelo preenchido com todas as informações da tela. Olha o print screen que massa!

Chega escorreu uma lágrima aqui. :D
Pessoal isso é só a ponta do iceberg, o KnockoutJS é uma biblioteca muito poderosa e nos auxilia a criar interfaces interativas de forma simples.
Espero que vocês tenham gostado e o código fonte desse artigo está aqui: http://1drv.ms/1myA7pt. Está hospedado no meu OneDrive.
Caso queria sugerir um assunto ou queira ver o como fica o Edit, compartilhe esse post, dá um +1, deixa um comentário... mostre que tem alguém vivo ai na frente da tela. Risos.
Até a próxima!

Comentários

Postagens mais visitadas