Reworking ASP.NET MVC Store with MVC# Framework
page 2 of 5
by Oleg Zhukov
Feedback
Average Rating: This article has not yet been rated.
Views (Total / Last 10 Days): 27440/ 56

Application Logic

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();
    }

View Entire Article

User Comments

No comments posted yet.

Product Spotlight
Product Spotlight 





Community Advice: ASP | SQL | XML | Regular Expressions | Windows


©Copyright 1998-2024 ASPAlliance.com  |  Page Processed at 2024-04-24 2:43:20 PM  AspAlliance Recent Articles RSS Feed
About ASPAlliance | Newsgroups | Advertise | Authors | Email Lists | Feedback | Link To Us | Privacy | Search