Blazor - CRUD com EF Core e Modals - I
 Neste post vamos iniciar uma série de tutoriais para mostrar como criar uma aplicação Blazor Server e realizar o CRUD usando o EF Core e outros recursos no ambiente do  .NET 6 e Net 7.


Vamos criar uma aplicação Blazor Server e gerenciar informações sobre tarefas do dia a dia que desejamos controlar e realizar a manutenção dessas informações fazendo um CRUD.

 


 

 


O objetivo é recordar como usar os seguintes recursos com o Blazor no .NET 6/7:

Como pré-requisitos temos

Assim, vamos ao trabalho...

Criando o projeto Blazor Server

No VS 2022 vamos criar um projeto Blazor usando o template abaixo com o nome BlazorTarefas :

 


Após criar o projeto vamos remover os componentes e arquivos que não vamos usar.


Vamos incluir no projeto os seguintes pacotes Nuget:

Para incluir você pode usar o menu Tools -> ...-> Manage Nuget Package for Solutions usando a guia Browse.

 

Vamos criar no projeto a pasta Data e criar a classe Tarefa que é o nosso domínio :

 

public class Tarefa
{
    [Key]
    public int Id { get; set; }

    [Required]
    [StringLength(80)]

    public string? Nome { get; set; }

    [Required]
    [StringLength(100)]

    public string? Status { get; set; }

    [Required]
    public DateTime ConclusaoEm { get; set; }
}

Vamos criar na pasta Data o arquivo  de contexto  ApplicationDbContext que faz o mapeamento ORM entre a entidade Tarefa e a tabela Tarefas no SQL Server:

using Microsoft.EntityFrameworkCore;

namespace BlazorTarefas.Data;

public class ApplicationDbContext : DbContext
{

    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options)
    {
    }

    public DbSet<Tarefa>? Tarefas { get; set; }

    public override int SaveChanges()
    {
        return base.SaveChanges();
    }
}

No arquivo appsettings.json defina a string de conexão com a instância do seu SQL Server local :

{
  "ConnectionStrings": {
    "DefaultConnection": "Data Source=.;Initial Catalog=BlazorTarefasDB;Integrated Security=True"
  },
...

Definimos o nome do banco de dados como BlazorTarefasDB usando a autenticação do Windows.

Não podemos esquecer de registrar o contexto no container DI na classe Program:

...
// Add services to the container.
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");

builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
...

Agora para aplicar o Migrations e gerar o banco e as tabelas podemos usar os comandos:

Criando o serviço para acessar os dados

Para simplificar o projeto não irei criar um repositório e um serviço contendo a lógica da aplicação que consome este repositório.

 

Vou criar o serviço para acessar os dados que vai atuar como um repositório. (Em uma aplicação mais complexa não é aconselhável fazer isso)

 

Assim, crie no projeto a pasta Services e nesta pasta crie a interface ITarefaService e sua implementação na classe concreta TarefaService :

 

public interface ITarefaService
{
    Task<List<Tarefa>> Get();
    Task<Tarefa> Get(int id);
    Task<Tarefa> Add(Tarefa tarefa);
    Task<Tarefa> Update(Tarefa tarefa);
    Task<Tarefa> Delete(int id);
}

 

Classe TarefaService :
 

using BlazorTarefas.Data;
using Microsoft.EntityFrameworkCore;

namespace BlazorTarefas.Services;

public class TarefaService : ITarefaService
{
    private readonly ApplicationDbContext _context;

    public TarefaService(ApplicationDbContext context)
    {
        _context = context;
    }

    public async Task<Tarefa> Add(Tarefa tarefa)
    {
        _context.Tarefas.Add(tarefa);
        await _context.SaveChangesAsync();
        return tarefa;
    }

    public async Task<Tarefa> Delete(int id)
    {
        var tarefa = await _context.Tarefas.FindAsync(id);
        _context.Tarefas.Remove(tarefa);
        await _context.SaveChangesAsync();
        return tarefa;
    }

    public async Task<List<Tarefa>> Get()
    {
        return await _context.Tarefas.ToListAsync();
    }

    public async Task<Tarefa> Get(int id)
    {
        var tarefa = await _context.Tarefas.FindAsync(id);
        return tarefa;
    }

    public async Task<Tarefa> Update(Tarefa tarefa)
    {
        _context.Entry(tarefa).State = EntityState.Modified;
        await _context.SaveChangesAsync();
        return tarefa;
    }
}

Agora vamos registrar este serviço no container DI na classe Program:

...

builder.Services.AddTransient<ITarefaService, TarefaService>();

var app = builder.Build();
...

Pronto ! já podemos realizar operações com tarefas.   

Blazor - Exibindo as tarefas

Agora podemos criar na pasta Pages o componente razor Tarefas.razor onde vamos definir o código para exibir as tarefas cadastradas e permitir a criação, edição e exclusão de tarefas.

@page "/tarefas"

@inject ITarefaService service
@*@inject IJSRuntime js;*@

<h3>Lista de Tarefas</h3>

@if (tarefas == null)
{
    <p><em>Carregando...</em></p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Tarefa</th>
                <th>Status</th>
                <th>Previsão</th>
                <th>Edita</th>
                <th>Deleta</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var tarefa in tarefas)
            {
                <tr>
                    <td>@tarefa.Nome</td>
                    <td>@tarefa.Status</td>
                    <td>@tarefa.ConclusaoEm.ToShortDateString()</td>
                    <td><input type="button" class="btn btn-primary" value="Edita" /></td>
                    <td><input type="button" class="btn btn-danger"  value="Deleta" /></td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    List<Tarefa> tarefas;

    protected override async Task OnInitializedAsync()
    {
        tarefas = await service.Get();
    }

}

Este código basicamente define uma tabela com dois botões de comando e no método OnInitializedAsync invoca o método Get do Serviço obtendo uma lista de tarefas que vamos exibir na interface.

Antes de executar vamos ajustar o componente Index.razor para exibir uma imagem:

@page "/"

<PageTitle>Index</PageTitle>

<h2>Tarefas</h2>
<img src="/images/tarefas2.jpg" class="img-fluid" alt="Imagem responsiva">

 

E no componente NavMenu.razor vamos criar apenas duas opções no menu :

...
<
div class="nav-item px-3">
   <
NavLink class="nav-link" href="" Match="NavLinkMatch.All">
       <
span class="oi oi-home" aria-hidden="true"></span> Home
   
</NavLink>
</
div>

<div class="nav-item px-3">
    <
NavLink class="nav-link" href="tarefas">
       <
span class="oi oi-plus" aria-hidden="true"></span> Tarefas
   
</NavLink>
</
div>
...

Executando o projeto teremos:

As duas tarefas que estamos exibindo foram incluidas diretamente na tabela Tarefas no SQL Server.

Componentes Child

Antes de prosseguirmos com as implementações das tarefas do CRUD, precisamos conhecer os componentes filhos. Os aplicativos Blazor são baseados em componentes que são blocos de construção reutilizáveis, e podem ser um controle individual ou um bloco com vários controles. Essas classes de componentes são escritas em razor markup.

Os componentes podem incluir outros componentes e podemos adicionar um componente dentro de outros usando o nome do componente em uma sintaxe HTML. Vamos usar este conceito para criar modais de bootstrap como componentes filhos para as caixas de diálogo Adicionar/Editar e Confirmar.

Criando o componente TarefaDetalhes

Vamos criar o componente filho TarefaDetalhes onde vamos usar os recursos do EditForm do Blazor para definir um formulário onde o usuário vai poder interagir com a interface incluindo e alterando dados das tarefas.

Assim podemos definir um formulário em um aplicativo Blazor usando o componente "EditForm".

O mecanismo Blazor valida apenas o valor da propriedade do modelo de entrada definido no componente "EditForm", e para isso o Blazor fornece um componente DataAnnotationsValidator que informa ao mecanismo Blazor para validar um modelo usando Data Annotations.

O Blazor fornece dois componentes para exibir erros de validação de modelo na tela.

  1. ValidationSummary - Resume as mensagens de validação;
  2. ValidationMessage - Exibe as mensagens de erro para uma entrada específica no formulário;

O componente Blazor EditForm fornece retorno de chamada de evento OnValidSubmit que é acionado quando um formulário é enviado com êxito sem nenhum erro de validação. O retorno de chamada do evento OnInvalidSubmit é acionado quando um formulário é enviado com um erro de validação.

O Blazor também fornece um conjunto de componentes de entrada integrados que recebem e validam a entrada do usuário. Esses componentes validam o conteúdo quando são alterados ou quando um formulário é enviado.

A seguir temos os componentes de entradas integrados ao EditForm :

Componente Renderiza como
 InputText  <input>
 InputCheckbox  <input type="checkbox">
 InputNumber  <input type="number">
 InputTextArea  <textarea>
 InputSelect  <select>
 InputDate  <input type="date">

Como o componente EditForm renderiza um elemento HTML <form> padrão, é realmente possível usar elementos de formulário HTML padrão, como <input> e <select>, em nossa marcação, mas, como no componente EditForm, é recomendado usar os vários Controles de entrada do Blazor, porque eles vêm com funcionalidades adicionais, como validação.

Assim vamos criar na pasta Pages o componente TarefaDetalhes.razor :

@inject ITarefaService service

<div class="modal" tabindex="-1" role="dialog" id="tarefaModal">
    <div class="modal-dialog" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title">@Cabecalho</h5>
                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                    <span aria-hidden="true">×</span>
                </button>

            </div>
            <div class="modal-body">
                <EditForm Model="@TarefaObj" OnValidSubmit="@HandleValidSubmit">
                    <div class="form-group mt-2 mb-2">
                        <label for="Nome">Nome</label>
                        <input type="hidden" @bind-value="@TarefaObj.Id" />
                        <InputText id="name" class="form-control"
                                                @bind-Value="@TarefaObj.Nome" />

                    </div>
                    <div class="form-group mb-2">
                        <label for="status">Status</label>
                        <InputSelect id="Summary" class="form-control"
                                                   @bind-Value="TarefaObj.Status">
                            <option value="">Selecione</option>
                            @foreach (var status in TarefaStatus)
                            {
                                    <option value="@status">
                                        @status
                                    </option>
                            }

                        </InputSelect>
                    </div>
                    <div class="form-group mb-3 mt-3">
                        <label for="ConclusaoEm">Conclusão em : </label>
                        <input type="date" id="addition" name="math"
                                              @bind-value="@TarefaObj.ConclusaoEm" />

                    </div>
                    <button type="submit" class="btn btn-primary">Envia</button>
                    <button type="button" class="btn btn-secondary"
                                                data-dismiss="modal">Cancela</button>
                </EditForm>
            </div>
        </div>
    </div>
</div>

@code {
    [Parameter]
    public Tarefa TarefaObj { get; set; }

    [Parameter]
    public RenderFragment Cabecalho { get; set; }

    [Parameter]
    public Action AlteracaoDados { get; set; }

    List<string> TarefaStatus = new List<string>() { "Nova", "Em andamento", "Concluída" };

    private async void HandleValidSubmit()
    {
    }
}

No código acima, temos um formulário definido usando o componente EditForm que tem um modelo TarefaObj que é passado do componente pai (Tarefas). As propriedades do modelo são vinculadas aos controles de entrada usando o bind-value e o método HandleValidSubmit é acionado quando o formulário é enviado com sucesso.

A seguir vamos ajustar o código do componente Tarefas.razor e declarar o componente filho TarefasDetalhes dentro do componente Tarefas e passando inicialmente um objeto Tarefa vazio. Vamos definir também um cabeçalho para exibir na operação :

@page "/tarefas"

@inject ITarefaService service

<h3>Lista de Tarefas</h3>

@if (tarefas == null)
{
    <p><em>Carregando...</em></p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Tarefa</th>
                <th>Status</th>
                <th>Previsão</th>
                <th>Edita</th>
                <th>Deleta</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var tarefa in tarefas)
            {
                <tr>
                    <td>@tarefa.Nome</td>
                    <td>@tarefa.Status</td>
                    <td>@tarefa.ConclusaoEm.ToShortDateString()</td>
                    <td><input type="button" class="btn btn-primary" value="Edita" /></td>
                    <td><input type="button" class="btn btn-danger"  value="Deleta" /></td>
                </tr>
            }
        </tbody>
    </table>

<div>
   <
input type="button" data-toggle="modal" data-target="#tarefaModal"
     
class="btn btn-primary" value="Nova Tarefa"
                        @
onclick="(() => InitializeTarefaObject())" />
</
div>

<
TarefaDetalhes TarefaObj=tarefaObject></TarefaDetalhes>

}

@code {
    List<Tarefa> tarefas;
    Tarefa tarefaObject = new Tarefa();
    string cabecalho = string.Empty;

    protected override async Task OnInitializedAsync()
    {
        tarefas = await service.Get();
    }
 
   private void InitializeTarefaObject()
    {
        tarefaObject = new Tarefa();
        tarefaObject.ConclusaoEm = DateTime.Now;
        cabecalho = "Nova Tarefa";
    }

   
}

Observe que incluímos a <div> onde temos um botão - Nova Tarefa -  que ao ser clicado aciona o método InitializedTarefaObject que cria um novo objeto Tarefa vazio.

Note também que incluimos a tag <TarefaDetalhes ...> para exibir os detalhes do componente filho.

Executando o projeto novamente e agora clicando no botão Nova Tarefa teremos o resultado a seguir:

Muuito bom !!!

Na proxima parte do artigo vamos continuar realizando a validação da entrada do usuário, salvar os dados no banco de dados e atualizar os dados na página para exibir um novo registro. ...

"Porque os que são segundo a carne inclinam-se para as coisas da carne; mas os que são segundo o Espírito para as coisas do Espírito. Porque a inclinação da carne é morte; mas a inclinação do Espírito é vida e paz."
Romanos 8:5,6

Referências:


José Carlos Macoratti