章节一:项目介绍与环境搭建
1.1 项目概述
本实训手册将指导您一步一步使用 C# 和 WPF 技术,并结合 MVVM (Model-View-ViewModel) 设计模式,构建一个简单的图书管理系统。该系统将包含以下核心功能:
- 图书管理:添加新图书、查看图书列表、修改图书信息、删除图书。
- 库存管理:记录图书的数量,并在添加相同 ISBN 图书时自动增加数量。
- 借阅管理:用户可以借阅图书,系统记录借阅者信息和借阅日期,允许多个用户借阅同一本书(如果库存足够),借阅者以逗号分隔显示。
- 归还管理:用户可以归还已借阅的图书。
- 用户管理:简单的用户登录和切换功能。
- 图书搜索:根据书名或作者搜索图书。
通过本项目的实践,您将深入理解 MVVM 模式的原理和应用,掌握 WPF 数据绑定、命令等核心概念,并学会如何使用 SQLite 作为本地数据库。
1.2 技术选型
- 编程语言:C#
- 用户界面框架:WPF (Windows Presentation Foundation)
- 设计模式:MVVM (Model-View-ViewModel)
- 数据库:SQLite (轻量级本地数据库)
- .NET 版本:本项目可以使用 .NET Framework 4.7.2 或更高版本,或者 .NET Core 3.1 / .NET 5/6/7/8。
1.3 开发环境搭建
在开始项目之前,请确保您的开发环境已配置好以下软件:
- Visual Studio:推荐使用 Visual Studio 2019 或更高版本。请在安装时确保勾选了 “.NET 桌面开发” 工作负载。
- SQLite 支持:
-
- System.Data.SQLite:我们将通过 NuGet 包管理器安装此库,用于在 C# 代码中操作 SQLite 数据库。
-
- SQLite 管理工具 (可选):为了方便查看和管理数据库文件,您可以安装一个 SQLite 图形化管理工具,例如 DB Browser for SQLite。
1.4 创建项目结构
- 打开 Visual Studio。
- 选择“创建新项目”。
- 在搜索框中输入 “WPF”,然后选择 “WPF 应用程序(.NET Framework)” 或 “WPF Application” (.NET Core/5+)。
- 设置项目名称(例如
MWWM01),选择合适的存储位置,然后点击“创建”。
创建项目后,建议组织以下文件夹结构,以更好地管理代码:
MWWM01/
├── Models/ # 存放数据模型类 (e.g., Book.cs, User.cs)
├── ViewModels/ # 存放视图模型类 (e.g., BookViewModel.cs, LoginViewModel.cs)
├── Views/ # 存放视图 XAML 文件 (e.g., BookView.xaml, LoginView.xaml)
├── DataAccess/ # (可选) 存放数据库访问相关类
├── Converters/ # (可选) 存放值转换器类
├── Resources/ # (可选) 存放资源文件,如样式、图片等
├── App.xaml
├── App.xaml.cs
├── MainWindow.xaml # 初始窗口,可以重命名或删除,根据项目需求调整
└── MainWindow.xaml.cs
在本实训中,我们主要关注 Models, ViewModels, 和 Views 这三个核心文件夹的构建。
章节二:MVVM 模式基础
2.1 什么是 MVVM?
MVVM (Model-View-ViewModel) 是一种软件架构设计模式,它是 MVC (Model-View-Controller) 和 MVP (Model-View-Presenter) 模式的演变,特别适用于具有图形用户界面 (GUI) 的应用程序,例如 WPF 和 UWP 应用。
MVVM 模式的核心思想是将应用程序的用户界面 (View) 与其业务逻辑和数据 (Model) 分离开来,并通过一个中间层 ViewModel 来进行协调。这种分离带来了诸多好处,例如:
- 可测试性 (Testability):ViewModel 不依赖于具体的 UI 元素,可以独立进行单元测试。
- 可维护性 (Maintainability):代码职责清晰,修改 UI 或业务逻辑时相互影响较小。
- 可重用性 (Reusability):ViewModel 可以被多个 View 重用。
- 关注点分离 (Separation of Concerns):UI 设计师可以专注于 View 的设计,而开发人员可以专注于 ViewModel 和 Model 的实现。
- 设计时数据 (Design-Time Data):方便 UI 设计师在设计阶段预览界面效果。
2.2 MVVM 的三个核心组件
- 模型 (Model):
-
- 代表应用程序的数据和业务逻辑。
-
- 它通常包含数据实体(例如,在我们的项目中是
Book和User类)以及对这些数据进行操作的业务规则。
- 它通常包含数据实体(例如,在我们的项目中是
-
- Model 不直接与 View 交互,也不知道 View 的存在。
-
- 它可以包含数据验证、数据持久化(例如与数据库交互)等逻辑。
- 视图 (View):
-
- 代表用户界面 (UI)。
-
- 在 WPF 中,View 通常由 XAML 文件定义,负责展示数据和接收用户输入。
-
- View 的主要职责是定义 UI 的结构、布局和外观。
-
- 它通过数据绑定 (Data Binding) 与 ViewModel 进行通信,将 ViewModel 中的数据显示出来,并将用户的操作(如点击按钮、输入文本)通知给 ViewModel。
-
- View 本身不包含任何应用程序逻辑,它应该是“哑”的 (dumb)。
- 视图模型 (ViewModel):
-
- 是 View 和 Model 之间的桥梁。
-
- 它暴露 Model 中的数据(通过属性)和操作(通过命令)给 View。
-
- ViewModel 将 Model 中的数据转换为 View 可以方便展示的格式,并处理来自 View 的用户输入和操作。
-
- 它通常实现了
INotifyPropertyChanged接口,以便在属性值发生变化时通知 View 更新显示。
- 它通常实现了
-
- ViewModel 不直接引用 View,而是通过数据绑定和命令与 View 进行解耦。
2.3 MVVM 中的通信机制
MVVM 模式中各个组件之间的通信主要依赖以下机制:
- 数据绑定 (Data Binding):
-
- 这是连接 View 和 ViewModel 的核心机制。
-
- 允许 View 中的 UI 元素(如
TextBox,ListBox)直接绑定到 ViewModel 中的属性。
- 允许 View 中的 UI 元素(如
-
- 数据绑定可以是单向的(ViewModel -> View 或 View -> ViewModel)或双向的(ViewModel <-> View)。
-
- 当 ViewModel 中的属性值发生变化时,绑定的 UI 元素会自动更新;反之,当用户在 UI 元素中输入数据时,绑定的 ViewModel 属性也会相应更新(对于双向绑定)。
- 命令 (Commands):
-
- 用于处理 View 中的用户操作,例如按钮点击。
-
- View 将用户的操作(如点击按钮)绑定到 ViewModel 中的命令对象 (通常实现
ICommand接口)。
- View 将用户的操作(如点击按钮)绑定到 ViewModel 中的命令对象 (通常实现
-
- 当用户执行操作时,绑定的命令会被执行,从而调用 ViewModel 中相应的方法来处理该操作。
-
- 命令使得 ViewModel 可以响应用户交互,而无需直接引用 View 中的事件处理器。
- 通知 (Notifications):
-
- ViewModel 通常实现
INotifyPropertyChanged接口。
- ViewModel 通常实现
-
- 当 ViewModel 中的属性值发生变化时,它会触发
PropertyChanged事件,通知数据绑定系统更新 View 中相应的 UI 元素。
- 当 ViewModel 中的属性值发生变化时,它会触发
2.4 MVVM 的优势总结
- 松耦合 (Loose Coupling):View 和 ViewModel 之间通过数据绑定和命令进行通信,降低了它们之间的依赖性。
- 代码复用 (Code Reusability):ViewModel 可以在不同的 View 中重用。
- 并行开发 (Parallel Development):UI 设计师和开发人员可以并行工作。
- 易于测试 (Testability):ViewModel 可以独立于 View 进行单元测试。
在接下来的章节中,我们将逐步实践如何在我们的图书管理系统中应用 MVVM 模式的这些核心组件和通信机制。
章节三:模型(Model)的设计与实现
3.1 什么是模型 (Model)?
在 MVVM 模式中,模型 (Model) 负责封装应用程序的数据和业务逻辑。它代表了应用程序的核心领域知识,独立于用户界面 (View) 和视图模型 (ViewModel)。
对于我们的图书管理系统,Model 将包含以下主要部分:
- 数据实体 (Data Entities):定义应用程序中操作的数据结构。例如,
Book类用于表示图书信息,User类用于表示用户信息。
- 业务逻辑 (Business Logic):处理数据的规则和操作。例如,如何计算图书的剩余库存、如何验证用户输入等。
- 数据访问 (Data Access):负责数据的持久化和检索,例如与数据库的交互。虽然有时数据访问逻辑可以放在单独的层(如 Repository 模式),但在简单的应用中,它也可以是 Model 的一部分。
3.2 设计 Book 模型
Book 模型将用于表示系统中的图书信息。我们需要定义图书的各种属性。
- 在
Models文件夹下创建一个新的 C# 类文件,命名为Book.cs。
- 在该文件中定义
Book类及其属性:
using System;
namespace MWWM01.Models
{
public class Book
{
public int Id { get; set; } // 图书唯一标识符
public string Title { get; set; } // 书名
public string Author { get; set; } // 作者
public string ISBN { get; set; } // 国际标准书号
public int Quantity { get; set; } // 库存数量
public DateTime PublishDate { get; set; } // 出版日期
public string? Borrower { get; set; } // 借阅者 (可以为 null,允许多个借阅者,用逗号分隔)
public DateTime? BorrowDate { get; set; } // 借阅日期 (可以为 null)
// 可以添加构造函数、验证逻辑等
public Book()
{
Title = string.Empty;
Author = string.Empty;
ISBN = string.Empty;
PublishDate = DateTime.Now; // 默认为当前日期
}
}
}
代码说明:
Id: 图书的唯一ID,通常由数据库自动生成。
Title: 书名。
Author: 作者。
ISBN: 国际标准书号,用于唯一标识一种图书。我们将基于此字段来合并相同图书的数量。
Quantity: 当前图书的库存数量。
PublishDate: 图书的出版日期。
Borrower: 记录借阅这本书的用户的用户名。我们允许为空,并且允许多个用户借阅同一本书(如果库存足够),用户名之间用逗号分隔。
BorrowDate: 记录图书被借出的日期。如果允许多人借阅,这个字段可能需要调整为记录最后一次借阅日期,或者更详细的借阅信息会存储在专门的借阅记录表中。
- 构造函数中对部分属性进行了初始化,例如将
Title、Author、ISBN初始化为空字符串,并将PublishDate默认为当前日期。
3.3 设计 User 模型 (简化版)
对于用户管理,我们可以设计一个简单的 User 模型来存储用户信息。在实际项目中,用户模型通常会更复杂,包含密码哈希、角色、权限等信息。
- 在
Models文件夹下创建一个新的 C# 类文件,命名为User.cs。
- 在该文件中定义
User类及其属性:
namespace MWWM01.Models
{
public class User
{
public int Id { get; set; } // 用户唯一标识符
public string Username { get; set; } // 用户名
public string Password { get; set; } // 密码 (在实际应用中应存储哈希值)
// 可以添加其他用户信息,如角色、注册日期等
public User()
{
Username = string.Empty;
Password = string.Empty;
}
}
}
代码说明:
Id: 用户的唯一ID。
Username: 登录用户名。
Password: 登录密码。重要提示:在生产环境中,绝不能明文存储密码。应该存储密码的哈希值,并使用安全的哈希算法(如 bcrypt 或 Argon2)。为了本实训的简化,我们暂时使用明文。
3.4 数据持久化 - SQLite 简介
为了存储图书和用户信息,我们将使用 SQLite。SQLite 是一个轻量级的、基于文件的关系型数据库,它不需要单独的服务器进程,非常适合桌面应用程序和小型项目。
我们将在 ViewModel 层处理与 SQLite 数据库的交互逻辑,例如创建表、插入数据、查询数据等。Model 本身只定义数据结构,不直接负责数据库操作的实现细节,这有助于保持 Model 的纯粹性。
在后续章节中,我们将在 BookViewModel.cs 中看到如何初始化数据库、创建表以及执行 CRUD (Create, Read, Update, Delete) 操作。
通过定义这些 Model 类,我们为应用程序的数据结构奠定了基础。接下来,我们将创建 ViewModel 来管理这些 Model,并为 View 提供数据和操作。
章节四:视图模型(ViewModel)的设计与实现
4.1 什么是视图模型 (ViewModel)?
视图模型 (ViewModel) 是 MVVM 模式的核心,它充当视图 (View) 和模型 (Model) 之间的中介。ViewModel 的主要职责包括:
- 暴露数据:将 Model 中的数据以属性的形式暴露给 View,供其绑定和显示。
- 封装行为:提供命令 (Commands) 来响应 View 中的用户操作,并执行相应的业务逻辑(通常通过调用 Model 的方法)。
- 状态管理:维护 View 所需的状态信息,例如当前选中的项、加载状态等。
- 逻辑转换:有时需要将 Model 中的数据转换为 View 更易于显示的格式,或者将 View 输入的数据转换为 Model 可以接受的格式。
- 解耦 View 和 Model:ViewModel 使得 View 不需要直接了解 Model 的具体实现,反之亦然。
一个好的 ViewModel 应该是“纯粹的”,意味着它不应该包含任何对特定 UI 框架(如 WPF 控件)的直接引用。这使得 ViewModel 更易于测试和重用。
4.2 实现 INotifyPropertyChanged 接口
为了使数据绑定能够正常工作,当 ViewModel 中的属性值发生变化时,需要通知 View 更新其显示。这通常通过实现 INotifyPropertyChanged 接口来实现。
- (可选) 可以创建一个基础的 ViewModel 类,实现
INotifyPropertyChanged接口,以便其他 ViewModel 类继承。
在ViewModels文件夹下创建一个ObservableObject.cs(或ViewModelBase.cs) 文件:
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace MWWM01.ViewModels
{
public class ObservableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
field = value;
OnPropertyChanged(propertyName);
return true;
}
}
}
代码说明:
-
INotifyPropertyChanged接口定义了一个PropertyChanged事件。
-
OnPropertyChanged方法用于触发此事件。[CallerMemberName]特性使得在调用此方法时可以自动获取调用该方法的属性的名称。
-
SetProperty是一个辅助方法,用于简化属性的 setter 实现。它会检查新值是否与旧值相同,如果不同则更新字段并触发PropertyChanged事件。
4.3 设计 BookViewModel
BookViewModel 将负责管理图书列表、处理图书的增删改查、借阅和归还等操作。
- 在
ViewModels文件夹下创建BookViewModel.cs文件。
- 让
BookViewModel继承自我们之前创建的ObservableObject(如果创建了的话),或者直接实现INotifyPropertyChanged。
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Data.SQLite;
using System.Linq;
using System.Windows;
using System.Windows.Input;
using MWWM01.Models;
// 如果创建了 ObservableObject,则取消下一行的注释
// namespace MWWM01.ViewModels
// {
// public class BookViewModel : ObservableObject // 或者 INotifyPropertyChanged
// {
// // ... 实现细节 ...
// }
// }
// 临时的基本结构,后续会逐步填充
namespace MWWM01.ViewModels
{
public class BookViewModel : INotifyPropertyChanged // 暂时直接实现接口
{
private readonly string connectionString = "Data Source=library.db;Version=3;";
private ObservableCollection<Book> _books;
public ObservableCollection<Book> Books
{
get { return _books; }
set
{
_books = value;
OnPropertyChanged(nameof(Books));
}
}
private Book _selectedBook;
public Book SelectedBook
{
get { return _selectedBook; }
set
{
_selectedBook = value;
OnPropertyChanged(nameof(SelectedBook));
// 当选中图书变化时,可以触发命令的可执行状态更新
((RelayCommand)BorrowBookCommand).RaiseCanExecuteChanged();
((RelayCommand)ReturnBookCommand).RaiseCanExecuteChanged();
}
}
private string _currentUser;
public string CurrentUser
{
get { return _currentUser; }
set
{
_currentUser = value;
OnPropertyChanged(nameof(CurrentUser));
}
}
// 命令 (ICommand)
public ICommand AddBookCommand { get; private set; }
public ICommand BorrowBookCommand { get; private set; }
public ICommand ReturnBookCommand { get; private set; }
public ICommand SearchBookCommand { get; private set; }
// ... 其他命令
public BookViewModel()
{
InitializeDatabase();
LoadBooks();
CurrentUser = "DefaultUser"; // 初始默认用户,后续会从登录模块获取
// 初始化命令 - 示例,具体实现在后续步骤
AddBookCommand = new RelayCommand(param => AddBook(new Book() { Title="New Book", Author="Author", ISBN="1234567890", Quantity=1, PublishDate=DateTime.Now }), param => true);
BorrowBookCommand = new RelayCommand(param => BorrowSelectedBook(), param => CanBorrowBook());
ReturnBookCommand = new RelayCommand(param => ReturnSelectedBook(), param => CanReturnBook());
// SearchBookCommand = new RelayCommand(param => SearchBooks(searchText), param => true);
}
private void InitializeDatabase()
{
using (var connection = new SQLiteConnection(connectionString))
{
connection.Open();
var bookCommandText = "CREATE TABLE IF NOT EXISTS Books (" +
"Id INTEGER PRIMARY KEY AUTOINCREMENT," +
"Title TEXT NOT NULL," +
"Author TEXT NOT NULL," +
"ISBN TEXT NOT NULL UNIQUE," + // ISBN应该是唯一的
"Quantity INTEGER NOT NULL," +
"PublishDate TEXT NOT NULL," +
"Borrower TEXT," +
"BorrowDate TEXT)";
var bookCommand = new SQLiteCommand(bookCommandText, connection);
bookCommand.ExecuteNonQuery();
// 用户表 (users.db 是独立数据库,这里仅为示例,实际项目中应统一管理)
// 如果要在 library.db 中创建用户表:
var userCommandText = "CREATE TABLE IF NOT EXIS

2210

被折叠的 条评论
为什么被折叠?



