- 浏览: 4199647 次
最新评论
WPF - MVVM Quick Start Tutorial
The Basics
- The most important thing about WPF is data binding. In short, you have some data, typically in a collection of some sort, and you want to display it to the user. You can 'bind' your XAML to the data.
- WPF has two parts, the XAML which describes your GUI layout and effects, and the code-behind that is tied to the XAML.
- The neatest and probably most reusable way to organise your code is to use the 'MVVM' pattern: Model, View, ViewModel. This has the aim of ensuring that your View contains minimal (or no) code, and should be XAML-only.
The Key Points You Need to Know
- The collection you should use to hold your data is the
ObservableCollection<T>
. Not alist
, not adictionary
, but anObservableCollection
. The word 'Observable
' is the clue here: the WPF window needs to be able to 'observe' your data collection. This collection class implements certain interfaces that WPF uses. - Every WPF control (including 'Window's) has a '
DataContext
' andCollection
controls have an 'ItemsSource
' attribute to bind to. - The interface '
INotifyPropertyChanged
' will be used extensively to communicate any changes in the data between the GUI and your code.
Example 1: Doing It (mostly) Wrong
The best way to start is an example. We will start with a
Book
class, rather than the usual Person
class nor the
Song class. We can arrange books into Categories, or by Author. A simple
Book
class would be as follows:
namespace Example1 { /// <summary> /// Model of a 'book'. /// </summary> public class Book { #region Properties /// <summary> /// Gets or sets the author. /// </summary> public string Author { get; set; } /// <summary> /// Gets or sets the book name. /// </summary> public string BookName { get; set; } #endregion } }In WPF terminology, this is our 'Model'. The GUI is our 'View'. The magic that data binds them together is our 'ViewModel', which is really just an adapter that turns our Model into something that the WPF framework can use. So just to reiterate, this is our 'Model'.
Since we've created a Book
as a reference type, copies are cheap and light on memory. We can create our
BookViewModel
quite easily. What we need to consider first is, what are we going to (potentially) display? Suppose we just care about the
book
's author, not the book
name, then the BookViewModel
could be defined as follows:
namespace Example1 { /// <summary> /// This class is a view model of a book. /// </summary> public class BookViewModel { #region Constructors /// <summary> /// Initializes a new instance of the <see cref="BookViewModel"/> class. /// </summary> public BookViewModel() { this.Book = new Book { Author = "Unknown", BookName = "Unknown" }; } #endregion #region Properties /// <summary> /// Gets or sets the book. /// </summary> public Book Book { get; set; } /// <summary> /// Gets or sets the author. /// </summary> public string Author { get { return Book.Author; } set { Book.Author = value; } } #endregion } }
Except that this isn't quite correct. Since we're exposing a property in our
ViewModel
, we would obviously want a change to the book
's author made in the code to be automatically shown in the GUI, and vice versa.Notice that in all the examples here, we create our view
model *declaratively*, i.e., we do this in the XAML:
<Window.DataContext> <!-- Declaratively create an instance of our BookViewModel --> <local:BookViewModel/> </Window.DataContext>
This is our view:
Clicking the button does not update anything, because we have not completely implemented data binding.
Data BindingRemember I said at the start that I would choose a property that stands out. In this example, we want to display the
Author
. I chose this name because it is NOT the same as any
WPF attribute. There are a countless number of examples on the web that choose a
Person
class and then a Name
attribute (the Name
attribute exists on multiple .NET
WPF classes). Perhaps the authors of the articles just don't realise that this is particularly confusing for beginners (who are, curiously enough, the target audience of these articles).
There are dozens of other articles about data binding out there, so I won't cover it here. I hope the example is so trivial that you can see what is going on.
To bind to the Author
property on our
BookViewModel
, we simply do this in the MainWindow.xaml:
<Label Content="{Binding Author}" />The '
Binding
' keyword binds the content of the control, in this case a
Label
, to the property 'Author
' of the object returned by
DataContext
. As you saw above, we set our DataContext
to an instance of
BookViewModel
, therefore we are effectively displaying BookViewModel.Author
in the
Label
.
Once again: Clicking the button does not update anything, because we have not completely implemented data binding. The GUI is not receiving any notifications that the property has changed.
Example 2: INotifyPropertyChanged
This is where we have to implement the cunningly named interface:
INotifyPropertyChanged
. As it says, any class that implements this interface, notifies any listeners when a property has changed. So we need to modify our
BookViewModel
class a little bit more:
namespace Example2 { using System.ComponentModel; /// <summary> /// This class is a view model of a book. /// </summary> public class BookViewModel : INotifyPropertyChanged { #region Constructor /// <summary> /// Initializes a new instance of the <see cref="BookViewModel"/> class. /// </summary> public BookViewModel() { this.Book = new Book { Author = "Unknown", BookName = "Unknown" }; } #endregion #region INotifyPropertyChanged Members /// <summary> /// The property changed. /// </summary> public event PropertyChangedEventHandler PropertyChanged; #endregion #region Properties /// <summary> /// Gets or sets the book. /// </summary> public Book Book { get; set; } /// <summary> /// Gets or sets the author. /// </summary> public string Author { get { return Book.Author; } set { if (Book.Author != value) { Book.Author = value; this.RaisePropertyChanged("Author"); } } } #endregion #region Methods /// <summary> /// The raise property changed. /// </summary> /// <param name="propertyName"> /// The property name. /// </param> private void RaisePropertyChanged(string propertyName) { // take a copy to prevent thread issues PropertyChangedEventHandler handler = this.PropertyChanged; if (handler != null) { handler(this, new PropertyChangedEventArgs(propertyName)); } } #endregion } }There are several things now happening here. Firstly, we check to see if we are going to really change the property: this improves performance slightly for more complex objects. Secondly, if the value has changed, we raise the
PropertyChanged
event to any listeners.
So now we have a Model
, and a
ViewModel
. We just need to define our View
. This is just our
MainWindow
:
<Window x:Class="Example2.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:Example2" Title="Example 2" SizeToContent="WidthAndHeight" ResizeMode="NoResize" Height="350" Width="525"> <Window.DataContext> <!-- Declaratively create an instance of our BookViewModel --> <local:BookViewModel /> </Window.DataContext> <Grid ShowGridLines="True"> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" /> </Grid.ColumnDefinitions> <Label Grid.Column="0" Grid.Row="0" Content="Example 2 - this works!" /> <Label Grid.Column="0" Grid.Row="1" Content="Author: " /> <Label Grid.Column="1" Grid.Row="1" Content="{Binding Author}" /> <Button Grid.Column="1" Grid.Row="2" Name="ButtonUpdateAuthor" Content="Update Author Name" Click="UpdateAuthor_Click" /> </Grid> </Window>
To test the databinding, we can take the traditional approach and create a button and wire to its
OnClick
event, so the XAML above has a button, and Click
event, giving the code behind:
namespace Example2 { using System.Windows; /// <summary> /// Interaction logic for MainWindow.xaml /// </summary> public partial class MainWindow { #region Fields /// <summary> /// The view model. /// </summary> private BookViewModel viewModel; /// <summary> /// The count. /// </summary> private int count; #endregion /// <summary> /// Initializes a new instance of the <see cref="MainWindow"/> class. /// </summary> public MainWindow() { InitializeComponent(); // We have declared the view model instance declaratively in the xaml. // Get the reference to it here, so we can use it in the button click event. this.viewModel = (BookViewModel)this.DataContext; } /// <summary> /// The update author_ click. /// </summary> /// <param name="sender"> /// The sender. /// </param> /// <param name="e"> /// The e. /// </param> private void UpdateAuthor_Click(object sender, RoutedEventArgs e) { ++this.count; this.viewModel.Author = string.Format("Author {0}", this.count); } } }
This is ok, but it is not how we should use
WPF: Firstly, we have added our 'update author' logic into our code-behind. It does not belong there. The
Window
class is concerned with windowing. The second problem is, suppose we want to move logic in the *button* click event to a different control, for example, making it a menu entry. It means we will be cut and pasting, and editing in multiple
places.
Here is our improved view, where clicking now works:
Example 3: Commands
Binding to GUI events is problematic.
WPF offers you a better way. This is ICommand
. Many controls have a
Command
attribute. These obey binding in the same way as Content
and
ItemsSource
, except you need to bind it to a *property* that returns an
ICommand
. For the trivial example that we are looking at here, we just implement a trivial class called 'RelayCommand
' that implements
ICommand
.
ICommand
requires the user to define two methods:
bool CanExecute
, and void Execute
. The CanExecute
method really just says to the user, can I execute this command? This is useful for controlling the context in which you can perform GUI actions. In our example, we don't
care, so we return true
, meaning that the framework can always call our 'Execute
' method. It could be that you have a situation where you have a command bound to button, and it can only execute if you have selected an item in a list.
You would implement that logic in the 'CanExecute
' method.
Since we want to reuse the ICommand
code, we use the
RelayCommand
class that contains all the repeatable code we do not want to keep writing.
To show how easy it is to reuse the ICommand
, we bind the Update Author command to both a button and a menu item. Notice that we no longer bind to Button specific Click event, or Menu specific Click event.
RelayCommand.cs:
namespace Example3 { using System; using System.Diagnostics; using System.Windows.Input; /// <summary> /// The relay command. /// </summary> public class RelayCommand : ICommand { #region Fields /// <summary> /// The can execute. /// </summary> private readonly Func<bool> canExecute; /// <summary> /// The execute. /// </summary> private readonly Action execute; #endregion #region Constructors /// <summary> /// Initializes a new instance of the <see cref="RelayCommand"/> class. /// </summary> /// <param name="execute"> /// The execute. /// </param> /// <param name="canExecute"> /// The can execute. /// </param> public RelayCommand(Action execute, Func<bool> canExecute = null) { if (execute == null) { throw new ArgumentNullException("execute"); } this.execute = execute; this.canExecute = canExecute; } #endregion #region ICommand Members /// <summary> /// The can execute changed. /// </summary> public event EventHandler CanExecuteChanged { add { if (this.canExecute != null) { CommandManager.RequerySuggested += value; } } remove { if (this.canExecute != null) { CommandManager.RequerySuggested -= value; } } } /// <summary> /// The can execute. /// </summary> /// <param name="parameter"> /// The parameter. /// </param> /// <returns> /// The <see cref="bool"/>. /// </returns> [DebuggerStepThrough] public bool CanExecute(object parameter) { return this.canExecute == null || this.canExecute(); } /// <summary> /// The execute. /// </summary> /// <param name="parameter"> /// The parameter. /// </param> public void Execute(object parameter) { this.execute(); } #endregion } }
BookViewModel.cs:
namespace Example3 { using System.ComponentModel; using System.Windows.Input; /// <summary> /// This class is a view model of a book. /// </summary> public class BookViewModel : INotifyPropertyChanged { #region Fields /// <summary> /// The count. /// </summary> private int count; #endregion #region Constructor /// <summary> /// Initializes a new instance of the <see cref="BookViewModel"/> class. /// </summary> public BookViewModel() { this.Book = new Book { Author = "Unknown", BookName = "Unknown" }; } #endregion #region INotifyPropertyChanged Members /// <summary> /// The property changed. /// </summary> public event PropertyChangedEventHandler PropertyChanged; #endregion #region Properties /// <summary> /// Gets or sets the book. /// </summary> public Book Book { get; set; } /// <summary> /// Gets or sets the author. /// </summary> public string Author { get { return Book.Author; } set { if (Book.Author != value) { Book.Author = value; this.RaisePropertyChanged("Author"); } } } /// <summary> /// Gets the update author name. /// </summary> public ICommand UpdateAuthorName { get { return new RelayCommand(this.UpdateAuthorNameExecute, this.CanUpdateAuthorNameExecute); } } #endregion #region Methods /// <summary> /// The can update author name execute. /// </summary> /// <returns> /// The <see cref="bool"/>. /// </returns> public bool CanUpdateAuthorNameExecute() { return true; } /// <summary> /// The update author name execute. /// </summary> public void UpdateAuthorNameExecute() { ++this.count; this.Author = string.Format("Author {0}", this.count); } /// <summary> /// The raise property changed. /// </summary> /// <param name="propertyName"> /// The property name. /// </param> private void RaisePropertyChanged(string propertyName) { // Take a copy to prevent thread issues. PropertyChangedEventHandler handler = this.PropertyChanged; if (handler != null) { handler(this, new PropertyChangedEventArgs(propertyName)); } } #endregion } }
Example 4: Frameworks
By now, if you have read closely, you'll probably notice that a lot of this is just repetitive code: raising property changed, or creating commands. This is mostly boilerplate, and for property changed, we can move it to
base class that we call 'ObservableObject
'. For the RelayCommand
class, we just move that into our .NET class library. This is how all of the
MVVM frameworks you find on the web begin.
As far as the ObservableObject
and
RelayCommand
classes are concerned, they are rather basic and are the inevitable result of refactoring.
So we move these classes into a small class library that we can reuse in future.
The view looks much the same as before:
Example 5: Collections of Books, Doing It Wrong
As I said before, in order to display collections of items in your
View
(i.e. the XAML), you need to use an ObservableCollection
. In this example, we create a
BookCategoryViewModel
, which nicely collects ourbooks together in something that people understand. We also introduce a simplebook database, purely so we can quickly produce somebook information for this example.
Your first attempt might be as follows:
namespace Example5 { using System.Collections.ObjectModel; using System.Windows.Input; using MicroMvvm; /// <summary> /// The book category view model. /// </summary> public class BookCategoryViewModel { #region Fields /// <summary> /// The database. /// </summary> private BookDatabase database = new BookDatabase(); /// <summary> /// The books. /// </summary> private ObservableCollection<Book> books = new ObservableCollection<Book>(); #endregion #region Constructor /// <summary> /// Initializes a new instance of the <see cref="BookCategoryViewModel"/> class. /// </summary> public BookCategoryViewModel() { for (int i = 0; i < 3; ++i) { this.books.Add(new Book { Author = this.database.GetRandomAuthorName, BookName = this.database.GetRandomBookName }); } } #endregion #region Properties /// <summary> /// Gets or sets the books. /// </summary> public ObservableCollection<Book> Books { get { return this.books; } set { this.books = value; } } /// <summary> /// Gets the update book category author. /// </summary> public ICommand UpdateBookCategoryAuthor { get { return new RelayCommand(this.UpdateBookCategoryAuthorExecute, this.CanUpdateBookCategoryAuthorExecute); } } /// <summary> /// Gets the add book category author. /// </summary> public ICommand AddBookCategoryAuthor { get { return new RelayCommand(this.AddBookCategoryAuthorExecute, this.CanAddBookCategoryAuthorExecute); } } #endregion #region Commands /// <summary> /// The update book category author execute. /// </summary> public void UpdateBookCategoryAuthorExecute() { if (this.books == null) { return; } foreach (var book in this.books) { book.Author = this.database.GetRandomAuthorName; } } /// <summary> /// The can update book category author execute. /// </summary> /// <returns> /// The <see cref="bool"/>. /// </returns> public bool CanUpdateBookCategoryAuthorExecute() { return true; } /// <summary> /// The add book category author execute. /// </summary> public void AddBookCategoryAuthorExecute() { if (this.books == null) { return; } this.books.Add(new Book { Author = this.database.GetRandomAuthorName, BookName = this.database.GetRandomBookName }); } /// <summary> /// The can add book category author execute. /// </summary> /// <returns> /// The <see cref="bool"/>. /// </returns> public bool CanAddBookCategoryAuthorExecute() { return true; } #endregion } }
You might think: "I have a different view model this time, I want to display the books as a
BookCategoryViewModel
, not a BookViewModel
".
We also create some more ICommands
and attach them to some buttons.
In this example, clicking 'Add Author' works fine. But clicking 'Update Author Names', fails.The following messageexplains why:
To fully support transferring data values from binding source objects to binding targets, each object in your collection that supports bindable properties must implement an appropriate property changed notification
mechanism such as the INotifyPropertyChanged
interface.
Our view looks like this:
Example 6: Collections of Bongs, the Right Way
In this final example, we fix the BookCategoryViewModel
to have an
ObservableCollection
of BookViewModel
that we created earlier:
namespace Example6 { using System.Collections.ObjectModel; using System.Windows.Input; using MicroMvvm; /// <summary> /// The book category view model. /// </summary> public class BookCategoryViewModel { #region Fields /// <summary> /// The database. /// </summary> private BookDatabase database = new BookDatabase(); /// <summary> /// The books. /// </summary> private ObservableCollection<BookViewModel> books = new ObservableCollection<BookViewModel>(); /// <summary> /// The count. /// </summary> private int count; #endregion #region Constructor /// <summary> /// Initializes a new instance of the <see cref="BookCategoryViewModel"/> class. /// </summary> public BookCategoryViewModel() { for (int i = 0; i < 3; ++i) { this.books.Add(new BookViewModel { AuthorName = this.database.GetRandomAuthorName, BookName = this.database.GetRandomBookName }); } } #endregion #region Properties /// <summary> /// Gets or sets the books. /// </summary> public ObservableCollection<BookViewModel> Books { get { return this.books; } set { this.books = value; } } /// <summary> /// Gets the add book category author. /// </summary> public ICommand AddBookCategoryAuthor { get { return new RelayCommand(this.AddBookCategoryAuthorExecute, this.CanAddBookCategoryAuthorExecute); } } /// <summary> /// Gets the update book category author. /// </summary> public ICommand UpdateBookCategoryAuthor { get { return new RelayCommand(this.UpdateBookCategoryAuthorExecute, this.CanUpdateBookCategoryAuthorExecute); } } /// <summary> /// Gets the update book names. /// </summary> public ICommand UpdateBookNames { get { return new RelayCommand(this.UpdateBookNamesExecute, this.CanUpdateBookNamesExecute); } } #endregion #region Commands /// <summary> /// The update book category author execute. /// </summary> public void UpdateBookCategoryAuthorExecute() { if (this.books == null) { return; } foreach (var book in this.books) { book.AuthorName = this.database.GetRandomAuthorName; } } /// <summary> /// The can update book category author execute. /// </summary> /// <returns> /// The <see cref="bool"/>. /// </returns> public bool CanUpdateBookCategoryAuthorExecute() { return true; } /// <summary> /// The add book category author execute. /// </summary> public void AddBookCategoryAuthorExecute() { if (this.books == null) { return; } this.books.Add(new BookViewModel { AuthorName = this.database.GetRandomAuthorName, BookName = this.database.GetRandomBookName }); } /// <summary> /// The can add book category author execute. /// </summary> /// <returns> /// The <see cref="bool"/>. /// </returns> public bool CanAddBookCategoryAuthorExecute() { return true; } /// <summary> /// The update book names execute. /// </summary> public void UpdateBookNamesExecute() { if (this.books == null) { return; } ++this.count; foreach (var book in this.books) { book.BookName = this.database.GetRandomBookName; } } /// <summary> /// The can update book names execute. /// </summary> /// <returns> /// The <see cref="bool"/>. /// </returns> public bool CanUpdateBookNamesExecute() { return true; } #endregion } }
Now all our buttons that are bound to commands operate on our collection. Our code-behind in MainWindow.cs is still completely empty.
Our view looks like this:
相关推荐
c#-的WPF---MVVM例子,大家共同学习
一个简单的WPF程序
WPF_MVVM_入门教程_Quick Start Tutorial_文章和源码
WPF-MVVM WPF MVVM模式开发的例子程序
WPF-MVVM分页功能例子WPF,MVVM,分页,Listview
WPF-MVVM实现CRUD操作
WPF-MVVM最小例程
WPF-MVVM井字棋游戏源码 源码描述: WPF-MVVM井字棋游戏源码 该源码演示了如何实现MVVM模式在一个WPF应用程序中 View:UI界面 ViewModel:它是View的抽象,负责View与Model之间信息转换,将View的Command传送到...
本项目使用WPF的mvvm设计模式编写,使用SQLite保存数据。希望对学习wpf的童鞋有帮助。 我也是刚刚上手的,写的不要勿怪,大神略过。
WPF计算器功能,用MVVM实现
深入浅出WPF-刘铁猛-MVVM视频源代码,MVVM入门与提高视频对应的源代码
MVVM模式编写的简单WPF程序,操作XML实现简单的增、删、改、查功能.
DEMO1-MVVM+RIA Service
这个资源主要任务是引导刚入门的初学者学习如何引用MvvmLight,及如何更快捷的创建ViewModel及了解ViewModel的作用。很简单的代码,大家可以下载学习之用。
对于wpf中的空间ProgressBar,大多数资料还是以codebehind的形式来讲解。但是实际工作中WPF主要应用MVVM工作模式,本例子展示了MVVM下,如何实时更新ProgressBar的显示而不造成卡顿。
使用MVVM模式的一个点餐系统,适合新手参考理解学习 MVVM模式
开发工具为VS2010,数据库采用了SQLServer2005,使用EntityFramework进行数据库建模。使用MVVMLight工具,来构建MVVM模式。...包含WPF程序主题的使用方法,其中包含两个主题MetroLight和MetroDark。
运动场-wpf-mvvm
vavatech-rittal-wpf-mvvm:Przykładyze szkolenia WPF MVVM
mvvm wpf silverlight 设计模式