`
阿尔萨斯
  • 浏览: 4174810 次
社区版块
存档分类
最新评论

WPF - MVVM Quick Start Tutorial

 
阅读更多

The Basics

  1. 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.
  2. WPF has two parts, the XAML which describes your GUI layout and effects, and the code-behind that is tied to the XAML.
  3. 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

  1. The collection you should use to hold your data is the ObservableCollection<T>. Not a list, not a dictionary, but an ObservableCollection. 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.
  2. Every WPF control (including 'Window's) has a 'DataContext' and Collection controls have an 'ItemsSource' attribute to bind to.
  3. 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 Binding

Remember 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:



分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics