Содержание
Базы данных 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
— путь к файлу БД. Для определения пути мы используем метод статического класса Path
— Combine()
и, при этом, в качестве первого параметра для этого метода указываем папку приложения:
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 мы можем использовать два варианта:
- Создавать каждый раз, при необходимости, объект класса
ProjectDatabase
и работать с ним, например, в модели представления - Зарегистрировать класс
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 мы воспользовались библиотекой с открытым исходным кодом SQLite.NET. В качестве примера мы разработали небольшое приложение, использующее базу данных SQLite для хранения данных о проектах. Проект использует шаблон MVVM и содержит модель, модель представления и представление. Рассмотренный способ работы с SQLite в .NET MAUI не единственный. При желании, мы можем также воспользоваться и «родной» технологией .NET — Microsoft Entity Framework Core.