Доступ к данным в .NET MAUI. Локальные базы данных SQLite в .NET MAUI

Базы данных SQLite в .NET MAUI используются довольно  В .NET и C# мы можем получать доступ к данным различными способами. Мы можем использовать прямой доступ к БД, манипулируя данными вручную — создавать и удалять таблицы, редактировать записи, используя SQL-запросы и так далее. А можем использовать какую-либо ORM-технологию, например, Entity Framework Core и работать с данными в БД, как с обычными объектами .NET.

Доступ к данным в БД SQLite в .NET MAUI

Для прямого доступа к БД SQLite в .NET MAUI нам потребуется добавить в проект необходимые NuGet-пакеты, наиболее популярные из которых — это:

  • sqlite-net-pcl (SQLite.NET) — сторонний проект с открытым исходным кодом, разрабатываемая изначально для Xamarin.iOS. На данный момент библиотека SQLite.NET работает как с MAUI.NET, так и с другими технологиями и платформами, например, с UWP, WPF и так далее. Поддерживает как синхронный, так и асинхронный метод доступа к данным.
  • Microsoft.Data.Sqlite — библиотека от Microsoft, представляющая собой упрощенный поставщик ADO.NET. Эта же библиотека используется для доступа к SQLite в EF Core.

Здесь мы будем использовать первый пакет — SQLite.NET, а со вторым пакетом познакомимся непосредственно при рассмотрении вопросов, связанных с использованием EF Core в .NET MAUI. Рассмотрим работу с SQLite в .NET MAUI на примере небольшого проекта.

Создание проекта и установка необходимых пакетов

Создадим новый проект .NET MAUI и через диспетчер пакетов NuGet добавим в проект пакет sqlite-net-pcl:

Убедимся, что пакет установился корректно и мы сможем работать с SQLite на любой платформе, поддерживаемой в .NET MAUI:Теперь создадим модель данных с которыми будем работать. Для этого создадим в проекте папку с именем Models и разместим в ней следующий класс:

public class Project
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public string Author { get; set; }
}

Объекты этого класса представляют собой некие проекты у которых определяются такие свойства как имя (Name), описание (Description) и автор проекта (Author). Структура нашего проекта теперь должна выглядеть следующим образом:Теперь настроим наш проект на работу с БД SQLite.

Создание класса доступа к базе данных SQLite

Создадим в проекте новую папку Services и добавим в неё класс ProjectDatabase:

Этот класс мы будем использовать для подключения к БД SQLite и выполнения операций CRUD (создание — Create, чтение — Read, обновление — Update и удаление — Delete записей). Вначале, определим в этом классе следующие константы:

public class ProjectDatabase
{
    public const string DatabaseFilename = "Projects.sqlite";

    public const SQLite.SQLiteOpenFlags Flags =
        // открываем БД в режиме чтения/записи
        SQLite.SQLiteOpenFlags.ReadWrite |
        // создаем файл базы данных, если он не обнаружен
        SQLite.SQLiteOpenFlags.Create |
        // разрешаем доступ к БД из нескольких потоков
        SQLite.SQLiteOpenFlags.SharedCache;

    public static string DatabasePath =>
        Path.Combine(FileSystem.AppDataDirectory, DatabaseFilename);
}

Здесь DatabaseFilename — имя файла базы данных, которое мы будем использовать в приложении.

DatabasePath — путь к файлу БД. Для определения пути мы используем метод статического класса PathCombine() и, при этом, в качестве первого параметра для этого метода указываем папку приложения:

public static string DatabasePath =>
    Path.Combine(FileSystem.AppDataDirectory, DatabaseFilename);

Flags — флаги, используемый при создании подключения к БД. В нашем случае мы разрешаем доступ к БД из нескольких потоков, открываем БД на чтение и запись, а также будем создавать файл БД, если он не будет обнаружен. Обратите внимание на то, что перечисление SQLiteOpenFlags содержит именно битовые флаги, поэтому значение Flags мы определяем через логическое сложение:

public const SQLite.SQLiteOpenFlags Flags = SQLite.SQLiteOpenFlags.ReadWrite | SQLite.SQLiteOpenFlags.Create | SQLite.SQLiteOpenFlags.SharedCache;

В целом, перечисление SQLiteOpenFlags содержит следующие флаги, которые мы можем использовать:

Значение Описание
Create подключение автоматически создаст файл базы данных, если он не существует.
FullMutex подключение открывается в режиме сериализованного потока. В этом режиме потоки могут как угодно использовать вызовы SQLite, но все вызовы блокируют друг друга и обрабатываются строго последовательно.
NoMutex подключение открывается в многопоточном режиме. В этом режиме нельзя использовать одно и то же соединение одновременно из нескольких потоков (но допускается одновременное использование разных соединений разными потоками). Обычно используется именно этот режим.
PrivateCache подключение не будет использовать общей кэш, даже если он включен. Это режим, при котором для каждого подключения к базе данных используется отдельный кэш
ReadWrite подключение может считывать и записывать данные.
SharedCache подключение будет использовать общий кэш, если он включен
ProtectionComplete файл шифруется и недоступен, пока устройство заблокировано
ProtectionCompleteUnlessOpen файл шифруется до его открытия, но затем доступен, даже если пользователь блокирует устройство.
ProtectionCompleteUntilFirstUserAuthentication файл шифруется до тех пор, пока пользователь не загрузил приложение и не разблокировал устройство
ProtectionNone файл базы данных не зашифрован

Теперь создадим подключение к БД. Изменим класс следующим образом:

public class ProjectDatabase
{
    public const string DatabaseFilename = "Projects.sqlite";

    public const SQLite.SQLiteOpenFlags Flags = SQLite.SQLiteOpenFlags.ReadWrite | SQLite.SQLiteOpenFlags.Create | SQLite.SQLiteOpenFlags.SharedCache;

    public static string DatabasePath =>
        Path.Combine(FileSystem.AppDataDirectory, DatabaseFilename);

    SQLiteAsyncConnection database;

    async Task Init()
    {
        if (database is not null)
            return;

        database = new SQLiteAsyncConnection(DatabasePath, Flags);
        var result = await database.CreateTableAsync<Project>();
    }
}

Метод Init() выполняет отложенную асинхронную инициализацию базы данных. Объект database будет создан при первом обращении к нему, а сам метод мы будем использовать при каждом доступе к БД.  Создадим методы доступа к данным. Библиотека SQLite.NET предоставляет нам слой ORM (object-relational mapping — отображения данных на реальные объекты), который позволяет хранить и извлекать объекты без написания инструкций SQL. Наши методы доступа к данным будут выглядеть следующим образом:

public class ProjectDatabase
{
    ...

    public async Task<List<Project>> GetAllItemsAsync()
    {
        await Init();
        return await database.Table<Project>().ToListAsync();
    }

    public async Task<Project> GetItemAsync(int id)
    {
        await Init();
        return await database.Table<Project>().Where(i => i.Id == id).FirstOrDefaultAsync();
    }

    public async Task<int> SaveItemAsync(Project item)
    {
        await Init();
        if (item.Id != 0)
            return await database.UpdateAsync(item);
        else
            return await database.InsertAsync(item);
    }

    public async Task<int> DeleteItemAsync(Project item)
    {
        await Init();
        return await database.DeleteAsync(item);
    }
}

При выполнении каждого метода мы вызываем Init(), чтобы быть уверенными, что файл БД создан и содержит необходимые для работы таблицы. При этом от нас не требуется составления никаких SQL-запросов — мы работаем с данными в БД точно также, как с обычными объектами .NET. Это и есть наглядный пример использования ORM-технологии. Итак, мы разработали класс для доступа к данным в БД SQLite в .NET MAUI и теперь нам осталось немного изменить класс Project, чтобы он мог использоваться при работе с БД, а именно — необходимо определить первичный ключ в таблице проектов. Сделать это просто, используя атрибуты из SQLite.NET:

public class Project
{
    [PrimaryKey, AutoIncrement]
    public int Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public string Author { get; set; }
}

Здесь свойство Id нашего класса будет выступать в качестве первичного ключа таблицы. Класс доступа к базе данных SQLite готов, теперь необходимо организовать доступ к БД в нашем приложении.

Организация доступа к БД SQLite в .NET MAUI

Чтобы организовать доступ к БД SQLite в .NET MAUI мы можем использовать два варианта:

  1. Создавать каждый раз, при необходимости, объект класса ProjectDatabase и работать с ним, например, в модели представления
  2. Зарегистрировать класс ProjectDatabase как сервис и запрашивать его по мере необходимости, например, через конструктор

Воспользуемся вторым способом. Для этого откроем файл MauiProgram.cs и зарегистрируем наш сервис:

using Microsoft.Extensions.Logging;

using Projects.Services;

namespace Projects;

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<App>()
            .ConfigureFonts(fonts =>
            {
                fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
            });
        builder.Services.AddSingleton<ProjectDatabase>(); //регистрируем класс как сервис
#if DEBUG
        builder.Logging.AddDebug();
#endif

        return builder.Build();
    }
}

Теперь создадим модель представления для страницы MainPage. Добавим в проект папку ViewModels и разместим в ней новый класс MainPageViewModel

using Projects.Models;
using Projects.Services;

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;

namespace Projects.ViewModels
{
    public class MainPageViewModel: INotifyPropertyChanged
    {

        private readonly ProjectDatabase _database;

        public ObservableCollection<Project> Projects { get; set; } = [];

        private string _name = string.Empty;

        public string Name 
        {
            get => _name;
            set {
                if (_name == value) return;
                _name = value;
                OnPropertyChanged();
            } 
        }

        private string _description = string.Empty;
        public string Description 
        { 
            get =>  _description;
            set {
                if (_description == value) return;
                _description = value; 
                OnPropertyChanged();
            } 
        }

        private string _author = string.Empty;
        public string Author
        {
            get => _author;
            set
            {
                if (_author == value) return;
                _author = value;
                OnPropertyChanged();
            }
        }

        private DateTime _deadline = DateTime.Now;
        public DateTime Deadline
        {
            get => _deadline;
            set 
            {
                if (_deadline == value) return;
                _deadline = value;
                OnPropertyChanged();
            }
        }

        public ICommand AddCommand { get; set; }
        public ICommand UpdateCommand { get; set; }
        public ICommand DeleteCommand { get; set; }
        public ICommand ReadCommand { get; set; }

        public ICommand SelectedItemCommand { get; set; }

        public MainPageViewModel(ProjectDatabase database)
        {
            _database = database;

            AddCommand = new Command(async () =>
            {
                Project project = new()
                {
                    Name = this.Name,
                    Description = this.Description,
                    Author = this.Author
                };
                
                int result = await _database.SaveItemAsync(project);
 
                if (result == 0)
                    throw new Exception("Ошибка добавления новой записи в базу данных");
                
                Projects.Add(project);
                
                Name = string.Empty;
                Description = string.Empty;
                Author = string.Empty;
                Deadline = DateTime.Now;
            });

            UpdateCommand = new Command<Project>(async (Project) => 
            {
                Project.Name = this.Name;
                Project.Description = this.Description;
                Project.Author = this.Author;
                int result = await _database.SaveItemAsync(Project);
                if (result == 0)
                    throw new Exception("Ошибка обновления записи в базе данных");
                ReadCommand?.Execute(null);
            },
            (Param) => { return Param != null; });

            DeleteCommand = new Command<Project>(async (Project) => 
            {
                int result = await _database.DeleteItemAsync(Project);
                Projects.Remove(Project);
                if (result == 0)
                    throw new Exception("Ошибка удаления записи из базы данных");
            },
            (Param) => { return Param != null; });

            ReadCommand = new Command(async () => 
            {
                var list = await _database.GetAllItemsAsync();
                Projects.Clear();
                foreach (var item in list)
                    Projects.Add(item);
            });

            SelectedItemCommand = new Command<Project>((Project) => 
            {
                Name = Project.Name;
                Description = Project.Description;
                Author = Project.Author;
            },
            (Project) => { return Project != null; });
        }

        public event PropertyChangedEventHandler? PropertyChanged;

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

Рассмотрим этот класс подробно. Итак, через конструктор класса мы запрашиваем сервис для работы с базой данных:

public class MainPageViewModel: INotifyPropertyChanged
{

    private readonly ProjectDatabase _database;

    ...

    public MainPageViewModel(ProjectDatabase database)
    {
        _database = database;
       ... 
    }
}

Здесь же, в конструкторе, мы определяем необходимые нам команды — на обновление списка проектов, добавление нового проекта, удаление и обновление проекта, а также команду для обновления свойств класса при выборе проекта в списке.

Команда ReadCommand используется для чтения списка проектов (обновления списка)

ReadCommand = new Command(async () => 
{
    var list = await _database.GetAllItemsAsync();
    Projects.Clear();
    foreach (var item in list)
        Projects.Add(item);
});

Здесь мы просто считываем весь список проектов и добавляем каждый проект с Projects — свойство нашей модели представления, которое выглядит следующим образом:

public ObservableCollection<Project> Projects { get; set; } = [];

Команда AddCommand добавляет новый проект в список:

AddCommand = new Command(async () =>
{
    Project project = new()
    {
        Name = this.Name,
        Description = this.Description,
        Author = this.Author
    };
    
    int result = await _database.SaveItemAsync(project);
 
    if (result == 0)
        throw new Exception("Ошибка добавления новой записи в базу данных");
    
    Projects.Add(project);
    
    Name = string.Empty;
    Description = string.Empty;
    Author = string.Empty;
    Deadline = DateTime.Now;
});

Наш метод сервиса ProjectDatabase SaveItemAsync() возвращает количество строк, добавленных в таблицу. Поэтому, если результат выполнения метода равен нулю, то мы генерируем исключение:

if (result == 0)
    throw new Exception("Ошибка добавления новой записи в базу данных");

После добавления нового проекта в базу данных, мы также добавляем его в список Projects и сбрасываем значения свойств модели представления к значениям по умолчанию:

Projects.Add(project);

Name = string.Empty;
Description = string.Empty;
Author = string.Empty;
Deadline = DateTime.Now;

Команда UpdateCommand обновляет запись в базе данных

UpdateCommand = new Command<Project>(async (Project) => 
{
    Project.Name = this.Name;
    Project.Description = this.Description;
    Project.Author = this.Author;
    int result = await _database.SaveItemAsync(Project);
    if (result == 0)
        throw new Exception("Ошибка обновления записи в базе данных");
    
    ReadCommand?.Execute(null);
},
(Param) => { return Param != null; });

Эта команда получает в качестве параметра объект типа Project, который нам необходимо обновить. При этом команда будет отключена, если значение параметра равно null:

(Param) => { return Param != null; }

После обновления объекта в базе данных мы просто вызываем команду обновления списка для того, чтобы увидеть изменения

ReadCommand?.Execute(null);

Способ обновления сведений в списке далеко не самый рациональный, но, в качестве примера подойдет.

Команда DeleteCommand удаляет проект из списка. В целом, команда похожа на предыдущую, но только здесь мы не обновляем полностью список проектов после удаления, а только удаляем проект из Projects

DeleteCommand = new Command<Project>(async (Project) => 
{
    int result = await _database.DeleteItemAsync(Project);
    Projects.Remove(Project);
    if (result == 0)
        throw new Exception("Ошибка удаления записи из базы данных");
},
(Param) => { return Param != null; });

Последняя команда — SelectedItemCommand используется для заполнения свойств модели представления значениями свойств проекта, выбранного пользователем в списке:

SelectedItemCommand = new Command<Project>((Project) => 
{
    Name = Project.Name;
    Description = Project.Description;
    Author = Project.Author;
},
(Project) => { return Project != null; });

Теперь нам необходимо привязать свойства и команды представления к визуальной части нашего приложения.

Привязка свойств и команд модели представления

Про привязку команд и передачу параметров мы уже знаем, поэтому просто приведем здесь пример страницы MainPage на которой и будет происходить вся работа с базой данных:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="Projects.MainPage"
             xmlns:local="clr-namespace:Projects.ViewModels"
             xmlns:models="clr-namespace:Projects.Models"
             x:DataType="local:MainPageViewModel">

    <ScrollView>
        <VerticalStackLayout
            Padding="30,0"
            Spacing="25">
            
            <VerticalStackLayout Padding="15" Margin="15">
                <Label Text="Название проекта" />
                <Entry Text="{Binding Name}" />
                <Label Text="Описание" />
                <Entry Text="{Binding Description}" />
                <Label Text="Автор" />
                <Entry Text="{Binding Author}" />

                <CollectionView x:Name="ProjectsView" 
                                SelectionMode="Single" 
                                ItemsSource="{Binding Projects, Mode=TwoWay}" 
                                Margin="15" 
                                SelectionChangedCommand="{Binding SelectedItemCommand}"
                                SelectionChangedCommandParameter="{Binding  Source={RelativeSource Mode=Self},Path=SelectedItem}">
                    <CollectionView.ItemTemplate>
                        <DataTemplate x:DataType="{x:Type models:Project}">
                            <Border StrokeShape="Rectangle" Background="LightGreen">
                                <VerticalStackLayout>
                                    <Label Text="{Binding Name}" FontSize="16" FontAttributes="Bold" TextColor="Blue"/>
                                    <Label Text="{Binding Id}"/>
                                    <Label Text="{Binding Description}"/>
                                    <Label Text="{Binding Author}"/>
                                </VerticalStackLayout>
                            </Border>
                        </DataTemplate>
                    </CollectionView.ItemTemplate>
                </CollectionView>
                
            </VerticalStackLayout>

            <HorizontalStackLayout HorizontalOptions="Center">
                <Button 
                    Margin="10"
                    Text="Обновить список" 
                    Command="{Binding ReadCommand}" />
                <Button 
                    Margin="10"
                    Text="Добавить"
                    Command="{Binding AddCommand}"
                    CommandParameter="{Binding Source={x:Reference ProjectsView}, Path=SelectedItem}"/>
                <Button 
                    Margin="10"
                    Text="Удалить" 
                    Command="{Binding DeleteCommand}" 
                    CommandParameter="{Binding Source={x:Reference ProjectsView}, Path=SelectedItem}"/>
                <Button 
                    Margin="10"
                    Text="Изменить" 
                    Command="{Binding UpdateCommand}" 
                    CommandParameter="{Binding Source={x:Reference ProjectsView}, Path=SelectedItem}"/>

            </HorizontalStackLayout>
            
            
        </VerticalStackLayout>
    </ScrollView>

</ContentPage>

Проверим работу приложения:

SQLite в .NET MAUI

Итого

Для работы с SQLite мы воспользовались библиотекой с открытым исходным кодом SQLite.NET. В качестве примера мы разработали небольшое приложение, использующее базу данных SQLite для хранения данных о проектах. Проект использует шаблон MVVM и содержит модель, модель представления и представление. Рассмотренный способ работы с SQLite в .NET MAUI не единственный. При желании, мы можем также воспользоваться и «родной» технологией .NET — Microsoft Entity Framework Core.

Подписаться
Уведомить о
guest
0 Комментарий
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии