Viết ASP.NET bằng MVP và NHibernate phần cuối - Áp dụng MVP
Posted On Monday, November 17, 2008 at at 5:13 PM by UnknownBài viết này được dịch, tóm tắt và bổ sung dựa vào bài viết NHibernate Best Practices with ASP.NET, 1.2nd Ed trên code Project. Các code sample trong bài dựa vào database Northwind của Microsoft và tham khảo 99,99% từ code mẫu của tác giả Billy McCafferty.
I. Thêm project Presenter với các lớp Presenter và các interface View
- Trong phần 4, chúng ta đã chuẩn bị xong mọi thứ để bắt đầu implement các thành phần web cho project. Chúng ta đã có các lớp Entity để sử dụng, các lớp Dao để truy xuất database và các lớp Factory để tạo ra những Dao object cần thiết. Như vậy chúng ta đã có thành phần Model trong Model – View – Presenter. Trong phần này, chúng ta sẽ hoàn thành các phần còn lại là View và Presenter.
- Khi áp dụng MVP, chúng ta hãy nghĩ rằng các trang web cũng như những winform. Chúng có textbox để ta viết vào, có button để click lên. Khi muốn thể hiện một list các dữ liệu gì đó chúng ta có thể sử dụng GridView cho Web hay ListViewControl cho Windows. Hay nói cách khác, giao diện web hay win là các cách để chúng ta thể hiện dữ liệu lưu trữ trong database. Khi tương tác với các control dù trên web hay trên win, sẽ phát sinh các event như Button_Clicked, SelectedIndex_Changed và ta sẽ implement những gì mình muốn trong các sự kiện này. Một giá trị string trong cơ sở dữ liệu có thể được thể hiện bằng một label trên web, hay một textbox,… Một danh sách các record trong một table nào đó có thể được hiển thị lên Web dựa vào một Repeater hay một GridView, tùy cách ta muốn thể hiện. Vì vậy bước đầu tiên để làm các trang ASPX hay những UserControl, ta phải khai báo các interface View tương ứng trước.
- Interface View trước hết nó là … những Interface gồm những Public Properties và một số những Method . Các Interface này giống như một khuôn mẫu mà khi bạn muốn làm một giao diện web hay win để hiển thị dữ liệu của bạn bạn phải tuân theo. Ví dụ: Chúng ta muốn làm một trang ASPX để hiển thị danh sách tất cả customer ra màn hình, thông tin ta cần ở đây là một List<Customer>. Dù là trên win hay trên web, nếu ta có một List<Customer> có dữ liệu thì ta có thể hiển thị list đó lên màn hình bằng cách sử dụng bất kì control thích hợp nào. Vậy interface IListCustomersView ở đây sẽ được khai báo như sau:
public interface IListCustomersView
{
IList<Customer> Customers { set; }
}
{
IList<Customer> Customers { set; }
}
Code 1: Nội dung interface IListCustomersView cho 1 view là 1 trang ASPX đơn giản
- Các bạn có chú ý tại sao property Customers chỉ được khai báo là set mà không có get không? Tại vì mục tiêu của chúng ta ở đây là thể hiện dữ liệu ra màn hình, nên dữ liệu đọc từ database sẽ được set vào view chứ không cần get cái gì từ view để save xuống database, nên khai báo get ở đây là không cần thiết.
- Vậy làm ra cái interface này để làm gì?
- Xin thưa rằng trang aspx để hiển thị list các customer sẽ implement interface này.
- Vậy implement interface này để làm gì?
- Để lớp ListCustomersPresenter sẽ tương tác với trang aspx thông qua một interface thông thường thay vì thông qua lớp ListCustomer (một lớp kết thừa từ System.UI.Web.Page).
- Tại sao lớp ListCustomersPresenter lại phải thông qua một interface mà không làm việc trực tiếp với lớp C# CodeBehind của trang aspx?
- Tại vì lớp ListCustomersPresenters và interface View được đặt trong một project dạng class library độc lập với project web, sau này muốn làm một giao diện cho windows thì lấy dll này ra sử dụng, chỉ cần viết một lớp winform implement interface trên là đủ.
- Tại sao phải làm vòng vòng, tạo ra interface rồi lớp presenter làm gì, sao không viết bình thường vô code behind của trang cho rồi?
- Tại vì mình đang sử dụng pattern MVP.
--------------------------------------------------------------
- Vậy bây giờ mình sẽ implement lớp ListCustomersPresenter. Theo như các bằng hữu trên giang hồ, coder ở Trung Nguyên Việt Nam cũng như cao thủ ở phương Tây … độc đều thích theo kiểu tà đạo như thế này. Các trang aspx, ascx thì gọi là các View và các lớp behind của nó implement interface View tương ứng là điều ai cũng biết là điều gì đấy rồi nên không nói nữa. Trong sự kiện Page_Load của các View này, sẽ khởi tạo một instance của Presenter tương ứng, và một trong những paramenter của các lớp Presenter sẽ là chính trang đang khởi tạo nó. Phức tạp quá , vậy coi hàm khởi tạo như sau là hiểu liền:
public class ListCustomersPresenter
{
public ListCustomersPresenter(IListCustomersView view, ICustomerDao customerDao)
{
Check.Require(view != null, "view may not be null");
Check.Require(customerDao != null, "customerDao may not be null");
this.view = view;
this.customerDao = customerDao;
}
public void InitView()
{
view.Customers = customerDao.GetAll();
}
private IListCustomersView view;
private ICustomerDao customerDao;
}
{
public ListCustomersPresenter(IListCustomersView view, ICustomerDao customerDao)
{
Check.Require(view != null, "view may not be null");
Check.Require(customerDao != null, "customerDao may not be null");
this.view = view;
this.customerDao = customerDao;
}
public void InitView()
{
view.Customers = customerDao.GetAll();
}
private IListCustomersView view;
private ICustomerDao customerDao;
}
Code 2: Code của 1 presenter đơn giản
- Lớp ListCustomersPresenter sẽ giữ một field thuộc kiểu của View mà nó sẽ chịu trách nhiệm “present”, ngoài ra nó sẽ có thêm các object khác để phục vụ cho việc present đó, trong trường hợp này là CustomerDao. Một số Presenter khác có thể cần nhiều Dao hơn, cho nên hàm khởi tạo của chúng có thể cũng cần nhiều parameter hơn để init giá trị cho các object Dao này. Thực ra việc init giá trị cho các Dao này trong constructor của lớp Presenter là optional, nếu ta có thể làm khởi tạo chúng trong lúc khởi tạo lớp Presenter thì cũng không cần sử dụng constructor của Presenter để init giá trị làm gì. Nhưng việc truyền instance của View cho lớp Presenter bằng constructor theo tui là một convention, và là một ràng buộc rằng một Presenter được tạo ra vì nó có vai trò “present” cho một View nào đó. Do đó một View tương ứng nên và phải tồn tại; và phải được truyền ngay cho Presenter của nó. Trong những cách hợp lý thì Constructor là cách hợp lý nhất vì lập trình viên sẽ không bao giờ bị quên.
- Ta hãy coi nội dung của file Code Behind để xem người ta đã làm điều đó như thế nào (How did they do that?)
public partial class ListCustomers : BasePage, IListCustomersView
{
protected override void PageLoad()
{
if (!IsPostBack)
{
InitView();
}
}
private void InitView()
{
ListCustomersPresenter presenter = new ListCustomersPresenter(this, DaoFactory.GetDao<ICustomerDao>("CustomerDao"));
presenter.InitView();
}
public IList<Customer> Customers
{
set
{
grdEmployees.DataSource = value;
grdEmployees.DataBind();
}
}
}
{
protected override void PageLoad()
{
if (!IsPostBack)
{
InitView();
}
}
private void InitView()
{
ListCustomersPresenter presenter = new ListCustomersPresenter(this, DaoFactory.GetDao<ICustomerDao>("CustomerDao"));
presenter.InitView();
}
public IList<Customer> Customers
{
set
{
grdEmployees.DataSource = value;
grdEmployees.DataBind();
}
}
}
Code 3: Code behind của một view là 1 page ASPX
- Như chúng ta thấy, khi set giá trị cho list Customer, đồng thời datagrid cũng được bind luôn. Mọi xử lý phức tạp đã được implement trong lớp Presenter tương ứng. Ở trên là sample cho lớp View và Presenter tương ứng đối với trường hợp View là 1 trang aspx. Vậy nếu View là một UserControl thì sao? Thông thường, như đã nói ở trên, các presenter thường được người ta khởi tạo trong khi Page_Load. Tuy sự kiện Page_Load cũng có trong UserControl nhưng để cho nó thống nhất, người ta thường khởi tạo tất cả các Presenter trong trang aspx rồi set cho UserControl thay vì làm trực tiếp bên trong UserControl. Do đó những UserControl sẽ có một private field kiểu presenter và sẽ có một method để set giá trị cho private field này. Ví dụ như interface của IEditCustomerView như sau:
public interface IEditCustomerView
{
void AttachPresenter(EditCustomerPresenter presenter);
Customer Customer { set; }
void UpdateValuesOn(Customer customer);
}
{
void AttachPresenter(EditCustomerPresenter presenter);
Customer Customer { set; }
void UpdateValuesOn(Customer customer);
}
Code 4: Nội dung interface IEditCustomerView cho 1 view là UserControl
- Trong một trang Edit Customer, theo cách nghĩ thông thường chúng ta sẽ load thông tin của một User nào đó đưa lên text box, người xem có thể sửa trực tiếp vào text box sau đó click vào 1 button là Save để lưu dữ liệu, hoặc click vào một button là Cancel để quay lại. Trong ASP.NET khi một button được click sẽ phát sinh ra event Button_Clicked, và những xử lý để save dữ liệu sẽ được thực hiện trong code của event này. Vậy làm cách nào chúng ta đưa những xử lý đó vào trong lớp Presenter? Thực sự thì có hai cách để làm điều này, cách thứ nhất cũng là cách đơn giản dễ hiểu nhất, bình dân nhất là ta sẽ implement một public method gọi là Update cho lớp Presenter, và trong sự kiện Button_Clicked, ta sẽ gọi method đó từ instance presenter; cách thứ hai là ta sẽ khai báo một delegate hay một EventHander trong interface IEditCustomerView, và trong sự kiện Button_Clicked, ta sẽ fire event này. Tất nhiên nếu làm theo cách thứ hai thì bên trong code của lớp Presenter phải implement event được khai báo trong View. Để cho đơn giản, dễ hiểu, trong ví dụ này chúng ta sẽ sử dụng cách thứ nhất tuy tui thích cách thứ hai hơn, vì nó có vẽ p…rồ hơn
public partial class Views_EditCustomerView : BaseUserControl, IEditCustomerView
{
public void AttachPresenter(EditCustomerPresenter presenter)
{
this.presenter = presenter;
}
public Customer Customer
{
set
{
Check.Require(value != null, "Customer may not be null");
// Must implement this function to show customer info
ShowCustomerDetails(value);
}
}
protected void btnUpdate_OnClick(object sender, EventArgs e)
{
presenter.Update(hidCustomerID.Value);
/*.....................
.......................
.....................*/
}
private EditCustomerPresenter presenter;
}
{
public void AttachPresenter(EditCustomerPresenter presenter)
{
this.presenter = presenter;
}
public Customer Customer
{
set
{
Check.Require(value != null, "Customer may not be null");
// Must implement this function to show customer info
ShowCustomerDetails(value);
}
}
protected void btnUpdate_OnClick(object sender, EventArgs e)
{
presenter.Update(hidCustomerID.Value);
/*.....................
.......................
.....................*/
}
private EditCustomerPresenter presenter;
}
Code 5: Code behind của một view là UserControl
II. Test các lớp Presenter
-Như đã biết một trong những lý do người ta áp dụng MVP vì khả năng dễ test của nó. Các lớp Presenter, interface View được đặt trong một project class library nên việc test chúng vô cùng đơn giản. Chúng ta hãy xem một ví dụ test lớp Presenter bằng cách sử dụng Rhino Mock:
[TestFixture]
public class ListCustomersPresenterTests
{
[Test]
public void TestInitView()
{
ListCustomersViewStub view = new ListCustomersViewStub();
ListCustomersPresenter presenter = new ListCustomersPresenter(view,
new MockCustomerDaoFactory().CreateMockCustomerDao());
presenter.InitView();
Assert.IsNotNull(view.Customers);
Assert.AreEqual(3, view.Customers.Count);
}
private class ListCustomersViewStub : IListCustomersView
{
public IList<Customer> Customers
{
set { customers = value; }
// Not required by IListCustomersView, but useful for unit test verfication
get { return customers; }
}
private IList<Customer> customers;
}
}
public class MockCustomerDaoFactory
{
public ICustomerDao CreateMockCustomerDao()
{
MockRepository mocks = new MockRepository();
ICustomerDao mockedCutomerDao = mocks.CreateMock<ICustomerDao>();
Expect.Call(mockedCutomerDao.GetAll())
.Return(new TestCustomersFactory().CreateCustomers());
Expect.Call(mockedCutomerDao.GetById(null, false)).IgnoreArguments()
.Return(new TestCustomersFactory().CreateCustomer());
mocks.Replay(mockedCutomerDao);
return mockedCutomerDao;
}
}
public class ListCustomersPresenterTests
{
[Test]
public void TestInitView()
{
ListCustomersViewStub view = new ListCustomersViewStub();
ListCustomersPresenter presenter = new ListCustomersPresenter(view,
new MockCustomerDaoFactory().CreateMockCustomerDao());
presenter.InitView();
Assert.IsNotNull(view.Customers);
Assert.AreEqual(3, view.Customers.Count);
}
private class ListCustomersViewStub : IListCustomersView
{
public IList<Customer> Customers
{
set { customers = value; }
// Not required by IListCustomersView, but useful for unit test verfication
get { return customers; }
}
private IList<Customer> customers;
}
}
public class MockCustomerDaoFactory
{
public ICustomerDao CreateMockCustomerDao()
{
MockRepository mocks = new MockRepository();
ICustomerDao mockedCutomerDao = mocks.CreateMock<ICustomerDao>();
Expect.Call(mockedCutomerDao.GetAll())
.Return(new TestCustomersFactory().CreateCustomers());
Expect.Call(mockedCutomerDao.GetById(null, false)).IgnoreArguments()
.Return(new TestCustomersFactory().CreateCustomer());
mocks.Replay(mockedCutomerDao);
return mockedCutomerDao;
}
}
Code 6: Test một lớp Presenter đơn giản bằng Rhino Mock
- Trong sample này thì những xử lý của lớp Presenter khá đơn giản nên có thể ta chưa thấy rõ được sự cần thiết khi test chúng. Theo tui trên nguyên tắc mọi code trong project dạng class library nên được test, ít nhất các hàm test sẽ cover những dòng code mà ta viết và nâng % coverage của chương trình lên. Nghe đồn có một tay coder bên Mỹ nổi tiếng viết chương trình không bao giờ debug, build 1 lần là chạy, không biết có thiệt không nhưng tui thà chịu khó viết unit test còn hơn ngồi debug khi chương trình đã go live.
III. Kết luận
- MVP tưởng khó nhưng không khó, tưởng phức tạp nhưng không đến nỗi vậy, khi xài là ghiền. Điều tui thích nhất ở MVP là khả năng tùy biến, tự mình thiết kế lớp lang chứ không ràng buộc như một số framework chẳng hạn như ASP.NET MVC, tuy nhiên mỗi cái nó có cái hay riêng. Ngoài MVP, còn có khá nhiều Enterprise Architecture khác rất đáng tìm hiểu, trong lúc đọc bài này trên Code Project thì tui cũng biết thêm kiến trúc như S#arp Architecture và Web Client Software Factory (cái này do một đại ca trong team giới thiệu). Chắc chắn tui sẽ tìm hiểu tiếp mấy cái này đặng viết blog chơi . Loạt bài dịch và chế về NHibernate và MVP sẽ dừng ở phần 5 này. Có một số tuyệt chiêu khác trong bài gốc của tác giả mà tui không nhắc tới như smoke test, cái đấy cũng hay và ai thích có thể tìm hiểu thêm cách dùng. Trong giai đoạn này thì Microsoft đưa ra rất nhiều công nghệ và nhiều khi làm cho lập trình viên chúng ta cảm thấy đuối, .NET framework 3.5 học chưa hết giờ mấy ổng sắp đưa ra .NET framework 4.0, C# 3.x chưa xài nhiều giờ đã có C# 4.0. Dù sao công nghệ vẫn là công nghệ, chúng sẽ thay đổi rất thường xuyên, nhưng mấy cái kiến trúc này thì hầu như không phụ thuộc công nghệ nên tui nghĩ ưu tiên nghiên cứu nó cho lành.
- Cuối cùng, mong là loạt bài này có ích cho những ai chưa biết - đang muốn - định tìm hiểu về NHibernate và MVP, xen kẽ trong các phần tui cố ý viết nhiều về các kĩ thuật liên quan như Windsor, Unit Test, … nên làm cho bài nào cũng dài ngoằng, nhiều người đọc thấy đuối, nhưng tui sẽ vẫn viết dài như vậy, thậm chí dài hơn, có điều sẽ bớt phần dịch và thêm phần bịa, chẳng hạn bài này toàn tui chế, ko dịch từ nguồn nào cả .
- Anyway, thanks for reading.
Code phần 5: http://nthoaiblog.googlepages.com/EnterpriseSample-part5.zip
Các đoạn code minh họa trong bài viết được tui rút gọn cho dễ hiểu, code được implement cuối cùng trong demo sẽ có một số điểm khác biệt...