09/10/2016

[WPF] Create Simple WPF App With MVVM - Part I


Để tìm kiếm một kiến thức có hệ thống về lập trình WPF, ngoài việc research online, tham khảo các samples của Microsoft, các đồng nghiệp trên các trang như stackoverflow, codeproject, google,bing hoặc bravohex.. ta cũng nên review một tí documents từ các nguồn. Nhưng điều tốt  nhất là tạo cho mình một demo đơn giản, dễ hiểu từ một mớ thông tin kể trên. Cho nên  tôi sẽ đi demo lại một ứng dụng nhỏ, cơ bản và post lên blog này. Nếu bạn đang chung mục tiêu tìm kiếm, mời xem tiếp nhé. Toàn bộ nội dụng demo, ta sẽ đi qua các phần như sau:



Môi trường phát triển:

  • Visual Studio 2015
  • WPF 4.5/ C# 6.0 
  • Windows 7 or higher (I'm working on Windows 10 Pro/64bit with .NET 4.6)

Có một điều bạn cần biết, các version của WPF, ASP.NET, UWP... và ngôn ngữ như C# phụ thuộc vào phiên bản Visual Studio bạn cài đặt và Windows có hỗ trợ không.
Chẳng hạn như tôi dùng Visual Studio 2015 đương nhiên hỗ trợ sẵn C# 6.0. Nhưng nếu build  lại trên Visual Studio 2013 một cách chính thống thì vài rắc rối nhỏ sẽ xảy ra. Vô số chức năng C# 6.0 sẽ lỗi toàn tập.
Thật ra, tôi quan tâm C# version mới nó coding ngắn gọn, nhanh hơn, đẹp hơn, các vấn đề hiệu năng ta không kể ở đây. Cái gì mà nó phức tạp thôi bỏ qua...
(Nếu Visual Studio của bạn không code được C# 6.0 thì convert lại 1 tí. Cũng không nhiều chỗ... )

Hoặc bạn có thể tải bản Visual Studio Community 2015 miễn phí để sử dụng. OK Let's go...


Bắt Đầu


Trước khi làm việc, ta định hình là mình sẽ làm cái chi trong phần này. Cụ thể là code những gì?
Phần này chỉ tập trung xây dựng demo nhỏ, triển khai mô hình MVVM. Kết nối tới database ta sẽ làm vào phần tiếp theo.
Vậy solution sẽ tạo 03 projects:

  • Business.Models: Project Class Library
  • Business.ViewModels: Project Class Library
  • Business.App: WPF App
Bạn đừng quan tâm đến cách đặt tên, làm xong bạn tự biết nó dùng để làm gì, nếu sai thì rename lại theo ý mình.
Note: Tạo class trong project thì namespace nó sẽ đặt theo tên project kết hợp folder mà bạn add vào. Nếu dài quá, ta rút ngắn cho nó có nghĩa tường minh
Sử dụng Ctrl + Shift + F để replace all

Solution



Trong hình thì ta tách các phần ra thành 3 project như đã nêu. Thứ tự add reference thì
Model thêm vào View Model
View Model thêm vào View.

1. Models

Ta làm nhanh các việc sau:
  • Create new project class library
  • Add new 2 class: Department, Employee
  • Add new Mvvm base (Bạn chỉ cần copy y chang trong source code)
  • Add new Repository (Sau này dùng CRUD với database thông qua Entity Framework)
Đọc thêm:
Action Func là delegate (con trỏ) đến method, có một tham số, nhiều tham số, hoặc không. Nhưng Func trả về giá trị (or reference) còn Action thì không.
Ngoài ra, còn có Predicate  là một kiểu đặc biệt của Func. Bạn có thể tìm hiểu thêm
Tôi implement ICommand, trông gần nhất với UWP mà tôi hay dùng, nếu qua chuyển qua cũng dễ hiểu.
Dù WPF và UWP cũng sử dụng XAML nhưng tính năng vẫn vô số chỗ khác xa nhau. Ví dụ như Binding Command, CommandParameter, EventArgs...

Giờ, tôi giới thiệu cách dùng Command, Binding Command, CommandParameter, Hoặc Pass EventArgs về ViewModel

a.) Class Command

using System;
using System.Diagnostics;
using System.Windows.Input;

namespace Models.Mvvm
{
    /// <summary>
    /// <see cref="ICommand"/> implementation that wraps an <see cref="Action"/> delegate.
    /// </summary>
    public class Command : ICommand
    {
        private readonly Action<object> m_Execute;
        private readonly Func<bool> m_CanExecute;
        public event EventHandler CanExecuteChanged
        {
            /* Must add reference PresentationCore from Framework*/
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }

        public Command(Action<object> execute)
            : this(execute, () => true)
        { /* empty */ }

        public Command(Action<object> execute, Func<bool> canexecute)
        {
            if (execute == null)
                throw new ArgumentNullException("execute");
            m_Execute = execute;
            m_CanExecute = canexecute;
        }

        [DebuggerStepThrough]
        public bool CanExecute(object p)
        {
            try
            {
                return m_CanExecute == null ? true : m_CanExecute();
            }
            catch
            {
                Debugger.Break();
                return false;
            }
        }

        public void Execute(object p)
        {
            if (CanExecute(p))
                try
                {
                    m_Execute(p);
                }
                catch { Debugger.Break(); }
        }
    }
}

b.) Class BindableBase

using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace Models.Mvvm
{
    public abstract class BindableBase : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        public void RaisePropertyChanged([CallerMemberName]string propertyName = null)
        {
            /*Only Support in C# 6.0 in Visual Studio 2015*/
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));

            /*ELSE Verison*/
            //if (PropertyChanged!=null)
            //{
            //    PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            //}
        }

        public void Set<T>(ref T storage, T value, [CallerMemberName()]string propertyName = null)
        {
            if (!Equals(storage, value))
            {
                storage = value;
                RaisePropertyChanged(propertyName);
            }
        }
    }
}

c.) Class ViewModelBase
namespace Models.Mvvm
{
    public abstract class ViewModelBase : BindableBase
    {
        //TODO: Implement Services if needed

        #region Properties

        private bool _IsBusy = default(bool);
        protected virtual bool IsBusy { get { return _IsBusy; } set { Set(ref _IsBusy, value); } }


        private bool _IsChange = default(bool);
        protected virtual bool IsChange { get { return _IsChange; } set { Set(ref _IsChange, value); } }
        #endregion
    }
}

(Tất cả đều có trong source code cuối bài viết).
Note:
Sự rườm rà này, là vì ta sử dụng mô hình MVVM. Bạn nhớ rõ, trong code-behind của Views sẽ không chứa thêm bất kỳ đoạn code xử lý nào (Nếu code vào đó thì phạm luật lý thuyết MVVM, chứ không có vấn đề gì đâu :D). Mọi việc do ViewModel xử lý, tất nhiên phải có sợi dây liên kết giữa 2 bên, đó là DataContext của Views. Bạn phải khai báo DataContext truy xuất tới ViewModel nào.
Lưu ý:
Do project được tách ra nhiều phần nên, DataContext phải khai báo trong code-behind. Tôi không biết vì sao mà XAML nó chưa chịu hiểu khi import DLL ViewModel vào project Views.  
 

  • BindableBase implement INotifyPropertyChanged: Nó sẽ raise khi properties có thay đổi value. Ví dụ khi thuộc tính FullName thay đổi, thông qua binding thì người dùng sẽ thấy thay đổi trên giao diện (UI).  Nói sâu tí, trên ViewModel tôi dùng ObservableCollection để chứa các danh sách dữ liệu, đây là một dynamic data collection nhưng nó chỉ raise cho UI khi add/remove. Vậy khi update ok thì UI không được báo, vì sao thì vì nó chỉ có chức năng đó quan tâm chi mệt. OK để khắc phục, ta implement BindableBase trong mỗi properties của Models sẽ giải quyết.
  • ICommand:  Trên Views ta  gọi phương thức dưới ViewModel. Mà Command hay CommandParameter chỉ thấy trong Button . Để gọi được phương thức, sự kiện dưới ViewModel trong các control khác, trên Views ta tăng cường bằng việc sử dụng Microsoft.Expression.InteractionsSystem.Windows.Interactivity. Hai thư viện này, khi làm việc trên Blend thì nó tự add vào, còn với Visual Studio bạn chịu khó thủ công để tăng cường sức khỏe nhé.  (Add reference | Assemblies | Extensions)
  • ViewModelBase: Class này không có gì đặc biệt, chỉ là hình thức kế thừa, cho clear tí thôi. Thôi bỏ qua nhé.
Figure 1: Add reference

(*) Dùng Blend để design giao diện sẽ nhanh hơn, đỡ mệt hơn.

2. ViewModels

  • Create new project class library
  • Import Model DLL
  • Create class ViewModel cho mỗi form View

Phần này là trọng tâm của bài này. Tất cả bài này bạn chỉ cần hiểu:
  • Update UI khi dữ liệu thay đổi như thế nào?
  • Gọi Command hoặc Method dưới ViewModel như thế nào?
  • Truyền Parameter về lại ViewModel như thế nào?
  • Truyền EventArgs về lại ViewModel như thế nào?

Chỗ truyền parameter khi gọi Command thì nó đơn giản. Trong UWP với CommandParameter bạn có thể truyền tham số của sự kiện luôn, nhưng WPF thì không thể.
Nhưng không sao,  bạn có thể gọi luôn cả Event dưới ViewModel luôn, tha hồ mà handler.

a.) Làm thế nào để Update UI khi dữ liệu thay đổi ?
Trước hết, mỗi form View bạn tạo một class ViewModel cho nó. Cũng suy ra, mỗi View chỉ set DataContext cho một ViewModel (nhiều hơn tôi không biết đâu, bạn search của các đồng nghiệp như đã nói).

Trong ViewModel, tôi implement class ViewModelBase. Vì ta cần dùng INotifyPropertyChanged, tôi học chia nhiều class để code nó clear thôi.

Figure 2.1.1: Implement INotifyPropertyChanged

- Với biến Title, ta sẽ binding nó trên View nhé
Figure 2.1.2: Set Binding in Control

- Mode; OneWay là dữ liệu dưới  ViewModel có thể lên View. TwoWay là View có thể xuống ViewModel. Với Title này là nhãn và không chỉnh sửa sau đó, ta chỉ cần OneWay thôi, xóa mode luôn (mặc định của mode là OneWay).

Figure 2.1.3: Define Collection

- Collection của chúng ta không cần implement INotifyPropertyChanged trong Setter. Như đã nói add/remove nó sẽ raise lên View, nhưng update thì khác nên ta triển khai trong Model như hình dưới đây. Về hiệu năng, tất nhiên càng làm nhiều càng mệt

Figure 2.1.4: Implement INotifyPropertyChanged for Model

- Nó cũng giống mấy properties khác trên ViewModel thôi phải không.

Figure 2.1.5: Set ItemSource for ListView

- Ta binding Collection lên ListView, phần màu xanh là các thuộc tính ở Model chứ không phải trong ViewModel đâu (Trong ViewModel ta khai báo các thuộc tính có tên giống cho dễ hiểu là vì cần binding cho TextBox, để lấy value nhập vào). Và vì Collection chứa object Employee mà ta đã tạo ở Model. Cho nên nếu bạn không implement INotifyPropertyChanged cho Model thì UI không được update khi edit là vậy.

b.) View Gọi Command hoặc Method dưới ViewModel như thế nào?

Figure 2.2.1: Add Command
Figure 2.2.2: Cancel Command

- Implement ICommand ta có 2 kiểu, một là không có parameter, hai là có parameter từ Views trả về. Từ đó cũng có 2 kiểu cú pháp lambada của action cho đúng như bạn có thể thấy ở 2 hình trên (Chú ý: Figure 2.2.1 và 2.2.2).

- Ở CancelCommand, tôi dùng để Close Window khi được gọi. Để Close ta cần handler cho được Window, cho nên tôi truyền qua CommandParameter x:Name của Window đó

Figure 2.2.3: Set Binding Command in Controls
c.) Truyền Parameter về lại ViewModel như thế nào?
- CommandParameter ở Cancel Button chính là Property Name của Window này (Trong Winforms thì  thường gọi là Form, UWP thì gọi là Page, tương tự  WPF tôi gọi là Window).

Figure 2.3: Define Xaml Behavior to call Event

- x:Name dùng để khai  báo property name của Control
- Ở Window, hình trên tôi cũng binding sự kiện Closing dưới ViewModel. Nó sẽ xử lý vài yêu cầu trước khi  Window chính thức Close.

d.) Truyền EventArgs về lại ViewModel như thế nào?
- Cũng chính CallMethodAction dùng để gọi Method bình thường hoặc sự kiện. Với sự kiện thì ta không cần truyền gì về cả, nó gọi và đưa về ViewModel sẵn cho ta rồi nhé.

Figure 2.4: Event Closing

- Bạn có thể handle 2 tham số sender, e của sự kiện Closing này bình thường như code-behind. Tới đây cũng dễ triển khai phải không? :D

3. Views

  • Create WPF Application
  • Create new Window bạn muốn
  • Import ViewModel DLL
  • Set DataContext cho Window

Tôi có nói ở trên, việc add DLL nên khi set DataContext trên View thì XAML không hiểu nên ta set trong code-behind như sau:
using System.Windows;

namespace Business.App.Views
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        private ViewModels.MainWindowViewModel viewModel= new ViewModels.MainWindowViewModel();
        public MainWindow()
        {
            InitializeComponent();
            DataContext = viewModel;
        }
        
    }
}


- Đoạn code bị comment là cách khai báo DataContext trên XAML.

- Phần highlight màu xanh, việc này giúp dữ liệu từ ViewModel hiển thị trên View trong lúc design để nhìn cho trực quan. Không có ý nghĩa khác đâu nhé, bỏ đi cũng không vấn đề gì.

4. Final

Kết quả phần này chỉ gói gọn trong bấy nhiêu đó. Source code nằm cuối bài viết này, bạn có thể tham khảo lúc rảnh rỗi. Kết thúc nhé!



Created: 07/10/2016
Updated: 08/10/2016
Updated: 09/10/2016
Source code: Tải về tại đây (conflict)

Share

Happy Reading!

[WPF] Create Simple WPF App With MVVM - Part I
4/ 5
Oleh

Buzz!

Stay updated via email new newsletter

2 nhận xét

Want:nhận xét

Don't
Use obscene or offensive language.
Personally attack people, their edits, or their comments.
Rant or otherwise harass, abuse, or intimidate others.
Post anything you don't want the world to see. This is a public space.
Infringe copyright.