第5篇 WPF C# 数据绑定Model-View-ViewModel模式
第5篇 WPF C# 数据绑定Model-View-ViewModel模式
参考资料:
John Sharp:《Microsoft Visual C# 2013 Step by Step》
周 靖 译:John Sharp《Visual C# 2012从入门到精通》
前言
Model-View-ViewModel模式即MVVM模式编程涉及五个文件:
1、MainWindow.xaml文件 UI界面文件
2、MainWindow.xaml.cs文件 UI架构文件
3、DataLib.cs数据类库文件 数据元素类库文件
4、ViewModel.cs文件 视图模型文件
5、Command.cs文件 UI命令文件
上面的文件名斜体为自由定义。正体为参考资料资料定义。
1、界面文件
MainWindow.xaml
<Window x:Class="StudyDisplay.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 Background="{DynamicResource {x:Static SystemColors.ControlDarkDarkBrushKey}}"> <!-- Grid定义 --> <Grid.ColumnDefinitions> …… </Grid.RowDefinitions> <StackPanel Grid.Row="0" Grid.Column="0" Grid.RowSpan="5" Background="{DynamicResource {x:Static SystemColors.ActiveCaptionBrushKey}}" /> <StackPanel Grid.Row="0" Grid.Column="1" Grid.RowSpan="5" Background="{DynamicResource {x:Static SystemColors.GradientInactiveCaptionBrushKey}}" /> <!--1--> <StackPanel Grid.Row="0" Grid.Column="2" Style="{StaticResource TheStackPanelStyle}" > …… </StackPanel> <!--2--> <StackPanel Grid.Row="1" Grid.Column="2" Style="{StaticResource TheStackPanelStyle}" > <StackPanel> <Label Style="{StaticResource TheLabeltyle}">采样周期</Label> <TextBox Style="{StaticResource TheTextBoxStyle}" Name="inputSC" Text="{Binding Current.SampleCircle,Mode=TwoWay}" GotFocus="inputSC_GotFocus" PreviewMouseLeftButtonDown="inputSC_PreviewMouseLeftButtonDown" /> </StackPanel> <StackPanel> <Label Style="{StaticResource TheLabeltyle}">公制/英制</Label> <ComboBox Style="{StaticResource TheComboBoxStyle}" Background="{DynamicResource {x:Static SystemColors.ActiveBorderBrushKey}}"> <ComboBoxItem Style="{StaticResource ThComboBoxItemStyle}">公制</ComboBoxItem> <ComboBoxItem Style="{StaticResource ThComboBoxItemStyle}">英制</ComboBoxItem> <ComboBoxItem Style="{StaticResource ThComboBoxItemStyle}">华制</ComboBoxItem> </ComboBox> </StackPanel> <StackPanel> <Label Style="{StaticResource TheLabeltyle}">采样周期1</Label> <TextBox Style="{StaticResource TheTextBoxStyle}" Name="outputSC" Text="{Binding Current.SampleCircle,Mode=TwoWay}"
GotFocus="outputSC_GotFocus" /> </StackPanel> <StackPanel> <Label Style="{StaticResource TheLabeltyle}">公制/英制1</Label> <ComboBox Style="{StaticResource TheComboBoxStyle}" Background="{DynamicResource {x:Static SystemColors.ActiveBorderBrushKey}}"> <ComboBoxItem Style="{StaticResource ThComboBoxItemStyle}">公制</ComboBoxItem> <ComboBoxItem Style="{StaticResource ThComboBoxItemStyle}">英制</ComboBoxItem> <ComboBoxItem Style="{StaticResource ThComboBoxItemStyle}">华制</ComboBoxItem> </ComboBox> </StackPanel> </StackPanel> <!--3--> <StackPanel Grid.Row="2" Grid.Column="2" Style="{StaticResource TheStackPanelStyle}"> <CheckBox Content="CheckBox" Checked="CheckBox_Checked_1" /> <CheckBox Content="CheckBox" Checked="CheckBox_Checked" /> <CheckBox Content="CheckBox" /> <CheckBox Content="CheckBox" Checked="CheckBox_Checked" /> <CheckBox Content="CheckBox" /> <CheckBox Content="CheckBox" /> <CheckBox Content="CheckBox" /> </StackPanel> <!--4--> <StackPanel Grid.Row="3" Grid.Column="2" Style="{StaticResource TheStackPanelStyle}">…… </StackPanel>
<!--5--> <StackPanel Grid.Row="4" Grid.Column="2" Style="{StaticResource TheStackPanelStyle}">…… </StackPanel>
</Grid> <!-- <Page.TopAppBar > <AppBar IsSticky="True"> <StackPanel Orientation="Horizontal" HorizontalAlignment="Center"> <AppBarButton x:Name="previousCustomer" Icon="Back" Command="{Binding Path=PreviousCustomer}"/> <AppBarButton x:Name="nextCustomer" Icon="Forward" Command="{Binding Path=NextCustomer}"/> </StackPanel> </AppBar> </Page.TopAppBar> --> </Window>
1.1 绑定方法
如上列代码中粗体所示。绑定语句为:
Text="{Binding Current.SampleCircle,Mode=TwoWay}"
与简单的数据绑定相比,增加了 Current. 。
1.2 视图调用ViewModel
代码末尾一段(<Page.TopAppBar ></Page.TopAppBar>)在Win7下无法运行,是Win8的AppBar控件。用"Back"和"Forward"按钮分别绑定命令上一个、下一个命令。该命令实现:数据引导ID的增减1,从而使上列绑定数据在视图模型中列出的各个数据表绑定。
2、架构文件
MainWindow.xaml.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.Forms; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; namespace StudyDisplay { /// <summary> /// MainWindow.xaml 的交互逻辑 /// </summary> public partial class MainWindow : Window { //架构绑定 public MainWindow() { InitializeComponent();
DataLib dataLib = new DataLib { SampleCircle = "John" };
//在MainPage构造器中删除创建Customer对象的代码,替换成创建ViewModel类实例 的一个语句。
ViewModel viewModel = new ViewModel();
//修改设置MainPage对象的DataContext属性的语句,来引用新的ViewModel对象。
this.DataContext = viewModel;
}
//交互逻辑 private void inputSC_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) { // var control = sender as System.Windows.Controls.TextBox; //if (control == null) // return; // Keyboard.Focus(control); // e.Handled = true; } private void inputSC_GotFocus(object sender, RoutedEventArgs e) { // inputSC_GotFocus.SelectAll(); } private void outputSC_GotFocus(object sender, RoutedEventArgs e) { //outputSC_GotFocus.SelectAll(); } } }
在MainPage构造器中删除创建Customer对象的代码,替换成创建ViewModel类实例 的一个语句。修改设置MainPage对象的DataContext属性的语句,来引用新的ViewModel对象。如加粗的语句所示。
3、数据元素类库文件
DataLib.cs
按照简单数据绑定所建立的数据类库文件无需改变。(建立方法见上1篇)
using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Text; using System.Threading.Tasks; namespace StudyDisplay { // 第一步:声明一个类,准备必要的属性 public class DataLib : INotifyPropertyChanged { public int _customerID; //准备必要的属性1:CustomerID public int CustomerID { get { return this._customerID; } set { this._customerID = value; this.OnPropertyChanged("CustomerID"); } } //准备必要的属性2:CustomerID public string _sampleCircle; public string SampleCircle { get { return this._sampleCircle; } set { this._sampleCircle = value; this.OnPropertyChanged("SampleCircle"); } } // 第二步:完成“具有双向的功能”
//OnPropertyChanged方法引发PropertyChanged事件。
public event PropertyChangedEventHandler PropertyChanged;
//PropertyChanged 事件的PropertyChangedEventArgs参数指定了发生改变的属性的名称。该值作为参
//数传给OnPropertyChanged方法。
protected virtual void OnPropertyChanged(string propertyName)
{ if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } } } }
4、视图模型文件
项目资源管理器—>项目—>右键|添加—>类—>文件名:ViewModel.cs 新建ViewModel.cs文件。
ViewModel.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.ComponentModel; //H、
namespace StudyDisplay
{
public class ViewModel : INotifyPropertyChanged //H、
{ private List<DataLib> dataLib;
//2、在ViewModel类中添加以下私有变量currentCustomer,在构造器中把它初始化为0; private int currentDataLib;
//添加NextCustomer和PreviousCustomer自动属性。 //视图将绑定到这些Command对象,允许在客户之间导航。 public Command NextDataLib { get; private set; } public Command PreviousDataLib { get; private set; } // public ViewModel() //ViewModel构造器 { this.currentDataLib = 0; //2、把currentCustomer初始化为0 this.IsAtStart = true; //设置IsAtStart和IsAtEnd属性 this.IsAtEnd = false; //设置IsAtStart和IsAtEnd属性 //设置NextCustomer和PreviousCustomer属性来引用新的Command对象, this.NextDataLib = new Command(this.Next, () => { return this.dataLib.Count > 0 && !this.IsAtEnd; }); //Lambda表达式 this.PreviousDataLib = new Command(this.Previous, () => { return this.dataLib.Count > 0 && !this.IsAtStart; }); //Lambda表达式 //1、ViewModel类将一个List<Customer>对象作为它的模型,构造器用示例数据填充该列表。 this.dataLib = new List<DataLib> { new DataLib { CustomerID = 1, SampleCircle = "John", }, new DataLib { CustomerID = 2, SampleCircle = "Diana", }, new DataLib { CustomerID = 3, SampleCircle = "Francesca", } }; // }
//添加以下字段和属性。将用这两个属性跟踪ViewModel的状态。如果ViewModel的
//currentCustomer字段定位在customers集合起始处,IsAtStart属性将设为true, //如果定位在customers集合末尾, IsAtEnd属性将设为true。 private bool _isAtStart; public bool IsAtStart { get { return this._isAtStart; } set { this._isAtStart = value; this.OnPropertyChanged("IsAtStart"); } } private bool _isAtEnd; public bool IsAtEnd { get { return this._isAtEnd; } set { this._isAtEnd = value; this.OnPropertyChanged("IsAtEnd"); } } //3、在 ViewModel类中添加Current属性,放到构造器之后。 public DataLib Current { get { return this.dataLib[currentDataLib]; } } //将加粗的私有方法Next和Previous添加到ViewModel类,放到Current属性之后。 //注意 Count属性返回集合中的数据项的数量,但记住集合项的编号是从0到Count – 1。 //这些方法更新currentCustomer变量来引用客户列表中的下一个(或上一个)客户。 //注意,方法负责维护IsAtStart和IsAtEnd属性的值,并通过为Current属性引发 //PropertyChanged事件来指出当前客户已发生改变。两个方法都是私有方法,它 //们不应从ViewModel类的外部访问。外部类通过命令来运行这些方法。命令将在下面的步骤中添加。 //!+++!此处涉及自定义的变量 private void Next() { if (this.dataLib.Count - 1 > this.currentDataLib) { this.currentDataLib++; this.OnPropertyChanged("Current"); this.IsAtStart = false; this.IsAtEnd = (this.dataLib.Count - 1 == this.currentDataLib); } } private void Previous() { if (this.currentDataLib > 0) { this.currentDataLib--; this.OnPropertyChanged("Current"); this.IsAtEnd = false; this.IsAtStart = (this.currentDataLib == 0); } } //I、在ViewModel类末尾添加PropertyChanged事件和OnPropertyChanged方法。其实就是在
//Customer类中添加的代码。记住,视图在控件的数据绑定表达式中通过Current属性来引用数据。
//ViewModel类移动至不同的客户时,必须引发PropertyChanged事件通知视图所显示的数据发生改变。
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this,
new PropertyChangedEventArgs(propertyName));
}
}
}
}
4.1 建立ViewModel
见上列代码中1、2、3、三步。
第一步://1、ViewModel类将一个List<Customer>对象作为它的模型,构造器用示例数据填充该列表。
第二步://2、在ViewModel类中添加以下私有变量currentCustomer,在构造器中把它初始化为0;
第三步://3、在 ViewModel类中添加Current属性,放到构造器之后。
到此,ViewModel已经建立。ViewModel通过 Current属性提供对Customer信息的访问,但它没有提供在不同Customer之间导航方式。
可实现方法来递增和递减currentCustomer变量,使Current属性能获取不同的Customer。 但在这样做的时候,又不能使视图对 ViewModel产生依赖。
最常见的解决方案是Command模式。在这个模式中,ViewModel用方法来实现可由视图调用的命令。这里关键在于不能在视图的代码中显式引用这些方法名。所以,需要将命令绑定到由UI控件触发的操作。这正是下一节的练习要做事情。
4.2 向ViewModel添加命令
(续5、命令文件)向ViewModel类添加NextCustomer和PreviousCustomer命令
H、在文件顶部添加using指令,修改ViewModel类的定义来实现INotifyPropertyChanged接口。
I、在ViewModel类末尾添加PropertyChanged事件和OnPropertyChanged方法。其实就是在Customer类中添加的代码。
5、命令文件
项目资源管理器—>项目—>右键|添加—>类—>文件名:Command.cs 新建VCommand.cs文件。
Command.cs
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Input; //增加;ICommand接口在该命名空间中。 //using Windows.UI.Xaml; //Win8控件的引用 namespace StudyDisplay { public class Command : ICommand {
//A、在Command类中添加以下私有字段。 private Action methodToExecute = null; private Func<bool> methodToDetectCanExecute = null; //private DispatcherTimer canExecuteChangedEventTimer = null;
//B、为Command类添加构造器。获取两个参数:一个Action对象和一个Func<T> 对象,参数值赋
//给methodToExecute和methodToExecute字段。
//Command构造器:
public Command(Action methodToExecute,Func<bool> methodToDetectCanExecute)
{
this.methodToExecute = methodToExecute; this.methodToDetectCanExecute = methodToDetectCanExecute; //G:添加这些代码: //this.canExecuteChangedEventTimer = new DispatcherTimer(); //this.canExecuteChangedEventTimer.Tick += canExecuteChangedEventTimer_Tick; //this.canExecuteChangedEventTimer.Interval = new TimeSpan(0, 0, 1); //this.canExecuteChangedEventTimer.Start(); //这些代码初始化DispatcherTimer对象,将计时器周期设为 1秒并启动计时器。 秒并启动计时器。 }
//C、使用methodToExecute和methodToDetectCanExecute字段引用的方法来实
//现Command类的Execute和CanExecute方法。 public void Execute(object parameter) { this.methodToExecute(); } public bool CanExecute(object parameter) { if (this.methodToDetectCanExecute == null) { return true; } else { return this.methodToDetectCanExecute(); } }
//D、为Command类添加公共CanExecuteChanged事件。 public event EventHandler CanExecuteChanged;
//F、在Command类末尾添加以下canExecuteChangedEventTimer__Tick方法。
void canExecuteChangedEventTimer_Tick(object sender, object e)
{
if (this.CanExecuteChanged != null)
{
this.CanExecuteChanged(this, EventArgs.Empty);
}
}
}
}