.NET MAUI Blazor - CRUD básico com SQLite


  Neste artigo vamos criar uma aplicação híbrida .NET MAUI Blazor e realizar o CRUD básico usando o SQLite.

As aplicações .NET MAUI Blazor são aplicações híbridas que combinam a tecnologia Blazor, que permite a construção de interfaces de usuário interativas usando C# e HTML, com o framework .NET MAUI (Multi-platform App UI), que é uma evolução do Xamarin.Forms e permite a criação de aplicativos multiplataforma.

Com as aplicações .NET MAUI Blazor, você pode criar aplicativos que são executados em várias plataformas, como Windows, macOS, iOS e Android, compartilhando uma base de código comum. Essas aplicações utilizam o modelo de desenvolvimento baseado em componentes do Blazor, onde você pode criar componentes reutilizáveis em C# e marcá-los com sintaxe semelhante ao HTML.

As aplicações .NET MAUI Blazor permitem que você desenvolva a interface do usuário e a lógica de negócios usando a mesma linguagem e tecnologia em todas as plataformas suportadas. Isso significa que você pode escrever a lógica de negócios, manipulação de eventos e acesso a dados em C#, enquanto a interface do usuário é renderizada usando a tecnologia de renderização do Blazor.

Essa abordagem simplifica o desenvolvimento de aplicativos multiplataforma, pois você pode reutilizar a maior parte do código e da lógica de negócios entre as plataformas, reduzindo a duplicação de esforços. Além disso, você pode aproveitar o ecossistema do .NET, incluindo bibliotecas, ferramentas e recursos existentes, ao construir suas aplicações .NET MAUI Blazor.

Para uma introdução ao assunto leia o artigo:
 
.NET MAUI Blazor - Criando aplicações híbridas multiplataforma

Neste artigo vamos criar uma aplicação  .NET MAUI Blazor para realizar o CRUD básico em informações sobre alunos representando pela entidade Aluno onde vamos usar o banco de dados SQLite.

recursos usados:

Criando o projeto

No VS 2022 Community vamos criar um projeto usando o template .NET MAUI Blazor App e nomeá-lo como MauiBlazorAlunos.

Após criar o projeto vamos remover as referências que não vamos usar e a seguir incluir os seguintes pacotes nugets no projeto:

<ItemGroup>
  <PackageReference Include="sqlite-net-pcl" Version="1.8.116" />
  <PackageReference Include="SQLitePCLRaw.bundle_green" Version="2.1.5" />
  <PackageReference Include="SQLitePCLRaw.core" Version="2.1.5" />
  <PackageReference Include="SQLitePCLRaw.provider.dynamic_cdecl" Version="2.1.5" />
  <PackageReference Include="SQLitePCLRaw.provider.sqlite3" Version="2.1.5" />
</ItemGroup>

1- sqlite-net-pcl  versão 1.8.116
2- SQLitePCLRaw.bundle_green
versão 2.1.5
3- SQLitePCL.Raw.core 
versão 2.1.5
4- SQLitePCLRaw.provider.dynamic_cdecl
versão 2.1.5
5- SQLitePCLRaw.provider.sqlite3
versão 2.1.5

Ao usar o SQLite em uma aplicação .NET MAUI, você precisa incluir este cinco  pacotes no seu projeto. Cada pacote desempenha um papel específico no acesso e funcionamento do SQLite no .NET.

  1. sqlite-net-pcl: É uma biblioteca simples e leve que fornece uma API fácil de usar para trabalhar com o SQLite no .NET. Ela oferece recursos para criar, consultar, atualizar e excluir registros em um banco de dados SQLite. É um pacote popularmente usado com o SQLite no ecossistema do .NET.
  2. SQLitePCLRaw.bundle_green: Esse pacote contém implementações de baixo nível para o SQLite, que são compartilhadas entre diferentes plataformas. Ele fornece suporte básico para o SQLite e é usado em conjunto com os pacotes SQLitePCLRaw.core e SQLitePCLRaw.provider.sqlite3 para obter acesso ao SQLite em diferentes plataformas.
  3. SQLitePCLRaw.core: Esse pacote contém a implementação principal do SQLitePCLRaw, que é uma camada de abstração de plataforma para o SQLite. Ele define interfaces e classes básicas para trabalhar com o SQLite e é usado para interagir com a implementação específica da plataforma fornecida pelo pacote SQLitePCLRaw.provider.sqlite3.
  4. SQLitePCLRaw.provider.dynamic_cdecl: Esse pacote contém uma implementação do provedor SQLitePCLRaw que usa a convenção de chamada "cdecl" para interagir com a biblioteca SQLite subjacente. É usado como um provedor para o SQLitePCLRaw e permite a interoperabilidade com a biblioteca SQLite nativa.
  5. SQLitePCLRaw.provider.sqlite3: Esse pacote contém a implementação específica da plataforma do provedor SQLitePCLRaw para a biblioteca SQLite nativa. Ele fornece acesso à biblioteca SQLite subjacente e lida com as operações de baixo nível necessárias para interagir com o SQLite em cada plataforma suportada.

Esses pacotes são necessários para permitir o acesso ao SQLite em uma aplicação .NET MAUI, fornecendo as funcionalidades e as abstrações necessárias para trabalhar com o banco de dados SQLite em diferentes plataformas suportadas.

Criando o domínio e os serviços

Vamos criar na pasta Data a classe Aluno que define o nosso domínio:
 
using SQLite;
using System.ComponentModel.DataAnnotations;

namespace MauiBlazorAlunos.Data;

public
class Aluno
{
   [PrimaryKey, AutoIncrement]
  
public int AlunoId { get; set; }
   [Required]
  
public string Nome { get; set; }
   [Required]
  
public string Email { get; set; }
   [Required]
  
public string Endereco { get; set; }
   [Required]
  
public string Genero { get; set; }
}

Nesta classe estamos usando os atributos da biblioteca SQLite.NET para realizar o mapeamento da classe para a tabela no SQLite

Os atributos mais comuns usados no .NET MAUI para mapear uma classe para tabelas no SQLite são:

- [Table]: Este atributo especifica o nome da tabela no banco de dados.
- [Column]: Este atributo especifica o nome da coluna na tabela.
- [PrimaryKey]: Este atributo especifica que a coluna é a chave primária da tabela.
- [NotNull]: Este atributo especifica que a coluna não pode ser nula.
- [ForeignKey]: Este atributo especifica que a coluna é uma chave estrangeira para outra tabela.
- [Unique]: Este atributo especifica que a coluna deve ter valores únicos.
- [AutoIncrement]: Este atributo especifica que a coluna deve ter um valor incrementado automaticamente.

A seguir vamos criar uma pasta Services no projeto onde vamos definir o serviço criando a interface IAlunoService e a classe AlunoService onde vamos implementar as operações de acesso aos dados usando o SQLite:

1- IAlunoService

public interface IAlunoService
{
  Task InitializeAsync();
  Task<List<Aluno>> GetAlunos();
  Task<Aluno> GetAlunoById(
int alunoId);
  Task<
int> AddAluno(Aluno aluno);
  Task<
int> UpdateAluno(Aluno aluno);
  Task<
int> DeleteAluno(Aluno aluno);
}

2- AlunoService

using MauiBlazorAlunos.Data;
using
SQLite;

namespace MauiBlazorAlunos.Services;

public class AlunoService : IAlunoService
{
 
private SQLiteAsyncConnection _dbConnection;

 
public async Task InitializeAsync()
  {
    
await SetUpDb();
  }

  private async Task SetUpDb()
  {
    
if (_dbConnection == null)
     {
      
string dbPath = Path.Combine(Environment.GetFolderPath
       Environment.SpecialFolder.LocalApplicationData),
"Aluno.db3");
       _dbConnection =
new SQLiteAsyncConnection(dbPath);
      
await _dbConnection.CreateTableAsync<Aluno>();
     }
  }

  public async Task<int> AddAluno(Aluno aluno)
  {
    
return await _dbConnection.InsertAsync(aluno);
  }
 

  public
async Task<int> DeleteAluno(Aluno aluno)
  {
   
return await _dbConnection.DeleteAsync(aluno);
  }

  public async Task<int> UpdateAluno(Aluno aluno)
  {
   
return await _dbConnection.UpdateAsync(aluno);
  }
 
 
public async Task<List<Aluno>> GetAlunos()
  {
   
return await _dbConnection.Table<Aluno>().ToListAsync();
  }

 
public async Task<Aluno> GetAlunoById(int alunoId)
  {
   
var aluno= await _dbConnection.QueryAsync<Aluno>($"Select
               * From
{nameof(Aluno)} where AlunoID={alunoId} ");

    return aluno.FirstOrDefault();
  }
}

 

Nesse código , o caminho do banco de dados é definido usando Environment.GetFolderPath para obter o diretório especial LocalApplicationData, que é usado para armazenar dados específicos do aplicativo. Em seguida, é definido o nome do arquivo do banco de dados ("aluno.db3") e o caminho completo do banco de dados é obtido usando Path.Combine.

Temos o método InitializeAsync() que será responsável por configurar o banco de dados. O método SetUpDb() foi alterado para ser assíncrono e retornar uma Task, permitindo o uso de await para aguardar a criação da tabela.

Dessa forma, ao utilizar o serviço, você poderá chamar explicitamente o método InitializeAsync() para realizar a inicialização do banco de dados de forma assíncrona.

Agora temos que registrar o serviço criado na classe MainProgram:

builder.Services.AddSingleton<IAlunoService, AlunoService>();

Criando as páginas Razor

Vamos criar as páginas Razor na pasta Page mas antes vamos ajustar o código do arquivo NavMenu.razor da pasta /Shared criando os seguintes links no menu : Home , Alunos e Manutenção :

...
<
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="alunos">
    <
span class="oi oi-list" aria-hidden="true"></span> Alunos
 
</NavLink>
</
div>
<
div class="nav-item px-3">
  <
NavLink class="nav-link" href="add_aluno">
    <
span class="oi oi-dashboard" aria-hidden="true"></span> Manutenção
</NavLink>
</
div>
...

A seguir vamos definir o código a seguir no arquivo _Imports.razor :

@using System.Net.Http
@
using Microsoft.AspNetCore.Components.Forms
@
using Microsoft.AspNetCore.Components.Routing
@
using Microsoft.AspNetCore.Components.Web
@
using Microsoft.AspNetCore.Components.Web.Virtualization
@
using Microsoft.JSInterop
@
using MauiBlazorAlunos
@
using MauiBlazorAlunos.Shared
@
using MauiBlazorAlunos.Data
@
using MauiBlazorAlunos.Services

Na página Index.razor vamos exibir uma imagem  alunos.png que esta contida na pasta wwwroot/images:

@page "/"

<h3>Alunos - 2023</h3>
<
center>
  <
img src="images/alunos.png" height="250" width="350"/>
</
center>
 

Vamos criar o componente Alunos.razor que vai exibir a relação de alunos existentes com opção para Deletar ou Editar os dados de um aluno selecionado :

@page "/alunos"

@inject IAlunoService alunoService
@inject NavigationManager navigation

<h1>Alunos</h1>

@if (alunos == null)
{
    <p><em>Carregando...</em></p>
}
else
{
    <div class="table-responsive">
        <table class="table">
            <thead>
                <tr>
                    <th>Nome</th>
                    <th>Email</th>
                    <th>Gênero</th>
                    <th>Endereco</th>
                </tr>
            </thead>
            <tbody>
                @foreach (var aluno in alunos)
                {
                    <tr>
                        <td>@aluno.Nome</td>
                        <td>@aluno.Email</td>
                        <td>@aluno.Genero</td>
                        <td>@aluno.Endereco</td>
                        <td>
                            <button type="submit" @onclick="@(()=> EditaAluno(aluno.AlunoId))"
                                                 class="btn btn-success">Edita</button>
                        </td>
                        <td>
                            <button type="submit" @onclick="@(()=> DeletaAluno(aluno))"
                                                  class="btn btn-danger">Deleta</button>
                        </td>
                    </tr>
                }
            </tbody>
        </table>
    </div>

}

@code {
    private List<Aluno> alunos;

    protected override async Task OnInitializedAsync()
    {
        await alunoService.InitializeAsync();
        alunos = await alunoService.GetAlunos();
    }

    private void EditaAluno(int alunoId)
    {
        navigation.NavigateTo($"update_aluno/{alunoId}");
    }

    private async void DeletaAluno(Aluno aluno)
    {
        var response = await alunoService.DeleteAluno(aluno);
        if (response > 0)
        {
            await OnInitializedAsync();
            this.StateHasChanged();
        }
    }
}

No componente Manutencao.razor temos o código que permite incluir um novo aluno ou excluir um aluno selecionado:

@page "/add_aluno"
@page "/update_aluno/{AlunoId:int}"

@inject IAlunoService alunoService
@inject NavigationManager navigation

<h3>Manutenção</h3>

<div class="form-group">
    <label>Nome</label>
    <input @bind="Nome" class="form-control" placeholder="Nome">
</div>

<div class="mt-2 form-group">
    <label>Endereço</label>
    <input @bind="Endereco" class="form-control" placeholder="Endereco">
</div>

<div class="mt-2 form-group">
    <label>Email</label>
    <input @bind="Email" type="email" class="form-control" placeholder="Email">
</div>

<div class="mt-2 form-group">
    <label>Gênero</label>
    <div class=" d-flex flex-row">
        <div class="col-6 d-flex justify-content-between">
            <div class="form-check">
                <input checked="@(Genero=="Masculino")" @onchange="@(()=> SetGenero("Masculino"))"
                                         class="form-check-input" type="radio" name="flexRadioDefault">
                <label class="form-check-label" for="flexRadioDefault1">
                    Masculino
                </label>
            </div>
            <div class="form-check">
                <input checked="@(Genero=="Feminino")" @onchange="@(()=> SetGenero("Feminino"))"
                                         class="form-check-input" type="radio" name="flexRadioDefault">

                <label class="form-check-label" for="flexRadioDefault2">
                    Feminino
                </label>
            </div>
        </div>
    </div>
</div>

 <button type="submit" @onclick="Manutencao" class="mt-2 btn btn-primary">Enviar</button>
 <button type="submit" @onclick="Retornar" class="mt-2 btn btn-dark">Retornar</button>

@code {

    [Parameter]
    public int AlunoId { get; set; }

    private string Nome;
    private string Endereco;
    private string Email;
    private string Genero;

    private void SetGenero(string genero)
    {
        this.Genero = genero;
    }

    protected async override Task OnInitializedAsync()
    {
        if (AlunoId > 0)
        {
            await alunoService.InitializeAsync();
            var response = await alunoService.GetAlunoById(AlunoId);
            if (response != null)
            {
                Nome = response.Nome;
                Endereco = response.Endereco;
                Email = response.Email;
                Genero = response.Genero;
            }
        }
    }

    private async Task Manutencao()
    {
        if (await Valida())
        {
            var aluno = new Aluno
                {
                    Nome = Nome,
                    Endereco = Endereco,
                    Email = Email,
                    Genero = Genero,
                    AlunoId = AlunoId
                };

            int response = -1;
            if (AlunoId > 0)
            {
                response = await alunoService.UpdateAluno(aluno);
            }
            else
            {
                response = await alunoService.AddAluno(aluno);
            }

            if (response > 0)
            {
                Nome = Endereco = Genero = Email = string.Empty;
                this.StateHasChanged();
                await App.Current.MainPage.DisplayAlert("Incluir Aluno",
                "Aluno salvo com sucesso", "OK");
            }
            else
            {
                await App.Current.MainPage.DisplayAlert("Opa!!!",
               "Algo deu errado ao incluir o aluno", "OK");
            }
        }
    }

    private void Retornar()
    {
        navigation.NavigateTo($"/");
    }

    public async Task<bool> Valida()
    {
        if (string.IsNullOrEmpty(Nome))
        {
            await App.Current.MainPage.DisplayAlert("Opa!!!",
            "O nome é obrigatório", "OK");
            return false;
        }

        if (string.IsNullOrEmpty(Email))
        {
            await App.Current.MainPage.DisplayAlert("Opa!!!",
            "O email é obrigatório", "OK");
            return false;
        }

        if (string.IsNullOrEmpty(Endereco))
        {
            await App.Current.MainPage.DisplayAlert("Opa!!!",
            "O endereço é obrigatório", "OK");
            return false;
        }

        if (string.IsNullOrEmpty(Genero))
        {
            await App.Current.MainPage.DisplayAlert("Opa!!!",
            "O gênero é obrigatório", "OK");
            return false;
        }
        return true;
    }
}

Neste código basicamente criamos um formulário sem usar o EditForm para que os dados de um aluno possam ser informados e definimos a validação no método Valida().

Executando o projeto em um emulador Android teremos o seguinte resultado:

1- A tela inicial

2- A exibição dos itens do menu

3- A exibição da lista de alunos

4- A tela onde podemos incluir ou editar os dados do aluno

Pegue o código do projeto aqui: MauiBlazorAlunos.zip

"Mas agora, morrendo para aquilo que antes nos prendia, fomos libertados da lei, para que sirvamos conforme o novo modo do Espírito, e não segundo a velha forma da lei escrita."
Romanos 7:6

Referências:


José Carlos Macoratti