ASP .NET MVC 3 - Implementando as funcionalidades CRUD básicas com EF - II


Este artigo é baseado no original: Creating an Entity Framework Data Model for an ASP.NET MVC Application (1 of 10) com adaptações e pequenos ajustes feitos por mim.

Nosso objetivo é criar uma aplicação ASP .NET MVC usando o Entity Framework e a nova tecnologia ASP .NET MVC3.

Alguns novos recurso da ASP  .NET MVC 3:
- A ASP .NET MVC 3 é compatível com a ASP.NET MVC 2 - o que significa que será fácil atualizar os projetos que você está escrevendo com a MVC 2 para a MVC 3;
- A ASP .NET MVC 3 pode ser instalada lado-a-lado com a ASP.NET MVC 2;
- A ASP .NET MVC 3 apresenta um novo conjunto de templates de projeto;
- A ASP .NET MVC 3 inclui um conjunto de melhorias específicas para a View; A janela Add-> View permite escolher a sintaxe que você deseja usar ao criar novos arquivos de modelos de visão;
- A ASP .NET MVC 3 permite usar o novo recurso Razor;
- A ASP .NET MVC 3 inclui vários aperfeiçoamentos específicos para o controlador;
- A ASP .NET MVC 3 incrementa a API ViewData com uma nova propriedade "ViewModel" no Controlador que é do tipo "dynamic" - e que permite que você use o novo suporte à linguagem dinâmica dentro de VB e C#;
- A ASP .NET MVC 3 inclui vários tipos de novos ActionResult e seus métodos de ajuda correspondentes.
- A ASP .NET MVC 3 inclui suporte para ligação de dados JSON;
- A ASP .NET MVC 3 incrementa o modelo de validação, e adiciona suporte para vários dos novos recursos de validação introduzidos dentro do namespace System.ComponentModel.DataAnnotations do .NET 4.
- A ASP .NET MVC 3 oferece um melhor suporte para aplicação de injeção de dependência (DI) e integração com containers de injeção de dependência/IOC;

Iremos criar uma aplicação ASP .NET para uma faculdade que inclui funcionalidades como matricular estudantes, criar cursos e realizar as atribuições dos instrutores.

Estarei usando o Visual Web Developer 2010 Express edition para criar o projeto que vai mostrar como usar o Entity Framework além dos seguintes tópicos:

Criação de um modelo de dados usando atributos e anotações de dados e a API fluente para o mapeamento do banco de dados.
Realizar operações básicas CRUD.
filtragem, ordenação e agrupamento de dados.
• Trabalhar com dados relacionados.
Tratamento de simultaneidade.
• Implementação de tabela por hierarquia de herança.
Implementar o repositório e unidade de padrões de trabalho.
Realizar consultas SQL.
• Trabalhar com classes proxy.
• Detecção de desativação automática de mudanças.
• Validação de desativação, quando salvar as alterações

Antes de iniciar verificar se você possui os seguintes recursos instalados:

Na  primeira parte  criamos o projeto ASP .NET MVC 3 e definimos o seguinte:

Neste artigo vamos revisar e customizar as operações CRUD (create, read, update e delete) que o MVC scaffolding criou de forma automática nos controllers e views na primeira parte do artigo.(O scaffolding criou literalmente um esqueleto de código que iremos revisar e customizar.)

O termo scaffolding é  usado em programação para indicar que o código a que se refere é apenas um esqueleto usado para tornar a aplicação funcional, e se espera que seja substituído por algoritmos mais complexos à medida que o desenvolvimento da aplicação progride.(wikipédia)

Revisando e Customizando o CRUD

Abra o Visual Web Developer 2010 Express Edition e no menu File clique em Open Project; a seguir selecione o projeto que já foi criado no artigo anterior com o nome UniversidadeMacoratti e clique em OK;

Observe na janela Solution Explorer a estrutura do projeto e perceba que na pasta Controllers e na pasta View/Estudante já foram criados os arquivos para realizar as operações CRUD de leitura, inclusão e exclusão de dados;

Se executarmos a aplicação e clicarmos na aba Estudantes iremos obter a página a seguir a partir da qual poderemos acionar as operações CRUD;

Vamos revisar e customizar as páginas criadas começando com a página Details.vbhtml que é exibida ao clicar no link Detalhes da página cima;

Abaixo vemos a página Details.vbhtml exibindo os detalhes de um Estudante;

O código gerado não incluiu a propriedade Matriculas porque esta propriedade refere-se a uma coleção.

Nosso objetivo será então exibir na página Details a relação de cursos matriculados para cada aluno que é justamente a coleção de matriculas.

Vamos espiar o código que foi gerado no controller na pasta Controllers/EstudanteController:

'' GET: /Estudante/Details/5
Function Details(id As Integer) As ViewResult
  
Dim estudante As Estudante = db.Estudantes.Find(id)
  
Return View(estudante)
End Function
 

O código usa o método Find para retornar uma única entidade Estudante correspondente ao valor da chave passada para o método como o parâmetro id.

O valor id vem da consulta string no hiperlink Details na página Index.(Ex: http://localhost:8563/Estudante/Details/2 )

Abra a View na pasta Views/Estudantes/Details.vbhtml e você verá o código abaixo:

@ModelType UniversidadeMacoratti.UniversidadeMacoratti.Models.Estudante

@Code

ViewData("Title") = "Details"

End Code

<h2>Details</h2>

<fieldset>

<legend>Estudante</legend>

<div class="display-label">SobreNome</div>

<div class="display-field">

@Html.DisplayFor(Function(model) model.SobreNome)

</div>

<div class="display-label">Nome</div>

<div class="display-field">

@Html.DisplayFor(Function(model) model.Nome)

</div>

<div class="display-label">DataMatricula</div>

<div class="display-field">

@Html.DisplayFor(Function(model) model.DataMatricula)

</div>

</fieldset>

<p>

@Html.ActionLink("Edit", "Edit", New With {.id = Model.EstudanteID}) |

@Html.ActionLink("Back to List", "Index")

</p>

 

Note que cada campo esta sendo exibido usando um helper DisplayFor. Vamos exibir uma lista de matriculas do estudante logo abaixo da informação data da matrícula.

Para isso vamos incluir um trecho código nesta view, antes do fechamento da tag fieldset. Abaixo o código incluído esta na cor azul em negrito:

@ModelType UniversidadeMacoratti.UniversidadeMacoratti.Models.Estudante
@Code
    ViewData("Title") = "Details"
End Code
<h2>Details</h2>
<fieldset>
    <legend>Estudante</legend>
    <div class="display-label">SobreNome</div>
    <div class="display-field">
        @Html.DisplayFor(Function(model) model.SobreNome)
    </div>
    <div class="display-label">Nome</div>
    <div class="display-field">
        @Html.DisplayFor(Function(model) model.Nome)
    </div>
    <div class="display-label">DataMatricula</div>
    <div class="display-field">
        @Html.DisplayFor(Function(model) model.DataMatricula)
    </div>
    <div class="display-label">
      @Html.LabelFor(Function(model) model.Matriculas)
    </div>
    <div class="display-field">
     <table>
        <tr>
            <th>Curso - titulo</th>
            <th>Grau</th>
        </tr>
        @For Each item In Model.Matriculas
            <tr>
                <td>
                    @Html.DisplayFor(Function(modelItem) item.Curso.Titulo)
                </td>
                <td>
                    @Html.DisplayFor(Function(modelItem) item.Grau)
                </td>
            </tr>
        next
     </table>
    </div>
</fieldset>
<p>
    @Html.ActionLink("Edit", "Edit", New With {.id = Model.EstudanteID}) |
    @Html.ActionLink("Back to List", "Index")
</p>

 

No código incluído temos um laço que percorre as entidades na propriedade de navegação Matriculas.

Para cada entidade matricula na propriedade será exibido o título do curso e o grau. Sendo que o título do curso é retornado a partir da entidade Curso que esta armazenada na propriedade de navegação Curso da entidade Matriculas.

Os dados são retornados do banco de dados de forma automática quando necessário, ou seja, estamos usando lazy loading. Como não definimos um eager loading para a propriedade de navegação Cursos a primeira vez que tentamos acessar a propriedade uma consulta será enviada ao banco de dados para retornar os dados.

Obs: Para saber mais sobre lazy loading e eager loading veja o meu artigo: Entity Framework - Usando Lazy Load e Eager Load - Macoratti.net

Se executarmos o projeto novamente iremos obter a seguinte página após nossa alteração ter sido feita:

Ela exibe os cursos para os quais o estudante esta matriculado exatamente como queríamos.

Ajustando a página para criar um Novo Estudante

Vamos agora realizar outro pequeno ajuste; agora na página Controllers/EstudanteController.vb.

Vamos incluir no método Action HttPost Create um bloco try/catch no bloco de código gerado pelo scaffolding.

Veja abaixo no lado esquerdo o código original e no lado direito o código com o blog try/catch incluído tratando a exceção DataException:

        '
        ' POST: /Estudante/Create
        '
        <HttpPost()>
        Function Create(estudante As Estudante) As ActionResult
            If ModelState.IsValid Then
                db.Estudantes.Add(estudante)
                db.SaveChanges()
                Return RedirectToAction("Index")
            End If
            Return View(estudante)
        End Function

 

Código  Alterado
 '
        ' POST: /Estudante/Create
        '

        <HttpPost()>
        Function Create(estudante As Estudante) As ActionResult
            Try
                If ModelState.IsValid Then
                    db.Estudantes.Add(estudante)
                    db.SaveChanges()
                    Return RedirectToAction("Index")
                End If
            Catch ex As DataException

                'log de erro (inclua uma variável nome depois de DataException)

                ModelState.AddModelError("", "Não foi possível salvar alterações. Tente novamente.")
            End Try

            Return View(estudante)
        End Function

Este código inclui a entidade Estudante criada pelo ASP .NET MVC model Binder ao conjunto de entidades Estudantes e então salva as alterações no banco de dados.

O Model Binder é uma funcionalidade do ASP .NET MVC que torna mais fácil trabalhar com dados submetidos por um formulário; o model binder converte os valores do formulário para os tipos do .NET Framework e passa os valores para método action nos parâmetros. Neste caso o model binder instancia uma entidade estudante para que você usando os valores das propriedades a partir da coleção Forms.

O bloco try/catch é a única diferença no código.

Assim se uma exceção que deriva de DataException é capturada enquanto as alterações estão sendo salvas uma mensagem de  erro genérico será exibida. Esses tipos de erros tem causas externas à aplicação e geralmente não são erros de programação por isso o usuário é avisado para tentar novamente.

O código da view correspondente na pasta Views/Estudante/Create.vbhtml é similar ao código da página Details.vbhtml que alteramos exceto que agora estamos usando os helpers EditorFor e ValidationMessageFor para cada campo ao invés de usa DisplayFor. A seguir vemos o código de Create.vbhtml:

@ModelType UniversidadeMacoratti.UniversidadeMacoratti.Models.Estudante
@Code
    ViewData("Title") = "Create"
End Code
<h2>Create</h2>
<script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>
@Using Html.BeginForm()
    @Html.ValidationSummary(True)
    @<fieldset>
        <legend>Estudante</legend>
        <div class="editor-label">
            @Html.LabelFor(Function(model) model.SobreNome)
        </div>
        <div class="editor-field">
            @Html.EditorFor(Function(model) model.SobreNome)
            @Html.ValidationMessageFor(Function(model) model.SobreNome)
        </div>
        <div class="editor-label">
            @Html.LabelFor(Function(model) model.Nome)
        </div>
        <div class="editor-field">
            @Html.EditorFor(Function(model) model.Nome)
            @Html.ValidationMessageFor(Function(model) model.Nome)
        </div>
        <div class="editor-label">
            @Html.LabelFor(Function(model) model.DataMatricula)
        </div>
        <div class="editor-field">
            @Html.EditorFor(Function(model) model.DataMatricula)
            @Html.ValidationMessageFor(Function(model) model.DataMatricula)
        </div>
        <p>
            <input type="submit" value="Create" />
        </p>
    </fieldset>
End Using
<div>
    @Html.ActionLink("Back to List", "Index")
</div>

 

Vamos executar novamente o projeto e clicar na aba Estudantes. A seguir clique no link  criar novo e informe alguns dados e uma data inválida.

Ao clicar no botão Create teremos uma mensagem de erro informando que a data é inválida:

Neste caso estamos recebendo a mensagem da validação feita no cliente que é implementada via JavaScript, mas no lado do servidor a validação também foi implementada e mesmo que a validação no lado do cliente falhar ela será capturada e uma exceção será lançada no código do servidor.

Corrigindo a data para o dia de hoje 23/06/2011 e clicando no botão Create teremos a página exibindo o novo estudante criado:

Ajustando a página de Edição de dados

Agora vamos fazer um pequeno ajuste na página Controllers/EstudanteController.vb.

O método Edit (HttGet) que usa o método Find para retornar a entidade Estudante selecionada permanece inalterado

Vamos incluir um bloco try/catch no método Edit (HttPost) conforme abaixo:

        '
        ' POST: /Estudante/Edit/5
        <HttpPost()>
        Function Edit(estudante As Estudante) As ActionResult
            If ModelState.IsValid Then
                db.Entry(estudante).State = EntityState.Modified
                db.SaveChanges()
                Return RedirectToAction("Index")
            End If
            Return View(estudante)
        End Function
       

 

Código Alterado
 '
        ' POST: /Estudante/Edit/5

        <HttpPost()>
        Function Edit(estudante As Estudante) As ActionResult
            Try
                If ModelState.IsValid Then
                    db.Entry(estudante).State = EntityState.Modified
                    db.SaveChanges()
                    Return RedirectToAction("Index")
                End If
            Catch ex As DataException
             
  'log de erro (inclua uma variável nome depois de DataException)
                ModelState.AddModelError("", "Não foi possível salver alterações. Tente novamente.")
            End Try
            Return View(estudante)

        End Function

Este código é parecido com o que usamos para ajustar o método Create (HttPost) a diferença é que ao invés de incluir uma entidade criada pelo model binder a conjunto de entidades o código acima define uma sinalizador na entidade indicando que ela foi alterada =>  db.Entry(estudante).State = EntityState.Modified

Quando o método SaveChanges for chamado o sinalizador Modified  faz com que o Entity Framework cria uma instrução SQL para atualizar a linha (registro) do banco de dados. Todas as colunas do registro do banco de dados serão atualizadas incluindo aquela que o usuário não alterou e os conflitos de concorrência serão ignorados.

O código na view Views/Estudante/Edit.vbhtml é similar ao que foi visto na página Create.vbhtml e não precisa ser alterada.

Executando a página, após rodar a aplicação e clicar na aba Estudante, e realizando uma alteração, após clicar no link Editar e no botão Save iremos verificar que a alteração feita pode ser vista na página Index:

 
 

Teoria : O estado das entidades e os métodos Attach e SaveChanges

Vamos dar uma pausa para falar de um assunto importante antes de continuar o nosso trabalho.

O contexto do banco se mantém informado se as entidades na memória estão em sincronia com suas linhas/registros correspondentes no banco de dados, e esta informação determina o que acontece quando você chama o método SaveChanges.  Por exemplo, quando você passa uma nova entidade para o método Add, o estado desta entidade é definido como Added, dessa forma quando você chama o método SaveChanges o contexto do banco de dados emite um comando SQL INSERT.

Uma entidade pode estar em um dos seguintes estados:

Added (Adicionado) . A entidade ainda não existe no banco de dados. O método SaveChanges deve emitir uma instrução INSERT.
Unchanged (
Inalterado). Nada precisa ser feito com esta entidade pelo método SaveChanges. Quando você lê uma entidade do banco de dados, a entidade começa com esse status.
Modified (Mod
ificado). Alguns ou todos os valores das propriedades da entidade têm sido modificadas. O método SaveChanges deve emitir uma declaração UPDATE.
Deleted (
Excluído). A entidade foi marcada para exclusão. O método SaveChanges deve emitir uma declaração DELETE.
Detached (
Independente). A entidade não está sendo controlado pelo contexto de banco de dados.

Em uma aplicação desktop, mudanças de estado são definidas automaticamente. Neste tipo de aplicação, você lê uma entidade e faz alterações em alguns dos valores de suas propriedades. Isso faz com que o estado da entidade seja automaticamente alterado para Modified. Então, quando você chama SaveChanges, o Entity Framework gera uma instrução SQL UPDATE, que atualiza apenas as propriedades que você alterou.

No entanto, em uma aplicação web esta seqüência é interrompida, porque a instância do contexto doe banco de dados que lê uma entidade é eliminada depois que uma página é processada.

Quando um método Action HttpPost Edit é chamado, este é o resultado de um novo request e você tem uma nova instância do contexto; assim você tem que definir manualmente o estado entidade para Modified. Então, quando você chama SaveChanges, o Entity Framework atualiza todas as colunas do registro/linha do banco de dados, porque o contexto não tem como saber quais as propriedades você alterou.

Se você quiser que a instrução SQL Update atualize somente os campos que o usuário realmente alterou, você pode salvar os valores originais de alguma forma (como campos ocultos(hidden)) para que eles estejam disponíveis quando o método HttpPost Edit for chamado. Depois, você pode criar uma entidade Estudante usando os valores originais, chamar o método Attach com a versão original da entidade, atualizar os valores da entidade para os novos valores, e depois chamar SaveChanges.

Ajustando a página de Exclusão de dados

Na pasta Controllers\EstudanteController.vb, o código modelo para o método Delete HttpGet usa o método Find para recuperar a entidade Estudante selecionado, como você viu nos métodos Details e Edit. No entanto, para implementar uma mensagem de erro personalizada quando a chamada para SaveChanges falhar, você irá adicionar alguma funcionalidade a este método e sua view correspondente.

Como ja vimos para as operações update e create, a operação delete (eliminar) requer dois métodos action. O método que é chamado em resposta a um pedido GET exibe uma visão que dá ao usuário a oportunidade de aprovar ou cancelar a operação. Se o usuário aprovar, um pedido POST é criado. Quando isso acontece, o HttpPost método Delete é chamado e, em seguida, o método realmente executa a operação de exclusão.

Vamos adicionar um bloco try-catch para o método HttPost Delete para tratar quaisquer erros que possam ocorrer quando o banco de dados for atualizado. Se ocorrer um erro, o método HttpPost Delete chama o método HttpGet Delete , passando um parâmetro que indica que ocorreu um erro. O método HttpGet Delete então exibe novamente a página de confirmação juntamente com a mensagem de erro, dando ao usuário a oportunidade de cancelar ou tentar novamente.

Vamos então substituir a action Delete (HttpGet) com o seguinte código que gerencia a exibição de erros:

        Public Function Delete(id As Integer, saveChangesError As System.Nullable(Of Boolean)) As ActionResult
            If saveChangesError.GetValueOrDefault() Then
                ViewBag.ErrorMessage = "Não foi possível salvar as alterações, tente novamente e so problema 
persistir avise o administrador."
            End If
            Return View(db.Estudantes.Find(id))
        End Function

Este código aceita um parâmetro booleano opcional que indica se ele foi chamado depois de uma falha para salvar as alterações. Este parâmetro é nulo (false) quando o método HttpGet Delete é chamado em resposta a uma solicitação de página. Quando ele for chamado pelo método Delete HttpPost em resposta a um erro de atualização do banco de dados, o parâmetro é verdadeiro e uma mensagem de erro é passado para a view.

Agora vamos substituir o método de action HttpPost Delete (chamado DeleteConfirmed) com o seguinte código, que executa a operação de exclusão e captura qualquer erros de atualização de banco de dados.

 <HttpPost(), ActionName("Delete")> _
        Public Function DeleteConfirmed(id As Integer) As ActionResult
            Try
                Dim estudante As Estudante = db.Estudantes.Find(id)
                db.Estudantes.Remove(estudante)
                db.SaveChanges()
            Catch generatedExceptionName As DataException
                'Log de erro (inclui uma varia´vel DataException) 
                Return RedirectToAction("Delete", New System.Web.Routing.RouteValueDictionary() From { _
                 {"id", id}, _
                 {"saveChangesError", True} _
                })
            End Try
            Return RedirectToAction("Index")
        End Function

Este código recupera a entidade selecionada, em seguida, chama o método Remove para definir o status da entidade para Deleted. Quando SaveChanges é chamado, um comando SQL DELETE é gerado.

Se melhorar o desempenho da aplicação for uma prioridade, você poderia evitar uma consulta SQL desnecessária para recuperar a linha, substituindo as linhas de código que chama os métodos Find e Remove com o seguinte código:


Dim
estudanteToDelete As New Estudante() With {.EstudanteID = id}

db.Entry(estudanteToDelete).State = EntityState.Deleted
 

Este código instancia uma entidade Estudante usando apenas o valor de chave primária e, em seguida, define o estado da entidade para Deleted. Isso é tudo que o Entity Framework necessita a fim de excluir a entidade

Como observamos, o método HttpGet Delete não apaga os dados. Executar uma operação de exclusão em resposta a um pedido GET (ou executar uma operação de edição, criação ou qualquer outra operação que altere dados) cria um risco de segurança.

Finalmente vamos alterar a view. Localize Views/Estudante/Delete.vbhtml e inclua o seguinte código entre o cabeçalho h2 e h3:

<p class="error">@ViewBag.ErrorMessage</p>

Agora execute o projeto e após clicar na guia Estudantes selecione um item e clique no link Deletar:

A tela de confirmação será exibida e se você clicar no botão Delete a página Index será apresentada sem o estudante excluído:

Para ter certeza de que as conexões de banco de dados estão devidamente fechadas e os recursos liberados, você deve fazer com que a instância do contexto seja descartada. É por isso que você vai encontrar um método Dispose no final da classe EstudanteController em EstudanteController.vb, como mostrado no exemplo a seguir:

Protected Overrides Sub Dispose(disposing As Boolean)

   db.Dispose()
  
MyBase.Dispose(disposing)

End Sub

A classe base Controller já implementa a interface IDisposable, assim esse código simplesmente adiciona um Overrides (substituição) para o método Dispose (Boolean) para explicitamente descartar a instância do contexto.

Ufa, agora acabamos...

Dessa forma concluímos todos os ajustes nos controllers e views para realizar as operações CRUD (criar, ler, atualizar, excluir) .

Acompanhe a continuação neste link:  ASP .NET MVC 3 - Ordenação, Filtragem e Paginação com EF - III

Pegue o projeto completo aqui:   UniversidadeMacoratti_2.zip

"E a ninguém na terra chameis vosso pai, porque um só é o vosso Pai, o qual esta nos céus."
"Nem vos chameis mestres, porque um só é o vosso Mestre, que é o Cristo."
Mateus 23:9-10

Referências:

  1. VB .NET - Implementando o relacionamento um-para-muitos

  2. Entity Framework 4.0 - Lazy Loading - Macoratti.net

  3. Entity Framework 4.0 - Carregando dados - Macoratti.net

  4. Entity Framework 4.0 - Características e operações ... - Macoratti.net

  5. Entity Framework - Mestre Detalhes - Macoratti.net

  6. VS 2010 Novidades

  7. C# - Os tipos Nullable (Tipos Anuláveis)

  8. VB .NET - Nothing , Null (DBNull.Value) e Nullabe Types

  9. Entity Framework 4.0 - Lazy Loading - Macoratti.net

  10. ASP .NET MVC - Introdução

  11. ASP .NET - Apresentando o ASP .NET MVC 3 - Macoratti.net


José Carlos Macoratti