Xamarin Forms - SQLite : CRUD com EF Core 3.1 - I


Neste artigo vamos usar os recursos do EF Core 3.1 para acessar dados no SQLite em uma aplicação Xamarin Forms e realizar um CRUD básico usando o Visual Studio 2019 e a linguagem C#.

Já estamos na versão 4.3 do Xamarin Forms e na versão 3.1 do Entity Framework Core e com o .NET Standard 2.0 padronizado nos projetos Xamarin.

Hoje veremos como realizar um CRUD básico usando o EF Core e o padrão MVVM onde teremos uma camada ViewModel distinta que não possui dependências de sua interface de usuário.

Nota: Este artigo e o projeto exemplo foram adaptados deste artigo original: https://almirvuk.blogspot.com/2017/08/xamarinforms-and-entity-framework-core.html

Esta arquitetura em si é otimizada para testes de unidade, bem como para o desenvolvimento multiplataforma.

A seguir um roteiro básico a seguir para preparar o projeto:

1 - Instalar o .NET Core SDK  3.1 (ou superior);
2 - Definir a class library para usar o .NET Standard 2.0;
3 - Instalar o pacote  Microsoft.EntityFrameworkCore.Sqlite em todos os projetos; (Isso instala todas as dependências)
4 - Definir o modelo de entidades;
5 - Definir a classe de contexto que herda de DbContext;
6 - Definir o provedor do banco de dados Sqlite e a string de conexão;

Você deve obter o caminho do banco de dados SQLite em cada plataforma. Segue abaixo a sintaxe usada para cada plataforma:

// Android
var dbPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), "banco.db");

// iOS
var dbPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "..", "Library", "banco.db")

// UWP
var dbPath = Path.Combine(Windows.Storage.ApplicationData.Current.LocalFolder.Path, "banco.db");

 

Além disso no iOS você deve incluir o código abaixo  no arquivo AppDelegate.cs :

public override bool FinishedLaunching(UIApplication app, NSDictionary options) 
{
            SQLitePCL.Batteries.Init();
            global::Xamarin.Forms.Forms.Init();
            LoadApplication(new App());
            return base.FinishedLaunching(app, options);
}

Agora vamos ao projeto.

Recursos usados:

Criando o projeto no Visual Studio 2019 Community

Abra o VS 2019 Community e clique em Create a New Project;

A seguir selecione :

Selecione o template:  Mobile App (Xamarin.Forms) e clique em Next;

A seguir informe o local e o nome do projeto : XF_Crud1

A seguir selecione o template Blank e as plataformas que deseja usar. Eu vou usar somente o Android:

Clique no botão OK.

Pronto, nosso projeto já esta criado.

Criando a estrutura do projeto

Vamos criar no projeto pastas para organizar o código do projeto. Assim crie na raiz do projeto as pastas:

A seguir temos a estrutura do projeto após a criação destas pastas:

Incluindo pacote do EF Core no projeto

Agora vamos incluir uma referência ao pacote do Entity Frameworkcore para o Sqlite.

No menu Tools clique em Nuget Package Manager -> Manage Nuget Packages for Solution e selecione o pacote Microsoft.EntityFrameworkCore.Sqlite:

Selecione todos os projetos e clique no botão Install.

Criando as classes de suporte

Na pasta Helpers vamos criar as classes que iremos usar em nosso projeto

A primeira é a classe BaseViewModel que será herdada por todas as classes das nossas view models :

using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace XF_Crud1.Helpers
{
    public class BaseViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        protected void OnPropertyChanged([CallerMemberName]string propertyName = "") =>
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

 

Esta classe herda de INotifyPropertyChanged e implementa o método OnPropertyChanged.

A interface INotifyPropertyChanged proporciona um mecanismo unificado para definir em um único evento as PropertyChanged que queremos definir em nosso objetos.  Quando criamos um objeto, um controle ou um componente, geralmente implementamos um sistema para notificar o cliente (aquele que vai usar o controle/objeto) quando alguma de suas propriedades for alterada.

Isso é feito escrevendo eventos do tipo OnPropertyChanged para cada propriedade passível de alteração.

A seguir criaremos a interface IDBPath onde definimos o método GetDbPath():

    public interface IDBPath
    {
        string GetDbPath();
    }

Usaremos essa interface para obter o caminho do banco de dados no ambiente Android.

Definindo o modelo de domínio da aplicação : Time e Jogador

Vamos criar uma aplicação para gerenciar informações de times e jogadores, logo vamos definir uma classe chamada Time e outra Jogador que farão parte do nosso modelo de domínio.

Na pasta Models crie as classes Time e Jogador com o código a seguir:

    public class Time
    {
        public int TimeId { get; set; }
        public string Nome { get; set; }
        public string Treinador { get; set; }
        public string Cidade { get; set; }
        public string Estadio { get; set; }
    }
    public class Jogador
    {
        public int JogadorId { get; set; }
        public string Nome { get; set; }
        public string Posicao { get; set; }
        public int TimeId { get; set; }
        public virtual Time Time { get; set; }
    }

Criando a pasta Helpers no projeto Android e a classe que implementa IDBPath

No projeto Android - XF_Crud1.Android vamos criar uma pasta Helpers e nesta pasta criar a classe DatabasePath que implementa a interface IDBPath e o método GetDbPath() que retorna o caminho do banco de dados SQLite para o ambiente Android:

 public class DatabasePath : IDBPath
    {
        public DatabasePath()
        {}
        public string GetDbPath()
        {
            return Path.Combine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal), "EFCoreDB.db");
        }
    }

Definindo a classe de contexto : AppDbContext

A seguir vamos criar na pasta DataAccess a classe de contexto: AppDbContext com o seguinte código:

    public class AppDbContext : DbContext
    {
        public DbSet<Time> Times { get; set; }
        public DbSet<Jogador> Jogadores { get; set; }
        public AppDbContext()
        {
            this.Database.EnsureCreated();
        }
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            var dbPath = DependencyService.Get<IDBPath>().GetDbPath();
            optionsBuilder.UseSqlite($"Filename={dbPath}");
        }
    }

Na classe de contexto que herda de DbContext definimos o mapeamento das entidades para as tabelas no banco de dados.

No método OnConfiguring() definimos o provedor do banco de dados usado e definimos a string de conexão com o banco de dados. Aqui usamos a classe DependencyService para obter o caminho local do banco de dados no ambiente Android para o SQLite. Esta classe é um localizador de serviço que permite que os aplicativos Xamarin.Forms invoquem a funcionalidade de plataforma nativa de um código compartilhado.

No construtor da classe estamos chamando o método chamado EnsureCreated.  Ele garante que o banco de dados para o contexto exista. Se existir, nenhuma ação será tomada. Se não existir, o banco de dados e todo o seu esquema serão criados. Se o banco de dados existir, não será feito nenhum esforço para garantir que seja compatível com o modelo para este contexto.

Definindo as ViewModels e as Views

A seguir vamos iniciar a criação das ViewModels e das Views em nosso projeto.

Iremos criar as seguintes ViewModels na pasta ViewModels :

Cada viewmodel vai herdar da classe BaseViewModel

E as seguintes Views na pasta Views:

Note que para cada ViewModel temos uma View relacionada.

Em cada ViewModel vamos usar a interface ICommand que permite que a ligação de dados ou databinding realize chamada de métodos na ViewModel diretamente a partir de um Button ou TapGestureRecognizer e outros elementos através do recurso conhecido como commanding.

Na respectiva view iremos realizar o databinding com a respectiva ViewModel.

Exibindo os dados na ListView : TimeListPage.xaml e TimeListViewModel.cs

Como nosso objetivo é exibir uma lista de times vamos começar criando a view TimeListPage.xaml :

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:d="http://xamarin.com/schemas/2014/forms/design"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    x:Class="XF_Crud1.Views.TimeListPage"
    Title="Times" >  
    <ContentPage.ToolbarItems>
        <ToolbarItem Text="Novo Time"
                     Icon="ic_add.png"
                     Command="{Binding AddTimeCommand}" />
    </ContentPage.ToolbarItems>
    <ContentPage.Content>
        <ListView ItemsSource="{Binding TodosTimes}"
                  HasUnevenRows="True"
                  ItemTapped="OnTeamTapped"
                  BackgroundColor="#f5f5f5">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <ViewCell>
                        <Grid BackgroundColor="White"
                              Margin="4">
                            <Grid.RowDefinitions>
                                <RowDefinition Height="Auto" />
                                <RowDefinition Height="Auto" />
                                <RowDefinition Height="Auto" />
                            </Grid.RowDefinitions>
                            <Label Grid.Row="0"
                                   Text="{Binding Nome}"
                                   FontSize="Medium"
                                   Margin="4"
                                   FontAttributes="Bold" />
                            <StackLayout Orientation="Horizontal"
                                         Grid.Row="1"
                                         Margin="4"
                                         Padding="2">
                                <Label Text="{Binding Estadio}"
                                       FontSize="Small" />
                                <Label Text="{Binding Cidade}"
                                       FontSize="Small" />
                            </StackLayout>
                        </Grid>
                    </ViewCell>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </ContentPage.Content>
</ContentPage>

A seguir vamos criar a ViewModel TimeListViewModel.cs :

using System.Collections.ObjectModel;
using System.Linq;
using System.Windows.Input;
using Xamarin.Forms;
using XF_Crud1.DataAccess;
using XF_Crud1.Helpers;
using XF_Crud1.Models;
using XF_Crud1.Views;
namespace XF_Crud1.ViewModels
{
    public class TimeListViewModel : BaseViewModel
    {
        public ObservableCollection<Time> TodosTimes { get; set; }
        public ICommand AddTimeCommand { get; private set; }
        private AppDbContext _context;
        public TimeListViewModel()
        {
            _context = new AppDbContext();

            var timeLista = _context.Times.ToList();

            TodosTimes = new ObservableCollection<Time>(timeLista);

            AddTimeCommand = new Command(async () =>
               await Application.Current.MainPage.Navigation.PushAsync(new AddTimePage()));
        }
    }
}

Neste código temos uma lista de times vinculada a uma ObservableCollection do tipo Time que estamos preenchendo com uma lista de times do banco de dados a partir do construtor onde precisamos inicializar o objeto AppDbContext e usando a instância da classe de contexto obtemos todos os times.

Observe a definição da interface ICommand definindo a propriedade AddTimeCommand() que irá acionar a view AddTimePage.

Adicionando um novo Time: AddTimePage.xaml e AddTimeViewModel.cs

Para incluir um novo time vamos criar a view AddTimePage.xaml:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
     xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
     xmlns:d="http://xamarin.com/schemas/2014/forms/design"
     xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
     mc:Ignorable="d"
    x:Class="XF_Crud1.Views.AddTimePage"
    Title="Incluir Time">

    <ContentPage.ToolbarItems>
        <ToolbarItem Text="Salvar"
                     Icon="ic_save.png"
                     Command="{Binding SaveTimeCommand}" />
    </ContentPage.ToolbarItems>
    <ContentPage.Content>
        <StackLayout>
            <Entry Placeholder="Nome do time"
                   Text="{Binding Nome}"
                   Margin="4" />
            <Entry Placeholder="Nome do treinador"
                   Text="{Binding Treinador}"
                   Margin="4" />
            <Entry Placeholder="Nome da cidade"
                   Text="{Binding Cidade}"
                   Margin="4" />
            <Entry Placeholder="Nome do estádio"
                   Text="{Binding Estadio}"
                   Margin="4" />
        </StackLayout>
    </ContentPage.Content>
</ContentPage>

A seguir a view model AddTimeViewModel :

using System.Windows.Input;
using Xamarin.Forms;
using XF_Crud1.DataAccess;
using XF_Crud1.Helpers;
using XF_Crud1.Models;
namespace XF_Crud1.ViewModels
{
    public class AddTimeViewModel : BaseViewModel
    {
        private string nome;
        public string Nome
        {
            get { return nome; }
            set
            {
                nome = value;
                OnPropertyChanged();
            }
        }
        private string treinador;
        public string Treinador
        {
            get { return treinador; }
            set
            {
                treinador = value;
                OnPropertyChanged();
            }
        }
        private string cidade;
        public string Cidade
        {
            get { return cidade; }
            set
            {
                cidade = value;
                OnPropertyChanged();
            }
        }
        private string estadio;
        public string Estadio
        {
            get { return estadio; }
            set
            {
                estadio = value;
                OnPropertyChanged();
            }
        }
        public ICommand SaveTimeCommand { get; private set; }
        private AppDbContext _context;
        public AddTimeViewModel()
        {
            _context = new AppDbContext();
            SaveTimeCommand = new Command(SaveTime);
        }
        async void SaveTime()
        {
            Time time = new Time
            {
                Cidade = Cidade,
                Estadio = Estadio,
                Nome = Nome,
                Treinador = Treinador
            };
            _context.Times.Add(time);
            _context.SaveChanges();
            await Application.Current.MainPage.Navigation.PopAsync();
        }
    }
}

Exibindo detalhes de um time: TimeDetailsPage.xaml e TimeDetailsViewModel.cs

Vamos criar a view TimeDetailsPage para exibir os detalhes dos times e seus jogadores:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
   xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
   xmlns:d="http://xamarin.com/schemas/2014/forms/design"
   xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
   mc:Ignorable="d"
   x:Class="XF_Crud1.Views.TimeDetailsPage"
   Title="Detalhes">
    <ContentPage.ToolbarItems>
        <ToolbarItem Text="Novo jogador"
                     Command="{Binding AddJogadorCommand}"
                     Icon="ic_add_player.png" />
        <ToolbarItem Text="Edita time"
                     Icon="ic_edit.png"
                     Command="{Binding EditTimeCommand}" />
        <ToolbarItem Text="Deleta time"
                     Icon="ic_delete.png"
                     Command="{Binding DeleteTimeCommand}" />
    </ContentPage.ToolbarItems>
    <ContentPage.Content>
        <StackLayout>
            <Label Text="{Binding Nome}"
                       Margin="8"
                       FontSize="Medium"
                       Grid.Row="0" />
            <Label Text="{Binding Treinador}"
                       Grid.Row="1"
                       FontSize="Small"
                       Margin="8" />
            <Label Text="{Binding Cidade}"
                       Grid.Row="2"
                       FontSize="Small"
                       Margin="8" />
            <Label Text="{Binding Estadio}"
                       Grid.Row="3"
                       FontSize="Small"
                       Margin="8" />
            <StackLayout Grid.Row="4"
                             Margin="8">
                <Label Text="Jogadores:"
                           FontSize="Small"
                           Margin="4" />
                <ListView ItemsSource="{Binding Jogadores}"
                              HasUnevenRows="True"
                              ItemTapped="OnPlayerTapped"
                              Margin="4">
                    <ListView.ItemTemplate>
                        <DataTemplate>
                            <ViewCell>
                                <StackLayout Orientation="Horizontal"
                                                 HorizontalOptions="StartAndExpand">
                                    <Label Text="{Binding Nome}"
                                               FontSize="Small"
                                               Margin="4" />
                                    <Label Text="{Binding Posicao}"
                                               FontSize="Small"
                                               Margin="4" />
                                    <Image Source="ic_edit.png"
                                               HeightRequest="17"
                                               HorizontalOptions="End"
                                               VerticalOptions="Center"
                                               Margin="4" />
                                </StackLayout>
                            </ViewCell>
                        </DataTemplate>
                    </ListView.ItemTemplate>
                </ListView>
            </StackLayout>
        </StackLayout>
    </ContentPage.Content>
</ContentPage>

 

Agora vamos criar a viewmodel TimeDetailsViewModel:

using System.Collections.ObjectModel;
using System.Linq;
using System.Windows.Input;
using Xamarin.Forms;
using XF_Crud1.DataAccess;
using XF_Crud1.Helpers;
using XF_Crud1.Models;
using XF_Crud1.Views;
namespace XF_Crud1.ViewModels
{
    class TimeDetailsViewModel : BaseViewModel
    {
        private string nome;
        public string Nome
        {
            get { return nome; }
            set
            {
                nome = value;
                OnPropertyChanged();
            }
        }
        private string treinador;
        public string Treinador
        {
            get { return treinador; }
            set
            {
                treinador = value;
                OnPropertyChanged();
            }
        }
        private string cidade;
        public string Cidade
        {
            get { return cidade; }
            set
            {
                cidade = value;
                OnPropertyChanged();
            }
        }
        private string estadio;
        public string Estadio
        {
            get { return estadio; }
            set
            {
                estadio = value;
                OnPropertyChanged();
            }
        }
        public ObservableCollection<Jogador> Jogadores { get; set; }
        public ICommand AddJogadorCommand { get; private set; }
        public ICommand EditTimeCommand { get; private set; }
        public ICommand DeleteTimeCommand { get; private set; }
        private AppDbContext _context;
        private int _timeId;
        public TimeDetailsViewModel(int timeId)
        {
            _timeId = timeId;
            _context = new AppDbContext();
            var time = _context.Times.Find(timeId);
            Nome = time.Nome;
            Cidade = time.Cidade;
            Estadio = time.Estadio;
            Estadio = time.Estadio;
            Jogadores = new ObservableCollection<Jogador>(_context.Jogadores.Where(p => p.TimeId == timeId));
            AddJogadorCommand = new Command(async () => await 
              Application.Current.MainPage.Navigation.PushAsync(new AddJogadorPage(time.TimeId)));

            EditTimeCommand = new Command(async () => await
              Application.Current.MainPage.Navigation.PushAsync(new EditTimePage(time.TimeId)));
            DeleteTimeCommand = new Command(DeleteTeam);
        }

        void DeleteTeam()
        {
            var time = _context.Times.Find(_timeId);
            _context.Times.Remove(time);
            _context.SaveChanges();
            Application.Current.MainPage.Navigation.PopAsync();
        }
    }
}

Na segunda parte do artigo vamos concluir a implementação das demais viewmodels e views do projeto.

"Bendito seja o Deus e Pai de nosso Senhor Jesus Cristo, o Pai das misericórdias e o Deus de toda a consolação;
Que nos consola em toda a nossa tribulação, para que também possamos consolar os que estiverem em alguma tribulação, com a consolação com que nós mesmos somos consolados por Deus."
2 Coríntios 1:3,4

Referências:


José Carlos Macoratti