WPF MVVM
- 基本常识
- MVVM设计模式详解
一、基本常识:
1.开发环境:
VS
Microsoft Prism 框架(对MVVM的完备支持)
Microsoft Blend SDK
2.必要知识的准备:
Data Binding
Dependeccy Property
命令(ICommand接口即可)
Lambda表达式
CodeSnippet(代码模板):
使用和创建
二、MVVM设计模式详解
MVVM=Model-View-ViewModel
为什么要使用MVVM模式:
统一思维方式和实现方法;
稳定,解耦(UI与业务逻辑),富有禅意。
可读,可测,可替换。
什么是Model:
现实世界中对象的抽象结果;
什么是View 和ViewModel:
View=UI
ViewModel=Model for View
ViewModel与View的沟通
传递数据--数据属性
传递操作--命令属性
NotificationObject与数据属性;
DelegateCommand与命令属性
View 与ViewModel的交互(技术难点)
DemoWithoutMVVM

<Window x:Class="DemoWithoutMVVM.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:DemoWithoutMVVM" mc:Ignorable="d" Title="MainWindow" Height="450" Width="800"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto"></RowDefinition> <RowDefinition Height="*"></RowDefinition> </Grid.RowDefinitions> <Button Content="SAVE" Name="save_button" Click="save_button_Click"></Button> <Grid Grid.Row="1"> <Grid.RowDefinitions> <RowDefinition Height="Auto"></RowDefinition> <RowDefinition Height="Auto"></RowDefinition> <RowDefinition Height="Auto"></RowDefinition> <RowDefinition Height="*"></RowDefinition> </Grid.RowDefinitions> <TextBox x:Name="tb1" Grid.Row="0" Background="LightBlue" FontSize="24" Margin="4"/> <TextBox x:Name="tb2" Grid.Row="1" Background="LightBlue" FontSize="24" Margin="4"/> <TextBox x:Name="tb3" Grid.Row="2" Background="LightBlue" FontSize="24" Margin="4"/> <Button x:Name="addButton" Grid.Row="3" Content="ADD" Width="120" Height="80" Click="addButton_Click"/> </Grid> </Grid> </Window>
using Microsoft.Win32; 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; namespace DemoWithoutMVVM { /// <summary> /// MainWindow.xaml 的交互逻辑 /// </summary> public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } private void save_button_Click(object sender, RoutedEventArgs e) { SaveFileDialog saveFileDialog = new SaveFileDialog(); saveFileDialog.ShowDialog(); } private void addButton_Click(object sender, RoutedEventArgs e) { double d1 = double.Parse(this.tb1.Text); double d2 = double.Parse(this.tb2.Text); double result = d1 + d2; this.tb3.Text = result.ToString(); } } }

新的需求:

问题:界面改了之后,后台代码要跟着改。

使用MVVM模式:
(涉及到Linkq和lambda知识,数据属性(NotificationObject),命令属性(DelegateCommand))
恢复界面文件
<Window x:Class="DemoWithoutMVVM.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:DemoWithoutMVVM" mc:Ignorable="d" Title="MainWindow" Height="450" Width="800"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto"></RowDefinition> <RowDefinition Height="*"></RowDefinition> </Grid.RowDefinitions> <Button Content="SAVE" Name="save_button""></Button> <Grid Grid.Row="1"> <Grid.RowDefinitions> <RowDefinition Height="Auto"></RowDefinition> <RowDefinition Height="Auto"></RowDefinition> <RowDefinition Height="Auto"></RowDefinition> <RowDefinition Height="*"></RowDefinition> </Grid.RowDefinitions> <TextBox x:Name="tb1" Grid.Row="0" Background="LightBlue" FontSize="24" Margin="4"/> <TextBox x:Name="tb2" Grid.Row="1" Background="LightBlue" FontSize="24" Margin="4" /> <TextBox x:Name="tb3" Grid.Row="2" Background="LightBlue" FontSize="24" Margin="4" /> <Button x:Name="addButton" Grid.Row="3" Content="ADD" Width="120" Height="80" /> </Grid> </Grid> </Window>
删掉事件处理程序
namespace DemoWithoutMVVM { /// <summary> /// MainWindow.xaml 的交互逻辑 /// </summary> public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } } }
创建五个文件夹(view 和modes现在是用不上)

(不借助框架的话)创建NotificationObject和DelegateCommand
viewmodel通知值的变化靠binding,属性值变化时,通知binding,binding把数据送上view
namespace DemoWithoutMVVM.ViewModels { /// <summary> /// NotificationObject是ViewModel的基类 /// </summary> class NotificationObject : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged;//继承自INotifyPropertyChanged的PropertyChanged 事件 //binding监听PropertyChanged事件有没有发生,当属性值变化时,binding把数据送到控件 //对事件进行简单封装,写一个方法 public void RaisePropertyChanged(string propertyName) { if(this.PropertyChanged!=null) { this.PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propertyName)); } } } }
建立一个简陋(不完备)的DelegateCommand派生自ICommand(用于实现命令传输)
namespace DemoWithoutMVVM.Commands { /// <summary> /// ICommand接口里有三个成员CanExecuteChanged、CanExecute、Execute /// </summary> class DelegateCommand : ICommand { //当命令能不能执行的状态发生改变时通知一下命令的调用者,告诉它状态changed public event EventHandler CanExecuteChanged; /// <summary> /// 帮助命令的呼叫着判断命令能不能执行 /// </summary> /// <param name="parameter"></param> /// <returns></returns> public bool CanExecute(object parameter) { //throw new NotImplementedException(); if(this.CanExecuteFunc==null) { return true; } return this.CanExecuteFunc(parameter); } /// <summary> /// 当命令执行的时候想要做什么事情 /// </summary> /// <param name="parameter"></param> public void Execute(object parameter) { // throw new NotImplementedException(); if(this.ExecuteAction==null) { return; } this.ExecuteAction(parameter); } //声明Action属性 public Action<object> ExecuteAction { get; set; } //再声明一个 public Func<object, bool> CanExecuteFunc { get; set; } } }
ViewModel是View的模型
创建ViewModel/MainWindowViewModel.cs
namespace DemoWithoutMVVM.ViewModels { /// <summary> /// 包含三个数据属性,两个命令属性 /// </summary> class MainWindowViewModel:NotificationObject { private double input1; public double Input1 { get { return input1; } set { input1 = value; this.RaisePropertyChanged("Input1"); } } private double input2; public double Input2 { get { return input2; } set { input2 = value; this.RaisePropertyChanged("Input2"); } } private double result; public double Result { get { return result; } set { result = value; this.RaisePropertyChanged("Result"); } } public DelegateCommand AddCommand { get; set; } public DelegateCommand SaveCommand { get; set; } private void Add(object parameter) { this.Result = this.input1 + this.input2; } private void Save(object parameter) { SaveFileDialog dlg = new SaveFileDialog(); dlg.ShowDialog(); } //把AddCommand和Add关联 public MainWindowViewModel() { this.AddCommand = new DelegateCommand(); this.AddCommand.ExecuteAction = new Action<object>(this.Add); this.SaveCommand = new DelegateCommand(); SaveCommand.ExecuteAction += Save; } } }
添加binding
<Window x:Class="DemoWithoutMVVM.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:DemoWithoutMVVM" mc:Ignorable="d" Title="MainWindow" Height="450" Width="800"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto"></RowDefinition> <RowDefinition Height="*"></RowDefinition> </Grid.RowDefinitions> <Button Content="SAVE" Name="save_button" Command="{Binding SaveCommand}"></Button> <Grid Grid.Row="1"> <Grid.RowDefinitions> <RowDefinition Height="Auto"></RowDefinition> <RowDefinition Height="Auto"></RowDefinition> <RowDefinition Height="Auto"></RowDefinition> <RowDefinition Height="*"></RowDefinition> </Grid.RowDefinitions> <TextBox x:Name="tb1" Grid.Row="0" Background="LightBlue" FontSize="24" Margin="4" Text="{Binding Input1}"/> <TextBox x:Name="tb2" Grid.Row="1" Background="LightBlue" FontSize="24" Margin="4" Text="{Binding Input2}"/> <TextBox x:Name="tb3" Grid.Row="2" Background="LightBlue" FontSize="24" Margin="4" Text="{Binding Result}"/> <Button x:Name="addButton" Grid.Row="3" Content="ADD" Width="120" Height="80" Command="{Binding AddCommand}"/> </Grid> </Grid> </Window>
WPF默认规定,如果Binding只指定Path,没指定Source,拿自己的DataContext,自己的DataContext没有,就一层一层往上找
设置DataContext,view 和ViewModel就关联上了
namespace DemoWithoutMVVM { /// <summary> /// MainWindow.xaml 的交互逻辑 /// </summary> public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); this.DataContext = new MainWindowViewModel(); } } }
运行

(现在即使把UI改变后,后台代码不动,程序正常编译运行,后台程序完全没有引用前台程序的东西)
2.进阶案例:餐馆点餐系统(mvvm在实际项目中的使用)(使用Miscrosoft Prism框架)
界面

(分析界面的本质:
标题使用Databinding
餐馆名字、地址、订餐电话 会变化
所以需要一个区域显示餐馆基本信息
菜品信息
需要数据属性显示选中了多少菜品
order按钮
checkbox和button的命令属性
涉及到集合元素某个元素和命令属性的关系
面向对象,所以餐馆名字地址电话来自于同一个数据属性)
项目结构:

Views是空的,因为只有一个主窗体,Services用于读取菜品和保存选中的菜,Data放菜单
添加Data文件夹,
添加Data.xml,
<?xml version="1.0" encoding="utf-8" ?> <Dishes> <Dish> <Name>土豆泥披萨</Name> <Category>披萨</Category> <Comment>好吃</Comment> </Dish> <Dish> <Name>榴莲披萨</Name> <Category>披萨</Category> <Comment>很好吃</Comment> </Dish> <Dish> <Name>牛肉披萨</Name> <Category>披萨</Category> <Comment>特色</Comment> </Dish> <Dish> <Name>奥尔良烤鸡翅</Name> <Category>鸡翅</Category> <Comment>堪比KFC</Comment> </Dish> </Dishes>
为了使得Data数据随着程序发布,需要修改属性

添加Services文件夹
为了应对数据源发生变化(存在xml改成存在数据库中),需要写一个接口
namespace CrazyElephent.Client.Services { public interface IDataService { List<Dish> GetAllDishes(); } }
上面的Dish和Model相关。
创建Models文件夹。
添加Dish,
namespace CrazyElephent.Client.Models { class Dish { public string Name { get; set; } public string Cateory { get; set; } public string Comment { get; set; } public double Score { get; set; } } }
Dish需不需要派生自NotificationObject,根据业务需求来定,如果觉得在Dish在界面上动态改变,就继承。
实现IDataService接口的数据文件读取功能,
namespace CrazyElephent.Client.Services { class XmlDataService:IDataService { public List<Dish> GetAllDishes() { List<Dish> dishList = new List<Dish>(); string xmlFileName = System.IO.Path.Combine(Environment.CurrentDirectory, @"Data\Data.xml"); XDocument xDoc = XDocument.Load(xmlFileName); var dishes = xDoc.Descendants("Dish"); foreach(var d in dishes) { Dish dish = new Dish(); dish.Name = d.Element("Name").Value; dish.Cateory = d.Element("Category").Value; dish.Comment = d.Element("Comment").Value; dish.Score =double.Parse( d.Element("Score").Value); dishList.Add(dish); } return dishList; } } }
上面实现的定义和实现分离
添加下订单的Service接口
namespace CrazyElephent.Client.Services { interface IOrderService { void PlaceOrder(List<string> dishes); } }
实现一个假的IOrderService,简单的把订单菜品写到txt,
namespace CrazyElephent.Client.Services { class MockOrderService : IOrderService { public void PlaceOrder(List<string> dishes) { System.IO.File.WriteAllLines(@"C:\order.txt", dishes.ToArray()); } } }
对餐馆信息进行抽象,
namespace CrazyElephent.Client.Models { public class Restaurant { public string Name { get; set; } public string Address { get; set; } public int PhoneNumber { get; set; } } }
接着进行View和ViewModel

把ViewModel和Model分开,ViewModel起到数据的校验和过滤的作用,从ViewModel传来的数据是干净的数据,来保护Model
菜是否被选中不是菜的属性,用户操作的不是菜本身,是菜的ViewModel
给每个Dish建立ViewModel
DishMenuItemViewModel有一个Dish属性,保存Dish的名字等等,ViewModel里有一个Model的关系,通过这种方式,可以获得dish的值
还有一种是是一个的方式,DishMenuItemViewModel派生自Dish,但是破坏了MVVM的设计原理,还可以DishMenuItemViewModel直接包含DIsh里面的的属性,但是Dish的属性很多会造成内存浪费。
(不自己编写NotifcationObject和DelegateCommand,安装Prism框架)


DishMenuItemViewModel编写
namespace CrazyElephent.Client.ViewModels { class DishMenuItemViewModel: BindableBase { //在 Prism.Mvvm中的BindableBase 类替代NotifcationObject 。NotificationObject 和 NotificationObject 类在Prism程序集中被标记为过时。 public Dish Dish { get; set; } private bool isSelected; public bool IsSelected { get { return isSelected; } set { IsSelected = value; this.RaisePropertyChanged("IsSelected"); } } } }
MainWindowViewModel编写
namespace CrazyElephent.Client.ViewModels { class MainWindowViewModel: BindableBase { //需要一个餐馆对象 //菜是否被选中不是菜的属性,用户操作的不是菜本身,是菜的ViewModel 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<DishMenuItemViewModel> dishMenus; public List<DishMenuItemViewModel>DishMenus { get { return dishMenus; } set { dishMenus = value; this.RaisePropertyChanged("DishMenus"); } } private void LoadRestaurant() { this.Restaurant = new Restaurant(); this.Restaurant.Name = "Crazy大象"; this.Restaurant.Address = "asdasdadsassdsadasda"; this.Restaurant.PhoneNumber = 2131231231; } private void LoadDishMenu() { XmlDataService ds = new XmlDataService(); var dishes = ds.GetAllDishes(); this.DishMenus = new List<DishMenuItemViewModel>(); foreach(var dish in dishes) { DishMenuItemViewModel item = new DishMenuItemViewModel(); item.Dish =dish; this.DishMenus.Add(item); } } public MainWindowViewModel() { this.LoadRestaurant(); this.LoadDishMenu(); this.PlaceOrderCommand = new DelegateCommand(new Action(this.PlaceOrderCommandExecute)); this.SelectMenuItemCommand = new DelegateCommand(new Action(this.SelectMenuItemCommandExecute)); } private void PlaceOrderCommandExecute() { var selectedDishes = this.DishMenus.Where(i => i.IsSelected == true).Select(i => i.Dish.Name).ToList(); IOrderService orderService = new MockOrderService(); orderService.PlaceOrder(selectedDishes); MessageBox.Show("订餐成功!"); } private void SelectMenuItemCommandExecute() { this.Count = this.DishMenus.Count(i => i.IsSelected == true); } } }
接下来可以进行单元测试、、、
编写MainWindow.xaml
<Window x:Class="CrazyElephent.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:CrazyElephent.Client" mc:Ignorable="d" Title="{Binding Restaurant.Name,StringFormat=\{0\}-在线订餐}" Height="600" Width="1000"> <Border BorderBrush="Orange" BorderThickness="3" CornerRadius="6" Background="Yellow"> <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" CanUserDeleteRows="False" CanUserAddRows="False" Margin="0,4" Grid.Row="1" FontSize="16" ItemsSource="{Binding DishMenus}"> <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"/> <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={x:Type 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
namespace CrazyElephent.Client { /// <summary> /// MainWindow.xaml 的交互逻辑 /// </summary> public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); this.DataContext = new MainWindowViewModel(); } } }
运行

界面不包含任何逻辑代码,后台MainWIndow.xaml.cs没有逻辑代码是因为使用MVVM(现象),有时候也有可能有,纯粹的UI逻辑,不能说使用mvvm之后后台代码就一定是空的。

浙公网安备 33010602011771号