MVVM и SQLite

Последнее обновление: 26.07.2022

В прошлой теме было рассмотрено, как подключаться к SQLite. Теперь посмотрим, как мы можем совместить это с паттерном MVVM.

Итак, возьмем проект WPF, который пусть называется SQLiteApp. В первую очередь нам надо добавить функциональность SQLite и Entity Framework в проект. Для этого добавим в проект через пакетный менеджер Nuget пакет Microsoft.EntityFrameworkCore.Sqlite, как это было показано в прошлой статье.

Model

Вначале определим в проекте класс, объекты которого будут храниться в базе данных. Пусть это будет класс 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".

View для отдельного объекта

Далее определим в проекте новое окно, которое назовем 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, который устанавливается в качестве контекста данных.

ViewModel

Основная логика в 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>

В итоге получится следующая структура проекта:

MVVM и SQLite в WPF и C#

И теперь все вызовы к базе данных SQLite будут идти через ApplicationViewModel.

Основные операции с базой данных SQLite в MVVM в приложении на WPF и C#
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850