Xamarin.Forms, NET Standard 2.0 et Entity Framework Core avec SQLite

Xamarin permet aujourd’hui de développer avec un seul code source et de déployer sur plusieurs plateformes en même temps (Android, iOS, Windows).  Si vous êtes développeur .NET, cette technologie ne devrait pas vous faire peur puisque le langage est le C# et tout développeur Microsoft devrait s’en sortir sans trop de soucis. De plus, les équipes de Xamarin ont développé un Framework afin de partager également les vues des applications mobiles : Xamarin.Forms. Avec ceci, le maximum de code est partagé. Le langage utilisé est le XAML, facilitant ainsi la montée en compétence des développeurs ayant déjà pratiqué du WPF ou de l’UWP.

L’intérêt de Xamarin Forms : plus de partage de code

Beaucoup de nouveautés sont apparus ces derniers temps, avec NET Standard 2.0, NET Core 2.0 et Entity Framework Core 2.0 et il n’est pas toujours évident de trouver des informations sur ces sujets en même temps. Avec NET Standard 2.0 et la fin des PCLs, il faut comprendre que ce genre de librairie est réellement le futur de .NET et la plateforme Xamarin ne déroge pas à la règle. J’ai ainsi voulu mettre les mains dans toutes ces nouvelles technologies et en particulier avec Entity Framework Core pour SQLite : comment configurer une BDD avec Entity Framework Core dans Xamarin ? Qu’apporte NET Standard 2.0 à ce sujet ?

Pour ce faire, nous allons partir d’une solution Xamarin.Forms vierge (une application du style ToDo, classique mais efficace), comportant les projets suivants :

  • ToDoApp.Standard – le projet NET Standard 2.0 avec toute la logique métier et les vues XAML ;
  • ToDoApp.Android – le projet Android ;
  • ToDoApp.iOS – le projet iOS ;
  • ToDoApp.UWP – le projet UWP.

Pour faire fonctionner NET Standard 2.0 avec UWP, vous devez installer la mise à jour Fall Creator Update et les dernières versions de .NET Core 2.0 et de Visual Studio 15.4

Une fois votre projet Xamarin.Forms créé, nous allons commencer par créer une librairie NET Standard 2.0. Pour ce faire, il suffit d’aller sur la solution > Clic droit > Nouveau projet > .NET Standard > Bibliothèque de classe (.NET Standard). Appelons la ToDoApp.Standard. Commençons par installer Xamarin.Forms dans ce projet via NuGet.

Paquet NuGet Xamarin.Forms

Dans les autres projets, mettez à jour également Xamarin.Forms. Vous pouvez faire ceci via la solution, dans la partie Consolider de NuGet. Au passage, n’hésitez pas à mettre le reste de vos packages à jour (notamment pour UWP). Ensuite, vous pouvez copier/coller vos fichiers source du projet PCL au projet NET Standard. Vous pouvez ensuite supprimer le projet PCL et référencer le projet .NET Standard dans les autres projets. La solution devrait alors compiler correctement et vous devriez déjà pouvoir lancer votre application (via émulateur ou device).

Attaquons-nous maintenant à la partie métier de notre application. Pour faire simple, nous allons simplement mettre en œuvre les éléments suivants :

  • Un Button afin de rajouter des entrées dans notre BDD ;
  • Une ListView afin d’afficher la liste des ToDo de notre application.

Dans MainPage.xaml, on retrouvera alors un XAML qui ressemble à ceci :

<ContentPage.Content>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <Button Text="Ajouter des éléments" Clicked="OnAddClicked" />

        <ListView ItemsSource="{Binding ToDoItems}" Grid.Row="1">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <ViewCell>
                        <Label Text="{Binding Text}" TextColor="Black"/>
                    </ViewCell>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </Grid>
</ContentPage.Content>

Afin de faire fonctionner la liaison de données et l’événement Clicked du bouton, nous avons besoin des lignes suivantes dans la classe MainPage:

public ObservableCollection<ToDoItem> ToDoItems { get; set; } = new ObservableCollection<ToDoItem>();

public MainPage()
{
    InitializeComponent();
    BindingContext = this;
}

void OnAddClicked(object sender, EventArgs e)
{
}

Le modèle ToDoItem ressemble alors à celui-ci :

public class ToDoItem
{
    public string Text { get; set; }
}

Commençons les choses sérieuses. Afin de faire fonctionner Entity Framework Core avec SQLite dans notre application, nous allons rajouter les paquets Nuget qui vont bien. Dans le projet NET Standard, nous allons rajouter le paquet Microsoft.EntityFrameworkCore.Sqlite (version 2.0.1 lors de l’écriture de cet article).

Paquet NuGet EF Core pour SQLite

Ce paquet va installer toutes les dépendances dont il a besoin pour fonctionner. Vous avez également besoin de l’installer dans les autres projets pour faire fonctionner EF Core. Nous pouvons enfin créer notre premier DbContext pour notre application dans le projet NET Standard.

public class ToDoContext : DbContext
{
    public ToDoContext()
    {
        this.Database.Migrate();
    }

    public DbSet<ToDoItem> ToDo { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        var dbPath = DependencyService.Get<IFileHelper>().GetLocalFilePath("MyDb.db3");
        optionsBuilder.UseSqlite($"Filename={dbPath}");
    }
}

Nous allons décortiquer le code ci-dessus afin de mieux le comprendre :

  • Le constructeur du DbContext appelle la méthode Database.Migrate() : cela permet de s’assurer que la base de données est créée et que les migrations sont appliqués. Les migrations permettent de créer les tables et colonnes de notre BDD ;
  • Le DbSet<ToDoItem> permet d’interroger la BDD afin de récupérer nos éléments ;
  • La méthode OnConfiguring permet d’indiquer au contexte plusieurs choses :
    1. Le chemin de la base de données sur le device. Cette opération ne peut pas se faire de manière partagée dans la librairie NET Standard, car chaque plateforme à son propre emplacement de stockage. Pour ce faire, nous allons utiliser une astuce qui s’appelle l’injection de dépendance (expliqué un peu plus bas).
    2. Le provider de base de données à utiliser. Ici, nous utilisons SQLite via la méthode .UseSqlite().

Au passage, nous allons enrichir notre modèle ToDoItem afin d’être utilisable via Entity Framework Core.

[Table("ToDo")]
public class ToDoItem
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }

    public string Text { get; set; }
}

Nous indiquons la table à utiliser, et nous avons rajouter une propriété Id qui fera office de clé primaire en auto-incrément.

Important – Le nom de la table doit être le même que la propriété DbSet dans votre DbContext.

La première étape pour notre DbContext est terminé. Nous avons fait remarquer plus haut qu’il fallait indiquer le chemin de la base de données afin de l’exploiter sur le device. Cependant, cette information n’est accessible que sur chaque plateforme, car l’emplacement est différent selon le système d’exploitation. Pour ce faire, nous allons utiliser une interface commune que nous allons créer : IFileHelper. Il nous suffit alors d’implémenter cette interface sur chaque plateforme, écrire le code qui permet de récupérer le chemin de la BDD dans une méthode GetLocalFilePath() et utiliser l’injection de dépendance dans notre librairie .NET Standard. Suivant la plateforme ciblé pour le runtime, la bonne classe sera injecté et nous récupérerons ainsi automatiquement le chemin de la BDD.

public interface IFileHelper
{
    string GetLocalFilePath(string filename);
}

Dans le projet Android, nous aurons alors la classe suivante :

[assembly: Dependency(typeof(FileHelper))]
namespace OneBelote.Droid.SQLite
{
    public class FileHelper: IFileHelper
    {
        public DatabasePath()
        {}
        
        public string GetLocalFilePath(string filePath)
        {
            return Path.Combine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal), filePath);
        }
    }
}

Pour iOS, nous aurons ceci :

[assembly: Dependency(typeof(FileHelper))]
namespace Todo.iOS
{
    public class FileHelper : IFileHelper
    {
        public string GetLocalFilePath(string filename)
        {
            string docFolder = Environment.GetFolderPath(Environment.SpecialFolder.Personal);
            string libFolder = Path.Combine(docFolder, "..", "Library", "Databases");

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

            return Path.Combine(libFolder, filename);
        }
    }
}

Et pour UWP :

[assembly: Dependency(typeof(FileHelper))]
namespace Todo.UWP
{
    public class FileHelper : IFileHelper
    {
        public string GetLocalFilePath(string filename)
        {
            return Path.Combine(ApplicationData.Current.LocalFolder.Path, filename);
        }
    }
}

Source : https://developer.xamarin.com/guides/xamarin-forms/application-fundamentals/databases/

Nous pouvons maintenant utiliser notre DbContext, et nous allons commencer par rajouter des éléments dans notre BDD via le bouton que nous avons placé dans notre page principale.

void OnAddClicked(object sender, EventArgs e)
{
    using (var context = new ToDoContext())
    {
        context.ToDo.Add(new ToDoItem
        {
            Text = "My first ToDo"
        });

        context.SaveChanges();
    }
}

Lançons l’application et testons afin de vérifier. Nous pouvons voir que l’instanciation du DbContext fonctionne très bien (en debug, vous pouvez vérifier qu’il récupère le bon chemin). J’utilise pour ma part un Android, donc je me retrouve avec un chemin du type "/data/user/0/com.companyname.ToDoApp/files/MyDb.db3". Cependant, lorsque nous tentons de rajouter un élément dans notre base et de sauvegarder les changements, nous avons une erreur peu parlante.

Erreur lors de la sauvegarde d’un élément

En parcourant rapidement le DbContext (toujours en debug), nous pouvons nous apercevoir qu’il y a un soucis avec notre DbSet :

Aucune table ToDo

Nous y voila ! Reprenons rapidement toutes les étapes que nous avons effectuées jusqu’à présent :

  • Création d’un DbContext pour notre application ;
  • Récupération du chemin de la BDD via injection de dépendance ;
  • Création de la base, application des migrations via la méthode .Migrate().

C’est bien cette dernière étape qui pose problème : la base est créée, mais comme aucune migration n’a été configurée dans notre projet, la table ToDo n’a jamais pu être créé.

C’est ici que les choses se compliquent. Afin de créer une migration, nous allons avoir besoin d’un projet runtime qui puisse lancer les outils EF Core via l’utilitaire dotnet. Notre projet qui contient le DbContext étant un projet NET Standard (et donc non runtime), cela n’est pas possible dans le projet actuel. Pour s’en sortir, nous allons créer un projet Console .NET Core qui va générer les migrations pour nous. Il nous suffira ensuite de copier les nouvelles migrations dans notre projet NET Standard pour que cela fonctionne. Suivons les étapes suivantes :

  • Créons un projet Console .NET Core ;
  • Dans le .csproj, rajoutons les 3 références suivantes :
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp2.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.0.1" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="2.0.1" />
    <DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.0" />
  </ItemGroup>

</Project>
  • Ensuite, copions notre DbContext et le modèle que nous avons besoin dans ce projet Console. Le DbSet en public est important, et dans le OnConfiguring, il suffit de tout supprimer sauf la méthode .UseSqlite() (vous pouvez mettre un faux nom de BDD). Avant de continuer, il faut vous assurer que votre projet compile ;
  • Dans un invite de commande, à la racine du projet Console, lançons les commandes suivantes :
dotnet restore
dotnet ef migrations add Initial
  • A la racine du projet Console, l’outil a créé un dossier Migrations. Il suffit alors de copier ce dossier dans le projet NET Standard. Lors de l’instanciation du DbContext, la méthode .Migrate() va chercher les migrations présentes dans votre projet et les appliquer à la base de données.

Si vous prêtez un peu attention au code généré pour les migrations, vous vous apercevrez que c’est simplement du code C# qui définit les tables et les colonnes selon votre modèle C#. L’outil a réussi à générer cela grâce au DbSet de votre DbContext.

Source : https://forums.xamarin.com/discussion/101805/xamarin-android-entity-framework-core-2-and-migrations

Lançons une nouvelle fois notre application (vous pouvez mettre des points d’arrêts dans vos migrations pour vous rendre compte qu’elles sont appliqués), et nous pouvons voir que notre élément est enfin bien rajouté dans notre base de données.

Pour finir, nous allons rafraîchir notre liste de ToDo via le code suivant :

void Refresh()
{
    this.ToDoItems.Clear();
    using (var context = new ToDoContext())
    {
        foreach (var toDo in context.ToDo)
        {
            this.ToDoItems.Add(toDo);
        }
    }
}

Et voila ! Le binding fait le reste. Votre application est prête maintenant pour travailler avec une base de données. N’hésitez pas à laisser un commentaire si vous trouvez mieux, notamment pour la partie migration EF Core !

Le projet final se trouve ici.

UPDATE 27/02 pour iOS : Crash lors du EnsureCreated(). (merci Fanch)

Afin de garantir de fonctionnement de l’application sur iOS, la ligne suivante s’avère nécessaire dans le AppDelegate, (méthode FinishedLaunching ) :

// Required on iOS for EFcore.
SQLitePCL.Batteries_V2.Init();

Source : https://github.com/Krumelur/EFCoreFormsDemo

Faites tourner ! Share on Facebook
Facebook
Tweet about this on Twitter
Twitter
Share on LinkedIn
Linkedin

8 réponses

  1. EhRom dit :

    Bonjour,
    J’ai tenté de reproduire la configuration, mais ne fonctionne pas.
    J’utilise le niveau 27.0.2 des API Android, et uniquement EntiryFramework.Core.SQLite, la version Xamarin 2.5.

    Votre solution embarque beaucoup plus de packages. Avez-vous une liste minimale plus complète qui fait que l’application ne plante pas (lors du start screen, sans message d’erreur).

    Merci d’avance
    Cordialrment

  2. Maxime dit :

    Bonjour Christophe et merci pour ce tuto 🙂
    Je l’ai suivi et ait pris en main assez facilement entity framework grâce à lui

    Je te transmet une petite astuce qu’un de mes collègues vient de me donner. Cela servira aussi aux prochains lecteurs.

    Pour éviter d’ouvrir une fenêtre de commande et de saisir les deux lignes de commandes « dotnet restore » « dotnet ef migrations add Initial », il est possible d’installer le package nuget Microsoft.EntityFrameworkCore.Tools sur le projet RuntimeEntityFramework.
    Ensuite, dans la console du Package Manager, on peut saisir la commande « add-migration Initial ».
    Cela fait le même boulot, mais l’avantage est qu’on reste dans Visual Studio pour générer la migration ! On n’a pas à lancer une invite de commande.

    Espérant que ça puisse servir 🙂

  3. Fanch dit :

    Bonjour,
    Après avoir suivi ce tuto et l’avoir implémenté et testé coté iOS, je me retrouvais avec une exception (NullReferenceException) soulevée lors de l’appel à EnsureCreated().
    Après quelques investigations, la ligne suivante s’avère nécessaire coté iOS dans le AppDelegate, (méthode FinishedLaunching ) :

    // Required on iOS for EFcore.
    SQLitePCL.Batteries_V2.Init();

    Source : https://github.com/Krumelur/EFCoreFormsDemo

  4. Christophe dit :

    Bonjour Christophe,

    Merci pour ce super tuto.
    De mon côté, j’ai eu une erreur lors de la première migration disant qu’une table était déjà créée:
    Microsoft.Data.Sqlite.SqliteException (0x80004005): SQLite Error 1: ‘table « Addresses » already exists

    En regardant ici, https://github.com/aspnet/Microsoft.Data.Sqlite/issues/219 , j’ai dû enlever la ligne EnsureCreated et désinstaller l’application de mon téléphone pour que tout rentre dans l’ordre.

    • Christophe Gigax dit :

      Bonjour Christophe,
      Merci pour le commentaire.
      Bien vu pour le EnsureCreated, normalement tu n’as pas besoin de lancer de migration si EnsureCreated est déjà passé.
      Il souvent utilisé avec GetPendingMigrationsAsync afin de savoir s’il y a des migrations en attente. Si ce n’est pas le cas, aucune migration ne devrait être appliqué pour éviter ces erreurs.

Répondre à Christophe Gigax Annuler la réponse

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *