MVVM是什么?

描述

前言

最近在学习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

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分