Monday, May 18, 2009

Separate Domain from Presentation – part II

Cross post from IRefactor

This is a second post in the series of posts about “Separate Domain from Presentation” Refactoring.

Previous Posts:
Separate Domain from Presentation – part I

Last time we discussed the ways to disconnect the Presentation (FrmMain) from the Domain Object (CoursesDS) in the IRefactor.CoursesView project. As a consequence, instead of the bloated all-in-one initial project we ended with the following:
  • IRefactor.CoursesView – represents the View (CoursesView) without the domain object.
  • IRefactor.Common – represents the domain object (CoursesDS) without the UI elements.
It’s time to continue the UI and BL separations further more. For that purpose I will use the MVP pattern. It seems that there are a lot of misunderstandings regarding definitions of the UI/BL separation patterns (take a look here), I will focus on the following definitions:

In my post, MVP = Model-View-Presenter will basically stand for:
  • Model – Hey, I am the domain model;
    I know how to manipulate model objects in order to perform required application logic and persistency.
    I don’t know how to visualize to the user any information or how to respond to any action the user may take.
  • View – Hey, I am the view model;
    I know how to visually represent to the user information (that will be provided for me by the Presenter).
    I know how to perform simple data binding and possible simple UI actions that modify the visual layout of the screen.
    I don’t know what to do when an application logic or persistency are required.
  • Presenter – Hey, I am the presenter model;
    I know how to handle user requests to the View (which are more complicated than a simple data binding) and how to delegate those requests to the Model.
    I know how to query the Model in order to delegate information to the View, if any should be displayed to the user.
    I don’t know how to draw widgets graphically (that’s View’s concern) and I don’t know how to perform any application logic in order to derive that information (that’s Model’s concern).
Those who have sharp eyes, will probably spot here the use of a Supervising Controller (that doesn’t introduce right away drastic changes to the code in the spirit of Refactoring. Later on, one could turn the View into the Passive View while continuing to refactor).

MVP - Supervising Controller

Refactoring Steps:
  • Rename the IRefactor.CoursesView.FrmMain class to CoursesView. Go to FrmMain, right click on it and use Refactor » Rename command to rename it easily.
  • Create a class CoursesPresenter in IRefactor.CoursesView.
SeparateDomainFromPresentation.CoursesPresenter
  • Add to CoursesPresenter class a pointer to the CoursesView (pay attention to the fact that the view is readonly)
public class CoursesPresenter
{
private readonly CoursesView view;
}
  • Add to CoursesPresenter a constructor that receives CoursesView instance.
public CoursesPresenter(CoursesView view)
{
this.view = view;
}
  • Compile the Solution and execute the Unit Tests.
  • Now we need to delegate user’s interactions from the view to the presenter. We can do it rather by inheriting the EventArgs and creating a CoursesEventArgs class, or we can let the CoursesPresenter query directly the CoursesView and grab the required data. Here I’ll grab the CoursesDS domain object directly. Add to the CoursesView the following:
public CoursesDS Courses
{
get { return coursesDS; }
}
  • Let’s start with the Save event delegation. If you look closely at the coursesBindingNavigatorSaveItem_Click event handler, you will notice that the method has two different responsibilities. It treats the required data binding and then performs data access operation in order to save the CoursesDS domain object. To separate the concerns, let's use another Refactoring step called “Extract Method”. Select the data access code, right click on it and use Refactor » Extract Method command to extract the code into a new method called “Save”.
SeparateDomainFromPresentation.SaveItem
// ...
this.Validate();
this.bsCourses.EndEdit();
// Changed from auto generated code.
Save();
// ...
private void Save()
{
if (coursesDS.HasChanges())
{
CoursesDS.CoursesDataTable changes =
this.coursesDS.Courses.GetChanges() as
CoursesDS.CoursesDataTable;
if (changes != null)
{
taCourses.Update(changes);
}
}
}
  • Compile the Solution and execute the Unit Tests.
  • After breaking the coursesBindingNavigatorSaveItem_Click method we suddenly realize that the Save method doesn’t belong to the CoursesView class as it does a data access operation. By all means this operation should be inside the domain model (business logic). In the meanwhile we will push the method inside the presenter.
  • In CoursesPresenter create a new method called Save. The method will retrieve the CoursesDS domain object from the CoursesView and save the object into the DB.
public void Save()
{
CoursesDS coursesDS = view.Courses;
//...
}
  • Compile the Solution and execute the Unit Tests.
  • Copy all the code from the CoursesView.Save method into the CoursesPresenter.Save method and adjust the new code to its new “place” (pay attention to the CoursesTableAdapter that needs to be redefined).
public void Save()
{
CoursesDS coursesDS = view.Courses;
if (coursesDS.HasChanges())
{
CoursesDS.CoursesDataTable changes =
coursesDS.Courses.GetChanges()
as CoursesDS.CoursesDataTable;
if (changes != null)
{
using (CoursesTableAdapter taCourses = new CoursesTableAdapter())
{
taCourses.Update(changes);
}
}
}
}
  • Compile the Solution.
  • Now, for the fun part; Put all the code within the CoursesView.Save method inside remarks and declare CoursesPresenter object that calls to its Save method.
private void Save()
{
//if (coursesDS.HasChanges())
//{
// CoursesDS.CoursesDataTable changes =
// this.coursesDS.Courses.GetChanges()
// as CoursesDS.CoursesDataTable;
// if (changes != null)
// {
// taCourses.Update(changes);
// }
//}
CoursesPresenter presenter = new CoursesPresenter(this);
presenter.Save();
}
  • Compile the Solution and execute the Unit Tests.
  • Walla! You have successfully moved a data access method from the view to the presenter. With continuous refactoring you can push that method even further in the data access layer.
A quick summary:

  • We introduced a new presenter class, called CoursesPresenter.
  • We moved the Save method (which does a data access operation) from the view into the presenter class. (Don’t worry we will eliminate the Save method from the CoursesView in the next post.)
  • The same should be applied to the Load method (FrmMain_Load). I won’t show it here, just use the same principle.
Here is the schematic view of the current IRefactor.CoursesView project.

SeparateDomainFromPresentation.MVP
Clearly, it’s not the same as the MVP pattern we depicted earlier. Future postings will explain additional steps to refactor towards the MVP pattern, by applying:
  • Events – the presenter handles complicated user events by subscribing to the view.
  • Interfaces – the presenter should manipulate the view only via an interface.Failing to do so, will break the view’s encapsulation.
References:
Take a look here for a good summary on Smart Client development.

No comments:

Post a Comment