WPF使用MVVM(一)-屬性繫結

醜萌氣質狗發表於2022-01-06

WPF使用MVVM(一)-屬性繫結

簡單介紹MVVM

MVVMModel(資料型別),View(介面),ViewModel(資料與介面之間的橋樑)的縮寫,是一種程式設計模式,優點一勞永逸,初步增加一些邏輯和工作量,但是為後期維護增加了極大的便利性,減少程式設計的關注點。

如:介面顯示某一資料,在資料有變動的情況下,傳統方式是更新此資料,同時需要手動更新介面中的資料顯示。在MVVM的模式下只需關心資料變更即可,資料可通過繫結的模式進行重新整理或不重新整理。

關於視窗中的行為(點選、滑鼠移入、移出。。。),也可以通過MVVM的方式進行繫結,後期就算窗體重新設計,依然可以在對應的地方繫結對應的行為,維護方式也非常靈活。

專案搭建

有了大概的層級描述之後, 我們可以嘗試建立一個新的WPF程式,此程式用來顯示某個員工的資訊(姓名年齡職業),這裡先建立我們的Model(資料模型),欄位不多就三個,就叫EmployeeModel

    public class EmployeeModel
    {
        /// <summary>
        /// 姓名
        /// </summary>
        public string Name { get; set; }

        /// <summary>
        /// 年齡
        /// </summary>
        public int Age { get; set; }

        /// <summary>
        /// 職業
        /// </summary>
        public string Profession { get; set; }
    }

然後來佈局一下我們的View(介面),這裡叫做MainWindow

image-20220105164759019

介面程式碼如下(老樣子,注意名稱空間與你的專案保持一致):

<Window
    x:Class="Test.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:local="clr-namespace:Test"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="MainWindow"
    Width="800"
    Height="450"
    mc:Ignorable="d">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition />
            <RowDefinition />
            <RowDefinition />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>

        <TextBlock
            Grid.Row="0"
            Grid.Column="0"
            HorizontalAlignment="Center"
            VerticalAlignment="Center"
            FontSize="50"
            Text="姓名:" />

        <TextBlock
            Grid.Row="0"
            Grid.Column="1"
            HorizontalAlignment="Center"
            VerticalAlignment="Center"
            FontSize="50"
            Foreground="#0078d4"
            Text="王虎" />

        <TextBlock
            Grid.Row="1"
            Grid.Column="0"
            HorizontalAlignment="Center"
            VerticalAlignment="Center"
            FontSize="50"
            Text="年齡:" />

        <TextBlock
            Grid.Row="1"
            Grid.Column="1"
            HorizontalAlignment="Center"
            VerticalAlignment="Center"
            FontSize="50"
            Foreground="#0078d4"
            Text="35" />
        <TextBlock
            Grid.Row="2"
            Grid.Column="0"
            HorizontalAlignment="Center"
            VerticalAlignment="Center"
            FontSize="50"
            Text="工種:" />

        <TextBlock
            Grid.Row="2"
            Grid.Column="1"
            HorizontalAlignment="Center"
            VerticalAlignment="Center"
            FontSize="50"
            Foreground="#0078d4"
            Text="程式設計師" />

        <Button
            Grid.Row="3"
            Grid.ColumnSpan="2"
            Margin="20"
            Content="更新一下資訊"
            FontSize="30"
            FontWeight="Bold" />
    </Grid>
</Window>

可以看到,上面的資料都是寫死的,需要我們將顯示的地方,繫結到我們的資料中,在開始繫結之前呢,這裡需要建立一下我們的橋樑,也就是ViewModel,為了和介面能對應上,名字叫做MainWindowVM,在這個ViewModel中,我們使用剛才建立的資料模型EmployeeModel,並在建構函式中,初始化一個員工的資訊:

    public class MainWindowVM
    {
        private EmployeeModel _employee;
        /// <summary>
        /// 員工資料
        /// </summary>
        public EmployeeModel EmployeeM
        {
            get { return _employee; }
            set { _employee = value; }
        }

        public MainWindowVM()
        {
            EmployeeM = new EmployeeModel();
            EmployeeM.Name = "小明(繫結)";
            EmployeeM.Age = 44;
            EmployeeM.Profession = "程式設計師";
        }
    }

現在我們只需要介面繫結一下MainWindowVM中資料EmployeeM的對應屬性就行了,修改介面程式碼如下:

<Window
    x:Class="Test.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:local="clr-namespace:Test"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="MainWindow"
    Width="800"
    Height="450"
    mc:Ignorable="d">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition />
            <RowDefinition />
            <RowDefinition />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>

        <TextBlock
            Grid.Row="0"
            Grid.Column="0"
            HorizontalAlignment="Center"
            VerticalAlignment="Center"
            FontSize="50"
            Text="姓名:" />

        <TextBlock
            Grid.Row="0"
            Grid.Column="1"
            HorizontalAlignment="Center"
            VerticalAlignment="Center"
            FontSize="50"
            Foreground="#0078d4"
            Text="{Binding EmployeeM.Name}" />

        <TextBlock
            Grid.Row="1"
            Grid.Column="0"
            HorizontalAlignment="Center"
            VerticalAlignment="Center"
            FontSize="50"
            Text="年齡:" />

        <TextBlock
            Grid.Row="1"
            Grid.Column="1"
            HorizontalAlignment="Center"
            VerticalAlignment="Center"
            FontSize="50"
            Foreground="#0078d4"
            Text="{Binding EmployeeM.Age}" />
        <TextBlock
            Grid.Row="2"
            Grid.Column="0"
            HorizontalAlignment="Center"
            VerticalAlignment="Center"
            FontSize="50"
            Text="工種:" />

        <TextBlock
            Grid.Row="2"
            Grid.Column="1"
            HorizontalAlignment="Center"
            VerticalAlignment="Center"
            FontSize="50"
            Foreground="#0078d4"
            Text="{Binding EmployeeM.Profession}" />

        <Button
            Grid.Row="3"
            Grid.ColumnSpan="2"
            Margin="20"
            Content="更新一下資訊"
            FontSize="30"
            FontWeight="Bold" />
    </Grid>
</Window>

這時候當我們執行專案的時候,發現介面空空如也,什麼資料也沒有:

image-20220105164337683

這是因為,雖然我們已經有了MainWindowVM(ViewModel),但是並沒有將它與MainWindow(View)進行一個關聯,所以需要在MainWindow的後臺指定一下當前的ViewModel是誰,設定MainWindowDataContext即可:

    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            this.DataContext = new MainWindowVM();
        }
    }

image-20220105164659556

這下就正常了,下面我們要來演示下MVVM的魔力!

屬性變更

之前已經說了,MVVM是資料驅動的,我們已經繫結了後臺的資料,現在改動下後臺資料來看看介面的更新效果。

為了演示,我們給介面中的Button按鈕,新增一個點選事件:

        <Button
            Grid.Row="3"
            Grid.ColumnSpan="2"
            Margin="20"
            Click="Button_Click"
            Content="更新一下資訊"
            FontSize="30"
            FontWeight="Bold" />

在點選事件中,我們修改當前繫結的MainWindowVM(ViewModel)中的EmployeeM屬性,觀察介面的變化,為了在Click事件中獲取繫結的例項,需要為MainWindowVM建立一個外部的欄位,程式碼如下:

    public partial class MainWindow : Window
    {
        MainWindowVM mainWindowVM;
        public MainWindow()
        {
            InitializeComponent();

            mainWindowVM = new MainWindowVM();
            this.DataContext = mainWindowVM;
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            EmployeeModel model = new EmployeeModel();
            model.Name = "王明(修改)";
            model.Age = 38;
            model.Profession = "外賣員";

            mainWindowVM.EmployeeM = model;
        }
    }

此時,我們在按鈕的點選事件中重新給EmployeeM賦值一個新值,執行程式,點選按鈕,發現什麼都沒變,斷點跟進去,發現屬性的值已經被修改,但是介面的資料並未發生更改:

image-20220105171927963

這跟之前說的MVVM模式介紹有出入,按理說應該是資料更新介面也更新。

原來繫結屬性才是第一步,僅僅繫結還不行,還需要讓屬性具備通知介面更新的能力。

那我們就來給屬性新增一下這個能力。

WPF中讓屬性具備通知介面更新的能力,需要讓我們的ViewModel也就是MainWindowVM,繼承型別INotifyPropertyChanged,並實現它的PropertyChanged屬性:

    public class MainWindowVM: INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        private void RaisePropertyChanged(string propertyName)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null)
                handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }

完成這一步呢,還沒完, 還差一個小地方,那就是我們EmployeeM屬性的Set中,需要再補上一句,這樣當我們的EmployeeM被設定值的時候,就會呼叫更新介面的方法RaisePropertyChanged

        private EmployeeModel _employee;
        /// <summary>
        /// 員工資料
        /// </summary>
        public EmployeeModel EmployeeM
        {
            get { return _employee; }
            set
            {
                _employee = value;
                //當前屬性的名稱
                RaisePropertyChanged("EmployeeM");
            }
        }

大功告成,現在執行專案,點選按鈕, 就能夠看到介面進行更新了。

這時候有些老哥就有疑問了,你這給EmployeeM賦值怎麼不一樣呢,為啥要新建一個資料模型,再賦值呢,我實際的情況是想在原有的資料上改個名字而已:

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            mainWindowVM.EmployeeM.Name = "王明(原屬性修改)";
        }

這種修改方式,介面還是沒變呢!

那我們就再來看看型別屬性修改怎麼通知介面:

型別屬性修改

上面我們知道資料變動通知介面更新,是我們在EmployeeMSet中呼叫RaisePropertyChanged方法實現的,所以要想實現型別屬性在原有資料的基礎上進行介面通知,我們就需要為該型別(EmployeeModel)中屬性(Name)的Set中呼叫RaisePropertyChanged方法,也就是:

        /// <summary>
        /// 姓名
        /// </summary>
        public string Name
        {
            set
            {
                //value;
                RaisePropertyChanged("Name")
            }
        }

這樣需要什麼條件呢,需要我們讓EmployeeModel也要繼承INotifyPropertyChanged的介面,並實現對應的方法。很先然違背了我們設計的初衷,畢竟我們希望所有的處理都放在橋樑MainWindowVM(ViewModel)中去弄,所以看來只能另闢蹊徑了!

不過這難不倒我們,現在我這邊有兩種方式可以這麼弄,一種中規中矩的,一種比較暴力,先看中規中矩的吧!

既然我們的在屬性的Set中呼叫通知介面更新的方法,那麼我們完全可以在MainWindowVM(ViewModel)新建幾個屬性,這些屬性返回我們模型資料的屬性,如下:

        /// <summary>
        /// 名稱
        /// </summary>
        public string EmployeeName
        {
            get { return EmployeeM.Name; }
            set
            {
                EmployeeM.Name = value;
                RaisePropertyChanged("EmployeeName");
            }
        }

此時我們的介面繫結的值也不是EmployeeM.Name,而是直接改為EmployeeName:

        <TextBlock
            Grid.Row="0"
            Grid.Column="1"
            HorizontalAlignment="Center"
            VerticalAlignment="Center"
            FontSize="50"
            Foreground="#0078d4"
            Text="{Binding EmployeeName}" />

現在我們直接修改Name屬性就可以了

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            //mainWindowVM.EmployeeM.Name = "王明(原屬性修改)";
            mainWindowVM.Name = "王明(原屬性修改)";
        }

另外一種方式更加直接,更加暴力的方式,上面都不用修改,直接重新賦值一遍:

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            mainWindowVM.EmployeeM.Name = "王明(原屬性修改)";
            mainWindowVM.EmployeeM = mainWindowVM.EmployeeM;
        }

既然它沒有走Set,我們手動幫助他走一遍!

總的來說,兩種方式都不是特別理想,這邊也想不出比較不錯的方式,如果有好的意見,還請分享下!非常感謝!

下一節來說一下命令,讓我們的介面後臺看起來更加的乾淨!也就是將Button_Click移動到MainWindowVM(ViewModel)中。

相關文章