ASP .NET MVC 5 - Cenário Mestre-Detalhes (Relacionamento Muitos-para-Muitos)


 Neste artigo vou mostrar como podemos exibir páginas mestre-detalhes em um cenário onde temos um relacionamento muitos-para-muitos em uma aplicação ASP .NET MVC 5.

Você vai encontrar centenas de artigos e vídeos na internet que abordam como criar aplicações ASP .NET MVC usando o Scaffolding, onde você cria os controladores e as views de forma automática. Dessa forma, à primeira vista fica a impressão que podemos gerar qualquer tipo de aplicação usando esses recursos.

Mas no mundo real as coisas não são tão simples e em muitos cenários você vai ter que ir além do que a ASP .NET MVC e o Scaffolding lhe oferece para poder criar uma aplicação funcional.

Um desses casos é quando temos que tratar com o relacionamento muitos-para-muitos, para este cenário o Scaffolding não funciona e você vai ter que arregaçar as mangas e por em prática os seus conhecimentos para poder estender os recursos da ASP .NET MVC.

Neste artigo eu vou mostrar com fazer isso na prática partindo do seguinte cenário:

- Desejamos criar um projeto que vai exibir informações de estudantes e cursos;
- Teremos assim uma página para exibir, criar, deletar e alterar dados dos estudantes e uma página para fazer a mesma coisa com os cursos;

Até aqui tudo normal, e, até este ponto podemos usar o recurso do Scaffolding para gerar os controladores e views para cursos e estudantes.

O problema é que queremos registrar os cursos nos quais os estudantes estão matriculados, e, então teremos um página para exibir os estudantes e os cursos onde devemos permitir que o estudante selecione os cursos que deseja fazer.

Assim, o estudante deverá poder selecionar e salvar as informações para os cursos nos quais deseja se registrar.

Para concluir devemos exibir em uma  página de detalhes do estudante os dados do mesmo e os cursos nos quais ele esta matriculado.

Com essas definições vamos à pratica...

Recursos usados:

Criando o projeto no VS Community

Abra o VS Community 2017  e clique em New Project;

A seguir selecione Visual C# -> Web -> ASP .NET Web Application(.NET Framework)

Informe o nome Mvc_CodeFirst_MuitosMuitos e clique no botão OK;

Selecione o template MVC sem autenticação, conforme figura a seguir:

Ao final da operação a solução criada com toda estrutura pronto onde iremos fazer os ajustes e a implementação do nosso projeto.

Incluindo a referência ao Entity Framework, definindo o modelo de entidades e o contexto

Vamos incluir a referência ao Entity Framework no projeto via Nuget. No menu Tools clique em Nuget Package Manager e a seguir em Manage Nuget Packages for Solution;

Selecione o pacote EntityFramework e marque para instalar no projeto:

Após isso vamos criar as classes das entidades que serão mapeadas pelo EF6 para as tabelas do banco de dados.

Na pasta Models crie o arquivo Curso.cs e a seguir defina o código abaixo na classe Curso:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Mvc_CodeFirst_MuitosMuitos.Models
{
    [Table("Cursos")]
    public class Curso
    {
        public Curso()
        {
            this.Estudantes = new HashSet<Estudante>();
        }
        [Key]
        public int CursoId { get; set; }
        public string Nome { get; set; }
        public Nullable<int> Creditos { get; set; }
        public virtual ICollection<Estudante> Estudantes { get; set; }
    }
}
Curso.cs

A seguir crie o arquivo Estudante.cs e defina o código da classe Estudante:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Mvc_CodeFirst_MuitosMuitos.Models
{
    [Table("Estudantes")]
    public class Estudante
    {
        public Estudante()
        {
            this.Cursos = new HashSet<Curso>();
        }
        [Key]
        public int EstudanteId { get; set; }
        public string Nome { get; set; }
        public Nullable<int> Idade { get; set; }
        public string Sexo { get; set; }
        public virtual ICollection<Curso> Cursos { get; set; }
    }
}
Estudante.cs

Agora vamos definir a classe que representa o contexto da nossa aplicação. Ainda na pasta Models crie o arquivo dbContexto.cs e defina o código abaixo na classe dbContexto:

using System.Data.Entity;
namespace Mvc_CodeFirst_MuitosMuitos.Models
{
    public class dbContexto : DbContext
    {
        public dbContexto()
            : base("name=dbContexto")
        {}
        public virtual DbSet<Curso> Cursos { get; set; }
        public virtual DbSet<Estudante> Estudantes { get; set; }
    }
}
dbContexto.cs

Pronto ! Definimos as classes Curso, Estudante e dbContexto e já estamos prontos para criar os controladores e as views da nossa aplicação.

Só falta definir a string de conexão com o banco de dados SQL Server. Abra o arquivo Web.Config e defina a string de conexão para um banco de dados no Sql Server.

Abaixo vemos um exemplo de conexão usando o nome do nosso contexto, dbcontexto, e apontando para o banco EscolaDemo:

....

 <connectionStrings>
    <add name="dbcontexto" connectionString="Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=EscolaDemo;Integrated Security=True" providerName="System.Data.SqlClient"/>
  </connectionStrings>
.....
 

Até aqui temos o caminho feliz e podemos gerar os controladores EstudantesController e CursosController na pasta Controllers conforme usando o Scaffolding na opção MVC 5 Controller and views, using Entity Framework, conforme as opção mostradas abaixo:

Ao final teremos os controladores e as views e executando o projeto poderemos acessar as páginas para gerenciar estudantes e cursos.

Mas não temos a opção em nenhuma das páginas geradas para poder selecionar os cursos para um estudante nem exibir os cursos nos quais um estudante esta matriculado.

Vamos então resolver esse problema...

Resolvendo o problema usando ViewModels

Para contornar o problema vamos criar duas ViewModels na pasta Models :

  1. CheckBoxViewModel - Esta ViewModel representa as opções de cursos que serão exibidas na página de edição do estudante;
  2. EstudantesViewModel - Esta ViewModel representa os estudantes e as opções de cursos nos quais ele esta matriculado;

Na pasta Models crie o arquivo CheckBoxViewModel.cs e defina o código abaixo na classe CheckBoxViewModel:

namespace Mvc_CodeFirst_MuitosMuitos.Models
{
    public class CheckBoxViewModel
    {
        public int Id { get; set; }
        public string Nome { get; set; }
        public bool Checked { get; set; }
    }
}

Ainda na pasta Models crie o arquivo EstudantesViewModel.cs e defina o código abaixo na classe EstudantesViewModel:

using System;
using System.Collections.Generic;
namespace Mvc_CodeFirst_MuitosMuitos.Models
{
    public class EstudantesViewModel
    {
        public int EstudanteId { get; set; }
        public string Nome { get; set; }
        public Nullable<int> Idade { get; set; }
        public string Sexo { get; set; }
        public List<CheckBoxViewModel> Cursos { get; set; }
    }
}

Definindo a Classe CursoEstudante no modelo e no contexto

Agora precisamos definir a classe CursoEstudante para podermos mapear para a tabela CursosEstudantes que foi gerada:

Na pasta Models crie o arquivo CursoEstudante.cs e define o código abaixo na classe CursoEstudante:

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Mvc_CodeFirst_MuitosMuitos.Models
{
    [Table("CursosEstudantes")]
    public class CursoEstudante
    {
        [Key]
        public int CursoEstudanteId { get; set; }
        public int CursoId { get; set; }
        public int EstudanteId { get; set; }
        public virtual Curso Curso { get; set; }
        public virtual Estudante Estudante { get; set; }
    }
}

E finalmente temos que definir manualmente esta classe no nosso contexto. Abra o arquivo dbContexto.cs e inclua a linha de código em azul:

using System.Data.Entity;
namespace Mvc_CodeFirst_MuitosMuitos.Models
{
    public class dbContexto : DbContext
    {
        public dbContexto()
            : base("name=dbContexto")
        {}
        public virtual DbSet<Curso> Cursos { get; set; }
        public virtual DbSet<Estudante> Estudantes { get; set; }
        public virtual DbSet<CursoEstudante> CursosEstudantes { get; set; }
    }
}

Agora podemos alterar o controlador EstudantesController para permitir a edição dos cursos e a exibição dos detalhes dos estudantes incluindo agora os cursos.

Ajustando o controlador EstudantesController :  métodos Edit e Details

Abra o controlador EstudantesController e altere o código do método Edit/Get conforme mostrado no código destacado em azul abaixo:

        // GET: Estudantes/Edit/5
        public ActionResult Edit(int? id)
        {
            if (id == null)
            {
                return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
            }
            Estudante estudante = db.Estudantes.Find(id);
            if (estudante == null)
            {
                return HttpNotFound();
            }
            var CursosEstudantes = from c in db.Cursos
                                   select new
                                   {
                                       c.CursoId,
                                       c.Nome,
                                       Checked = ((from ce in db.CursosEstudantes
                                                   where (ce.EstudanteId == id) & (ce.CursoId == c.CursoId)
                                                   select ce).Count() > 0)
                                   };
            var estudanteViewModel = new EstudantesViewModel();
            estudanteViewModel.EstudanteId = id.Value;
            estudanteViewModel.Nome = estudante.Nome;
            estudanteViewModel.Idade = estudante.Idade;
            estudanteViewModel.Sexo = estudante.Sexo;
            var checkboxListCursos = new List<CheckBoxViewModel>();
            foreach (var item in CursosEstudantes)
            {
                checkboxListCursos.Add(new CheckBoxViewModel { Id = item.CursoId, Nome = item.Nome, 
Checked = item.Checked });
            }
            estudanteViewModel.Cursos = checkboxListCursos;
            return View(estudanteViewModel);
        }

Observe que neste código estamos usando e retornando a nossa ViewModel EstudantesViewModel para poder selecionar os cursos e exibí-los na página do estudante onde o estudante poderá selecionar um curso marcando o checkbox.

Devemos também alterar o método Edit/Post para poder salvar a seleção feita pelo usuário.

        // POST: Estudantes/Edit/5
        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Edit(EstudantesViewModel estudante )    
       {
            if (ModelState.IsValid)
            {
                var estudanteSelecionado = db.Estudantes.Find(estudante.EstudanteId);
                estudanteSelecionado.Nome = estudante.Nome;
                estudanteSelecionado.Idade = estudante.Idade;
                estudanteSelecionado.Sexo = estudante.Sexo;
                foreach(var item in db.CursosEstudantes)
                {
                    if (item.EstudanteId==estudante.EstudanteId)
                    {
                        db.Entry(item).State = EntityState.Deleted;
                    }
                }
                foreach(var item in estudante.Cursos)
                {
                    if(item.Checked)
                    {
                        db.CursosEstudantes.Add(new CursoEstudante() { EstudanteId = estudante.EstudanteId, 
CursoId = item.Id });
                    }
                }
                db.SaveChanges();
                return RedirectToAction("Index");
            }
            return View(estudante);
        }

Neste código estamos usando a viewmodel EstudantesViewModel e incluindo na tabela CursosEstudantes as seleções de cursos feitas pelo estudante.

Agora basta alterarmos as views geradas para o estudante na pasta Views/Estudantes.

Ajustando as Views para o Estudante : Edit.cshtml e Details.cshtml

Abra o arquivo Edit.cshtml gerado pelo Scaffolding na pasta Views/Estudantes e altere o seu código conforme mostrado abaixo no código em azul:

@model Mvc_CodeFirst_MuitosMuitos.Models.EstudantesViewModel
@{
    ViewBag.Title = "Edit";
}
<h2>Editar Estudante/Curso</h2>
@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()
    
    <div class="form-horizontal">
        <hr />
        @Html.ValidationSummary(true, "", new { @class = "text-danger" })
        @Html.HiddenFor(model => model.EstudanteId)
        <div class="form-group">
            @Html.LabelFor(model => model.Nome, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Nome, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Nome, "", new { @class = "text-danger" })
            </div>
        </div>
        <div class="form-group">
            @Html.LabelFor(model => model.Idade, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Idade, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Idade, "", new { @class = "text-danger" })
            </div>
        </div>
        <div class="form-group">
            @Html.LabelFor(model => model.Sexo, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Sexo, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Sexo, "", new { @class = "text-danger" })
            </div>
        </div>
        <div class="form-group">
            @Html.LabelFor(model => model.Cursos, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @for (int i = 0; i < Model.Cursos.Count(); i++)
                {
                    @Html.EditorFor(m => Model.Cursos[i].Checked)<text>&nbsp;</text>
                    @Html.DisplayFor(m => Model.Cursos[i].Nome)<text>&nbsp;</text>
                    <br />
                    @Html.HiddenFor(m => Model.Cursos[i].Nome)
                    @Html.HiddenFor(m => Model.Cursos[i].Id)
                }
            </div>
        </div>
        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Salvar" class="btn btn-success" />
            </div>
        </div>
    </div>
}
<div>
    @Html.ActionLink("Retornar", "Index")
</div>
@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
}

Note que alteremos Model usado na página para EstudantesViewModel.

Abra o arquivo Details.cshtml na pasta Views/Estudantes e altere o código conforme mostrado abaixo em azul:

@model Mvc_CodeFirst_MuitosMuitos.Models.EstudantesViewModel
@{
    ViewBag.Title = "Details";
}
<h2>Detalhes Estudante</h2>
<div>
    <hr />
    <dl class="dl-horizontal">
        <dt>@Html.DisplayNameFor(model => model.Nome)</dt>
        <dd> @Html.DisplayFor(model => model.Nome)</dd>
        <dt>@Html.DisplayNameFor(model => model.Idade)</dt>
        <dd> @Html.DisplayFor(model => model.Idade)</dd>
        <dt>@Html.DisplayNameFor(model => model.Sexo)</dt>
        <dd>@Html.DisplayFor(model => model.Sexo)</dd>

        <dt>@Html.DisplayNameFor(model => model.Cursos)</dt>
        <dd>
          @if (Model.Cursos != null)
          {
               foreach (var item in Model.Cursos)
               {
                   if (item.Checked)
                   {
                       <div><span class="glyphicon glyphicon-list-alt"></span><Text>&nbsp;</Text>
@item.Nome</div>
                   }
               }
          }
          else
          {
             <span class="glyphicon glyphicon-list-alt"></span><text>Sem Cursos Registrados</Text>
          }
        </dd>
    </dl>
</div>
<p>
    @Html.ActionLink("Edit", "Editar", new { id = Model.EstudanteId }) |
    @Html.ActionLink("Retornar", "Index")
</p>

Agora é só alegria...

Executando o projeto iremos obter o seguinte resultado:

Pegue o projeto aqui : Mvc_CodeFirst_MuitosMuitos.zip (sem as referências)

Mas longe esteja de mim gloriar-me, a não ser na cruz de nosso Senhor Jesus Cristo, pela qual o mundo está crucificado para mim e eu para o mundo. 
Gálatas 6:14

Veja os Destaques e novidades do SUPER DVD Visual Basic (sempre atualizado) : clique e confira !

Quer migrar para o VB .NET ?

Quer aprender C# ??

Quer aprender os conceitos da Programação Orientada a objetos ?

Quer aprender o gerar relatórios com o ReportViewer no VS 2013 ?

Referências:


José Carlos Macoratti