一、前言
作為一個初入軟件業的新手,各種設計模式與框架對我是眼花繚亂的。所以當我接觸到這些新知識的時候就希望自己能總結幾個步驟,以便更好更方便的在日常工作中進行使用。
MVVM顧名思義就是Model-View-View Model的縮寫。老司機們一直說綁定綁定,我就納悶了View是展示,Model是模型,那View Model怎么寫處理的邏輯呢?它是如何將Model和View聯系到一起的呢?這是我第一次聽到MVVM時產生的疑惑。經過了一些編程經歷,大致明白了整個過程。本文不會過分強調MVVM中一些特別深入的技術(我暫時也沒那本事),只是從一個初學者的角度去學會如何最快速的使用MVVM。
本文將以MVVM Light作為例子,因為它是個輕量化的MVVM框架,非常方便使用。以后會逐步介紹些其他的MVVM框架,如DevExpress的等等。知識是互通的,明白了其中一個,另一種也差不多不離其宗了。
二、准備
下載MVVM Light的方式多種多樣,可以使用NuGet包管理器或者直接登錄官網,一搜就找到了。
本項目安裝完MVVM Light后可以看到引用:
還有一個ViewModel文件夾:
三、MVVM
假設我們有這樣一個產品的Model:IsChecked屬性大家一看就知道是用於在前端與CheckBox有聯系而設置的屬性。
namespace StudyMVVM { public class ProductInfo { public bool IsChecked { get; set;} public string ProductName { get; set; } public string ProductIcon { get; set; } public string ProductUrl { get; set; } public string OldVersion { get; set; } public string NewVersion { get; set; } } }
假設我們有一個WPF頁面MainView.xaml,也就是View是這么寫的:首先別管那個 ItemsSource,下面會慢慢說到
<Grid Name="GridName" Grid.Row="2" Margin="30,5" > <ListBox Name="lb_Update" VerticalAlignment="Center" Height="115" ItemsSource="{Binding UpdateProducts}" Margin="0,6,0,10"></ListBox> </Grid>
那么我們想要把多個對象的屬性填充到一個ListBoxItem里,然后將若干個ListBoxItem放到ListBox里,所以:
<Style TargetType="ListBoxItem"> <Setter Property="Template"> <Setter.Value> <ControlTemplate> <Grid Width="510" Height="120" MaxHeight="150" > <Grid.ColumnDefinitions> <ColumnDefinition Width="22*"/> <ColumnDefinition Width="32*"/> <ColumnDefinition Width="68*"/> <ColumnDefinition Width="100*"/> <ColumnDefinition Width="154*"/> <ColumnDefinition Width="105*"/> <ColumnDefinition Width="29*"/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="*"></RowDefinition> <RowDefinition Height="*"></RowDefinition> </Grid.RowDefinitions> <Grid Grid.Column="1" Grid.RowSpan="2"> <CheckBox VerticalAlignment="Center" HorizontalAlignment="Center" IsChecked="{Binding IsChecked}"/> </Grid> <Grid Grid.Column="2" Grid.RowSpan="2"> <Image Source="{Binding ProductIcon}" Width="50"></Image> </Grid> <Grid Grid.Column="3" Grid.RowSpan="2"> <TextBlock VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="15" Text="{Binding ProductName}"/> </Grid> <Grid Grid.Column="4" Grid.Row="0"> <TextBlock VerticalAlignment="Bottom" HorizontalAlignment="Center" Margin="0,0,0,5" Text="{Binding OldVersion}"/> </Grid> <Grid Grid.Column="4" Grid.Row="1"> <TextBlock VerticalAlignment="top" HorizontalAlignment="Center" Margin="0,5,0,0" Text="{Binding NewVersion}"/> </Grid> <Grid Grid.Column="5" Grid.RowSpan="2"> <TextBlock VerticalAlignment="Center" HorizontalAlignment="Center"> <Hyperlink NavigateUri="{Binding ProductUrl}" Click="Hyperlink_Click">日志</Hyperlink> </TextBlock> </Grid> </Grid> </ControlTemplate> </Setter.Value> </Setter> </Style>
我們可以非常清楚的看到Model中的屬性都綁定到了View中!下面就是很關鍵的ViewModel了,我們還沒用到上述的ItemsSource呢。
在MainViewModel.cs中,是這樣的:
using GalaSoft.MvvmLight; using GalaSoft.MvvmLight.Command; using System.Collections.Generic; using System.IO; using System; using System.ComponentModel; using System.Threading; using System.Threading.Tasks; using Newtonsoft.Json; using System.Diagnostics; namespace StudyMVVM.ViewModel { public class MainViewModel : ViewModelBase { public List<ProductInfo> UpdateProducts { get; set; } // public MainViewModel() { UpdateProducts = new List<ProductInfo>(); for(int i=0;i<10;i++) { ProductInfo productinfo = new ProductInfo(); productinfo.IsChecked = true; productinfo.ProductName = str_Name; productinfo.ProductIcon = str_Path; productinfo.ProductUrl = "www.baidu.com"; productinfo.OldVersion = "0.0.1"; productinfo.NewVersion = "0.0.2"; UpdateProducts.Add(productinfo); } } } }
這樣,多個ProductInfo的對象被包裝在名為UpdateProducts內,並且通過ItemsSource綁定到ListBox中,數據就這樣填充上了。
四、如何寫事件
當你在前端有個按鈕,想處理若干個ListBoxItem,比如下載所有Checked為true的對象,你是否會懷念Winform的Click事件? 當然WPF也有Click事件。既然你已經用了MVVM,那么請少用,最好不用Click事件去處理這些東西,特別是你要寫的事件是與你的ItemsSource所綁定的東西相關的。
說白了,在例子里就是和UpdateProducts有關系的,你就別用Click了。
在View中假設有一個Button:它的Command綁定了GetCheckedUpdateProducts事件
<Grid Name="UpdateBtn" Grid.Row="0" Grid.Column="1"> <Button Name="btn_update" Width="80" Height="30" Cursor="Hand" Content="更新" Foreground="White" FontSize="14" Command="{Binding GetCheckedUpdateProducts}"> </Grid>
在ViewModel中,注意引用GalaSoft.MvvmLight.Command;
using GalaSoft.MvvmLight; using GalaSoft.MvvmLight.Command; namespace StudyMVVM.ViewModel { public class MainViewModel : ViewModelBase { public RelayCommand GetCheckedUpdateProducts { get; set; } public MainViewModel() { this.GetCheckedUpdateProducts = new RelayCommand(GetProducts); } } private void GetProducts() { //Your Button Command: Download checked products } }
將真正的事件邏輯GetProducts()賦值給RelayCommand GetCheckedUpdateProducts,前端通過Command=“{Binding GetCheckedUpdateProducts}” 即可。
五、RaisePropertyChanged
這個RaisePropertyChanged是專門來照顧沒媽媽(ItemsSource)的孩子的(properties)。
假設xaml前端有一個進度條,當你按下按鈕下載checked=true的產品時,進度條要實時顯示下載情況:
<ProgressBar Grid.Row="0" Height="10" VerticalAlignment="Top" Margin="10,0,8,0" Maximum="{Binding MaxValue}" Minimum="{Binding MinValue}" Value="{Binding ProgressValue}"/>
Maximum和Minimum一般是個定值,但ProgressValue是變化的,並且和Model里屬性字段的沒半毛錢的關系啊,咋辦?我得告訴View我在改變啊,那么在ViewModel中:
using GalaSoft.MvvmLight; using GalaSoft.MvvmLight.Command; namespace StudyMVVM.ViewModel { public class MainViewModel : ViewModelBase { public int MaxValue { get; set; } public int MinValue { get; set; } public int ProgressValue { get; set; }
public RelayCommand GetCheckedUpdateProducts { get; set; }
public MainViewModel() { MaxValue = 100; MinValue = 0; ProgressValue = 0;
this.GetCheckedUpdateProducts = new RelayCommand(GetProducts); } private void GetProducts()
{
BackgroundWorker bgWorker = new BackgroundWorker();
bgWorker.DoWork += new DoWorkEventHandler(worker_Dowork);
bgWorker.ProgressChanged += new ProgressChangedEventHandler(worker_ProgressChanged);
bgWorker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(worker_RunWorkerCompleted);
bgWorker.RunWorkerAsync();
}
void worker_Dowork(object sender, DoWorkEventArgs e) { //do work } void worker_ProgressChanged(object sender, ProgressChangedEventArgs e) { UpdateMessage = (string)e.UserState; ProgressValue = e.ProgressPercentage; RaisePropertyChanged(() => ProgressValue); // I'm Here!!!! Hey! Look At Me ! RaisePropertyChanged(() => UpdateMessage); } void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { if (e.Result is Exception) { UpdateMessage = (e.Result as Exception).Message; } else { UpdateMessage = (string)e.Result; } } } }
PS:上述代碼還用到了BackgroundWorker,這是一個不錯的異步顯示進度條的控件,有興趣的可以試試,非常方便使用。
六、DataContext
看到這里,有些新手覺得ViewModel中的東西可以很順利成章的綁定到View上了,錯!不覺得奇怪嗎?憑什么這個MainViewModel就要和上述的View建立聯系,而不是和其他的View有聯系呢?
為了防止View上錯老婆(為什么我不說防止ViewModel找到隔壁老王呢?各位可以思考想想),我們需要在某一個View中指定其DataContext是哪個ViewModel!
using YourProject.ViewModel; using System.Collections.Generic; using System.Windows; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Diagnostics; namespace StudyMVVM { /// <summary> /// MainView.xaml 的交互邏輯 /// </summary> public partial class MainView : Window { public MainView() { this.DataContext = new MainViewModel();// find correct wife } } }
還有一個辦法能指定DataContext,MVVM Light提供了ViewModelLocator.cs來幫助你綁定view的DataContext;Xaml里也可以綁定DataContext。不過我還是喜歡用上述最原始的方法。至於ViewModelLocator怎么使用,博園有相當多的牛人及文章,想要深入了解的可以去搜下。
其實DataContext在你引入MVVM框架之后就應該進行綁定了,寫在這里只是為了提醒大家其重要性!
七、大結局
終於寫完了,科科,擺了個白!