Application logic is the middle tier of 3-tier systems. It
contains the logic of how the domain model is applied and of what actually
happens (application flow in other words). In MVP/MVC the Application logic
tier consists mainly of Controller objects. Some frameworks (including MVC#)
also add Task objects to the Application logic tier. Next we will talk about
controllers and tasks and how they are used in our example application.
Understanding Controllers
Probably the main difference between MVC and MVP is the
difference between what is called Controller in these patterns. For this reason
the Controller in MVP is even referred to as "Presenter;"
nevertheless, many authors prefer calling it "Controller" (and so do
we). Let us look into the Controllers' peculiarities in the
Model-View-Controller and Model-View-Presenter patterns.
MVC pattern (and, specifically, ASP.NET MVC framework)
breaks the standard ASP.NET web request processing scheme. According to MVC web
requests (user gestures) are processed by the appropriate Controller objects
instead of being processed by web pages. Based on the incoming request URL,
ASP.NET MVC framework chooses the controller to process the request. The chosen
controller then makes necessary calls to the Model tier and chooses the view to
be shown, also passing the needed data to that view.
Figure 3

MVP pattern, on the contrary, does not violate the standard
ASP.NET web request processing model. According to MVP, web pages do receive
web requests (user gestures). But, instead of processing requests itself, a web
page delegates this job to the associated controller. The controller then, just
as in MVC, makes necessary calls to the model and decides what to show to the
user.
The said difference underlines one major drawback of the MVC
pattern. The thing is that MVC requires all user gestures to trigger a request
of URL in a specific form (say, http://domain/[Controller]/[Action]). However,
most of ASP.NET controls do not conform to this requirement. Instead, they
generate web page events in response to user gestures. It means that ASP.NET
server controls (especially third-party) better fit MVP scheme, with user
gestures handled by views (web pages), and do not clearly fit MVC.
Task concept
By task we mean a set of views which a user traverses to fulfill
some job. For example, an airline ticket booking task may consist of two views:
one for choosing the flight, the other for entering personal information. Tasks
often correspond to certain use cases of the system: there may be "Login
to the system" and "Process order" use cases and tasks of the
same names. Finally, a task may be associated with a state machine or a
workflow: for example "Process order" task could be implemented with
the "Process order" workflow in WWF.
Although not present in MVC and MVP patterns, task concept
proves useful in various applications. That is why it is implemented in MVC# in
addition to the controller and view concepts.
Real-life systems may consist of dozens of tasks; however,
our simple application will include only one task - Main Task. This task will
consist of four views mentioned earlier: "Welcome," "Product
Categories," "Products" and "Edit Product." A
view-controller is referred to as interaction point. Thus, we need to declare a
task with four interaction points. In MVC# interaction points are defined by
string constant fields equipped with [IPoint] attribute.
Listing 1
public class MainTask : TaskBase
{
[IPoint(typeof(ControllerBase), true)]
public const string Welcome = "Welcome";
[IPoint(typeof(ProductCategoriesController), true, Products)]
public const string ProductCategories = "Product Categories";
[IPoint(typeof(ProductsController), EditProduct)]
public const string Products = "Products";
[IPoint(typeof(EditProductController))]
public const string EditProduct = "Edit Product";
}
Each interaction point declaration includes the name of the
view (value of the constant), the type of the controller and navigation
information.
Navigation information specifies the possible order in which
views can be activated. For instance, true parameter value in the [IPoint(typeof(...),
true)] attribute definition declares the view as a common navigation target. It
means that this view can be activated at any time, regardless of the current
active view. In our example the "Welcome" and "Product
Categories" views are common targets. Next, [IPoint] attribute applied to,
say, "View 1" specifies views which can be navigated to from
"View 1." In our example [IPoint(typeof(...), EditProduct)] declares
that "Edit Product" view can be activated (in other words, navigated
to) if "Products" view is active.
Tasks often contain global information used among several
views/controllers. In our example a product category is chosen inside the
"Product Categories" view and is browsed in the "Products"
view. To be accessible from both these views the selected category will be
stored in a new MainTask.SelectedCategory property. In the same way we
introduce a MainTask.SelectedProduct property.
Listing 2
public class MainTask : TaskBase
...
private NorthwindDataSet.CategoriesRow selectedCategory =
NorthwindDataSet.Instance.Categories[0];
private NorthwindDataSet.ProductsRow selectedProduct =
NorthwindDataSet.Instance.Products[0];
public event EventHandler SelectedCategoryChanged;
public event EventHandler SelectedProductChanged;
public NorthwindDataSet.CategoriesRow SelectedCategory
{
get { return selectedCategory; }
set
{
selectedCategory = value;
if (SelectedCategoryChanged != null)
SelectedCategoryChanged(this, EventArgs.Empty);
}
}
public NorthwindDataSet.ProductsRow SelectedProduct
{
get { return selectedProduct; }
set
{
selectedProduct = value;
if (SelectedProductChanged != null)
SelectedProductChanged(this, EventArgs.Empty);
}
}
The last thing left to do with the task is to define actions
performed on task start. This is done by implementing the ITask.OnStart(...)
method.
Listing 3
public class MainTask : TaskBase
...
public override void OnStart(object param)
{
Navigator.NavigateDirectly(Welcome);
}
As seen above, we are simply activating the
"Welcome" view when the task is started. NavigateDirectly method is
used instead of Navigate to switch views ignoring any navigation routes.
Product Categories Controller
As we already know, processing of every user gesture should
be delegated to the corresponding controller method. If no user gestures are
applicable, a simple operation-less ControllerBase may be used. This is the
case with the "Welcome" view in our example.
Inside the "Product Categories" view a user can
choose a specific category. The processing of this gesture should be delegated
to the ProductCategoriesController.CategorySelected(...) method. We could
immediately proceed to writing the body of this method. But instead, let us
first write the test case for it, in full accordance with Test Driven
Development (TDD) principles.
Listing 4
[TestFixture]
public class TestProductCategoriesController
...
[Test]
public void TestCategorySelected()
{
NorthwindDataSet.CategoriesRow cat =
NorthwindDataSet.Instance.Categories.NewCategoriesRow();
controller.CategorySelected(cat);
Assert.AreSame(cat, controller.Task.SelectedCategory);
Assert.AreEqual(MainTask.Products, controller.Task.CurrViewName);
}
The first NUnit assertion checks that the controller stores
the selected category in the task's SelectedCategory property. The second
assertion ensures that the controller performs navigation to the "Products"
view (by checking the task's CurrViewName property).
Of course, some test setup is needed to link the
participating objects together. In the setup we are using the stub navigator
implementation - it does not perform navigation, only changes the task state,
which is ideal for test cases.
Listing 5
[TestFixture]
public class TestProductCategoriesController
...
[SetUp]
public void TestSetup()
{
controller = new ProductCategoriesController();
controller.Task = new MainTask();
controller.Task.Navigator = new StubNavigator();
controller.Task.Navigator.Task = controller.Task;
}
Now that the test for the CategorySelected method is ready,
let us write its body.
Listing 6
public class ProductCategoriesController : ControllerBase<MainTask,
IProductCategoriesView>
...
public void CategorySelected(NorthwindDataSet.CategoriesRow selectedCat)
{
Task.SelectedCategory = selectedCat;
Task.Navigator.Navigate(MainTask.Products);
}
Note that controllers in MVC# should implement the IController
interface. But instead of implementing it manually, it is recommended to
inherit its base generic implementation ControllerBase<TTask, TView>
(specifying the expected task and view types as generic parameters).
Another thing that a controller should do is the view
initialization. View initialization is usually done when a controller is linked
to its view, i.e. in the IController.View setter method. In our example the product
categories view should be initialized to list all possible categories. Again,
we will start with the test case which will check the correctness of
initialization.
Listing 7
[TestFixture]
public class TestProductCategoriesController
...
[Test]
public void TestViewInitialization()
{
controller.View = new StubProductCategoriesView();
Assert.AreSame((NorthwindDataSet.Instance.Categories as IListSource)
.GetList(), controller.View.CategoriesList);
}
StubProductCategoriesView is a simple test-aimed IProductCategoriesView
implementation with a backing field. Code to satisfy the above test case will
look as follows:
Listing 8
public class ProductCategoriesController : ControllerBase<MainTask,
IProductCategoriesView>
...
public override IProductCategoriesView View
{
get { return base.View; }
set
{
base.View = value;
View.CategoriesList = (NorthwindDataSet.Instance.Categories
as IListSource).GetList();
}
}
Running the tests indicates our success.
Figure 4

Products Controller
In the "Products" view a user may click
"Edit" against some product. Processing of this gesture should be
delegated to the controller's EditProduct(...) method. This method, in its
turn, should store the product chosen in the task's SelectedProduct property,
and then navigate to the "Edit Product" view. For brevity we will omit
the test case listing here, and will list only the EditProduct method itself.
Listing 9
public class ProductsController : ControllerBase<MainTask, IProductsView>
...
public void EditProduct(NorthwindDataSet.ProductsRow product)
{
Task.SelectedProduct = product;
Task.Navigator.Navigate(MainTask.EditProduct);
}
View initialization code should setup the view to show the contents
of the current category.
Listing 10
public class ProductsController : ControllerBase<MainTask, IProductsView>
...
public override IProductsView View
{
get { return base.View; }
set
{
base.View = value;
View.Category = Task.SelectedCategory;
}
}
The Products Controller should also track the change of the
selected category and, if a user selects another category, it should
accordingly change the category browsed in the products view. To track the
change of the current category we will subscribe to the Task's SelectedCategoryChanged
event inside the ProductsController.Task setter method.
Listing 11
public class ProductsController : ControllerBase<MainTask, IProductsView>
...
public override MainTask Task
{
get { return base.Task; }
set
{
base.Task = value;
Task.SelectedCategoryChanged += SelectedCategoryChanged;
}
}
private void SelectedCategoryChanged(object sender, EventArgs e)
{
View.Category = Task.SelectedCategory;
}
Edit Product Controller
The only operation applicable in the "Edit
Product" view is committing the changes done to the edited product. The
corresponding controller method will be EditProductController.Commit().
Listing 12
public class EditProductController : ControllerBase<MainTask, IEditProductView>
...
public void Commit()
{
NorthwindDataSet.ProductsRow product = Task.SelectedProduct;
product.ProductName = View.ProductName;
product.CategoriesRow = View.Category;
product.SuppliersRow = View.Supplier;
product.UnitPrice = View.UnitPrice;
}
As always, the controller should initialize the view.
Listing 13
public class EditProductController : ControllerBase<MainTask, IEditProductView>
...
public override IEditProductView View
{
get { return base.View; }
set
{
base.View = value;
InitViewData();
}
}
private void InitViewData()
{
View.ProductName = Task.SelectedProduct.ProductName;
View.Category = Task.SelectedProduct.CategoriesRow;
View.Supplier = Task.SelectedProduct.SuppliersRow;
View.UnitPrice = Task.SelectedProduct.UnitPrice;
}
And the controller should track the change of the selected
customer, and accordingly fill the view.
Listing 14
public class EditProductController : ControllerBase<MainTask, IEditProductView>
...
public override MainTask Task
{
get { return base.Task; }
set
{
base.Task = value;
Task.SelectedProductChanged += SelectedProductChanged;
}
}
private void SelectedProductChanged(object sender, EventArgs e)
{
InitViewData();
}