1. 程式人生 > >WPF-MVVM模式學習筆記2——MVVM簡單樣例

WPF-MVVM模式學習筆記2——MVVM簡單樣例

 一. MVVM理解  

  1. 先建立一個簡單的WPF樣例,並逐步將它重構成為MVVM模式。

   這個Demo需求是:在介面上放置文字框用來顯示定義的類Student中的名字,放置Button來修改Student的名字。

   剛建立好的樣例工程文件如下圖:

   

  緊接著新增一個Student類,

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MVVMDemo
{
    public class Student : INotifyPropertyChanged
    {
        string firstName;
        public string FirstName
        {
            get
            {
                return firstName;
            }
            set
            {
                firstName = value;
                OnPropertyChanged("FirstName");
            }
        }

        string lastName;
        public string LastName
        {
            get
            {
                return lastName;
            }
            set
            {
                lastName = value;
                OnPropertyChanged("LastName");
            }   
        }

        public Student(string firstName, string lastName)
        {
            this.firstName = firstName;
            this.lastName = lastName;
        }


        void OnPropertyChanged(string propName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propName));
            }
        }

        #region INotifyPropertyChanged Members
        public event PropertyChangedEventHandler PropertyChanged;
        #endregion
    }
}

   此時工程結構圖如下圖

  

   然後修改 MainWindow.xaml,內容如下

<Window x:Class="MVVMDemo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid x:Name="gridLayout">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="5*" />
            <ColumnDefinition Width="5*" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="5*" />
            <RowDefinition Height="5*" />
            <RowDefinition Height="5*" />
            <RowDefinition Height="5*" />
        </Grid.RowDefinitions>
        
        <TextBlock Text="FirstName:" Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" HorizontalAlignment="Center"/>
        <TextBlock Text="{Binding Path=FirstName,Mode=TwoWay}" Grid.Row="0" Grid.Column="1" VerticalAlignment="Center" HorizontalAlignment="Left"/>
        <TextBlock Text="LastName:" Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" HorizontalAlignment="Center"/>
        <TextBox Text="{Binding Path=LastName,Mode=TwoWay}" Grid.Row="1" Grid.Column="1" VerticalAlignment="Center" HorizontalAlignment="Left"/>
        <Button x:Name="BtnView" Content="I am View" Grid.Row="2" Grid.Column="0" Width="150" Height="50" VerticalAlignment="Center" HorizontalAlignment="Right"/>
    </Grid>
</Window>

下圖為MainWindow檢視


   緊接著,在MainWindow.cs新增如下內容

        public MainWindow()
        {
            InitializeComponent();
            Student student = new Student("Wang", "WenSong");
            gridLayout.DataContext = student;
            BtnView.Click += new RoutedEventHandler(delegate(object sender, RoutedEventArgs e)
                {
                    student.FirstName = "BBK工作室";
                    student.LastName = "www.bigbearking.com";
                });
        }


   此時執行程式,如下圖


點選按鈕BtnView,此時介面如下


    上述程式碼工程,點此下載

   2.問題來了

      如果我們需要讓頁面的值和Student例項的值保持一致,則必須要讓型別繼承自INotifyPropertyChanged介面,並像下面這樣編碼:

public class Student : INotifyPropertyChanged
    {
        string firstName;
        public string FirstName
        {
            get
            {
                return firstName;
            }
            set
            {
                firstName = value;
                OnPropertyChanged("FirstName");
            }
        }

        string lastName;
        public string LastName
        {
            get
            {
                return lastName;
            }
            set
            {
                lastName = value;
                OnPropertyChanged("LastName");
            }   
        }

        public Student(string firstName, string lastName)
        {
            this.firstName = firstName;
            this.lastName = lastName;
        }


        void OnPropertyChanged(string propName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propName));
            }
        }

        #region INotifyPropertyChanged Members
        public event PropertyChangedEventHandler PropertyChanged;
        #endregion
    }

  如果應用程式中存在多個這樣的型別,比如還有Teacher類,則每個類都要實現自己的OnPropertyChanged方法,這顯然是不合理的。所以,需要一個超類來包裝這種需求,當然這個超類繼承自INotifyPropertyChanged。

   3.下面,在工程中新增這個超類NotificationObject,如下結構圖


這個超類的程式碼為

    public abstract class NotificationObject : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void RaisePropertyChanged(string propertyName)
        {
            PropertyChangedEventHandler handler = this.PropertyChanged;
            if (handler != null)
            {
                handler(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        protected void RaisePropertyChanged(params string[] propertyNames)
        {
            if (propertyNames == null) throw new ArgumentNullException("propertyNames");

            foreach (var name in propertyNames)
            {
                this.RaisePropertyChanged(name);
            }
        }

        protected void RaisePropertyChanged<T>(Expression<Func<T>> propertyExpression)
        {
            var propertyName = ExtractPropertyName(propertyExpression);
            this.RaisePropertyChanged(propertyName);
        }

        public static string ExtractPropertyName<T>(Expression<Func<T>> propertyExpression)
        {
            if (propertyExpression == null)
            {
                throw new ArgumentNullException("propertyExpression");
            }

            var memberExpression = propertyExpression.Body as MemberExpression;
            if (memberExpression == null)
            {
                throw new ArgumentException("PropertySupport_NotMemberAccessExpression_Exception", "propertyExpression");
            }

            var property = memberExpression.Member as PropertyInfo;
            if (property == null)
            {
                throw new ArgumentException("PropertySupport_ExpressionNotProperty_Exception", "propertyExpression");
            }

            var getMethod = property.GetGetMethod(true);
            if (getMethod.IsStatic)
            {
                throw new ArgumentException("PropertySupport_StaticExpression_Exception", "propertyExpression");
            }

            return memberExpression.Member.Name;
        }

    }

  相應的,將Student型別修改為:
    public class Student : NotificationObject
    {
        string firstName;
        public string FirstName
        {
            get
            {
                return firstName;
            }
            set
            {
                firstName = value;
                //OnPropertyChanged("FirstName");
                this.RaisePropertyChanged("FirstName");
            }
        }

        string lastName;
        public string LastName
        {
            get
            {
                return lastName;
            }
            set
            {
                lastName = value;
                //OnPropertyChanged("LastName");
                this.RaisePropertyChanged("LastName");
            }   
        }

        public Student(string firstName, string lastName)
        {
            this.firstName = firstName;
            this.lastName = lastName;
        }


        void OnPropertyChanged(string propName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propName));
            }
        }

        #region INotifyPropertyChanged Members
        public event PropertyChangedEventHandler PropertyChanged;
        #endregion
    }

     這部分程式碼,點此下載

    4.問題再次出現,經過修改後的Student型別,是什麼?

      是實體Model,領域Model,還是別的什麼?實際上,因為沒有采用任何架構模式,當前的Student型別什麼也不是,揉雜了很多功能。它既要負責提供屬性,也要負責控制。

      在MVVM架構模式中,和MVC稱謂不同的地方,就是VM(ViewModel)部分。VM負責:接受View請求並決定呼叫哪個模型構件去處理請求,同時它還負責將資料返回給View進行顯示。也就是說,VM完成的角色可以理解為MVC中的Control。(另外需要注意的一點是,在MVC中有一個概念叫做表現模型,所謂表現模型是領域模型的一個扁平化投影,不應和MVVM中的VIEW MODEL相混淆)。

    所以,我們現在要明確這些概念。首先,將Student型別的功能細分化,VM的部分,我們跟頁面名稱對應起來應該叫做MainViewModel。實際專案中,功能頁面會相應名為StudentView.xaml,則對應的VM名便稱之為StudentViewModel.cs。我們繼續重構上面的程式碼。

二.建立MVVM的各個部分

    現在重構程式碼,工程的結構變化比較大,我會把這部分程式碼也傳上去的。

    首先,在原有的工程上建立三個資料夾 Model、View、ViewModel,如下圖

    

   1. 領域模型DomainModel部分

   然後將Student.cs移到Model資料夾內,並修改Student.cs裡的程式碼,修改後的Student.cs內容如下(注意名稱空間的變化)

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MVVMDemo.Model
{
    public class Student
    {
        string firstName;
        public string FirstName
        {
            get
            {
                return firstName;
            }
            set
            {
                firstName = value;
            }
        }

        string lastName;
        public string LastName
        {
            get
            {
                return lastName;
            }
            set
            {
                lastName = value;
            }   
        }

        public Student()
        {
            //模擬獲取資料
            //這裡為什麼會有模擬資料一說呢?我是這樣認為的,有時候類的屬性會存在資料庫或者本地檔案系統等上面,
            //我們需要讀取操作將這些資料載入到咱們定義的類裡。
            Mock();
        }

        public void Mock()
        {
            FirstName = "firstName:" + DateTime.Now.ToString();
            LastName = "lastName:" + DateTime.Now.ToString();
        }


    }
}
   此時的檔案工程結構變為下圖

   

   2.ViewModel部分

   接著,在ViewModel資料夾右擊新增一個StudentViewModel類,內容如下

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using MVVMDemo.Model;

namespace MVVMDemo.ViewModel
{
    public class StudentViewModel:NotificationObject
    {
        private Student student;
        public Student Student
        {
            get
            {
                return this.student;
            }
            set
            {
                this.student = value;
                //下面這一句話的用法以後再拿出一章具體介紹
                this.RaisePropertyChanged(() => this.student);
            }
        }

        public StudentViewModel()
        {
            student = new Student();
        }

    }
}

   此時檔案工程結構為下圖

   

  3.View部分

  再接著在View資料夾下新增一個使用者控制元件,命名為StudentView,它的XAML程式碼為下

<UserControl x:Class="MVVMDemo.View.StudentView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:vm="clr-namespace:MVVMDemo.ViewModel"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="400">
    <Grid x:Name="gridLayout">
        <Grid.DataContext>
            <vm:StudentViewModel />
        </Grid.DataContext>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="5*" />
            <ColumnDefinition Width="5*" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="5*" />
            <RowDefinition Height="5*" />
            <RowDefinition Height="5*" />
            <RowDefinition Height="5*" />
        </Grid.RowDefinitions>

        <TextBlock Text="FirstName:" Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" HorizontalAlignment="Center"/>
        <TextBlock Text="{Binding Path=Student.FirstName,Mode=Default}" Grid.Row="0" Grid.Column="1" VerticalAlignment="Center" HorizontalAlignment="Left"/>
        <TextBlock Text="LastName:" Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" HorizontalAlignment="Center"/>
        <TextBox Text="{Binding Path=Student.LastName,Mode=TwoWay}" Grid.Row="1" Grid.Column="1" VerticalAlignment="Center" HorizontalAlignment="Left"/>
        <Button x:Name="BtnView" Content="I am View" Grid.Row="2" Grid.Column="0" Width="150" Height="50" VerticalAlignment="Center" HorizontalAlignment="Right"/>
    </Grid>
</UserControl>
  注意,此時的XAML程式碼繫結有些變化,繫結的是Student.FirstName和Student.LastName,而不是FirstName和LastName。

  此時檔案工程結構圖為下圖

  

   然後在MainWindow裡需要引用這個控制元件,修改MainWindow.xaml的程式碼,內容如下

<Window x:Class="MVVMDemo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:view="clr-namespace:MVVMDemo.View"
        Title="MainWindow" Height="350" Width="525">
    <Grid >
        <view:StudentView />
    </Grid>
</Window>

   再將MainWindow.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;

namespace MVVMDemo
{
    /// <summary>
    /// MainWindow.xaml 的互動邏輯
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

        }
    }
}

   編譯工程,執行,效果如圖

 

  該部分的程式碼,點此下載

   4.若干解釋

   在上述的工程Demo中,領域模型Student負責獲取資料,而資料來源於何處不是我們關心的重點(可能是資料庫,也可能是配置檔案,等等),所以,我們直接在Student中模擬了獲取資料的過程,即Mock方法。這相當於完成了一次OneWay的過程,即把後臺資料推送到前臺進行顯示,這隻能算是完成跟UI互動的一部分功能。UI互動還需要包括從UI中將資料持久化(如儲存到資料庫)。而UI跟後臺的互動,就需要通過命令繫結的機制去實現了。

   5.命令繫結

     在接下來的工程裡,我們演示兩類命令,一類是屬性類命令繫結,一類是事件類命令繫結 。

     首先,我們知道,VM負責UI和領域模型的聯絡,所以,繫結所支援的方法一定是在VM中,於是,我們在StudentViewModel中定義一個屬性CanSubmit,及一個方法Submit

        public bool CanSubmit
        {
            get
            {
                return true;
            }
        }

        public void Submit()
        {
            student.Mock();
        }

    此時StudentViewModel的內容如下

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using MVVMDemo.Model;

namespace MVVMDemo.ViewModel
{
    public class StudentViewModel:NotificationObject
    {
        private Student student;
        public Student Student
        {
            get
            {
                return this.student;
            }
            set
            {
                this.student = value;
                //下面這一句話的用法以後再拿出一章具體介紹
                this.RaisePropertyChanged(() => this.student);
            }
        }

        public StudentViewModel()
        {
            student = new Student();
        }

        public bool CanSubmit
        {
            get
            {
                return true;
            }
        }

        public void Submit()
        {
            student.Mock();
        }

    }
}

    注意,上述Submit方法中為了簡單起見,使用了模擬方法。由於Mock方法中仍然可能涉及到UI的變動(如隨資料庫的某些具體的值變動而變動),故領域模型Student可能也會需要繼承NotificationObject,在本例中,Student改變如下

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MVVMDemo.Model
{
    public class Student : NotificationObject
    {
        string firstName;
        public string FirstName
        {
            get
            {
                return firstName;
            }
            set
            {
                firstName = value;
                this.RaisePropertyChanged("FirstName");
            }
        }

        string lastName;
        public string LastName
        {
            get
            {
                return lastName;
            }
            set
            {
                lastName = value;
                this.RaisePropertyChanged("LastName");
            }   
        }

        public Student()
        {
            //模擬獲取資料
            //這裡為什麼會有模擬資料一說呢?我是這樣認為的,有時候類的屬性會存在資料庫或者本地檔案系統等上面,
            //我們需要讀取操作將這些資料載入到咱們定義的類裡。
            Mock();
        }

        public void Mock()
        {
            FirstName = "firstName:" + DateTime.Now.ToString();
            LastName = "lastName:" + DateTime.Now.ToString();
        }
    }
}

    其次,需要改變StudentView,由於該VIEW用到命令和屬性繫結,所以需要新增兩個引用

   

   新增完上述兩個引用後,修改StudentView.xaml的內容如下:

<UserControl x:Class="MVVMDemo.View.StudentView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" 
             xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions" 
             xmlns:vm="clr-namespace:MVVMDemo.ViewModel"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="400">
    <Grid x:Name="gridLayout">
        <Grid.DataContext>
            <vm:StudentViewModel />
        </Grid.DataContext>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="5*" />
            <ColumnDefinition Width="5*" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="5*" />
            <RowDefinition Height="5*" />
            <RowDefinition Height="5*" />
            <RowDefinition Height="5*" />
        </Grid.RowDefinitions>

        <TextBlock Text="FirstName:" Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" HorizontalAlignment="Center"/>
        <TextBlock Text="{Binding Path=Student.FirstName,Mode=Default}" Grid.Row="0" Grid.Column="1" VerticalAlignment="Center" HorizontalAlignment="Left"/>
        <TextBlock Text="LastName:" Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" HorizontalAlignment="Center"/>
        <TextBox Text="{Binding Path=Student.LastName,Mode=TwoWay}" Grid.Row="1" Grid.Column="1" VerticalAlignment="Center" HorizontalAlignment="Left"/>
        <Button x:Name="BtnView" Content="I am View"  IsEnabled="{Binding CanSubmit}"  Grid.Row="2" Grid.Column="0" Width="150" Height="50" VerticalAlignment="Center" HorizontalAlignment="Right">
            <i:Interaction.Triggers>
                <i:EventTrigger EventName="Click">
                    <ei:CallMethodAction TargetObject="{Binding}" MethodName="Submit"/>
                </i:EventTrigger>
            </i:Interaction.Triggers>
        </Button>
    </Grid>
</UserControl>

   編譯執行,點選按鈕BtnView,可以看到現實內容更新。

  上述工程程式碼,點此下載

   6.後言

   經過這一次的重構之後,基本滿足了一個簡單的MVVM模型的需要,我也對MVVM大概有了認識,但是學習的過程中還設計到一些問題,我需要繼續探究,比如類NotificationObject裡的Lambda表示式,還有命令繫結。本學習筆記系列還沒有結束,一步一步來吧。