WPF 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的沟通

  传递数据--数据属性

  传递操作--命令属性

 

1.初级案例:

  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>
MainWindow.xaml
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();
        }
    }
}
MainWindow.xaml.cs

 

 

 

新的需求:

 

 

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

使用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>
MainWindow.xaml

删掉事件处理程序

namespace DemoWithoutMVVM
{
    /// <summary>
    /// MainWindow.xaml 的交互逻辑
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
          
        }
    }
}
MainWindow.xaml.cs

创建五个文件夹(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));
            }
        }
    }
}
ViewModels/NotificationObject.cs

建立一个简陋(不完备)的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; }

    }
}
Commands/DelegateCommand.cs

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;
        }
    }
}
ViewModels/MainWindowViewModel.cs

添加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>
MainWindow.xaml

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();
        }
    }
}
MainWindow.xaml.cs

运行

(现在即使把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/Data.xml

为了使得Data数据随着程序发布,需要修改属性

 

 

添加Services文件夹

为了应对数据源发生变化(存在xml改成存在数据库中),需要写一个接口

namespace CrazyElephent.Client.Services
{
    public interface IDataService
    {
        List<Dish> GetAllDishes();
    }
}
Services/IDataService.cs

上面的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; }
    }
}
Models/Dish.cs

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;
        }
    }
}
Services/XmlDataService.cs

上面实现的定义和实现分离

添加下订单的Service接口

namespace CrazyElephent.Client.Services
{
    interface IOrderService
    {
        void PlaceOrder(List<string> dishes);
    }
}
Services/IOrderService.cs

实现一个假的IOrderService,简单的把订单菜品写到txt,

namespace CrazyElephent.Client.Services
{
    class MockOrderService : IOrderService
    {
        public void PlaceOrder(List<string> dishes)
        {
            System.IO.File.WriteAllLines(@"C:\order.txt", dishes.ToArray());
        }
    }
}
Services/IOrderService.cs

对餐馆信息进行抽象,

namespace CrazyElephent.Client.Models
{
    public class Restaurant
    {
        public string Name { get; set; }
        public string Address { get; set; }
        public int PhoneNumber { get; set; }
    }
}
Models/Restaurant.cs

接着进行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");
            }
        }
    }
}
ViewModels/DishMenuItemViewModel.cs

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);
        }
    }
}
ViewModels/MainWindowViewModel.cs

接下来可以进行单元测试、、、

编写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

编写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

运行

 

 

 

 

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

posted @ 2021-02-08 15:39  KnowledgePorter  阅读(172)  评论(0)    收藏  举报