Mobiele apps programmeren met Xamarin – Entity Framework – SQLite met Xamarin

print

Deze handleiding maakt deel uit van het programmeertraject:


Inhoud


Wat vooraf ging


Inleiding

  • SQLite is een lichtgewicht lokale databank die vaak gebruikt wordt met Xamarin. Wij gaan dan ook gebruik maken van SQLite.
  • Ondanks het feit dat EF Core nog in volle ontwikkeling is, en nog niet zo matuur als EF 6, is EF Core de toekomst en onze keuze voor het bouwen van cross platform apps in Xamarin. Trouwens, EF 6 werkt gewoon niet met Xamarin (enkel onder Windows).
  • SQLite en Xamarin zijn uitermate geschikt voor een Code-First benadering. We gaan dan ook de Code-First benadering toepassen.
  • De migratie in Xamarin verloopt (voorlopig, want Microsoft is zich bewust van het ongemak en werkt aan een oplossing) niet automatisch en vraagt een handmatige tussenkomst. De migratie is dus wat omslachtig maar weet dat migratie enkel nodig is om wijzigingen aan de databank vast te leggen na publicatie van de app.

Om de SQLite databank te bekijken hebt u een DB Browser for SQLite nodig.

Onderstaande video geeft wat meer uitleg over SQLite.


Migratie voorbereiden

Migratie is de methode die Entity Framework gebruikt om, na publicatie, wijzigingen aan de databank door te voeren, met behoud van de reeds toegevoegde gegevens of anders gezegd, het synchroniseren tussen het Model en de databank met behoudt van de ingevoerde gegevens.

Migration is a way to keep the database schema in sync with the EF Core model by preserving data.

Op het ogenblik van dit schrijven is directe migratie in een Xamarin project niet mogelijk (Microsoft is zich bewust van dit probleem en wellicht komt er een gemakkelijkere oplossing in een volgende versie van Entity Framework).

Omdat directe migratie niet mogelijk is moeten we een kleine omweg maken. Die omweg kan via een Console App (.NET Core) project.

  • Klik rechts op de Solution en kies voor AddNew Project….
  • Kies voor .NET CoreConsole App (.NET Core) en geef een passende naam (bv. Migratie).

Vervolgens moeten er een referentie gelegd worden naar het basis, portable project.

  • Klik rechts op het Console App (.NET Core) project MigratieDependenciesAdd Reference….

  • Vink het basis/portable project aan (hier TestEF) en druk op OK.

Het Console (.NET Core) project is toegevoegd en de referentie gelegd, het staat klaar en we komen hier later op terug.


Entity Framework SQLite toevoegen aan een Xamarin Project

Voor Entity Framework dienen volgende NuGet Packages geïnstalleerd te worden.

  • Entity Framework Core DB provider (voor het volledige project)
  • Entity Framework Core tools (enkel het migratieproject)

Kies minimaal versie Entity Framework 2.1.

  • Klik rechts op de Solution en kies Manage NuGet Packages for Solution….

  • Ga naar het tabblad Browse en zoek naar Microsoft.EntityFrameworkCore.

U vindt verschillende opties waaronder Microsoft.EntityFrameworkCore.SQLServer, Microsoft.EntityFrameworkCore.InMemory,… en ook Microsoft.EntityFrameworkCore.Sqlite die wij gaan gebruiken.

  • Selecteer Microsoft.EntityFrameworkCore.Sqlite.
  • Vink alle projecten aan en selecteer de laatste stabiele versie (minimaal 2.1).

  • Klik op Install en bevestig het nodige.
  • Selecteer vervolgens Microsoft.EntityFrameworkCore.Tools, selecteer enkel het Migratieproject, selecteer dezelfde versie als hierboven (de laatste stabiele) en klik op Install.

iOS

Na het installeren van Entity Framework Core moet er een kleine aanpassing gebeuren aan het iOS-project.

  • Ga naar het iOS-project en open AppDelegate.cs.
  • Voeg volgende code toe SQLitePCL.Batteries_V2.Init();.

De volledige code van AppDelegate.cs:

using Foundation;
using UIKit;

namespace TestEF.iOS
{
    // The UIApplicationDelegate for the application. This class is responsible for launching the 
    // User Interface of the application, as well as listening (and optionally responding) to 
    // application events from iOS.
    [Register("AppDelegate")]
    public partial class AppDelegate : global::Xamarin.Forms.Platform.iOS.FormsApplicationDelegate
    {
        //
        // This method is invoked when the application has loaded and is ready to run. In this 
        // method you should instantiate the window, load the UI into it and then make the window
        // visible.
        //
        // You have 17 seconds to return from this method, or iOS will terminate your application.
        //
        public override bool FinishedLaunching(UIApplication app, NSDictionary options)
        {
            // Required on iOS for EFcore.
            SQLitePCL.Batteries_V2.Init();

            global::Xamarin.Forms.Forms.Init();
            LoadApplication(new App());

            return base.FinishedLaunching(app, options);
        }
    }
}

MVVM

We gaan gebruik maken van de MVVM-architectuur (ModelViewViewModel). Dit is niet verplicht maar het is stilaan wel een standaard aan het worden voor het ontwerpen van gegevensgeoriënteerde applicaties. Tenslotte staat de M voor Model.

Maak een folder Models, Views en ViewModels aan onder het basisproject.

  • Klik rechts in het basisproject en klik op AddNew Folder.
  • Maak de folders Models, Views en ViewModels aan.

Omdat we meerdere ViewModels gaan nodig hebben kunnen we een BaseViewModel aanmaken.

  • Klik rechts op de folder ViewModels en kies voor AddNew Item….
  • Selecteer een Class en geef het de naam BaseViewModel.

Voeg onderstaande code toe aan BaseViewModel.cs:

using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace TestEF.ViewModels
{
    public class BaseViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        public void OnPropertyChanged([CallerMemberName] string name = "")
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
        }

    }
}

De lokale locatie van de databank bepalen

SQLite maakt een lokale databank aan.

Het probleem is dat voor ieder platform: Android, UWP en iOS deze locatie anders is. We gaan dus moeten bepalen waar onze databank moet komen. Niet dat we zoveel keuze hebben want bv. iOS bepaalt zelf waar de data bewaard moet worden.

We hebben reeds gezien dat de connectie met de databank gemaakt wordt in een klasse geërfd van de klasse DBContext.

Nu we toch bezig zijn mapjes aan te maken, laten we dit in een eigen mapje steken, bv. een mapje met de naam Services.

  • Klik rechts in het basisproject en klik op AddNew Folder.
  • Maak de folder Services aan.
  • Klik rechts op de folder Services en kies voor AddNew Item….
  • Selecteer een Class en geef het een passende naam (bv. DatabaseContext).

Voeg onderstaande code toe:

using Microsoft.EntityFrameworkCore;

namespace TestEF.Services
{
    public class DatabaseContext : DbContext
    {
        string _dbPath;

        public DatabaseContext(string dbPath)
        {
            _dbPath = dbPath;
        }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlite($"Filename={_dbPath}");
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
        }
    }
}
  • Maak gebruik van Entity Framework Core using Microsoft.EntityFrameworkCore;.
  • Erf van DbContext.
  • De constructor ontvangt een parameter die het pad naar de databank bevat.
  • Binnen de functie OnConfiguring(DbContextOptionsBuilder optionsBuilder) wordt het pad gelegd naar de SQLite databank optionsBuilder.UseSqlite($"Filename={_dbPath}");.

Vervolgens moet we de locatie van de databank ophalen bij het opstarten van de applicatie.

  • Open App.xaml.cs.

Pas de code als volgt aan:

using Xamarin.Forms;
using Xamarin.Forms.Xaml;
using TestEF.Services;

[assembly: XamlCompilation(XamlCompilationOptions.Compile)]
namespace TestEF
{
    public partial class App : Application
    {
        public static DatabaseContext Databank;

        public App(string dbPath)
        {
            InitializeComponent();

            Databank = new DatabaseContext(dbPath);

            MainPage = new MainPage();
        }

        protected override void OnStart()
        {
            // Handle when your app starts
        }

        protected override void OnSleep()
        {
            // Handle when your app sleeps
        }

        protected override void OnResume()
        {
            // Handle when your app resumes
        }
    }
}

De locatie wordt meegegeven als parameter bij het opstarten public App(string dbPath) en vervolgens toegewezen aan de DatabaseContext Databank = new DatabaseContext(dbPath);.

Vervolgens kan de variabele public static DatabaseContext Databank gebruikt worden om de databank aan te spreken via App.Databank.

Oké, nu we het pad gelegd hebben moeten we enkel nog bepalen wat het pad eigenlijk is.

  • De naam van onze databank wordt database.sqlite.
  • Om het Path te leggen moeten we gebruik maken van using System.IO;.
  • De variabele die het pad bevat wordt meegegeven als parameter bij het laden van de applicatie LoadApplication(new App(dbPath));.

Dit dient dus zowel voor Android, UWP als iOS.

Android

Ga naar het Android-project en open MainActivity.cs.

De code die het pad bepaalt is:

var dbPath = Path.Combine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyDocuments), "database.sqlite");

De volledige code voor MainActivity.cs:

using Android.App;
using Android.Content.PM;
using Android.OS;
using System.IO;

namespace TestEF.Droid
{
    [Activity(Label = "TestEF", Icon = "@mipmap/icon", Theme = "@style/MainTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation)]
    public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity
    {
        protected override void OnCreate(Bundle savedInstanceState)
        {
            TabLayoutResource = Resource.Layout.Tabbar;
            ToolbarResource = Resource.Layout.Toolbar;

            base.OnCreate(savedInstanceState);
            global::Xamarin.Forms.Forms.Init(this, savedInstanceState);

            var dbPath = Path.Combine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyDocuments), "database.sqlite");
            LoadApplication(new App(dbPath));
        }
    }
}

UWP

Ga naar het UWP-project en open MainPage.xaml.cs.

De code die het pad bepaalt is:

var dbPath = Path.Combine(ApplicationData.Current.LocalFolder.Path, "database.sqlite");

Voeg ook een using Windows.Storage; toe.

De volledige code voor MainPage.xaml.cs:

using System.IO;
using Windows.Storage;

namespace TestEF.UWP
{
    public sealed partial class MainPage
    {
        public MainPage()
        {
            this.InitializeComponent();

            var dbPath = Path.Combine(ApplicationData.Current.LocalFolder.Path, "database.sqlite");
            LoadApplication(new TestEF.App(dbPath));
        }
    }
}

iOS

Ga naar het iOS-project en open AppDelegate.cs.

De code die het pad bepaalt is:

var libPath = Path.Combine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyDocuments), "..", "Library", "data");

if (!Directory.Exists(libPath))
{
     Directory.CreateDirectory(libPath);
}

var dbPath = Path.Combine(libPath, "database.sqlite");

Let op: iOS is hier heel strikt op, het niet plaatsen in bovenstaande map kan reden zijn om publicatie van uw app te wijgeren.

De volledige code voor AppDelegate.cs:

using System.IO;
using Foundation;
using UIKit;

namespace TestEF.iOS
{
    // The UIApplicationDelegate for the application. This class is responsible for launching the 
    // User Interface of the application, as well as listening (and optionally responding) to 
    // application events from iOS.
    [Register("AppDelegate")]
    public partial class AppDelegate : global::Xamarin.Forms.Platform.iOS.FormsApplicationDelegate
    {
        //
        // This method is invoked when the application has loaded and is ready to run. In this 
        // method you should instantiate the window, load the UI into it and then make the window
        // visible.
        //
        // You have 17 seconds to return from this method, or iOS will terminate your application.
        //
        public override bool FinishedLaunching(UIApplication app, NSDictionary options)
        {
            // Required on iOS for EFcore.
            SQLitePCL.Batteries_V2.Init();

            global::Xamarin.Forms.Forms.Init();
            var libPath = Path.Combine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyDocuments), "..", "Library", "data");

            if (!Directory.Exists(libPath))
            {
                Directory.CreateDirectory(libPath);
            }

            var dbPath = Path.Combine(libPath, "database.sqlite");
            LoadApplication(new App(dbPath));

            return base.FinishedLaunching(app, options);
        }
    }
}

Vindt u deze workflow wat omslachtig dan kan u het ook uitwerken via een DependencyService.


De databank aanmaken

Nu het pad bepaald is moeten we nog de databank aanmaken. Dit gebeurt in de klasse die overerft van DBContext, dus onze klasse DatabaseContext.

  • Open DatabaseContext.

Databank aanmaken

Voeg het commando Database.EnsureCreated(); toe aan de constructor van de klasse.

using Microsoft.EntityFrameworkCore;

namespace TestEF.Services
{
    public class DatabaseContext : DbContext
    {
        string _dbPath;

        public DatabaseContext(string dbPath)
        {
            _dbPath = dbPath;
            Database.EnsureCreated();
        }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlite($"Filename={_dbPath}");
        }
    }
}

Is de databank niet aangemaakt dan wordt ze aangemaakt, maar is ze reeds aangemaakt dan wordt ze verder niet bekeken. Er wordt dus ook niet gekeken of er wijzigen zijn aan het achterliggende model.

Databank verwijderen en opnieuw aanmaken tijdens ontwerpen

Om ook wijzigingen aan de achterliggende modellen op te nemen in de databank gaan we de databank eerst moeten verwijderen Database.EnsureDeleted(); en vervolgens weer aan te maken.

using Microsoft.EntityFrameworkCore;

namespace TestEF.Services
{
    public class DatabaseContext : DbContext
    {
        string _dbPath;

        public DatabaseContext(string dbPath)
        {
            _dbPath = dbPath;
            Database.EnsureDeleted();
            Database.EnsureCreated();
        }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlite($"Filename={_dbPath}");
        }
    }
}

Uiteraard verliest u tijdens het verwijderen alle ingevoerde gegevens. Waar dit geen ramp is tijdens het ontwerpen en testen van de app is dit niet aan te raden eens de app gepubliceerd is.

Databank migreren na productie

Migratie laat toe wijzigingen aan de achterliggende modellen vast te leggen in databank met behoudt van de reeds ingevoerde gegevens.

U gebruikt hiertoe het commando Database.Migrate(); of de asynchrone variant Database.MigrateAsync();.

using Microsoft.EntityFrameworkCore;

namespace TestEF.Services
{
    public class DatabaseContext : DbContext
    {
        string _dbPath;

        public DatabaseContext(string dbPath)
        {
            _dbPath = dbPath;
            Database.Migrate();
        }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlite($"Filename={_dbPath}");
        }
    }
}

Aangezien migratie een proces is dat ook enig werk vraagt buiten de programmeercode is migratie niet aan te raden tijdens het ontwerpen en testen. Het is wel nodig tijdens de productie als de achterliggende modellen zouden kunnen wijzigen (dus in bijna alle gevallen).

Let op: bent u 100% zeker dat er na het produceren van de app geen wijzigingen aan de achterliggende modellen zullen plaats vinden dan is de beste optie Database.EnsureCreated();.

Migratie wordt later in detail besproken.

Zo, we hebben nu een soort template gemaakt dat we als basis kunnen gebruiken voor al onze verdere Entity Framework projecten.

Geef een reactie

  • Abonneer je op deze website d.m.v. e-mail

    Voer je e-mailadres in om je in te schrijven op deze website en e-mailmeldingen te ontvangen van nieuwe berichten.