I. Introduction

Mục đích của bài viết này nhằm giới thiệu về MVVM patern (Model – View – ViewModel). Những nội dung trong bài này được tổng hợp lại từ 2 bài viết trên codeproject (xem thêm tại phần tham khảo). Nói sơ qua một chút về pattern này, MVVM thường được sử dụng trong lập trình các ứng dụng WPF và Silverlight, nó cho phép chúng ta dễ dàng thay đổi giao diện (GUI) của ứng dụng mà không cần phải thay đổi code quá nhiều. Việc triển khai MVVM cho project thực sự đơn giản hơn nhiều so với những gì mà người ta tưởng tưởng về nó.

II. Background

Tại sao bạn nên sử dụng Model – View – ViewModel pattern? Thực tế việc sử dụng pattern này trong quá trình phát triển các ứng dụng WPF hoặc Silverlight mang lại cho developer nhiều lợi ích, có thể kể đến như sự tương tác hiệu quả giữa designer và developer, khả năng sử dụng lại các component hay việc thay đổi giao diện chương trình mà không cần thiết phải viết lại code quá nhiều…

Có một vài đánh giá sai lầm về MVVM đại loại như: “MVVM chỉ thích hợp với các project có giao diện phức tạp”, hay “MVVM sẽ làm phình to kích thước cũng như chi phí đối với các ứng dụng nhỏ”. Và một nhận xét nữa về đó là: “MVVM không cân bằng (scale)”. Theo ý kiến chủ quan thì những nhận xét như thế này chỉ phù hợp khi nói đến cách thức cài đặt cũng như kiến thức về MVVM của developer chứ không phải bản thân của MVVM pattern. Nói cách khác, nếu bạn mất hàng giờ đồng hồ để triển khai MVVM, điều đó có nghĩa là bạn đang thực hiện sai phương pháp.

III. MVVM

MVVM là viết tắt của Model –View – ViewModel. Hình ảnh dưới đây sẽ mô tả về mối quan hệ giữa các thành phần trong pattern này.

        1. Model

Trong thế giới lập trình, model đại diện cho các dữ liệu, thông tin mà chúng ta cần thao tác. Một ví dụ về model đó là contact (thông tin liên lạc), nó sẽ bao gồm tên, số điện thoại, địa chỉ, … Đơn vị của Model chính là Class. Như vậy xét ví dụ về model contact, chúng ta sẽ có class Contact, hay một ví dụ khác như class Customer dùng để lưu trữ thông tin về khách hàng, class Supplier lưu trữ các thông tin về các nhà cung cấp… Các class trong Model của MVVM đều giống như các class bình thường khác, chúng đều có một số các phương thức, properties… hay khả năng tự động lưu dữ liệu của nó và cơ sở dữ liệu.

Tuy nhiên cần phải lưu ý đó là model chỉ lưu giữ thông tin mà thôi, nó không quan tâm đến các hoạt động hay dịch vụ có thể thay đổi, điều khiển các thông tin đó. Ví dụ như nó không có trách nhiệm phải định dạng đoạn văn bản hiển thị như thế nào, hay làm sao để lấy một danh sách các item về từ remote server.

     2. ViewModel

ViewModel là class định nghĩa cách dữ liệu tương tác với người dùng thông qua view. Nói cách khác ViewModel là model của View.

Một lưu ý quan trọng đó là ViewModel không mô tả giao diện sẽ trông như thế nào. Nó chỉ mô tả cách mà view hoạt động và thông tin nào sẽ được cung cấp cho người dùng.

Vấn đề cần bàn luận ở đây là, liệu ViewModel sẽ ảnh hưởng như thế nào đến phần hiển thị của View, ví dụ như việc xác định nội dung của một label, bạn sẽ sử dụng View hay ViewModel? Theo tôi, điều này tùy thuộc hoàn toàn vào đối tượng cũng như dự án mà bạn đang làm. Đôi khi label đó sẽ có nội dung do ViewModel quy định (bởi vì bạn cần thay đổi nội dung của label tùy theo sự kiện xảy ra), hay có khi designer sẽ quyết định label sẽ hiển thị như thế nào. Nói chung, nếu nó không phụ thuộc vào cơ sở dữ liệu thì nó sẽ thuộc về thẩm quyền của designer – tức là được định nghĩa trong View chứ không phải ViewModel.

Một ví dụ cụ thể về ViewModel như sau: giả sử boss của bạn yêu cầu rằng “Chúng ta cần phải hiển thị các thông tin chi tiết của người dùng ra bên ngoài, đồng thời cho phép họ thay đổi các thông tin đó”.

Như các bạn thấy, ông chủ của chúng ta không hề quy định bất cứ chi tiết nào về việc tên của người dùng phải được viết bằng font Tahoma Bold, phải sử dụng listbox hay combobox để hiển thị danh sách các user. Yêu cầu trên chính là định nghĩa về chức năng cũng như các dữ liệu cần thiết cho class CustomerEditViewModel. Phân tích kĩ hơn, dữ liệu yêu cầu ở đây chính là Customer và chức năng của nó là khả năng hiển thị và thay đổi nội dung các thuộc tính của Customer class.

Tuy nhiên, chúng ta không muốn View lại biết và thao tác trực tiếp với các thông tin của Model (trong trường hợp này là class Customer). Do đó, bạn có thể xem ViewModel như là một thông dịch viên – nó dùng một đối tượng Customer và thực hiện việc chuyển đổi từ dữ liệu trong class này để hiển thị chính xác trong View.

     3. View

View là thành phần duy nhất mà người dùng có thể tương tác được trong chương trình, nó chính là thành phần mô tả dữ liệu. Trong WPF, view là một UserControl, lưu ý mặc dù View chính là UserControl nhưng không nhất thiết UserControl phải là View.

Vậy làm thế nào để phân biệt được? Rất đơn giản. Ban đầu bạn khởi tạo class ViewModel trước, như vậy bạn đã có ViewModel để quản lí các chức năng cần thiết, việc cuối cùng là tạo class View nhằm cho phép người dùng sử dụng các chức năng đó. Nếu một tính năng hiển thị nào đó nằm trên view và là một phần của ViewModel đã được khai báo trước đó, nhưng lại không có ViewModel của riêng nó thì đó đơn thuần chỉ là một UserControl.

Ví dụ, trong class CustomerEditViewModel mô tả ở trên, địa chỉ của khách hàng sẽ được hiển thị và có thể thay đổi được bởi người dùng. Ta có thể đưa việc hiển thị và thay đổi nội dung vào trong cùng một UserControl duy nhất, tuy nhiên UserControl này sẽ sử dụng data source từ class CustomerEditViewModel chứ không phải là class CustomerAddressViewModel hay AddressViewModel.

Có một nhận định sai lầm trong MVVM đó là developer không nên viết thêm bất kì đoạn code nào trong phần code behind của View (file view.xaml.cs). Tuy nhiên bạn cần hiểu chính xác về vấn đề này đó là developer nên tránh viết vào file code behind của view những đoạn code hoàn toàn không liên quan hay ảnh hưởng đến giao diện của chương trình.

Mặc dù không nên nhưng đôi khi nếu việc viết code behind tiện lợi hơn so với việc viết mã XAML thì sử dụng C# cũng không phải là ý kiến tồi. Ví dụ bạn muốn bind Command property của Button vào một đối tượng ICommand trong ViewModel của View, bạn có thể thực hiện điều này bằng XAML rất đơn giản và ngắn gọn. Tuy nhiên nếu bạn muốn xử lí sự kiện MouseOver của Button này thì sao? Chắc chắn bạn cũng có thể thực hiện được với XAML với các Storyboard, behaviours… tuy nhiên tại sao lại không viết code trong code-behind để xử lí sự kiện này bằng cách gọi Command tương ứng trong ViewModel?

     4. Kết nối các thành phần trong MVVM

Ý tưởng chính của MVVM đó là chúng ta sẽ tạo mẫu cho phần View (model the views), và giữ cho các View class tách biệt khỏi ViewModel. Về lí thuyết, việc này cho phép chúng ta thay đổi giao diện của ứng dụng mà chỉ cần viết lại phần View, các tính năng của View được định nghĩa trong ViewModel sẽ vẫn tiếp tục được giữ lại như trước. Chính vì lẽ đó nên View cần phải biết rõ về ViewModel của nó, tuy nhiên ViewModel ‘hầu như’ không nên biết về View.

Với việc sử dụng MVVM pattern, bạn có thể giao phần View lại cho designer thực hiện công việc của họ. Họ có thể thay đổi giao diện như thế nào tùy thích (tất nhiên là vẫn phải theo một quy định nào đó) trong khi data source và các tính năng tương tác giữa giao diện với người dùng trong ViewModel vẫn giữ nguyên như cũ và hoạt động một cách chính xác.

Ở đây chúng ta sử dụng WPF do đó thứ cụ thể mà chúng ta cần phải quan tâm chính là DataContext của phần View. Nói rõ hơn, mỗi element nằm trong View đều được kết nối tới các properties trong ViewModel bằng data binding. ViewModel sẽ lại lấy dữ liệu từ Model (chính là class Customer) và liên kết các properties có thể được thay đổi bởi user vào các Observable properties.

Observable properties là các properties của ViewModel, chúng có thể tự động thông báo cho giao diện của chương trình biết mỗi khi giá trị của nó thay đổi bằng cách cài đặt INotifyPropertyChanged interface.

     5. Điều khiển hoạt động của Application

Trong hầu hết các ứng dụng WPF sẽ có một cửa sổ đầu tiên được khởi tạo gọi là MainWindow – và hầu hết các ứng dụng MVVM xem nó như là một View và tạo ra một class ViewModel tương ứng gọi là MainWindowViewModel.

Trong mô hình MVVM mà tác giả của bài viết này xây dựng có sử dụng một “thứ” gọi là Controller. Nó là một class bình thường (không liên quan gì đến giao diện) có nhiệm vụ điều khiển hoạt động của ứng dụng. Đối với các ứng dụng lớn, có thể có nhiều Controllers với một vài chức năng cơ bản. Tuy nhiên không phải mỗi ViewModel sẽ phải có một Controller tương ứng.

Có thể gói gọn các nhiệm vụ chính của Controller đó là:

  • Khởi tạo View cùng với ViewModel tương ứng
  • Gởi các yêu cầu dữ liệu
  • Quản lí việc cập nhật dữ liệu
  • Quản lí hoạt động của View phù hợp theo từng thời điểm sự kiện xảy ra.

Như vậy, khi ứng dụng được khởi chạy, một đối tượng Controller được khởi tạo. Nó sẽ truyền tham chiếu của chính nó vào mọi ViewModel, cho phép các ViewModel đó sử dụng các chức năng được cung cấp bởi Controller.

Phần ViewController tạo ra cũng có thể có nhiều view con (child view) khác và được định vị ở lúc thiết kế – trong trường hợp này ViewModel cũng sẽ cần phải tạo ra các ViewModel con tương ứng.

Như vậy, Controller có thể vừa có khả năng đẩy dữ liệu đến ViewModel, hoặc ViewModel cũng có khả năng yêu cầu dữ liệu từ Controller. Việc này tùy thuộc vào sở thích và suy nghĩ của mỗi người.

Đi sâu hơn vào ví dụ về yêu cầu của ông chủ được trình bày ở trên, nội dung yêu cầu bây giờ được mô tả chi tiết hơn: “Hiển thị một danh sách khách hàng cho người dùng và cho phép họ được phép sửa đổi thông tin đó. Khi một item được lựa chọn, họ có thể thay đổi và sau đó lưu lại chúng”.

Công việc của Controller lúc này là:

  • Khởi tạo một đối tượng CustomerSelectionViewModel
  • Cung cấp một danh sách các đối tượng Customer để CustomerSelectionViewMode có thể sử dụng và thao tác trên đó
  • Khởi tạo đối tượng CustomerSelectionView và gán giá trị thuộc tính DataContext của nó là CustomerSelectionViewModel
  • Hiển thị CustomerSelectionView
  • Và chờ đợi…

Bây giờ, khi người dùng lựa chọn một customer từ trong danh sách được View hiển thị, nó sẽ send một command đến ViewModel. ViewModel lại báo với Controller rằng vừa có một đối tượng Customer được select (và tất nhiên là phải biết chính xác đó là đối tượng nào). Lúc này, Controller sẽ:

  • Lấy dữ liệu cho đối tượng Customer được yêu cầu (trong trường hợp chưa lấy toàn bộ thông tin)
  • Khởi tạo CustomerEditViewModel và truyền cho nó thông tin về đối tượng Customer được chọn.
  • Khởi tạo CustomerEditView và gán DataContext của nó cho CustomerEditViewModel
  • Hiển thị CustomerEditView
  • Và chờ đợi…

Lúc này, khi người dùng tiến hành việc chỉnh sửa dữ liệu trong View và click vào nút save, một command có nội dung ‘Save’ sẽ được gởi đến ViewModel. ViewModel yêu cầu Controller lưu lại thông tin Customer đó. Và công việc của Controller lúc này sẽ là

  • Lưu dữ liệu
  • Gởi Message thông báo thông tin về customer đó đã được lưu lại thành công

     6. Data vs Function

ViewModel trong hầu hết các ví dụ về MVVM đều có cả 2 phần đó là functionality (xử lí các Command, truy xuất và cập nhật dữ liệu) và Data (các observable properties).

Ở đây, tác giả của muốn giới thiệu một class mới – ViewData.

Một class ViewModel sẽ có một property kiểu ViewData. ViewData này chính là nơi chứa tất cả các Observable Properties được liên kết ở View.

ViewModel lúc này càng đúng nghĩa là một model của View hơn, nó miêu tả yêu cầu của boss đưa ra khá trọn vẹn: “Hiển thị một list chứa các đối tượng Customer cho người dùng và cho phép họ được phép sửa đổi thông tin đó. Khi một item được lựa chọn, họ có thể thay đổi và sau đó lưu lại chúng”. List các Customer chính là ViewData, ViewModel chỉ xử lí việc lấy danh sách và hành động khi người dùng lựa chọn một item trong danh sách đó (chính là phần functionality). Khi chúng ta sửa đổi đối tượng Customer, CustomerEditViewData sẽ có tất cả các properties của các “editable field” của một customer object, trong khi đó CustomerEditViewModel sẽ xử lí các tiến trình xảy ra tiếp theo (khi người dùng click vào nút Save chẳng hạn).

Từ đây chúng ta giải quyết được vấn đề đặt ra từ đầu – khi nào thì ViewModel nên khai báo các properties hướng giao diện (GUI oriented) hay hướng dữ liệu (Data oriented). Với ViewData, chúng ta đã phân tách ra thành 2 nội dung riêng biệt khác nhau. Đối tượng ViewData sẽ chứa các thông tin xoay quanh vào dữ liệu (data-centric). Nếu chúng ta muốn đưa data hiển thị ở View thì đó là view-centric, tuy nhiên ở đây bạn sẽ không muốn có sự nhập nhằn giữa 2 mảng này với sự chen giữa của Convertors. Ví dụ để hiểu rõ hơn về tình trạng này:

Chúng ta có đối tượng CustomerViewData dùng để liên kết các properties của Customer Model đến các Observable properties để View có thể hiển thị ra cho người dùng – ví dụ như CustomerFirstName, CustomerSurname, CustomerAddressLine1, …

Chúng ta cũng có một đối tượng CustomerDisplayViewModel chứa thể hiện của CustomerViewData.

Tất cả chi tiết ở trên đều hợp lí, tuy nhiên designer thông báo rằng họ không thể định dạng kiểu hiển thị của Address sao cho đẹp được bởi vì họ không biết làm cách nào để xử lí dòng trắng của chuỗi lưu trữ thông tin về địa chỉ (giả sử tình huống là như vậy) kể cả khi sử dụng Converter.

Giải pháp của chúng ta là thêm vào một observable property trong ViewModelstring FormattedAddress. Như vậy các designer có thể bind trực tiếp property này vào element nào đó trong view, đồng thời vẫn đảm bảo sự phân biệt nội dung với ViewData.

     7. Window

Theo quan điểm của tác giả, mỗi một View là một chức năng độc lập nằm trong chức năng hiển thị mà ứng dụng đó cần có. Nó có thể là một cửa sổ Window, nó có thể nằm trong window và được định vị ở nơi nào đó trên panel, hay cũng có thể là một dialog, có thể được load ở run time hay được khai báo sẵn trong khâu thiết kế… Như vậy, chúng ta có thể thiết kế phần View cho thao tác lựa chọn một Customer object từ danh sách các Customer dưới dạng một Window độc lập. ViewModel của nó sẽ lấy dữ liệu từ Customer Model để view hiển thị, đồng thời xử lí các command khi người dùng lựa chọn một item nào đó trong danh sách để edit.

Giả sử bây giờ, ông chủ lại đến và nói rằng: “Chúng ta cần tạo một giao diện cho phép việc Edit thông tin cũng như xem thông tin của Customer trên cùng một cửa sổ”. Lúc này bạn chỉ cần đơn giản chuyển View của CustomerEditView thành UserControl thay vì Window như trước và đặt UserControl này vào một Window khác.

Tất nhiên, khi ông chủ thấy được sự thay đổi này, ông ta sẽ nghĩ rằng bạn đã phải thay đổi rất nhiều.

Ở đây chúng ta nên thiết kế mọi view đều là UserControl với ViewModel của riêng nó. Và Controller sẽ cần phải biết được tại thời điểm nào thì view nào nên được hiển thị dưới dạng một cửa sổ window hay phải được đặt trong một container nào đó trong cùng một window với các view khác.

Như vậy quay trở lại ví dụ lúc nãy, chúng ta thiết kế phần view cho việc hiển thị danh sách customer dưới dạng UserControl thay vì Window. Controller sẽ hiển thị nó trong một window; khi xử lí sự kiện ‘Customer Selected’, nó sẽ khởi tạo CustomerEditView và hiển thị view này trong cùng window đó. Tất cả đều do Controller xử lí. Và nếu “the Boss” lại đổi ý thêm lần nữa, ta chỉ việc thay đổi Controller tương ứng theo yêu cầu.

Tuy nhiên, một câu hỏi đặt ra là làm sao Controller có thể hiển thị View trong một Window?

Giải pháp ở đây đó là tạo ra một base View class, từ đó các View class khác sẽ kế thừa từ nó. Nội dung của base View class này sẽ chứa các phương thức cần thiết để có thể hiển thị chính nó trong một container có sẵn, hoặc trong một cửa sổ mới… Như vậy, chúng ta có thể hiểu đơn giản tình huống xảy ra lúc này là controller nói với view rằng “Tôi muốn anh tự hiển thị trong một cửa sổ mới”, sau đó View sẽ tự động lo nốt phần việc còn lại và controller không cần phải biết view sẽ thực hiện như thế nào.

     8. Giao tiếp giữa các ViewModel

Tất nhiên sẽ có lúc bạn gặp phải trường hợp có nhiều hơn 1 ViewModel trong project của bạn, và mỗi khi thay đổi nội dung trong ViewModel này thì ViewModel cũng phải thay đổi tương ứng. Trong ví dụ về Selection/Edit Customer mà nãy giờ chúng ta quan tâm, Selection cần phải được refresh mỗi khi dữ liệu được lưu lại sau khi Edit xong.

Trong trường hợp này, nhiều kiểu mô hình MVVM sử dụng giải pháp là tạo ra class Messenger hoặc Mediator. Nó cho phép các ViewModel yêu cầu được thông báo mỗi khi có một thông điệp được gởi từ một ViewModel khác nào đó, và tất nhiên cũng cho phép các ViewModel được gởi thông điệp của chính nó cho các ViewModel còn lại.

Quay trở lại với ví dụ của chúng ta, CustomerSelection có thể yêu cầu được thông báo mỗi khi CustomerUpdated message được gởi đi và CustomerEdit cũng sẽ có thể gởi thông điệp CustomerUpdated mỗi khi nó update dữ liệu của một Customer object.

Tuy nhiên, theo quan điểm của tác giả bài viết, ViewModel lại không nên có quyền gởi thông điệp. Khi ViewModel muốn thực hiện một chức năng nào đó, nó sẽ yêu cầu Controller thực thi chức năng đó. Controller sau đó lại xử lí việc gởi các thông điệp. Nói một ngắn gọn thì Controller sẽ là nơi thực hiện việc gởi thông điệp chứ không phải là ViewModel

Một ví dụ cụ thể để hiểu rõ hơn về vấn đề này, đối tượng CustomerEditViewModel khi biết được người dùng vừa cập nhật dữ liệu của Customer object, nó sẽ thông báo cho Controller biết rằng ‘Khách hàng 123 vừa mới cập nhật dữ liệu xong’.

Có thể thấy rằng tất cả các hoạt động đều xoay quanh Controller, hình ảnh dưới đây sẽ mô tả rõ hơn vai trò của Controller trong mô hình MVVM.

Reference

     1. MVVM# – Part 1, _Maxxx_

     2. Model-View-ViewModel (MVVM) Explained, Jeremy Likness

Download source code – 66.19 KB