1. 程式人生 > >WPF MVVM學習

WPF MVVM學習

點擊 格式轉換 lse 聲明 五步 rgs ltr 方式 靜態

轉自https://my.oschina.net/unpluggedcoder/blog/536301

簡介

簡單的三層架構示例和 GLUE(膠水)代碼問題

第一步:最簡單的 MVVM 示例 - 把後臺代碼移到類中

第二步:添加綁定 - 消滅後臺代碼

第三步:添加執行動作和“INotifyPropertyChanged”接口

第四步:在 ViewModel 中解耦執行動作

第五步:利用 PRISM

WPF MVVM 的視頻演示

簡介

從我們還是兒童到學習成長為成年人,生命一直都在演變。 對於軟件架構, 同樣適用這個道理, 從一個基礎的架構開始, 隨著每個需求和情境在不斷演化。

如果你問任何一個 .NET 開發者, 什麽是最小的基礎架構, 首先浮現的就是"三層架構"。 在這個框架中, 我們把項目分為三個邏輯層次: UI 層, 業務邏輯層和數據訪問層, 每一層都負責各自對應的功能。

技術分享圖片

UI 負責顯示功能, 業務邏輯層負責校驗, 數據訪問層負責 SQL 語句。3層架構有如下的好處:

  • 包容變化: 每一層的變化不會重復跨越到其它層次。

  • 重用性: 增強可重用性, 因為每一層都是分離, 自包容的獨立實體

MVVM 是三層架構的一個演化。我知道我的經歷不夠證明這點, 但是我個人對 MVVM 進行了演化和觀察。 那我們先從三層基礎架構開始, 去理解三層架構存在的問題, 看 MVVM 架構是如何解決這些問題, 然後升級到去創建一個自定義的 MVVM 框架代碼。 下面是本文接下來的路線圖。

技術分享圖片

簡單的三層架構示例和 GLUE(膠水) 代碼問題

首先, 讓我們來理解三層架構以及它存在的問題, 然後看 MVVM 如何解決這個問題。

直覺和現實是兩種不同的事物。 當你看到三層架構的圖, 你首先的直覺是每個功能可能都分布在各自層次。 但是當你實際編寫代碼時, 有些層次被強迫去做一些它們不應該做的額外的工作(破壞了SOLID 原則)。 如果你對 SOLID 原則還不熟悉可以參考這個視頻: SOLID principle video(譯者註: SOLID 指 Single responsibility, Open-closed, Liskov substitution, Interface segregation and Dependency inversion, 即單一功能、開閉原則、裏氏替換、接口隔離以及依賴反轉)。

技術分享圖片

這部分額外工作就在 UI 與Model之間, 以及 Model 與 Data access 之間。 我們把這類代碼稱為"GLUE"(膠水, 譯者註:由於作者全用大寫字母表示, 因此後續延用 GLUE)代碼。"GLUE"代碼主要有兩種邏輯類型。

鄙人淺見薄識, 如果你有更多的"GLUE"類型實例, 請在留言中指出。

  • 映射邏輯(綁定邏輯): 每一層通過屬性、方法和集合和其它層鏈接。例如, 一個在 UI 層中名為“txtCustomerName”的 Textbox 控件,將其映射到 customer 類的"CustomerName"屬性。

txtCustomerName.text = custobj.CustomerName; // 映射代碼

現在誰應該擁有上述綁定邏輯代碼,UI 還是 Model?開發者往往把這個代碼推到 UI 層次中。

  • 轉換邏輯:每個層次使用的數據格式都是不同的。比如一個 Model 類"Person"有一個性別屬性,可取值分別為 "F"(Female) 和 "M"(Male) 分別代表女性和男性。但是在 UI 層中,希望將這個值可視化為一個復選框控件,勾選則代表男性,不勾選則代表女性。下面是一個轉換代碼示例。

if (obj.Gender == “M”) // 轉換代碼 {chkMale.IsChecked = true;}
else
{chkMale.IsChecked = false;}

大多數開發者最終會將"GLUE"代碼寫到UI層中。通常可以在後臺代碼中定位到這類代碼,例如 .cs 文件。如果UI 是 XAML,則對應的 XAML.cs 包含 GLUE代碼;如果 UI 是 ASPX,則對應的 ASPX.cs 包含 GLUE 代碼,以此類推。

那麽問題來了:是UI負責這類GLUE代碼嗎?讓我們看下WPF應用中的一個簡單的三層結構例子,以及更詳細的GLUE代碼細節。

下面是一個簡單的模型類"Customer",它有三個屬性“CustomerName”,“Amount” 和“Married”。

技術分享圖片

但是,當這個模型顯示到 UI 上時它又表現如下。所以,你可以看出來它包含了該模型的所有屬性,以及一些額外的元素:顏色標簽和 Married 復選框控件。

技術分享圖片

下面有一張簡單的表,左邊是 Model,右邊是 UI,中間是談過的映射和轉換邏輯。

你可以看到前兩行沒有轉換邏輯,只有映射邏輯,另外兩行則同時包含轉換邏輯和映射邏輯。

Model GLUE CODE UI
Customer Name No conversion needed only Mapping Customer Name
Amount No conversion needed only Mapping Amount
Amount Mapping + Conversion logic. > 1500 = BLUE
< 1500 = RED
Married Mapping + Conversion logic. True – Married
False - UnMarried

這些轉換和映射邏輯代碼通常會在“xaml.cs”文件中。下面是上圖對應的後臺代碼,你可以看到映射代碼和顏色判定、性別格式轉換代碼。我在代碼中用註釋標註出來,這樣你可以看到哪些是映射代碼,哪些是轉換代碼。

lblName.Content = o.CustomerName; // mapping code
lblAmount.Content = o.Amount; // mapping code

if (o.Amount > 2000) // transformation code
{
lblBuyingHabits.Background = new SolidColorBrush(Colors.Blue);
}
else if (o.Amount > 1500) // transformation code
{
lblBuyingHabits.Background = new SolidColorBrush(Colors.Red);
}
if (obj.Married == "Married") // transformation code
{
chkMarried.IsChecked = true;
}
else
{
chkMarried.IsChecked = false;
}

現在這些 GLUE 代碼存在的問題:

  • 單一責任原則被破壞(SRPViolation): 是 UI 負責這些 GLUE 代碼嗎?這種情況下改變了 Amount 數量,同時也需要修改 UI 代碼。現在,數據的改變為什麽會讓我去修改 UI 的代碼?這裏可以聞到壞代碼的味道。UI 應該只在我修改樣式,顏色和布局的時候才改變。

  • 重用性: 如果我想把同樣的顏色邏輯和性別格式轉換用到下面的編輯界面,我該怎麽做?拷貝粘帖重復的代碼?

技術分享圖片

如果我想走得更遠一點,把這個 GLUE 代碼用在不同的 UI 技術體系上,比如 MVC、Windows Form 或者 Mobile 應用上。

技術分享圖片

但是這裏跨 UI 技術平臺的重用實際上是不可能的,因為每個平臺 UI 背後都和各自的 UI 技術體系耦合得很緊密。

比如,下面的後臺代碼是繼承自“Windows”類,而“Windows”類是集成在 WPF UI 體系中。如果我們想在 Web 應用或者 MVC 中應用這些邏輯,卻又無法去創建一個這樣的類對象來使用。

public partial class MainWindow : Window
{
// Behind code is here
}

那麽我們要怎麽重用後臺代碼?怎麽遵循 SRP 原則?

第一步:最簡單的 MVVM 示例 - 把後臺代碼移到類中

我想大部分開發者已經知道怎麽解決這個問題。毫無疑問地把後臺代碼(GLUE 代碼)移到一個類庫中。這個類庫代表了描述了 UI 的屬性和行為。任何移入到這個類庫的代碼都可以編譯成 DLL,然後被所有 .NET 項目(Windows,Web 等等)所引用。因此,在這一節我們將創建一個最簡單的 MVVM 示例,然後在後續的章節中我們將基於這個示例創建更高級的 MVVM 示例。

技術分享圖片

我們創建一個“CustomerViewModel”類來包含 GLUE 代碼。“CustomerViewModel”類代表了你的 UI,所以我們想保持它的屬性和UI命名約定一致。你可以從下圖看出來“CustomerViewModel”類的屬性是如何從之前的 CustomerModel 類中映射過來: “TxtCustomerName”對應“CustomerName”,“TxtAmount”對應“Amount”等等。

技術分享圖片

下面是實際代碼:

public class CustomerViewModel 
    {
        private Customer obj = new Customer();

        public string TxtCustomerName
        {
            get { return obj.CustomerName; }
            set { obj.CustomerName = value; }
        }        

        public string TxtAmount
        {
            get { return Convert.ToString(obj.Amount) ; }
            set { obj.Amount = Convert.ToDouble(value); }
        }


        public string LblAmountColor
        {
            get 
            {
                if (obj.Amount > 2000)
                {
                    return "Blue";
                }
                else if (obj.Amount > 1500)
                {
                    return "Red";
                }
                return "Yellow";
            }
        }

        public bool IsMarried
        {
            get
            {
                if (obj.Married == "Married")
                {
                    return true;
                }
                else
                {
                    return false;
                }
            }

        }}

關於“CustomerViewModel”這個類有以下幾點註意:

  • 類屬性都以 UI 的命名方式來約定,這樣看上去會更形象一些;

  • 這個類負責了類型轉換的代碼,使得 UI 看上去更輕量級。例如代碼中的“TxtAmount”屬性。在 Model 類中的“Amount”屬性是數字,而轉換的過程是在 ViewModel 類中完成。換句話說這個類負責了 UI 顯示的所有職責(譯者註:邏輯上的業務職責)讓 UI 後臺代碼看上去更簡潔;

  • 所有轉換邏輯的代碼都在這個類中,例如“LblAmountColor”屬性和“IsMarried”屬性;

  • 所有的屬性數據都保持了簡單的字符類型,這樣可以在大多 UI 技術平臺上適用。例如,“LblAmountColor”屬性把顏色值用字符串來傳遞,這樣可以在任何 UI 類型中重用,同時我們也保持了最小的數據共性。

現在“CustomerViewModel”類包含了所有的後臺代碼邏輯,我們可以創建這個類的對象並綁定到 UI 元素上。你可以在下面代碼看到我們只剩下了映射邏輯的代碼部分,而轉換邏輯的"GLUE"代碼已經沒有了。

private void DisplayUi(CustomerViewModel o)
{
lblName.Content = o.TxtCustomerName;
lblAmount.Content = o.TxtAmount;
BrushConverter brushconv = new BrushConverter();
lblBuyingHabits.Background = brushconv.ConvertFromString(o.LblAmountColor) as SolidColorBrush;
chkMarried.IsChecked = o.IsMarried;
}

第二步:添加綁定 - 消滅後臺代碼

第一步的方法很好,但是我們知道後臺代碼仍然還有問題,在 WPF 中消滅所有後臺代碼是完全可能的。接下來 WPF 綁定和命令登場了。

WPF 以其綁定(Binding)、命令(Commands)和聲明式編程(Declarative programming)而著稱。聲明式編程意味著你可以使用 XMAL 來表達你的 C# 代碼,而不用編寫完整的C#代碼。綁定功能幫助一個 WPF 對象連接到其它的 WPF 對象,從而他們可以發送和接收數據。

當前的映射 C# 代碼有三個步驟:

  • 導入:我們要做的第一件事情是導入“CustomerViewModel”名稱空間。

  • 創建對象:下一步要創建“CustomerViewModel”類的對象。

  • 綁定代碼:最後將 WPF UI 綁定到這個 ViewModel 對象。

下面表格展示了 C# 代碼和與其對應相同的 WPF XAML 代碼。

C# code XAML code
Import using CustomerViewModel; xmlns:custns="clr-
namespace:CustomerViewModel;assembly=Custo
merViewModel"
Create
object
CustomerViewModelobj = new
CustomerViewModel();
obj.CustomerName = "Shiv";
obj.Amount = 2000;
obj.Married = "Married";
<Window.Resources>
<custns:CustomerViewModel 
x:Key="custviewobj" 
TxtCustomerName="Shiv" TxtAmount="1000" IsMarried=”true”/>
Bind lblName.Content = o.CustomerName;
<Label x:Name="lblName"  Content="{Binding 
TxtCustomerName, 
Source={StaticResourcecustviewobj}}"/>

你不需要寫後臺的代碼,我們可以選中 UI 元素,按 F4,如下圖中選擇指定綁定。這個步驟會把綁定代碼插入到 XAML中。

技術分享圖片

選擇“StaticResource”來指定映射,然後在 UI 元素和 ViewModel 對象之間指定綁定路徑。

技術分享圖片

這時你查看 XAML.CS 文件,它已經沒有任何 GLUE 代碼,同樣也沒有轉換和映射代碼。唯一的代碼就是標準的 WPF UI 初始化代碼。

{

public MVVMWithBindings()

{InitializeComponent();}

}

第三步:添加執行動作和“INotifyPropertyChanged”接口

應用程序不僅僅只是有 textboxs 和 labels, 同樣還需要執行動作,比如按鈕,鼠標事件等。 因此讓我們添加一個按鈕來看看如何把 MVVM 類應用起來。 我們在同樣的 UI 上添加了一個‘Calculate tax’按鈕,當用戶按下按鈕,它將根據“Sales Amount”值計算出稅值並顯示在界面上。

技術分享圖片

因此為了在 Model 類實現上面的功能,我們添加一個“CalculateTax()”方法。當這個方法被執行,它根據薪水範圍計算出稅值,並將值保存在“Tax”屬性值中。

public class Customer
{ 
....
....
....
....
private double _Tax;
public double Tax
{
get { return _Tax; }
}
        public void CalculateTax()
        {
    if (_Amount > 2000)
            {
                _Tax = 20;
            }
            else if (_Amount > 1000)
            {
                _Tax = 10;
            }
            else
            {
                _Tax = 5;
            }
        }
}

由於 ViewModel 類是 Model 類的一個封裝,因此我們需要在 ViewModel 類中創建一個方法來調用 Model 的“CalculateTax”方法。

public class CustomerViewModel 
{
        private Customer obj = new Customer();
....
....
....
....
        public void Calculate()
        {
            obj.CalculateTax();
        }
}

現在,我們想要在 XAML 的視圖中調用這個“Calculate”方法,而不是在後臺編寫。不過你不能直接通過 XAML 調用“Calculate”方法,你需要用 WPF 的 command 類。

我們通過使用綁定屬性將數據發送給 ViewModel 類,而發送執行動作給 ViewModel 類則需要使用命令。

技術分享圖片

所有從視圖元素產生的動作都發送給 command 類,所以第一步是創建一個 command 類。為了創建自定義的 command 類,我們需要實現"ICommand"接口(如下圖)。

"ICommand"接口有兩個必須要重載的方法:“CanExecute”和“Execute”。在“Execute”中我們放的是希望動作發生時實際執行的邏輯代碼(比如按鈕按下,右鍵按下等)。在“CanExecute”中我們放的是驗證邏輯來決定“Execute”代碼是否應該執行。

技術分享圖片

public class ButtonCommand : ICommand
{
        public bool CanExecute(object parameter)
        {
      // When to execute
      // Validation logic goes here
        }

        public event EventHandler CanExecuteChanged;

        public void Execute(object parameter)
        {
// What to Execute
      // Execution logic goes here
    }
}

現在所有的動作調用都發送到 command 類,然後被路由到 ViewModel 類。換句話說,command 類需要組合ViewModel 類(譯註:command 類需要一個 ViewModel 類的引用)。

技術分享圖片

下面是簡短的代碼片段,有四點需要註意:

  1. ViewModel 對象是作為一個私有的成員對象。

  2. 該 ViewModel 對象將通過構造函數參數的方式傳遞進來。

  3. 目前為止,我們沒有在“CanExecute”中添加驗證邏輯,它始終返回 true。

  4. 在“Execute”方法中我們調用了 ViewModel 類的“Calculate”方法。

    public class ButtonCommand : ICommand
        {
            private CustomerViewModel obj; // Point 1
            public ButtonCommand(CustomerViewModel _obj) // Point 2
            {
                obj = _obj;
            }
            public bool CanExecute(object parameter)
            {
                return true; // Point 3
            }
            public void Execute(object parameter)
            {
                obj.Calculate(); // Point 4
            }
        }

    上面的 command 代碼中,ViewModel 對象是通過構造函數傳遞進來。所以 ViewModel 類需要創建一個 command 對象來暴露這個對象的“ICommand”接口。這個“ICommand”接口將被 WPF XAML 使用並調用。下面是一些關於“CustomerViewModel”類使用 command 類的要點:

    1. command 類是“CustomerViewModel”類的私有成員。

    2. 在“CustomerViewModel”類的構造函數中將當前對象的實例傳遞給 command 類。在之前解釋 command 類的一節中我們說了 command 類構造函數獲取 ViewModel 類的實例。因此在這一節中我們正是將當前實例傳遞給 command 類。

    3. command 對象是通過以“ICommand”接口的形式暴露出來,這樣才可以被 XAML 所使用。

      using System.ComponentModel;
      
      public class CustomerViewModel 
      {
      …
      …
      private ButtonCommand objCommand; //  Point 1
              public CustomerViewModel()
              {
                  objCommand = new ButtonCommand(this); // Point 2
              }
              public ICommand btnClick // Point 3
              {
                  get
                  {
                      return objCommand;
                  }
              }
      ….
      ….
      }

      在你的 UI 中添加一個按鈕,這樣就可以把按鈕的執行動作連接到暴露的“ICommand”接口。現在打開 button 的屬性欄,選擇 command 屬性,右擊創建一個數據綁定。

      技術分享圖片

      然後選擇靜態資源(Static Resource),並將“ButtonCommand”附加到button上。

      技術分享圖片

      當你點擊了 Calculate Tax 按鈕,它就執行了“CalculateTax”方法。並將稅值結果存在“_tax”變量中。關於“CalculateTax”方法代碼,可以閱讀前面的小節“第三步:添加執行動作和“INotifyPropertyChanged”接口”。

      換句話說,稅值計算過程並不會自動通知給 UI。所以我們需要從對象發送某種通知給 UI,告訴它稅值已經變化了,UI 需要重新載入綁定值。

      技術分享圖片

      因此,在 ViewModel 類中我們需要發送 INotify 事件給視圖。

      技術分享圖片

      為了讓你的 ViewModel 類能夠實現通知,我們必須做三件事情。這三件事情都在下面的代碼註釋中指出,例如 Point1, Point2 和 Point3。

      Point1: 如下面代碼那樣實現“INotifyPropertyChanged”接口。一旦你實現了該接口,它就創建了對象的“PropertyChangedEventHandler”事件。

      Point2 和 3: 在“Calculate”方法中用“PropertyChanged”對象去觸發事件,並在其中指定了某個屬性的通知。在這裏是“Tax”屬性。安全起見,我們同樣也要檢查“PropertyChanged”是否不為空。

      public class CustomerViewModel : INotifyPropertyChanged // Point 1
      {
      ….
      ….
              public void Calculate()
              {
                  obj.CalculateTax();
                  if (PropertyChanged != null) // Point 2
                  {
                      PropertyChanged(this,new PropertyChangedEventArgs("Tax"));
                  // Point 3
                  }
              }
      
              public event PropertyChangedEventHandler PropertyChanged;
      }

      如果你運行程序,你應該可以看見當點擊按鈕後“Tax”值被更新了。

      第四步:在 ViewModel 中解耦執行動作

      到目前為止,我們用 MVVM 框架創建了一個簡單的界面。這個界面同時包含了屬性和命令實現。我們擁有了一個視圖,它的 UI 輸入元素(例如 textbox)通過綁定和 ViewModel 連接起來,它的任何執行動作(例如按鈕點擊)通過命令和 ViewModel 連接起來。ViewModel 和內部的 Model 通訊。

      技術分享圖片

      但是在上面的結構中還有一個問題:command 類和 ViewModel 類存在著過度耦合的情況。如果你還記得 command 類代碼(我在下面貼出來了)中的構造函數是傳遞了 ViewModel 對象,這意味著這個 command 類無法被其它的 ViewModel 類所復用。

      public class ButtonCommand : ICommand
          {
              private CustomerViewModel obj; // Point 1
              public ButtonCommand(CustomerViewModel _obj) // Point 2
              {
                  obj = _obj;
              }
      ......
      ......
      ......
      
      }

      技術分享圖片

      但是在考慮了所有情況之後,讓我們邏輯地思考下“什麽是一個動作?”。它是一個事件,可以由用戶從鼠標點擊(左鍵或右鍵),按鈕點擊,菜單點擊,功能鍵按下等。所以應該有一種方式通用化這些動作,並且讓各種 ViewModel 有一種更通用的方法去綁定它。

      邏輯上講,如果你認為任務動作是一些方法和函數的封裝邏輯。那有什麽是“方法”和“函數”的通用表達方式呢?......努力想想.......再想想.......“委托”,“委托”,沒錯,還是“委托”。

      我們需要兩個委托,一個給“CanExecute”,另一個給“Execute”。“CanExecute”返回一個布爾值用來驗證以及根據驗證來使能(Enable)或者禁用(Disable)用戶界面。“Execute”委托則將在“CanExecute”委托返回 true 時執行。

      public class ButtonCommand : ICommand
          {
              public bool CanExecute(object parameter) // Validations
              {
              }
              public void Execute(object parameter) // Executions
              {
              }
          }

      因此,換句話說,我們需要兩個委托,一個返回布爾值,另一個執行動作並返回空。所以,創建一個“Func”和一個“Action”如何?“Func”和“Action”都可以用來創建委托。

      通過使用委托的方法,我們試著創建一個通用的 command 類。我們對 command 類做了三個修改(代碼參見下面),同時我也標註了三點 Point 1,2 和 3。

      Point1: 我們在構造函數中移除了 ViewModel 對象,改為接受兩個委托,一個是“Func”,另一個是“Action”。“Func”委托用作驗證(例如驗證何時動作將被執行),而“Action”委托用來執行動作。兩個委托都是通過構造函數參數傳遞進來,並賦值給類內部的對應私有成員變量。

      Point2 和 3: Func<> 委托(WhentoExecute)被“CanExecute”調用,執行動作的委托 Whattoexecute 則是在“Execute”中被調用。

      public class ButtonCommand : ICommand
      {
      private Action WhattoExecute;
      private Func<bool> WhentoExecute;
              public ButtonCommand(Action What , Func<bool> When) // Point 1
              {
                  WhattoExecute = What;
                  WhentoExecute = When;
              }
      public bool CanExecute(object parameter)
              {
                  return WhentoExecute(); // Point 2
              }
      public void Execute(object parameter)
              {
                  WhattoExecute(); // Point 3
              }
      }

      在 Model 類中我們已經知道要執行什麽了(例如“CalculateTax”),我們也創建一個簡單的函數“IsValid”來驗證“Customer”類是否有效。

      public class Customer
          {
      public void CalculateTax()
              {
      if (_Amount > 2000)
                  {
                      _Tax = 20;
                  }
      else if (_Amount > 1000)
                  {
                      _Tax = 10;
                  }
      else
                  {
                      _Tax = 5;
                  }
              }
      
      public bool IsValid()
              {
      if (_Amount == 0)
                  {
      return false;
                  }
      else
                  {
      return true;
                  }
              }
          }

      在 ViewModel 類中我們同時傳遞函數和方法給 command 類的構造函數,一個給“Func”,一個給“Action”。

      public class CustomerViewModel : INotifyPropertyChanged
      {
      private Customer obj = new Customer();
      privateButtonCommandobjCommand;
      publicCustomerViewModel()
              {
      objCommand = new ButtonCommand(obj.CalculateTax,
      obj.IsValid);
              }
      }

      這樣使得框架更好,更解耦, 使得這個 command 類可以以一個通用的方式被其它 ViewModel 引用。下面是改善後的架構, 需要註意 ViewModel 如何通過委托(Func和Action)和 command 類交互。

      技術分享圖片

      第五步:利用 PRISM

      最後如果有一個框架能幫助實現我們的 MVVM 代碼那就更好了。PRISM 就是其中一個可復用的框架。PRISM 的主要用途是為了提供模塊化開發,但是它提供了一個很好的“DelegateCommand”類拿來代替我們自己創建的 command 類。

      所以,第一件事情就是從這裏下載 PRISM,編譯這個解決方案,添加“Microsoft.Practices.Prism.Mvvm.dll”和“Microsoft.Practices.Prism.SharedInterfaces.dll”這兩個 DLL 庫的引用。

      你可以去掉自定義的 command 類,導入“Microsoft.Practices.Prism.Commands”名稱空間, 然後以下面代碼的方式使用 DelegateCommand。

      public class CustomerViewModel : INotifyPropertyChanged
      {
      private Customer obj = new Customer();
      private DelegateCommand  objCommand;
      public CustomerViewModel()
              {
      objCommand = new DelegateCommand(obj.CalculateTax,
                                              obj.IsValid);
              }
      …………
      …………
      …………
      …………
      
      }    
      }

WPF MVVM學習