Cross post from IRefactor
This is the second post in the series of posts about “Separate Domain from Presentation” Refactoring.
Previous Posts
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).
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
.
- 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 the following to the CoursesView
:
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
”.
this.Validate();
this.bsCourses.EndEdit();
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 its Save
method.
private void Save()
{
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.
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.
codeproject