前言
最近在學(xué)習(xí)WPF,買了一本劉鐵錳老師的《深入淺出WPF》書籍,受益頗深。劉老師是微軟社區(qū)精英,他的C#視頻課程也是很受歡迎,感興趣的小伙伴可以去b站觀看。
這里,以一個在線訂餐系統(tǒng)為例,和大家一起分享MVVM的魅力。
MVVM
首先,我們來看看MVVM到底是什么?
MVVM是Model-View-ViewModel的簡寫,它是一種極度優(yōu)秀的設(shè)計模式,也是MVC的增強版。
View:用戶界面,也叫視圖
由控件構(gòu)成的、與用戶進(jìn)行交互的界面,用于把數(shù)據(jù)展示給用戶并響應(yīng)用戶的輸入。
ViewModel:MVVM的核心
ViewModel通過雙向數(shù)據(jù)綁定將View和Model連接了起來,而View和Model之間的同步工作都是完全自動的,無需人為操作。
Model:數(shù)據(jù)模型
現(xiàn)實世界中事物和邏輯的抽象。
在WPF中,數(shù)據(jù)占據(jù)主導(dǎo)地位,數(shù)據(jù)與界面之間的橋梁是數(shù)據(jù)關(guān)聯(lián),通過這個橋梁,數(shù)據(jù)可以流向界面,也可以從界面流回數(shù)據(jù)源。
ViewModel的存在,使得界面交互業(yè)務(wù)邏輯處理導(dǎo)致的屬性變更會通知到View前端,讓View前端實時更新;View的變動,也會自動反應(yīng)到ViewModel上。
MVVM的出現(xiàn)促進(jìn)了前端開發(fā)與后端的分離,極大提高了前端的開發(fā)效率;幾乎完全解耦了視圖和業(yè)務(wù)邏輯的關(guān)系。
案例剖析
基于以上理解,我們來剖析下面這個在線訂餐系統(tǒng)。
將主界面劃分為三個區(qū)域:第一個區(qū)域是餐館的信息(名字、地址以及電話),中間區(qū)域是菜單列表,每個菜品都有名字、種類、點評、評分以及價格,還有選中框;第三個區(qū)域有菜品的選中總數(shù)和訂餐按鈕。
這里,我們忽視菜品數(shù)據(jù)的來源是來自數(shù)據(jù)庫還是其他什么存儲方式,也忽視點擊訂餐按鈕后訂餐數(shù)據(jù)的處理,主要是想將更多精力集中在MVVM實現(xiàn)上。
我們來找找有多少個數(shù)據(jù)屬性和命令屬性。
顯而易見能看出有一個 餐館Model ,它有名字、地址和電話三個屬性,因此,有一個餐館類數(shù)據(jù)屬性。
右下角有個Order按鈕,明顯是一個命令屬性。
當(dāng)我們選中某個菜品時,這是一個命令屬性;同時共計框那里會有數(shù)值變化,所以,也會有一個菜品選中總數(shù)數(shù)據(jù)屬性。
中間區(qū)域菜單列表中,有很多不同的菜品,所以會有一個 菜品Model ,它有名字、種類、點評、評分以及價格屬性。
以上,我們都能輕易分析出來,但是還有一個選中框,理解起來有一點點難度。
當(dāng)我們打開軟件時,所有的菜品以列表形式展示出來,它的屬性是固定不變的,只有后面的選中框是用戶點擊的,它的值是動態(tài)變化的。
因此,我們把不變的菜品和變化的選中框當(dāng)做是一個ViewModel,它有兩個數(shù)據(jù)屬性,菜品類和是否被選中。
我們的主界面,也會有一個與之對應(yīng)的ViewModel 。 它有三個數(shù)據(jù)屬性,餐館類、選中菜品總數(shù)和Dish列表;有兩個命令屬性,訂餐和菜品是否選中命令。
到這里,基于在線訂餐系統(tǒng)的MVVM各個模型,都已經(jīng)清楚明了了。接下來,我們編程實現(xiàn)它。
案例實現(xiàn)
要實現(xiàn)MVVM,我們需借助Prism。
Prism是一個框架,用于在WPF和Xamarin Forms中構(gòu)建松散耦合,它提供了一組設(shè)計模式的實現(xiàn),這些設(shè)計模式有助于編寫結(jié)構(gòu)良好且可維護(hù)的XAML應(yīng)用程序,包括MVVM、依賴注入、命令、EventAggregator等。
這里用到的是Prism的NotificationObject基類和DelegateCommand,旨在幫助我們借助ViewModel實現(xiàn)View與Model的數(shù)據(jù)自動更新。
打開VS2019,新建一個解決方案,再新建幾個文件夾,分別是Data、Services、Views、Models和ViewModels。這樣,我們的項目整體架構(gòu)就搭建好了。
右擊引用,通過管理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區(qū)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,是兩個基接口,前一個用于獲取菜品數(shù)據(jù);后一個用于訂餐處理。
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文件,獲取菜品數(shù)據(jù)。
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="推薦分?jǐn)?shù)" 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的方式,我們的業(yè)務(wù)邏輯代碼就不會變動,通用性很強。實現(xiàn)了前端界面與后端邏輯分離,開閉原則應(yīng)用的很到位。
寫在最后
基本上,絕大多數(shù)軟件所做的工作無非就是從數(shù)據(jù)存儲中讀出數(shù)據(jù),展現(xiàn)到用戶界面上,然后從用戶界面接收輸入,寫入到數(shù)據(jù)存儲里面去。所以,對于數(shù)據(jù)存儲(Model)和界面(View)這兩層,大家基本沒什么異議。但是,如何把Model展現(xiàn)到View上,以及如何把數(shù)據(jù)從View寫入到Model里,不同的人有不同的意見。
MVC派的看法是,界面上的每個變化都是一個事件,我只需要針對每個事件寫一堆代碼,來把用戶的輸入轉(zhuǎn)換成Model里的對象就行了,這堆代碼可以叫Controller。
而MVVM派的看法是,我給View里面的各種控件也定義一個對應(yīng)的數(shù)據(jù)對象,這樣,只要修改這個數(shù)據(jù)對象,View里面顯示的內(nèi)容就自動跟著刷新;而在View里做了任何操作,這個數(shù)據(jù)對象也跟著自動更新,這樣多美。
所以,ViewModel就是與View對應(yīng)的Model。因為,數(shù)據(jù)庫結(jié)構(gòu)往往是不能直接跟界面控件一一對應(yīng)上的,因此,需要再定義一個數(shù)據(jù)對象專門對應(yīng)View上的控件。而ViewModel的職責(zé)就是把Model對象封裝成可以顯示和接受輸入的界面數(shù)據(jù)對象。
至于ViewModel的數(shù)據(jù)隨著View自動刷新,并且同步到Model里去,這部分代碼可以寫成公用的框架,不用程序員自己操心了。
簡單的說,ViewModel就是View與Model的連接器,View與Model通過ViewModel實現(xiàn)數(shù)據(jù)雙向綁定。
END
-
Model
+關(guān)注
關(guān)注
0文章
339瀏覽量
25061 -
設(shè)計模式
+關(guān)注
關(guān)注
0文章
53瀏覽量
8626 -
MVC
+關(guān)注
關(guān)注
0文章
73瀏覽量
13852
發(fā)布評論請先 登錄
相關(guān)推薦
評論