Phân chia module với Interface và Unity DI – Series Giải pháp winforms (3)

9

Trong phần 2 – Thiết kế giao diện winform – bạn đã thực hiện chia project ra các thành phần. Tuy nhiên, các thành phần này đang phụ thuộc chặt (tight coupling) với nhau. Ví dụ, B gọi là phụ thuộc chặt và A hiểu đơn giản là trong code của B có các lệnh như khai báo A, khởi tạo A, sử dụng phương thức/thuộc tính của A. Sự phụ thuộc chặt này tạo ra trình tự bắt buộc để xây dựng các class. Nếu B phụ thuộc vào A thì A phải xây dựng trước, sau đó mới xây dựng B. Như vậy, A và B không thể xây dựng đồng thời.

Phụ thuộc chặt gây khó khăn cho làm việc nhóm, cũng như việc thay thế thành phần phụ thuộc. Vì vậy, khi phát triển ứng dụng người ta thường cố tạo ra sự phụ thuộc lỏng (loose coupling) giữa các thành phần.

Loose Coupling là một nguyên lý yêu cầu phân chia project ra các thành phần (module) tương đối độc lập giúp bạn dễ dàng thay thế/nâng cấp/bảo trì các thành phần riêng rẽ mà không ảnh hưởng đến những phần còn lại. Nguyên lý này được thực hiện nhờ sử dụng Interface và một DI container nào đó.

Trong bài này, chúng ta tiếp tục đem đến một số cải tiến cho project, bao gồm: (1) phân chia project chính thành các project con để phát triển độc lập; (2) sử dụng interface và Unity DI container để giảm sự phụ thuộc giữa các project con.

Các cải tiến hướng tới giúp các bạn làm việc nhóm, cũng như giúp dễ dàng thay thế các thành phần khi cần thiết.

Loạt bài “Các giải pháp dành cho lập trình winform”:
Phần 1 – Lỗi thường gặp trong lập trình winforms
Phần 2 – Thiết kế giao diện với Data Sources và BindingSource
Phần 3 – Phân chia code thành module sử dụng Interface
Phần 4 – Sử dụng thư viện DevExpress cho winforms
Phần 5 – Sử dụng Data Binding
Phần 6 – Sử dụng Entity Framework

Tạo các project con

Chúng ta bắt đầu ngay bằng phần thực hành. Việc phân tích sẽ để đến cuối bài.

Project ViewModels

Chúng ta có thể thấy, lớp ContactsViewModel rất độc lập. Nó phụ thuộc duy nhất vào các lớp models mà không có liên hệ gì với các form. Class này (cùng các class view model khác) nên được đưa vào một project riêng. Các class kiểu này có thể phát triển ngay sau khi có models và phát triển song song với view.

Trong phần này chúng ta sẽ tách view model thành một project riêng.

Bước 1. Tạo project ViewModels thuộc loại Class Library (.NET Framework).

Xóa file Class1.cs tự động tạo. Chúng ta không cần đến file này.

Cho ViewModels tham chiếu tới project Models và thư viện System.Windows.Forms (để sử dụng lớp BindingSource).

Bước 2. Cho App tham chiếu tới lớp ViewModels vừa tạo.

Bước 3. Chuyển file ContactsViewModel.cs sang project ViewModels. Việc chuyển file này chỉ đơn giản là kéo file ContactsViewModel.cs từ App sang ViewModels (trong cửa sổ Solution Explorer).

Mở file này ra và chỉnh namespace thành ViewModels (namespace cũ là App.ViewModels).

Bước 4. Xóa bỏ thư mục ViewModels của App.

Kết quả thu được như sau:

Project ViewModels và App sau khi điều chỉnh
App tham chiếu tới Model và ViewModels. ViewModels tham chiếu tới Models và System.Windows.Forms

Trong một đề tài mới, các bạn có thể xây dựng project ViewModels ngay từ đầu.

Project Interfaces

Project ViewModels độc lập với App, nhưng các form của App lại phụ thuộc chặt vào ViewModels. Chúng ta tiến thêm một bước nữa để làm cho App độc lập khỏi ViewModels giúp xây dựng các form (bao gồm cả code) mà không cần ngay ViewModels.

Bước 1. Tạo project Interfaces thuộc kiểu Class Library (.NET Framework). Tham chiếu project này tới thư viện System.Windows.Forms (để sử dụng được lớp BindingSource).

Bước 2. Cho ViewModels và App cùng tham chiếu tới Interfaces.

Bước 3. Tạo inteface IContactsViewModel trong project Interfaces và code như sau:

using System.ComponentModel;
using System.Windows.Forms;
namespace ViewModels
{
    public interface IContactsViewModel
    {
        BindingSource ContactBindingSource { get; set; }
        BindingSource EmailBindingSource { get; set; }
        BindingSource PhoneBindingSource { get; set; }
        string Title { get; }
        event PropertyChangedEventHandler PropertyChanged;
        void Delete();
        void DeleteEmail();
        void DeletePhone();
        void First();
        void Initialize();
        void Last();
        void Load();
        void New();
        void NewEmail();
        void NewPhone();
        void Next();
        void Previous();
        void Save();
    }
}
Intefaces project và IContactsViewModel
Intefaces project và IContactsViewModel

Bạn có thể sử dụng Quick Action “Extract interface” của Visual studio để nhanh chóng tạo ra một interface từ một class.

Bước 4. Điều chỉnh code của ContactsViewModel như sau

using Interfaces;
namespace ViewModels
{
    public class ContactsViewModel : INotifyPropertyChanged, IContactsViewModel
    {

Cài đặt Unity container

Unity (đừng nhầm lẫn với Unity game engine) là một IoC container của Microsoft. Nó giúp tự động hóa việc khởi tạo object. Nói cách khác, dùng Unity IoC container, bạn không cần phải dùng cấu trúc khởi tạo object bình thường của C# nữa. Chúng ta sẽ phân tích lợi ích của Unity IoC container sau.

Bước 1. Tìm kiếm và cài đặt Unity từ NuGet. Nếu bạn chưa biết dùng NuGet, hãy đọc bài viết này.

Bước 2. Mở file Program.cs của App và điều chỉnh code như sau:

using System;
using System.Windows.Forms;
using Unity;
using ViewModels;
using Interfaces;
namespace App
{
    static class Config
    {
        public static UnityContainer Container { get; private set; }  = new UnityContainer();
        public static void Register()
        {
            Container.RegisterType<IContactsViewModel, ContactsViewModel>();
        }
    }
    static class Program
    {
        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        [STAThread]
        static void Main()
        {
            Config.Register();
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new Contacts());
        }
    }
}

Bước 3. Điều chỉnh code behind của Contacts form

using System.Windows.Forms;
using Unity;
using Interfaces;
namespace App
{
    public partial class Contacts : Form
    {
        private IContactsViewModel _vm = Config.Container.Resolve<IContactsViewModel>();
        public Contacts()
        {

Dịch và chạy thử chương trình. Kết quả vẫn sẽ giống như bài trước!!!

Ô hay! Mất bao nhiêu công chỉnh sửa để vẫn thu được kết quả như trước thì cải tiến làm gì?

Hiệu quả của giải pháp thực hiện ở trên là gì?

Thay thế các thành phần khi cần thiết

Giả sử bạn cần thay một thiết kế khác cho form. Không vấn đề gì. Hãy thiết kế giao diện khác đi. Nó có phụ thuộc gì vào view model đâu. Bạn đâu cần biết đến view model nào khi thiết kế form. Cái bạn sử dụng trong code behind là một interface đấy chứ. Sau khi hoàn thành thiết kế mới, trong Program.cs, phương thức Register của lớp static Config, bạn thay tên class cụ thể mà IoC container cần khởi tạo là được.

Nếu bạn cảm thấy view model không hợp lý. Không sao. Hãy tạo một view model khác và cho nó implement cái interface tương ứng là được. Trong phương thức Register bạn đổi sang view model khác là xong.

Như vậy, hiệu quả của giải pháp tách rời view và view model như đã làm ở trên nằm ở chỗ, chúng ta có thể dễ dàng thay thế các thành phần khi cần thiết mà không ảnh hưởng gì đến code cũ.

Phát triển các thành phần song song

Nếu bạn làm việc một mình các giải pháp trên quả thực hơi lãng phí công sức. Giả sử bạn làm project theo team, độ 3 người chẳng hạn. Sau khi hoàn thành phân tích nghiệp vụ, nhóm bạn có thể đưa ra thiết kế models (các lớp thực thể). Việc code cho model rất nhanh. Sau đó thì sao?

Nếu view (form) phụ thuộc vào view model, bạn phải xây dựng view model trước, sau đó với thiết kế + code form. Giờ nhóm làm view và nhóm làm view model đưa ra một thỏa thuận về những phương thức + thuộc tính nào hai bên cùng phải sử dụng. Thỏa thuận này thể hiện bằng một (nhóm) interface. Một khi có interface, hai nhóm hoàn toàn hoạt động độc lập được với nhau.

Interface, loose coupling, dependency inversion, inversion of control, dependency injection, IoC container

Những gì chúng ta đã làm ở trên là cách vận dụng sơ đẳng của một loạt kỹ thuật và nguyên lý lằng nhằng này. Đây là những yêu cầu cơ bản nhất khi phát triển ứng dụng enterprise.

Loose coupling là một nguyên lý, hướng tới sự quan hệ lỏng giữa các thành phần. Mục đích của nó là giúp thay thế các thành phần khi cần thiết mà không ảnh hưởng đến các thành phần còn lại. Trong lập trình hướng đối tượng, loose coupling có thể thực hiện thông qua interface.

Dependency inversion là nguyên lý cuối trong bộ nguyên lý SOLID. Trong đó, các module (class) cấp cao không nên phụ thuộc vào các module cấp thấp. Các class nên tương tác với nhau qua interface chứ không qua implementation.

Inversion of control (IoC) trong lập trình có thể xem là một cách để thực thi nguyên lý Dependency Inversion ở trên (bên cạnh những nguyên lý khác). Dependency Injection là một cách cụ thể để thực hiện IoC. IoC container là một dạng thư viện/framework cụ thể cho IoC và DI.

Nói chung đây là một nhóm nguyên lý / kỹ thuật rất khó tiêu với đa số sinh viên. Ngay cả với lập trình viên mới vào nghề cũng vậy. Không hi vọng gì ở một bài viết con con này sẽ giải thích được đầy đủ. Chúng tôi đưa ra chỉ để bạn biết tới chúng. Chúng có thể được áp dụng gần như trong mọi loại công nghệ phát triển ứng dụng.

Áp dụng: Gọi Detail từ Contacts

Form Detail chúng ta đã tạo ra từ bài trước nhưng chưa sử dụng tới. Ý tưởng là khi người dùng double click vào một dòng contact thì sẽ bật form này ra. Trên form này sẽ là thông tin chi tiết của riêng contact đó. Chúng ta lại tiếp tục vận dụng nguyên lý loose coupling để các form độc lập nhau.

Bước 1. Tạo interface IDetailView trong project Interfaces và code như sau:

using System.Windows.Forms;
namespace Interfaces
{
    public interface IDetailView
    {
        void ShowModal();
        BindingSource BindingSource { get; set; }
    }
}

Bước 2. Bổ sung mô tả phương thức sau vào IContactsViewModel

void ShowDetail(IDetailView detail);

Bước 3. Bổ sung phương thức sau vào lớp ContactsViewModel (thực thi phương thức ShowDetail ở trên)

public void ShowDetail(IDetailView detail)
{
    detail.BindingSource.DataSource = ContactBindingSource.Current;
    detail.ShowModal();
}

Bước 4. Bổ sung dòng lệnh sau vào phương thức Register (lớp Config, file Program.cs của App)

Container.RegisterType<IDetailView, Detail>();

Bước 5. Bổ sung dòng lệnh sau vào cuối hàm tạo của Contacts

contactDataGridView.MouseDoubleClick += delegate { _vm.ShowDetail(Config.Container.Resolve<IDetailView>()); };

Bước 6. Thiết kế form Detail theo kỹ thuật như đã biết ở bài trước.

Thiết kế form Detail

Các bạn có thể để ý, ngoại trừ hai interface phải chỉnh sửa đầu tiên, các bước còn lại có thể làm theo trật tự tùy ý.

Dịch và chạy thử chương trình. Double click vào mỗi dòng sẽ hiện ra form detail của contact đó.

Detail của mỗi contact trên form riêng

Có lẽ các bạn lại thắc mắc, tại sao chỉ là mở một form lên mà phải làm rắc rối thế. Vấn đề giống như đã giải thích ở bên trên: chúng ta không cho các form phụ thuộc vào nhau để có thể dễ dàng thay đổi thiết kế form này bằng form khác. Trong bài viết tiếp theo chúng ta sẽ sử dụng bộ control của Devexpress bạn sẽ thấy hiệu quả của nó.

Kết phần

Trong phần này chúng ta đã thực hiện những thay đổi lớn để giúp tạo ra các thành phần tương đối độc lập cho ứng dụng. Những cải tiến này giúp bạn làm việc nhóm, dễ thay đổi các thành phần khi cần thiết. Tuy vậy, đây lại là một nội dung tương đối khó nhằn về mặt kỹ thuật cũng như ý tưởng, mặc dù nhìn code thì có vẻ không phức tạp.

Trong phần tiếp theo chúng ta sẽ thay đổi thiết kế form bằng cách sử dụng bộ thư viện của Devexpress. Khi đó bạn sẽ thấy rõ hơn tác dụng của những thay đổi trong bài này.

[wpdm_package id=’10295′]

+ Nếu bạn thấy site hữu ích, trước khi rời đi hãy giúp đỡ site bằng một hành động nhỏ để site có thể phát triển và phục vụ bạn tốt hơn.
+ Nếu bạn thấy bài viết hữu ích, hãy giúp chia sẻ tới mọi người.
+ Nếu có thắc mắc hoặc cần trao đổi thêm, mời bạn viết trong phần thảo luận cuối trang.
Cảm ơn bạn!

Loạt bài “Các giải pháp dành cho lập trình winform”:
Phần 1 – Lỗi thường gặp trong lập trình winforms
Phần 2 – Thiết kế giao diện với Data Sources và BindingSource
Phần 3 – Phân chia code thành module sử dụng Interface
Phần 4 – Sử dụng thư viện DevExpress cho winforms
Phần 5 – Sử dụng Data Binding
Phần 6 – Sử dụng Entity Framework

Theo dõi
Thông báo của
guest

9 Thảo luận
Cũ nhất
Mới nhất
Phản hồi nội tuyến
Xem tất cả bình luận
caokhoa

Chào ban quản trị.
trong bài có mục này “Cài đặt Unity container
Unity (đừng nhầm lẫn với Unity game engine) là một IoC container của Microsoft. Nó giúp tự động hóa việc khởi tạo object. Nói cách khác, dùng Unity IoC container, bạn không cần phải dùng cấu trúc khởi tạo object bình thường của C# nữa. Chúng ta sẽ phân tích lợi ích của Unity IoC container sau.”
Rất mong ban quản trị làm rõ nếu không dùng Unity thì việc khởi tạo object sẽ như thế nào ?.
Cám ơn nhiều!

Mai Chi

Chào bạn. Trong bài này chúng ta sử dụng Unity DI để tự động khởi tạo object của ContactsViewModel ở những nơi cần dùng. Chỗ đầu tiên bạn cần dùng object của ContactsViewModel chính là form Contacts. Nếu dùng Unity thì lệnh khởi tạo là private IContactsViewModel _vm = Config.Container.Resolve(); Nếu không muốn dùng Unity thì bạn khởi tạo bình thường: private IContactsViewModel _vm = new ContactsViewModel(); Tuy nhiên, nếu sử dụng cách khởi tạo thông thường, bạn làm cho Contacts form phụ thuộc chặt vào ContactsViewModel. Khi này nó làm mất ý nghĩa của việc sử dụng Unity DI… Đọc tiếp »

Hoang Trong Tin

Chào Admins!
Ở phần gọi Detail từ Contacts, Bước 4: Container.RegisterType<IDetailView, Details>(); mình gặp lỗi:
Error CS0311 The type ‘App.Details’ cannot be used as type parameter ‘TTo’ in the generic type or method ‘UnityContainerExtensions.RegisterType<TFrom, TTo>(IUnityContainer, params InjectionMember[])’. There is no implicit reference conversion from ‘App.Details’ to ‘Interfaces.IDetailView’.
Mong được sự giúp đỡ từ các Admin. Mình cảm ơn!

Nhật Linh

Bạn kiểm tra lại xem form Detail có thực thi giao diện IDetailView không nhé. Nghĩa là khi định nghĩa form Detail phải là

public partial class Detail : Form, Interfaces.IDetailView
{
 ...
}
trung kiên

mình cũng bị lỗi y như trên nhưng làm theo cách của bạn thì nó báo sai lỗi cú pháp

Minh Hoang
public partial class Detail : Form,Interfaces.IDetailView
    {
        public BindingSource BindingSource { get; set; }
        public Detail()
        {
           
            InitializeComponent();
            BindingSource = contactBindingSource;
        }
        public void ShowModal()
        {
            this.Show();
        }
    }

Bạn cần định nghĩa hàm ShowModal và khai báo BindingSource

Lam phat

Mình làm như vậy nhưng gặp lỗi Stack overlow tại dòng lệnh This.Show()

TaiDC

Chào ban quản trị.
Đầu tiên, mình cực kỳ thích Series này của bạn vì nó cực kỳ chi tiết và dễ thực hành.Và mình có 1 thắc mắc là nếu trên DetailView mình muốn thêm 1 nút Lưu để lưu dữ liệu trực tiếp thì mình sẽ phải làm thế nào?
Theo như những gì mình học được thì sẽ phải thêm 1 file DetailViewModel nhưng mình không biết liên kết nó và gọi nó từ ContactViewModel như thế nào.

Xin cảm ơn!

hieuvu

Khi mình chạy chương trình thì bị lỗi này. Unity.ResolutionFailedException: ‘Resolution failed with error: No public constructor is available for type Interfaces.IContactViewModels do không khởi tạo được object _vm trong form Contact. Admin kiểm tra giúp mình với ạ