前言
最近在学习WPF,买了一本刘铁锰老师的《深入浅出WPF》书籍,受益颇深。刘老师是微软社区精英,他的C#视频课程也是很受欢迎,感兴趣的小伙伴可以去b站观看。
这里,以一个在线订餐系统为例,和大家一起分享MVVM的魅力。
MVVM
首先,我们来看看MVVM到底是什么?
MVVM是Model-View-ViewModel的简写,它是一种极度优秀的设计模式,也是MVC的增强版。
View:用户界面,也叫视图
由控件构成的、与用户进行交互的界面,用于把数据展示给用户并响应用户的输入。
ViewModel:MVVM的核心
ViewModel通过双向数据绑定将View和Model连接了起来,而View和Model之间的同步工作都是完全自动的,无需人为操作。
Model:数据模型
现实世界中事物和逻辑的抽象。
在WPF中,数据占据主导地位,数据与界面之间的桥梁是数据关联,通过这个桥梁,数据可以流向界面,也可以从界面流回数据源。
ViewModel的存在,使得界面交互业务逻辑处理导致的属性变更会通知到View前端,让View前端实时更新;View的变动,也会自动反应到ViewModel上。
MVVM的出现促进了前端开发与后端的分离,极大提高了前端的开发效率;几乎完全解耦了视图和业务逻辑的关系。
案例剖析
基于以上理解,我们来剖析下面这个在线订餐系统。
将主界面划分为三个区域:第一个区域是餐馆的信息(名字、地址以及电话),中间区域是菜单列表,每个菜品都有名字、种类、点评、评分以及价格,还有选中框;第三个区域有菜品的选中总数和订餐按钮。
这里,我们忽视菜品数据的来源是来自数据库还是其他什么存储方式,也忽视点击订餐按钮后订餐数据的处理,主要是想将更多精力集中在MVVM实现上。
我们来找找有多少个数据属性和命令属性。
显而易见能看出有一个 餐馆Model ,它有名字、地址和电话三个属性,因此,有一个餐馆类数据属性。
右下角有个Order按钮,明显是一个命令属性。
当我们选中某个菜品时,这是一个命令属性;同时共计框那里会有数值变化,所以,也会有一个菜品选中总数数据属性。
中间区域菜单列表中,有很多不同的菜品,所以会有一个 菜品Model ,它有名字、种类、点评、评分以及价格属性。
以上,我们都能轻易分析出来,但是还有一个选中框,理解起来有一点点难度。
当我们打开软件时,所有的菜品以列表形式展示出来,它的属性是固定不变的,只有后面的选中框是用户点击的,它的值是动态变化的。
因此,我们把不变的菜品和变化的选中框当做是一个ViewModel,它有两个数据属性,菜品类和是否被选中。
我们的主界面,也会有一个与之对应的ViewModel 。 它有三个数据属性,餐馆类、选中菜品总数和Dish列表;有两个命令属性,订餐和菜品是否选中命令。
到这里,基于在线订餐系统的MVVM各个模型,都已经清楚明了了。接下来,我们编程实现它。
案例实现
要实现MVVM,我们需借助Prism。
Prism是一个框架,用于在WPF和Xamarin Forms中构建松散耦合,它提供了一组设计模式的实现,这些设计模式有助于编写结构良好且可维护的XAML应用程序,包括MVVM、依赖注入、命令、EventAggregator等。
这里用到的是Prism的NotificationObject基类和DelegateCommand,旨在帮助我们借助ViewModel实现View与Model的数据自动更新。
打开VS2019,新建一个解决方案,再新建几个文件夹,分别是Data、Services、Views、Models和ViewModels。这样,我们的项目整体架构就搭建好了。
右击引用,通过管理NuGet程序包,在弹出的窗口浏览中输入“Prism.MVVM”,在线安装Prism。
Data文件夹中,存放的是Data.xml,里面是菜品信息。
<Dishes>
<Dish>
<Name>水煮肉片Name>
<Category>徽菜Category>
<Comment>招牌菜Comment>
<Score>9.7Score>
<Price>45元Price>
Dish>
<Dish>
<Name>椒盐龙虾Name>
<Category>川菜Category>
<Comment>招牌菜Comment>
<Score>9.2Score>
<Price>43元Price>
Dish>
<Dish>
<Name>京酱猪蹄Name>
<Category>湘菜Category>
<Comment>招牌菜Comment>
<Score>9.8Score>
<Price>51元Price>
Dish>
<Dish>
<Name>爆炒鱿鱼Name>
<Category>徽菜Category>
<Comment>招牌菜Comment>
<Score>9.3Score>
<Price>54元Price>
Dish>
<Dish>
<Name>可乐鸡翅Name>
<Category>湘菜Category>
<Comment>招牌菜Comment>
<Score>9.4Score>
<Price>44元Price>
Dish>
<Dish>
<Name>凉拌龙须Name>
<Category>湘菜Category>
<Comment>凉拌Comment>
<Score>8.6Score>
<Price>18元Price>
Dish>
<Dish>
<Name>麻辣花生Name>
<Category>湘菜Category>
<Comment>凉拌Comment>
<Score>8.7Score>
<Price>19元Price>
Dish>
<Dish>
<Name>韭菜炒肉Name>
<Category>湘菜Category>
<Comment>炒菜Comment>
<Score>9.4Score>
<Price>25元Price>
Dish>
<Dish>
<Name>青椒肉丝Name>
<Category>湘菜Category>
<Comment>炒菜Comment>
<Score>9.1Score>
<Price>26元Price>
Dish>
<Dish>
<Name>红烧茄子Name>
<Category>湘菜Category>
<Comment>招牌菜Comment>
<Score>9.4Score>
<Price>24元Price>
Dish>
<Dish>
<Name>红烧排骨Name>
<Category>湘菜Category>
<Comment>招牌菜Comment>
<Score>9.4Score>
<Price>42元Price>
Dish>
<Dish>
<Name>番茄蛋汤Name>
<Category>湘菜Category>
<Comment>招牌菜Comment>
<Score>9.4Score>
<Price>21元Price>
Dish>
<Dish>
<Name>山药炒肉Name>
<Category>湘菜Category>
<Comment>招牌菜Comment>
<Score>9.4Score>
<Price>27元Price>
Dish>
<Dish>
<Name>极品肥牛Name>
<Category>湘菜Category>
<Comment>招牌菜Comment>
<Score>9.4Score>
<Price>58元Price>
Dish>
<Dish>
<Name>香拌牛肉Name>
<Category>湘菜Category>
<Comment>招牌菜Comment>
<Score>9.4Score>
<Price>48元Price>
Dish>
<Dish>
<Name>手撕包菜Name>
<Category>湘菜Category>
<Comment>招牌菜Comment>
<Score>9.4Score>
<Price>16元Price>
Dish>
<Dish>
<Name>香辣花甲Name>
<Category>湘菜Category>
<Comment>招牌菜Comment>
<Score>9.4Score>
<Price>36元Price>
Dish>
<Dish>
<Name>酸菜鱼Name>
<Category>湘菜Category>
<Comment>招牌菜Comment>
<Score>9.4Score>
<Price>56元Price>
Dish>
Dishes>
Models文件夹中,存放的是Dish类和Restaurant类。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace zy.CrazyElephant.Client.Models
{
public class Dish
{
public string Name { get; set; }
public string Category { get; set; }
public string Comment { get; set; }
public double Score { get; set; }
public string Price { get; set; }
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace zy.CrazyElephant.Client.Models
{
public class Restaurant
{
public string Name { get; set; }
public string Address { get; set; }
public string PhoneNumber { get; set; }
}
}
ViewModels中,存放的是两个ViewModel。
using Microsoft.Practices.Prism.ViewModel;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using zy.CrazyElephant.Client.Models;
namespace zy.CrazyElephant.Client.ViewModels
{
public class DishMenuItemViewModel:NotificationObject
{
public Dish Dish{ get; set; }
private bool isSelected;
public bool IsSelected
{
get { return isSelected; }
set
{
isSelected = value;
this.RaisePropertyChanged("IsSelected");
}
}
}
}
using Microsoft.Practices.Prism.Commands;
using Microsoft.Practices.Prism.ViewModel;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using zy.CrazyElephant.Client.Models;
using zy.CrazyElephant.Client.Services;
namespace zy.CrazyElephant.Client.ViewModels
{
public class MainWindowViewModel:NotificationObject
{
public MainWindowViewModel()
{
this.LoadRestaurant();
this.LoadMenu();
this.PlaceOrderCommand = new DelegateCommand(PlaceOrderCommandExecute);
this.SelectMenuItemCommand = new DelegateCommand(SelectMenuItemExecute);
}
public DelegateCommand PlaceOrderCommand { get; set; }
public DelegateCommand SelectMenuItemCommand { get; set; }
private int count;
public int Count
{
get { return count; }
set
{
count = value;
this.RaisePropertyChanged("Count");
}
}
private Restaurant restaurant;
public Restaurant Restaurant
{
get { return restaurant; }
set
{
restaurant = value;
this.RaisePropertyChanged("Restaurant");
}
}
private List dishMenu;
public List DishMenu
{
get { return dishMenu; }
set
{
dishMenu = value;
this.RaisePropertyChanged("DishMenu");
}
}
private void LoadRestaurant()
{
this.Restaurant = new Restaurant();
this.Restaurant.Name = "聚贤庄";
this.Restaurant.Address = "xx省xx市xx区xx街道xx楼xx层xx号";
this.Restaurant.PhoneNumber = "18888888888 or 6666-6666666";
}
private void LoadMenu()
{
XmlDataService ds = new XmlDataService();
var dishes = ds.GetAllDishes();
this.DishMenu = new List();
foreach (var dish in dishes)
{
DishMenuItemViewModel item = new DishMenuItemViewModel();
item.Dish = dish;
this.DishMenu.Add(item);
}
}
private void PlaceOrderCommandExecute()
{
var selectedDishes = this.DishMenu.Where(i => i.IsSelected == true).Select(i => i.Dish.Name).ToList();
IOrderService os = new MockOrderService();
os.PlaceOrder(selectedDishes);
MessageBox.Show("订餐成功!");
}
private void SelectMenuItemExecute()
{
this.Count = this.DishMenu.Count(i => i.IsSelected == true);
}
}
}
Services中,IDataService和IOrderService,是两个基接口,前一个用于获取菜品数据;后一个用于订餐处理。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using zy.CrazyElephant.Client.Models;
namespace zy.CrazyElephant.Client.Services
{
public interface IDataService
{
List GetAllDishes();
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace zy.CrazyElephant.Client.Services
{
public interface IOrderService
{
void PlaceOrder(List dishes);
}
}
XmlDataService类,继承自IDataService,用于读取xml文件,获取菜品数据。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Linq;
using zy.CrazyElephant.Client.Models;
namespace zy.CrazyElephant.Client.Services
{
public class XmlDataService : IDataService
{
public List GetAllDishes()
{
List dishList = new List();
string xmlFileName = System.IO.Path.Combine(Environment.CurrentDirectory, @"Data\\Data.xml");
XDocument doc = XDocument.Load(xmlFileName);
var dishes = doc.Descendants("Dish");
foreach (var d in dishes)
{
Dish dish = new Dish();
dish.Name = d.Element("Name").Value;
dish.Category = d.Element("Category").Value;
dish.Comment = d.Element("Comment").Value;
dish.Score = Convert.ToDouble(d.Element("Score").Value);
dish.Price = d.Element("Price").Value;
dishList.Add(dish);
}
return dishList;
}
}
}
MockOrderService类,继承自IOrderService,用于处理订餐逻辑,这里是把选中的菜品名字以txt文件保存到硬盘中。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace zy.CrazyElephant.Client.Services
{
public class MockOrderService : IOrderService
{
public void PlaceOrder(List dishes)
{
System.IO.File.WriteAllLines(Environment.CurrentDirectory + "\\\\order.txt", dishes.ToArray());
}
}
}
只有一个界面,即MainWindow.xaml,代码如下。
<Window x:Class="zy.CrazyElephant.Client.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"
xmlns:local="clr-namespace:zy.CrazyElephant.Client"
mc:Ignorable="d"
Title="{Binding Restaurant.Name,StringFormat=\\{0\\}-在线订餐}" Height="600" Width="1000" WindowStartupLocation="CenterScreen">
<Border BorderBrush="Orange" BorderThickness="3" CornerRadius="6" Background="AliceBlue">
<Grid x:Name="Root" Margin="4">
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="auto"/>
Grid.RowDefinitions>
<Border BorderBrush="Orange" BorderThickness="1" CornerRadius="6" Padding="4">
<StackPanel>
<StackPanel Orientation="Horizontal">
<StackPanel.Effect>
<DropShadowEffect Color="LightGray"/>
StackPanel.Effect>
<TextBlock Text="欢迎光临-" FontSize="60" FontFamily="LiShu"/>
<TextBlock Text="{Binding Restaurant.Name}" FontSize="60" FontFamily="LiShu"/>
StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="小店地址:" FontSize="24" FontFamily="LiShu"/>
<TextBlock Text="{Binding Restaurant.Address}" FontSize="24" FontFamily="LiShu"/>
StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="订餐电话:" FontSize="24" FontFamily="LiShu"/>
<TextBlock Text="{Binding Restaurant.PhoneNumber}" FontSize="24" FontFamily="LiShu"/>
StackPanel>
StackPanel>
Border>
<DataGrid AutoGenerateColumns="False" GridLinesVisibility="None" CanUserAddRows="False" CanUserDeleteRows="False" Margin="0.4" Grid.Row="1" FontSize="16" ItemsSource="{Binding DishMenu}">
<DataGrid.Columns>
<DataGridTextColumn Header="菜品" Binding="{Binding Dish.Name}" Width="120"/>
<DataGridTextColumn Header="种类" Binding="{Binding Dish.Category}" Width="120"/>
<DataGridTextColumn Header="点评" Binding="{Binding Dish.Comment}" Width="120"/>
<DataGridTextColumn Header="推荐分数" Binding="{Binding Dish.Score}" Width="120"/>
<DataGridTextColumn Header="价格" Binding="{Binding Dish.Price}" Width="120"/>
<DataGridTemplateColumn Header="选中" SortMemberPath="IsSelected" Width="120">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding Path=IsSelected,UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Center" HorizontalAlignment="Center" Command="{Binding Path=DataContext.SelectMenuItemCommand,RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=DataGrid}}"/>
DataTemplate>
DataGridTemplateColumn.CellTemplate>
DataGridTemplateColumn>
DataGrid.Columns>
DataGrid>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Grid.Row="2">
<TextBlock Text="共计" VerticalAlignment="Center"/>
<TextBox IsReadOnly="True" TextAlignment="Center" Width="120" Text="{Binding Count}" Margin="4,0"/>
<Button Content="Order" Height="24" Width="120" Command="{Binding PlaceOrderCommand}"/>
StackPanel>
Grid>
Border>
Window>
MainWindow.xaml.cs中,代码很简单,只需要添加一行即可。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using zy.CrazyElephant.Client.ViewModels;
namespace zy.CrazyElephant.Client
{
/// <summary>
/// MainWindow.xaml 的交互逻辑
/// summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new MainWindowViewModel();
}
}
}
无论界面如何变化,只要符合这种逻辑形式的软件通过前端界面Binding的方式,我们的业务逻辑代码就不会变动,通用性很强。实现了前端界面与后端逻辑分离,开闭原则应用的很到位。
写在最后
基本上,绝大多数软件所做的工作无非就是从数据存储中读出数据,展现到用户界面上,然后从用户界面接收输入,写入到数据存储里面去。所以,对于数据存储(Model)和界面(View)这两层,大家基本没什么异议。但是,如何把Model展现到View上,以及如何把数据从View写入到Model里,不同的人有不同的意见。
MVC派的看法是,界面上的每个变化都是一个事件,我只需要针对每个事件写一堆代码,来把用户的输入转换成Model里的对象就行了,这堆代码可以叫Controller。
而MVVM派的看法是,我给View里面的各种控件也定义一个对应的数据对象,这样,只要修改这个数据对象,View里面显示的内容就自动跟着刷新;而在View里做了任何操作,这个数据对象也跟着自动更新,这样多美。
所以,ViewModel就是与View对应的Model。因为,数据库结构往往是不能直接跟界面控件一一对应上的,因此,需要再定义一个数据对象专门对应View上的控件。而ViewModel的职责就是把Model对象封装成可以显示和接受输入的界面数据对象。
至于ViewModel的数据随着View自动刷新,并且同步到Model里去,这部分代码可以写成公用的框架,不用程序员自己操心了。
简单的说,ViewModel就是View与Model的连接器,View与Model通过ViewModel实现数据双向绑定。
END
全部0条评论
快来发表一下你的评论吧 !