ASP .NET MVC - Atualizando dados relacionados com Entity Framework - VI


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.

Antes de prosseguir verifique se você possui os seguintes recursos instalados:

Esta é sexta parte do artigo, e, se você esta chegando agora deve ler obrigatoriamente as partes anteriores:

  1. ASP .NET MVC 3 - Criando uma aplicação ASP .NET MVC - I
  2. ASP .NET MVC 3 - Implementando as funcionalidades CRUD básicas com EF - II
  3. ASP .NET MVC 3 - Ordenação, Filtragem e Paginação com EF - III
  4. ASP .NET MVC 3 - Alterando o modelo de dados - Data Annotations - IV
  5. ASP .NET MVC 3 - Lendo dados relacionados com Entity Framework - V

Neste artigo vamos mostrar como atualizar dados relacionados com Entity Framework. Para muitos relacionamentos a atualização pode ser feita usando o apropriado campo chave-estrangeira. Para relacionamentos muitos-para-muitos o Entity Framework não expõe diretamente a tabela join, de forma que precisamos adicionar e remover entidades de e a partir das devidas propriedades de navegação.

isto é, os dados que o Entity Framework através das propriedades de navegação.

A partir deste tutorial eu estou usando o Visual Studio 2012 Express For web para abrir e criar os projetos.

Customizando as páginas para Criar e Editar Cursos

Quando uma nova entidade curso é criada, ela deve ter uma relação com um departamento existente. Para facilitar este processo, o código gerado inclui métodos do controlador e as views para Criar e Editar que incluem uma lista dropdown para selecionar o departamento.

A lista drop-down define a propriedade de chave estrangeira Curso.DepartamentoID, e isso é tudo que o Entity Framework precisa para carregar a propriedade de navegação Departamento com a entidade departamento apropriada. Vamos usar o código gerado alterando-o um pouco para adicionar o tratamento de erros e classificar a lista dropdown.

Vamos abrir o arquivo CursoController da pasta Controllers e excluir os métodos Edit e Create substituindo-os pelo código abaixo:

...
       ' GET: /Course/Create
        Function Create() As ViewResult
            PopularDepartamentosDropDownList()
            Return View()
        End Function

        ' POST: /Course/Create
        <HttpPost()>
        Function Create(_curso As Curso) As ActionResult
            Try
                If ModelState.IsValid Then
                    db.Cursos.Add(_curso)
                    db.SaveChanges()
                    Return RedirectToAction("Index")
                End If
            Catch ex As DataException
                'Log the error 
                ModelState.AddModelError("", "Não foi possível salvar as alterações. Tente Novamente, e se o problema persistir contate o suporte.")
            End Try
            PopularDepartamentosDropDownList(_curso.DepartamentoID)
            Return View(_curso)
        End Function

        '
        ' GET: /Course/Edit/5
        Function Edit(id As Integer) As ViewResult
            Dim _curso As Curso = db.Cursos.Find(id)
            PopularDepartamentosDropDownList(_curso.DepartamentoID)
            Return View(_curso)
        End Function
        '
        ' POST: /Course/Edit/5
        <HttpPost()>
        Function Edit(_curso As Curso) As ActionResult
            Try
                If ModelState.IsValid Then
                    db.Entry(_curso).State = EntityState.Modified
                    db.SaveChanges()
                    Return RedirectToAction("Index")
                End If
            Catch ex As DataException
                'Log the error
                ModelState.AddModelError("", "Não foi possível salvar as alterações. Tente Novamente, e se o problema persistir contate o suporte.")
            End Try
            PopularDepartamentosDropDownList(_curso.DepartamentoID)
            Return View(_curso)
        End Function

        ''' <summary>
        ''' Preenche um dropdownlist com departamentos
        ''' </summary>
        ''' <param name="selectedDepartment"></param>
        ''' <remarks></remarks>
        Private Sub PopularDepartamentosDropDownList(Optional departamentoSelecionado As Object = Nothing)
            Dim departamentoConsulta = From d In db.Departamentos Order By d.Nome Select d
            ViewBag.DepartamentoID = New SelectList(departamentoConsulta, "DepartamentoID", "Nome", departamentoSelecionado)
        End Sub
......

O método PopularDepartamentosDropDownList obtém uma lista de todos os departamentos ordenados por nome, cria uma coleção SelectList para uma lista dropdown, e passa a coleção para a view em uma propriedade ViewBag. O método aceita um parâmetro que permite que o chamador especifique opcionalmente o item que será selecionado inicialmente quando a lista dropdown for processado.

O método Create HttpGet chama o método PopularDepartamentosDropDownList sem definir o item selecionado, porque, para um novo curso o departamento ainda não foi definido.

O método Edit HttpGet define o item selecionado, com base na identificação do departamento que já está atribuído ao curso que está sendo editado.

Ambos métodos HttpPost tanto para criar e editar também incluem um código que define o item selecionado quando reexibir a página depois de um erro.

Vamos agora ajustar as views Create e Edit.

Abra a view Views/Curso/Create.vbhtml e adicione um novo campo antes do campo Título para permitir que o usuário digite o número do curso. Para isso inclua o código abaixo:

       ....
       @Html.LabelFor(Function(model) model.CursoID)
        </div>
        <div class="editor-field">
            @Html.EditorFor(Function(model) model.CursoID)
            @Html.ValidationMessageFor(Function(model) model.CursoID)
        </div>
        ....

Agora nas views Views\Course\Edit.vbhtml, Views\Course\Delete.vbhtml e Views\Course\Details.vbhtml inclua um novo campo antes do campo Titulo para exibir o número do curso incluindo o código a seguir nestes arquivos:

 .......
 <div class="display-label">
       @Html.LabelFor(Function(model) model.CursoID)
  </div>
  <div class="display-field">
      @Html.DisplayFor(Function(model) model.CursoID)
 </div>
......

Vamos executar o projeto e após exibir a página de Cursos (Index.vbhtml) clique no link Criar Novo para abrir a página para criar um novo Curso:

Ao clicar no botão Create, a página Index será exibida exibindo o novo curso adicionado na lista. O nome do departamento na página vem da propriedade de navegação mostrando que o relacionamento foi estabelecido corretamente.

Agora volte à página dos Cursos e clique no link Edit para editar um curso.

Altere o número de créditos e clique no botão Save para salvar as alterações feitas. Você deverá a página de Cursos exibindo o valor alterado.

Adicionando uma página de edição para os instrutores

Quando você edita um registro de um instrutor, você quer ser capaz de atualizar a diretoria do instrutor. A entidade instrutor tem um relacionamento um-para zero-ou-um com a entidade Diretoria, o que significa que você deve lidar com as seguintes situações:

Abra o controlador InstrutorController na pasta Controllers e altere o código do método Edit (HttpGet)

 Function Edit(id As Integer) As ViewResult

    Dim _instrutor As Instrutor = db.Instrutores.
                                            Include(Function(i) i.Cursos).
                                            Where(Function(i) i.InstrutorID = id).Single()
         Return View(_instrutor)
 End Functiont:
 ****  Código anterior substituido *****
  Dim instrutor As Instrutor = db.Instrutores.Find(id)
  ViewBag.InstrutorID = New SelectList(db.Diretorias, "InstrutorID", "Localizacao", instrutor.InstrutorID)
  Return View(instrutor)
  ****  Código anterior substituido *****

Este código elimina a declaração ViewBag e acrescenta o eager loading para as entidades Diretoria e Cursos associadas. (Você não precisa de cursos agora, mas você vai precisar dele mais tarde.) Não é possível realizar o eager loading com o método Find, por isso os métodos Where e Single são usados ??em vez de selecionar o instrutor.

Agora substitua o método Edit (HttPost) do controlador InstrutorController pelo código abaixo o qual trata da atualização das diretorias:

        <HttpPost()>
        Public Function Edit(id As Integer, formCollection As FormCollection) As ActionResult

            Dim instrutorAtualizar = db.Instrutores.Include(Function(i) i.Diretoria).Include(Function(i) i.Cursos).Where(Function(i) i.InstrutorID = id).Single()
            If TryUpdateModel(instrutorAtualizar, "", Nothing, New String() {"Cursos"}) Then
                Try
                    If [String].IsNullOrWhiteSpace(instrutorAtualizar.Diretoria.Localizacao) Then
                        instrutorAtualizar.Diretoria = Nothing
                    End If

                    db.Entry(instrutorAtualizar).State = EntityState.Modified
                    db.SaveChanges()

                    Return RedirectToAction("Index")
                Catch generatedExceptionName As DataException
                    ModelState.AddModelError("", "Não foi possível alterar. Tente novamente e se o problema persistir contate o suporte.")
                    Return View()
                End Try
            End If
            Return View(instrutorAtualizar)
        End Function

Neste código faz o seguinte:

No arquivo Edit.vbhtml da pasta Views/Instrutor , inclua o campo para editar a diretoria depois da <div> DataAdmissao conforme abaixo:

.....
 <div class="editor-label">
            @Html.LabelFor(Function(model) model.Diretoria.Localizacao)
        </div>
        <div class="editor-field">
            @Html.EditorFor(Function(model) model.Diretoria.Localizacao)
            @Html.ValidationMessageFor(Function(model) model.Diretoria.Localizacao)
 </div>
....

Execute o projeto e abra a página para Instrutores e a seguir clique no link Edit para editar um instrutor:

Vamos alterar a Localização (Diretoria) e clicar no botão Save;

Você deverá o novo valor exibido na página Instrutores e se você abrir a tabela Diretoria irá ver o novo valor atualizado na respectiva coluna:

Se você retornar para a página de edição e limpar a informação sobre a localização o registro será removido da tabela Diretoria:

E se retornamos para a página de edição e incluirmos um novo valor para a localização o mesmo será salvo na tabela Diretoria:

Note que depois de selecionar a entidade Curso , o código carrega explicitamente a propriedade de navegação Matricula do curso.

Incluindo a atribuição de Cursos para a página de Instrutores

Como os instrutores podem ensinar qualquer número de cursos vamos melhorar a pagina dos instrutores incluindo a possibilidade de alterar os cursos atribuídos incluindo um grupo de caixa de verificação que será usada para selecionar os cursos.

A relacionamento entre as entidades Curso e Instrutor é de muitos-para-muitos, o que significa que não temos acesso direto à tabela de junção ou campos de chave estrangeira. Em vez disso, vamos adicionar e remover entidades de e para a propriedade de navegação Instrutor.Cursos.

A interface do usuário que permite alterar quais cursos um instrutor ministra será um grupo de caixas de seleção. A caixa de seleção para cada curso no banco de dados será exibida, e os cursos que o instrutor está atualmente ministrando serão selecionados. O usuário pode marcar ou desmarcar as caixas de seleção para alterar as atribuições do curso.

Para fornecer dados para a view para a lista de caixas de seleção, vamos usar uma classe de model view criando a classe CursosAtribuidos.vb na pasta ViewModels com o seguinte código:

Imports System.Collections.Generic
Imports System.ComponentModel.DataAnnotations

Public Class CursosAtribuidos

    Public Property CursoID As Integer
    Public Property Titulo As String
    Public Property Atribuido As Boolean

End Class

No controlador InstrutorController.vb da pasta Controllers, no método Edit (Httpget) vamos chamar um novo método que fornece informação para grupo de caixa de seleção usando a nova classe view-model com o seguinte código:

 Public Function Edit(id As Integer) As ActionResult
            Dim _instrutor As Instrutor = db.Instrutores.Include(Function(i) i.Diretoria).Include(Function(i) i.Cursos).Where(Function(i) i.InstrutorID = id).Single()
            PopularCursosAtribuidos(_instrutor)
            Return View(_instrutor)
  End Function

  Private Sub PopularCursosAtribuidos(byval _instructor As Instrutor)
            Dim todosCursos = db.Cursos
            Dim instrutorCursos = New HashSet(Of Integer)(_instructor.Cursos.[Select](Function(c) c.CursoID))
            Dim viewModel = New List(Of CursosAtribuidos)()
            For Each _curso In todosCursos
                viewModel.Add(New CursosAtribuidos() With { _
               .CursoID = _curso.CursoID, _
               .Titulo = _curso.Titulo, _
               .Atribuido = instrutorCursos.Contains(_curso.CursoID) _
          })
            Next
            ViewBag.Cursos = viewModel
  End Sub

O código no novo método lê todas as entidades do curso, a fim de carregar uma lista de cursos que utilizam a classe view-model. Para cada curso, o código verifica se o curso existe na propriedade de navegação Cursos do instrutor. Para criar uma pesquisa eficiente quando da verificação se um curso esta atribuido ao instrutor, os cursos designados para o instrutor são colocados em uma coleção HashSet. A propriedade Atribuido dos cursos que são atribuídas ao instrutor é definido como True(verdadeiro). A exibição vai usar essa propriedade para determinar quais as caixas de seleção devem ser exibidas como selecionadas. Finalmente, a lista é passado para a view na propriedade ViewBag.

A seguir vamos adicionar o código o que será executado quando o usuário clicar em Salvar. Substitua o método Edit HttpPost com o código abaixo, que chama de um novo método que atualiza a propriedade de navegação Cursos da entidade Instrutor.

       <HttpPost> _
        Public Function Edit(id As Integer, formCollection As FormCollection, cursosSelecionados As String()) As ActionResult

            Dim _instrutorAtualizar = db.Instrutores.Include(Function(i) i.Diretoria).Include(Function(i) i.Cursos).Where(Function(i) i.InstrutorID = id).Single()

            If TryUpdateModel(_instrutorAtualizar, "", Nothing, New String() {"Cursos"}) Then
                Try
                    If [String].IsNullOrWhiteSpace(_instrutorAtualizar.Diretoria.Localizacao) Then
                        _instrutorAtualizar.Diretoria = Nothing
                    End If

                    AtualizaCursosInstrutores(cursosSelecionados, _instrutorAtualizar)
                    db.Entry(_instrutorAtualizar).State = EntityState.Modified
                    db.SaveChanges()

                    Return RedirectToAction("Index")
                Catch generatedExceptionName As DataException
                    'Log the error 
                    ModelState.AddModelError("", "Não foi possível salvar as alterações. Tente novamente e se o problema persistir contate o suporte.")
                End Try
            End If

            PopularCursosAtribuidos(_instrutorAtualizar)
            Return View(_instrutorAtualizar)

        End Function
 Private Sub AtualizaCursosInstrutores(_cursosSelecionados As String(), _instrutorAtualizar As Instrutor)
            If _cursosSelecionados Is Nothing Then
                _instrutorAtualizar.Cursos = New List(Of Curso)()
                Return
            End If

            Dim _cursoSelecionaedosHS = New HashSet(Of String)(_cursosSelecionados)
            Dim instructorCourses = New HashSet(Of Integer)(_instrutorAtualizar.Cursos.Select(Function(c) c.CursoID))
            For Each course In db.Cursos
                If _cursoSelecionaedosHS.Contains(course.CursoID.ToString()) Then
                    If Not instructorCourses.Contains(course.CursoID) Then
                        _instrutorAtualizar.Cursos.Add(course)
                    End If
                Else
                    If instructorCourses.Contains(course.CursoID) Then
                        _instrutorAtualizar.Cursos.Remove(course)
                    End If
                End If
            Next
        End Sub

vamos entender o código:

Na view /Views/Instrutor/Edit.vbhtml vamos incluir o campo Cursos com um array de caixas de seleção usando o código a seguir depois da div elementos do campo Diretoria:

    <div class="editor-field">
            <table style="width: 100%">
                <tr>
                    @Code
                        Dim cnt as Integer = 0
                        Dim _cursos As List(Of UniversidadeMacoratti.CursosAtribuidos) = ViewBag.Cursos
                        For Each _curso As UniversidadeMacoratti.CursosAtribuidos In _cursos
                            If cnt Mod 3 = 0 Then
                                @:</tr> <tr> 
                            End If
                            cnt += 1
                            @<td> 
                                <input type="checkbox" name="_cursosSelecionados" value=@Html.Raw(("""" & _curso.CursoID & """")) 
                                @IIf(_curso.Atribuido, "checked =""checked""", "") />
                                  @_curso.CursoID &nbsp; @_curso.Titulo
                            </td>
                        Next
                        @:</tr>
                    End Code
            </table>
        </div>

Este código cria uma tabela HTML que tem três colunas. Em cada coluna temos uma caixa de seleção seguida de uma legenda que consiste no número curso e seu título. Todas caixas de seleção têm o mesmo nome ("_cursosSelecinados"), que informa ao model binder que eles devem ser tratados como um grupo. O atributo de valor cada caixa de seleção está definido para o valor de CursoID. Quando a página é postada, o model binder passa uma matriz para o controlador, que consiste dos valores CursoID somente para as caixas de seleção que são selecionadas.

Quando as caixas são inicialmente renderizadas, aquelas que são para os cursos já atribuídos ao professor possui o atributo checked sendo assim marcada.

Depois de alterar as atribuições do curso, queremos poder verificar as alterações quando o site retorna para a página Index. Portanto, precisamos adicionar uma coluna para os cursos para a tabela na página Index.vbhtml conforme abaixo:

 <tr>
        <th>SobreNome</th>
        <th>Nome</th>
        <th>DataAdmissao</th>
        <th>Diretoria</th>
        <th>Cursos</th>
        <th></th>
    </tr>

E ainda temos que incluir um nova célula para exibir os cursos logo após a célula que exibe a Diretoria. Para isso vamos incluir o código abaixo na view Index.vbhtml do Instrutor:

  <td>
     @Code
     For Each _curso As UniversidadeMacoratti.UniversidadeMacoratti.Models.Curso In item.Cursos
         @_curso.CursoID @:&nbsp; @_curso.Titulo <br />
     Next
    End Code
 </td>

Executando o projeto após essas alterações a acionando a página de instrutores teremos o seguinte resultado:

Observe que agora os cursos de cada instrutor são exibidos juntamente com o código de cada curso.

Clicando no link Edit para um instrutor iremos obter:

Vemos a exibição dos cursos sendo que os cursos atribuídos ao instrutor possuem as caixas de seleção marcadas.

Pegue o projeto completo aqui:   UniversidadeMacoratti_6.zip (abra no Visual Studio 2012 Express for web)

1Co 2:14 Ora, o homem natural não aceita as coisas do Espírito de Deus, porque para ele são loucura; e não pode entendê-las, porque elas se discernem espiritualmente.

1Co 2:15 Mas o que é espiritual discerne bem tudo, enquanto ele por ninguém é discernido.

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