此入門教程是記錄下方參考資料視頻的學習過程
開發工具:Visual Studio 2019
目錄
基本常識
開發/學習環境的准備
- Visual Studio
- Microsoft Prism:這個框架包含了對 MVVM 開發模式的支持。現在 NuGet 里有
- Microsoft Blend SDK:不是必須,能解決 MVVM 力不從心的問題。現在 NuGet 里也有
必要知識的准備
- 熟悉 Data Binding 和 Dependency Property
- 了解 WPF 中的命令(知道 ICommand 接口即可)
- 熟悉 Lambda 表達式
創建 Code Snippet (代碼模板)
- Code Snippet 的使用
- for
- class
- prop
- ctor
- ......
- Code Snippet 的創建
- Tools --> Code Snippets Manager 找到想修改的文件,從文件管理器復制出來修改,這里復制修改 propfull 的 Snippet
對應修改代碼模板,修改文件名稱,保存並復制回去就可以使用了
- Tools --> Code Snippets Manager 找到想修改的文件,從文件管理器復制出來修改,這里復制修改 propfull 的 Snippet
<Title>propn</Title>
<Shortcut>propn</Shortcut>
<Description>Code Snippet for NotificationObject property and backing field</Description>
<Code Language="csharp"><![CDATA[private $type$ $field$;
public $type$ $property$
{
get { return $field$;}
set
{
$field$ = value;
this.RaisePropertyChanged("$property$");
}
}
$end$]]>
</Code>
這個代碼模板就綁定到 propn
上了
this.RaisePropertyChanged("$property$");
功能通知屬性改變,這個類以后再寫,"$property$"
可以使用 nameof($property$)
更不容易錯
MVVM 設計模式詳解
MVVM = Model - View - ViewModel
為什么要使用 MVVM 模式
- 團隊層面:統一思維方式和實現方法
- 架構層面:穩定、解耦、富有禪意
- 代碼層面:可讀、可測、可替換
解耦是指 UI 與業務邏輯分離
什么是 Model
- 現實世界中對象的抽象結果,比如 學生 --> Student 之類的
什么是 View 和 ViewModel
- View = UI
- ViewModel = Model for View
- View 需要顯示什么,ViewModel 就要准備什么
- View 能做什么操作,ViewModel 就要能夠響應這個操作
- ViewModel 與 View 的溝通
- 傳遞數據依靠數據屬性
- 傳遞操作依靠命令屬性
程序的 UI 由 Model 驅動,用戶的數據和操作給 Model
View(User Interface) <-->(雙向的數據屬性) View Model(Model for View)
View(User Interface) -->(單向的命令屬性) View Model(Model for View)
View Model(Model for View) <--> Services <--> Database
View Model(Model for View) <--> Models <--> Services <--> Database
案例講解
初級案例
- NotificationObject 與 數據屬性
- DelegateCommand 與 命令屬性
- View 與 ViewModel 的交互(技術難點)
業務邏輯:
- 兩個輸入數據
- 一個按鈕做加法操作
- 一個顯示兩個輸入相加的結果
- 一個保存文件操作
新建項目
新建 WPF 項目,項目名稱 SimpleMvvmDemo.Client
先新建文件夾
- Views
- ViewModels
- Models
- Services
- Commands
雙向的數據屬性 Binding
NotificationObject 用來做數據傳輸
ViewModels 目錄下新建一個 ViewModel 的基類,命名 NotificationObjector ,具有通知能力的基類
當 ViewModel 里某個屬性的值變化之后,通過某個機制通知 Binding,Binding 把數據送到 View 上
當然,也可以用 Model 去實現這個類/接口,這樣在 ViewModel 中的引用類型的屬性也可以通知 View 上
NotificationObjector 實現 INotifyPropertyChanged 接口,這個接口有一個事件 PropertyChanged
當 ViewModel 的某個屬性借助 數據屬性Binding 關聯到 View 的某個控件上的話,當這個值變化的時候,Binding 就是在監聽着 PropertyChanged 事件,一旦事件發生,就把變化后的值送到界面上去
public class NotificationObjector : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void RasisePropertyChanged(string propertyName)
{
if (null != this.PropertyChanged)
{
this.PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
PropertyChangedEventHandler 委托
表示將處理 PropertyChanged 事件的方法,該事件在更改組件上的屬性時引發。
單向的命令屬性 Binding
DelegateCommand 用來做操作傳輸
Commands 目錄下新建一個類,命名 DelegateCommand ,實現 ICommand 接口,里面有三個東西
- 事件
CanExecuteChanged
當這個命令能不能執行的狀態發生改變的時候,有機會通知命令的調用者 - 方法
CanExecute(object parameter)
用來幫助命令的呼叫者判斷這個命令能不能執行 - 方法
Execute(object parameter)
當命令執行的時候,你想做什么,就寫在這個地方
簡陋的 DelegateCommand ,只用於講解本案例
public class DelegateCommand : ICommand
{
public event EventHandler CanExecuteChanged;
public bool CanExecute(object parameter)
{
if (null == this.CanExecuteFunc)
{
return true;
}
return this.CanExecuteFunc(parameter);
}
public void Execute(object parameter)
{
if (null == this.ExecuteAction)
{
return;
}
this.ExecuteAction(parameter);
}
public Action<object> ExecuteAction { get; set; }
public Func<object, bool> CanExecuteFunc { get; set; }
}
ViewModel
在 ViewModels 目錄新建 MainWindowViewModel ,繼承剛才的 NotificationObject 類,這里面的代碼會用到上面寫的 propn
代碼模板
實現三個數據屬性和兩個命令操作
public class MainWindowViewModel : NotificationObjector
{
#region 關聯屬性
private double _input_01;
public double Input_01
{
get { return _input_01; }
set
{
_input_01 = value;
this.RaisePropertyChanged(nameof(Input_01));
}
}
private double _input_02;
public double Input_02
{
get { return _input_02; }
set
{
_input_02 = value;
this.RaisePropertyChanged(nameof(Input_02));
}
}
private double _result;
public double Result
{
get { return _result; }
set
{
_result = value;
this.RaisePropertyChanged(nameof(Result));
}
}
#endregion
#region 關聯操作
public DelegateCommand AddCommand { get; set; }
public DelegateCommand SaveCommand { get; set; }
private void Add(object parameter)
{
this.Result = this.Input_01 + this.Input_02;
}
private void Save(object parameter)
{
SaveFileDialog dlg = new SaveFileDialog();
dlg.ShowDialog();
}
public MainWindowViewModel()
{
this.AddCommand = new DelegateCommand();
this.SaveCommand = new DelegateCommand();
//this.AddCommand.ExecuteAction = new Action<object>(this.Add);
//this.SaveCommand.ExecuteAction = new Action<object>(this.Save);
this.AddCommand.ExecuteAction = this.Add;
this.SaveCommand.ExecuteAction = this.Save;
}
#endregion
}
View
使用 {Binding}
綁定數據源和操作,要對應屬性,沒有智能提示
MainWindow.xaml
<Window x:Class="SimpleMvvmDdeml.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:SimpleMvvmDdeml.Client"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Menu>
<MenuItem Header="_File">
<MenuItem Header="_Save" Command="{Binding SaveCommand}"/>
</MenuItem>
</Menu>
<Grid Grid.Row="1">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Slider x:Name="Slider_01" Grid.Row="0" Background="LightBlue" Minimum="-100" Maximum="100" Margin="4" Value="{Binding Input_01}"/>
<Slider x:Name="Slider_02" Grid.Row="1" Background="LightBlue" Minimum="-100" Maximum="100" Margin="4" Value="{Binding Input_02}"/>
<Slider x:Name="Slider_03" Grid.Row="2" Background="LightBlue" Minimum="-100" Maximum="100" Margin="4" Value="{Binding Result}"/>
<Button x:Name="Button_Add" Grid.Row="3" Content="Add" Width="120" Height="80" Command="{Binding AddCommand}"/>
</Grid>
</Grid>
</Window>
沒有指定 Source 會拿 DataContext 當作 Source ,當 DataContext 也沒有就會往上級找,最后找到 Window
所以,我們需要在構造函數中綁定 ViewModel
MainWindow.cs
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new MainWindowViewModel();
}
}
效果
只修改前台代碼,同樣可以運行
<Window x:Class="SimpleMvvmDdeml.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:SimpleMvvmDdeml.Client"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Menu>
<MenuItem Header="_File">
<MenuItem Header="_Save" Command="{Binding SaveCommand}"/>
</MenuItem>
</Menu>
<Grid Grid.Row="1">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Slider x:Name="Slider_01" Grid.Row="0" Background="LightBlue" Minimum="-100" Maximum="100" Margin="4" Value="{Binding Input_01}"/>
<Slider x:Name="Slider_02" Grid.Row="1" Background="LightBlue" Minimum="-100" Maximum="100" Margin="4" Value="{Binding Input_02}"/>
<Slider x:Name="Slider_03" Grid.Row="2" Background="LightBlue" Minimum="-100" Maximum="100" Margin="4" Value="{Binding Result}"/>
<Button x:Name="Button_Add" Grid.Row="3" Content="Add" Width="120" Height="80" Command="{Binding AddCommand}"/>
</Grid>
</Grid>
</Window>
效果
同樣的業務邏輯,只是界面不同,所以只需要修改 View ,ViewModel 和 Model 不需要改變