В прошлой теме было рассмотрено, как подключаться к SQLite. Теперь посмотрим, как мы можем совместить это с паттерном MVVM.
Итак, возьмем проект WPF, который пусть называется SQLiteApp. В первую очередь нам надо добавить функциональность SQLite и Entity Framework в проект. Для этого добавим в проект через пакетный менеджер Nuget пакет Microsoft.EntityFrameworkCore.Sqlite, как это было показано в прошлой статье.
Вначале определим в проекте класс, объекты которого будут храниться в базе данных. Пусть это будет класс User:
using System.ComponentModel; using System.Runtime.CompilerServices; namespace SQLiteApp { public class User : INotifyPropertyChanged { string? name; int age; public int Id { get; set; } public string? Name { get { return name; } set { name = value; OnPropertyChanged("Name"); } } public int Age { get { return age; } set { age = value; OnPropertyChanged("Age"); } } public event PropertyChangedEventHandler? PropertyChanged; public void OnPropertyChanged([CallerMemberName] string prop = "") { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(prop)); } } }
Класс User содержит свойство Id, которое будет выполнять роль уникального идентификатора пользователя. Кроме того, класс определяет свойство Name (имя пользователя) и Age (возраст пользователя). Класс реализует интерфейс INotifyPropertyChanged, что позволяет ему уведомлять систему об изменении значений свойств Name и Age.
Теперь добавим в проект новый класс, который назовем ApplicationContext и который будет выполнять роль контекста данных:
using Microsoft.EntityFrameworkCore; namespace SQLiteApp { public class ApplicationContext : DbContext { public DbSet<User> Users {get;set; } = null!; protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlite("Data Source=helloapp.db"); } } }
И для взаимодействия с таблицей, которая будет хранить данные объектов User, здесь определено
свойство Users
. Чтобы связать контекст данных с конкретной базой данных, в методе OnConfiguring
устанавливается используемая база данных. В частности методу UseSqlite передается строка подключения, в которой есть только один параметр - Data Source
,
который указывает на имя базы данных. То есть приложение будет использовать базу данных с именем "helloapp.db".
Далее определим в проекте новое окно, которое назовем UserWindow.xaml. В коде xaml у страницы UserWindow.xaml определим следующее содержимое:
<Window x:Class="SQLiteApp.UserWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" Title="UserWindow" Height="200" Width="300"> <Window.Resources> <Style TargetType="TextBlock"> <Setter Property="Margin" Value="8" /> </Style> <Style TargetType="TextBox"> <Setter Property="Margin" Value="8" /> </Style> <Style TargetType="Button"> <Setter Property="MinWidth" Value="60" /> <Setter Property="Margin" Value="8" /> </Style> </Window.Resources> <Grid> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition /> <RowDefinition /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="100" /> <ColumnDefinition /> </Grid.ColumnDefinitions> <TextBlock Text="Имя" /> <TextBlock Text="Возраст" Grid.Row="1" /> <TextBox Text="{Binding Name}" Grid.Column="1" /> <TextBox Text="{Binding Age}" Grid.Column="1" Grid.Row="1" /> <StackPanel HorizontalAlignment="Center" Orientation="Horizontal" Grid.Row="3" Grid.Column="1"> <Button IsDefault="True" Click="Accept_Click" >OK</Button> <Button IsCancel="True" >Отмена</Button> </StackPanel> </Grid> </Window>
Здесь определены два поля ввода для каждого свойства модели User и две кнопки для сохранения и отмены.
В файле связанного кода UserWindow.xaml.cs определим контекст для этой страницы:
using System.Windows; namespace SQLiteApp { public partial class UserWindow : Window { public User User { get; private set; } public UserWindow(User user) { InitializeComponent(); User = user; DataContext = User; } void Accept_Click(object sender, RoutedEventArgs e) { DialogResult = true; } } }
Данное окно будет диалоговым. Через конструктор оно будет получать объект User, который устанавливается в качестве контекста данных.
Основная логика в MVVM заключена в компоненте ViewModel, с которым взаимодействует представление или графический интерфейс посредством команд. Поэтому вначале добавим в проект новый класс RelayCommand, который будет представлять команду:
using System; using System.Windows.Input; namespace SQLiteApp { public class RelayCommand : ICommand { Action<object?> execute; Func<object?, bool>? canExecute; public event EventHandler? CanExecuteChanged { add { CommandManager.RequerySuggested += value; } remove { CommandManager.RequerySuggested -= value; } } public RelayCommand(Action<object?> execute, Func<object?, bool>? canExecute = null) { this.execute = execute; this.canExecute = canExecute; } public bool CanExecute(object? parameter) { return canExecute == null || canExecute(parameter); } public void Execute(object? parameter) { execute(parameter); } } }
Этот класс реализует интерфейс ICommand, благодаря чему с помощью подобных команды мы сможем направлять вызовы к ViewModel.
Ключевым здесь является метод Execute()
, который получает параметр и выполняет действие, переданное через конструктор команды.
Затем определим в проекте собственно компонент ViewModel. Добавим новый класс ApplicationViewModel:
using System.Collections.ObjectModel; using Microsoft.EntityFrameworkCore; namespace SQLiteApp { public class ApplicationViewModel { ApplicationContext db = new ApplicationContext(); RelayCommand? addCommand; RelayCommand? editCommand; RelayCommand? deleteCommand; public ObservableCollection<User> Users { get; set; } public ApplicationViewModel() { db.Database.EnsureCreated(); db.Users.Load(); Users = db.Users.Local.ToObservableCollection(); } // команда добавления public RelayCommand AddCommand { get { return addCommand ?? (addCommand = new RelayCommand((o) => { UserWindow userWindow = new UserWindow(new User()); if (userWindow.ShowDialog() == true) { User user = userWindow.User; db.Users.Add(user); db.SaveChanges(); } })); } } // команда редактирования public RelayCommand EditCommand { get { return editCommand ?? (editCommand = new RelayCommand((selectedItem) => { // получаем выделенный объект User? user = selectedItem as User; if (user == null) return; User vm = new User { Id = user.Id, Name = user.Name, Age = user.Age }; UserWindow userWindow = new UserWindow(vm); if (userWindow.ShowDialog() == true) { user.Name = userWindow.User.Name; user.Age = userWindow.User.Age; db.Entry(user).State = EntityState.Modified; db.SaveChanges(); } })); } } // команда удаления public RelayCommand DeleteCommand { get { return deleteCommand ?? (deleteCommand = new RelayCommand((selectedItem) => { // получаем выделенный объект User? user = selectedItem as User; if (user == null) return; db.Users.Remove(user); db.SaveChanges(); })); } } } }
В конструкторе класса загружаем данные из бд в локальный кэш:
public ApplicationViewModel() { db.Database.EnsureCreated(); db.Users.Load(); Users = db.Users.Local.ToObservableCollection(); }
Для каждого из действий (добавления, изменения, удаления) определяется своя команда - объект RelayCommand.
Команда добавления отображает диалоговое окно UserWindow, которое было создано в прошлой теме, и добавляет в базу данных новый объект:
public RelayCommand AddCommand { get { return addCommand ?? (addCommand = new RelayCommand((o) => { UserWindow userWindow = new UserWindow(new User()); if (userWindow.ShowDialog() == true) { User user = userWindow.User; db.Users.Add(user); db.SaveChanges(); } })); } }
Команда изменения получает объект, который надо изменить, также вызывает для него диалоговое окно UserWindow и сохраняет изменения в базу данных:
public RelayCommand EditCommand { get { return editCommand ?? (editCommand = new RelayCommand((selectedItem) => { // получаем выделенный объект User? user = selectedItem as User; if (user == null) return; User vm = new User { Id = user.Id, Name = user.Name, Age = user.Age }; UserWindow userWindow = new UserWindow(vm); if (userWindow.ShowDialog() == true) { user.Name = userWindow.User.Name; user.Age = userWindow.User.Age; db.Entry(user).State = EntityState.Modified; db.SaveChanges(); } })); } }
В команде удаления также получаем объект, который надо удалить, и удаляем его из БД:
public RelayCommand DeleteCommand { get { return deleteCommand ?? (deleteCommand = new RelayCommand((selectedItem) => { User? user = selectedItem as User; if (user == null) return; db.Users.Remove(user); db.SaveChanges(); })); } }
То есть практически все действия, которые в прошлой теме произодились в коде главного окна, теперь вынесены в команды в ApplicationViewModel.
Теперь код файла MainWindow.xaml.cs:
using System.Windows; namespace SQLiteApp { public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); DataContext = new ApplicationViewModel(); } } }
Теперь в качестве контекста данных выступает объект ApplicationViewModel. И далее в коде XAML у MainWindow определим выражения привязки к командам и к списку Users:
<Window x:Class="SQLiteApp.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" Title="MainWindow" Height="300" Width="425"> <Window.Resources> <Style TargetType="Button"> <Setter Property="MinWidth" Value="60" /> <Setter Property="Margin" Value="8" /> </Style> </Window.Resources> <Grid> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition Height="50" /> </Grid.RowDefinitions> <ListBox x:Name="usersList" ItemsSource="{Binding Users}"> <ListBox.ItemTemplate> <DataTemplate> <StackPanel> <TextBlock Text="{Binding Name}" FontSize="16" /> <TextBlock Text="{Binding Age}" FontSize="13" /> </StackPanel> </DataTemplate> </ListBox.ItemTemplate> </ListBox> <StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Center"> <Button Content="Добавить" Margin="10" Padding="3" Command="{Binding AddCommand}" /> <Button Content="Изменить" Margin="10" Command="{Binding EditCommand}" CommandParameter="{Binding ElementName=usersList, Path=SelectedItem}" /> <Button Content="Удалить" Margin="10" Command="{Binding DeleteCommand}" CommandParameter="{Binding ElementName=usersList, Path=SelectedItem}" /> </StackPanel> </Grid> </Window>
В итоге получится следующая структура проекта:
И теперь все вызовы к базе данных SQLite будут идти через ApplicationViewModel.