在前三章中我们完成了登录窗口, 并掌握了使用 Conductor 来切换窗口, 但这些其实都是在为我们的系统打基础.
而本章中我们就要开始开发系统的核心功能, 即图书管理功能了.
通过本章, 我们会接触到以下知识点:
使用 Stylet 内置 IoC
使用 ViewModel First 解耦 UI
让我们开始吧!
关于 UI
有朋友说我们的系统界面有点简陋, 有点辜负 WPF 的美名. 其实 UI 并不是本系列文章主要关注的内容. 但是既然有朋友指出来, 那么这里就稍微美化一下 UI, 这样看起来也赏心悦目一些.
WPF 的 UI 库有很多, 这里我们使用一个很有名的开源 UI 库: Material Design In XAML http://materialdesigninxaml.net/ .
WPF 使用 UI 库是很简单的, 这里就不做过多说明了, 朋友们可直接看代码. 使用后本章最终效果如下:
Book Model
MVVM 中第一个 M 即为 Model 的意思, 接下来我们就为图书创建 Model 类, 做为图书信息的模型.
在工程中创建一个名为 "Models" 的文件夹, 并在该文件夹下创建 Book 类和 BookType 枚举, 分别代表图书类和图书类型枚举:
Book 类内容如下:
- /// <summary>
- /// 图书
- /// </summary>
- public class Book
- {
- /// <summary>
- /// 书名
- /// </summary>
- public string Name { get; set; }
- /// <summary>
- /// 类型
- /// </summary>
- public BookType Type { get; set; }
- /// <summary>
- /// 出版年月
- /// </summary>
- public DateTime PublishDate { get; set; }
- /// <summary>
- /// 价格
- /// </summary>
- public float Price { get; set; }
- /// <summary>
- /// 封面 URL
- /// </summary>
- public string CoverUrl { get; set; }
- public Book(string name, BookType type, DateTime publishDate, float price, string coverUrl)
- {
- Name = name;
- Type = type;
- PublishDate = publishDate;
- Price = price;
- CoverUrl = coverUrl;
- }
- }
BookType 内容如下:
- public enum BookType
- {
- /// <summary>
- /// 未定义
- /// </summary>
- Undefined,
- /// <summary>
- /// 传记
- /// </summary>
- Biography,
- /// <summary>
- /// 奇幻
- /// </summary>
- Fantastic,
- /// <summary>
- /// 恐怖
- /// </summary>
- Horror,
- /// <summary>
- /// 科幻
- /// </summary>
- ScienceFiction,
- /// <summary>
- /// 悬疑
- /// </summary>
- Mystery,
- /// <summary>
- /// 编程
- /// </summary>
- Programming,
- }
两个文件内容都很简单, 无需做过多解释.
Book 服务
虽然我们的简易系统并不使用数据库, 但是我们仍然需要将图书信息的获取抽象为一个单独的服务, 这样将来如果要实现从数据库 (或其它位置, 如网络) 获取图书信息, 只需要提供相关实现即可.
创建一个名为 "Services" 的文件夹, 并创建 IBookService 接口和 BookService 实现类
IBookService 接口定义如下:
- public interface IBookService
- {
- IEnumerable<Book> GetAllBooks();
- }
现在只需要有一个方法: GetAllBooks - 获取所有图书
BookService 类实现如下:
- public class BookService : IBookService
- {
- private readonly List<Book> _bookStore;
- public BookService()
- {
- _bookStore = new List<Book>
- {
- new Book("阿米尔. 汗: 我行我素", BookType.Biography, DateTime.Parse("2017-6"), 52.8f, "https://img1.doubanio.com/view/subject/l/public/s29467958.jpg"),
- new Book("三体:" 地球往事 "三部曲之一", BookType.ScienceFiction, DateTime.Parse("2008-1"), 23f, "https://img1.doubanio.com/view/subject/l/public/s2768378.jpg"),
- new Book("三体 Ⅱ: 黑暗森林", BookType.ScienceFiction, DateTime.Parse("2008-5"), 32f, "https://img3.doubanio.com/view/subject/l/public/s3078482.jpg"),
- new Book("三体 Ⅲ: 死神永生", BookType.ScienceFiction, DateTime.Parse("2010-11"), 32f, "https://img9.doubanio.com/view/subject/l/public/s26012674.jpg"),
- new Book("肖申克的救赎", BookType.Mystery, DateTime.Parse("2006-7"), 26.9f, "https://img9.doubanio.com/view/subject/l/public/s4007145.jpg"),
- };
- }
- public IEnumerable<Book> GetAllBooks()
- {
- return _bookStore;
- }
- }
在构造方法中, 创建一个 List 用来存储 Book 信息, 并使用代码初始化图书信息. 实际应用中, 这里一般是从数据库中取得数据.
GetAllBooks 中直接返回 list
在 Bootstrapper 类中的 ConfigureIoC 方法中, 注册服务:
- protected override void ConfigureIoC(IStyletIoCBuilder builder)
- {
- // Configure the IoC container in here
- builder.Bind<IBookService>().To<BookService>();
- }
这样我们就可以将 IBookService 注入到需要使用的类中了.
Book 项目
MVVM 中, 我们可将界面拆解成一个个的小组件, 然后将它们组合在一起形成一个复杂的界面. 这样的好处有很多:
不同的开发者可负责不同的组件, 便于开发
每个组件有自己 View 和 ViewModel, 便于测试
组件可以复用, 提高开发效率
组件可替换, 便于扩展维护
总之, 就是将 UI 的部分进行解耦, 达到分而治之的目的.
在未接触 MVMM 之前, 也许我会将图书显示的 UI 代码直接放在 IndexView 中, 而学习了 MVVM 之后, 我们就会很自然的想到将每个图书项目的显示做成一个组件, 然后在 IndexView 中将所有图书组合成一个列表来显示.
所以, 接下来看一下是如何创建图书项目的.
在 "Pages\Books" 文件夹下, 创建 "BookItems" 文件夹, 并创建一个 BookItemViewModel:
- public class BookItemViewModel : Screen
- {
- public Book Book { get; }
- public BookItemViewModel(Book book)
- {
- Book = book;
- }
- }
该 ViewModel 非常简单, 通过构造方法接收一个 Book model, 然后通过只读属性将 Book 暴露出来.
在同一文件夹内, 创建 BookItemView:
- <UserControl ...
- d:DataContext="{d:DesignInstance bookItems:BookItemViewModel}"
- >
- <materialDesign:Card Background="WhiteSmoke">
- <Grid>
- <Grid.RowDefinitions>
- <RowDefinition Height="Auto" />
- <RowDefinition Height="Auto" />
- </Grid.RowDefinitions>
- <Image
- Margin="0 10 0 0"
- Source="{Binding Book.CoverUrl}"
- Height="150"
- Stretch="Uniform" />
- <DockPanel Grid.Row="1">
- <TextBlock Margin="0 10 0 0" DockPanel.Dock="Top" FontWeight="Bold" Text="{Binding Book.Name}" HorizontalAlignment="Center"></TextBlock>
- <TextBlock Text="{Binding Book.Price, StringFormat='¥{0}'}" Margin="0 20 10 10" Foreground="Red" FontWeight="Bold" VerticalAlignment="Bottom" HorizontalAlignment="Right"></TextBlock>
- </DockPanel>
- </Grid>
- </materialDesign:Card>
- </UserControl>
使用 UserControl 做为图书组件
与 Login 类似, 使用 d:DataContext 指定 BookItemViewModel 为设计时实例, 为 XAML 提供智能提示
使用 MaterialDesign 提供的 Card 控件来做为图书项目 UI 的容器
然后分别显示了图书的封面, 书名和价格. 这里未用到 Stylet 的功能, 都是使用了 WPF 基本的绑定语法
实际开发中, 可充分利用第一章中讲解的 Hot Reload 功能, 在运行时调整 XAML.
完成后的工程结构是这样的:
Book 列表
有了图书项目组件, 我们就可以来填充图书列表了.
我们使用 ListView 来显示图书信息, WPF 中的 ListView 是一个非常灵活的控件, 配合 WPF 强大的模板特性, 在展现集合数据时, 几乎可以实现任何效果.
改造 IndexViewModel 如下:
- public class IndexViewModel : Screen
- {
- private readonly IBookService _bookService;
- public ObservableCollection<BookItemViewModel> BookItems { get; set; } = new ObservableCollection<BookItemViewModel>();
- public IndexViewModel(IBookService bookService)
- {
- _bookService = bookService;
- }
- protected override void OnViewLoaded()
- {
- var viewModels = _bookService.GetAllBooks()
- .Select(book => new BookItemViewModel(book))
- ;
- BookItems = new ObservableCollection<BookItemViewModel>(viewModels);
- }
- }
为图书项目创建一个
ObservableCollection
类型的属性, 名为 BookItems. 使用 ObservableCollection 可以在图书增加或减少时自动发送通知
在构造方法中, 注入 IBookService, 并存储为成员变量
在 OnViewLoaded 方法中, 调用
IBookService.GetAllBooks
然后转换成图书列表 ViewModel
改造 IndexView 如下:
- <UserControl
- ...
- >
- <ListView ItemsSource="{Binding BookItems}" ScrollViewer.HorizontalScrollBarVisibility="Disabled">
- <ListView.ItemsPanel>
- <ItemsPanelTemplate>
- <WrapPanel/>
- </ItemsPanelTemplate>
- </ListView.ItemsPanel>
- <ListView.ItemTemplate>
- <DataTemplate>
- <ContentControl s:View.Model="{Binding}"></ContentControl>
- </DataTemplate>
- </ListView.ItemTemplate>
- </ListView>
- </UserControl>
将 ListView 的 ItemSource 绑定到 IndexViewModel 中 BookItems
使用 ContentControl 做为 ListView.ItemTemplate 的数据模板
同 ShellView 中的写法类似, 使用 Stylet 提供的 s:View.Model 为 ContentControl 绑定一个 ViewModel(这里即是 BookItemViewModel), Stylet 会自动为该 ContentControl 加载 View(即 BookItemView)
可以看到, IndexView 并不知道 BookItemView 的存在, 一切都是由后面的 ViewModel 关联在一起的, 这样我们就实现了 View 之间的解耦.
最后运行程序, 确认运行正常.
本章的任务就完成了. 在本章中, 我们创建了一个图书组件, 并使用 ViewModel First 来驱动各个 UI 部分. 下一章中我们会讲解 Master-Detail 这种经典的数据表现形式. 希望朋友们能多多留言, 交流心得. 源码托管在 GitHub 上.
Happy Coding~
来源: https://www.cnblogs.com/waku/p/12231354.html