Introduction
Clear separation of responsibilities along with a low coupling level is a sign of a well-designed application. Whereas design patterns are proven solutions to reduce coupling between small sets of objects, architectural patterns help to improve a system’s design on the whole. One popular architectural pattern is Model-View-Controller. Originally came from Smalltalk now it has implementations in various languages. In Java for example such frameworks as Spring and Struts have gained high popularity and are widely used. However in .NET world the existing implementations aren’t much spread, being mainly inspired by Java implementations they fit well only for Web applications and are not suitable for Windows thick client applications.
Another architectural pattern that in fact is an evolution of MVC is Model-View-Presenter. The underlying ideas of MVP are very similar to those in MVC however MVP is designed to be more usable and understandable. MVP has much less implementations then MVC does, one known MVP implementation for .NET is the Microsoft User Interface Process application block (UIPAB). In spite of numerous problems, and hence low usability of UIPAB, its reasonable key concepts inspired us to create something new.
This article starts a series covering the development of a native Model-View-Presenter framework for .NET platform. Although it will be an MVP framework we will still use the term "Controller" as it seems to be be more pertinent than the "Presenter" notation (UIPAB follows the same manner describing itself as MVC, though in fact it is closer to MVP). Let us start with clarifying the basic concepts of an application’s structure. After that we will proceed to the discussion of the existing architectural solutions (including MVC and MVP).
Basic concepts
Every application can be split into a number of tasks. Here by task we mean a sequence of actions which accomplish a job, problem or assignment. There are different ways of breaking an application into tasks for example tasks may be associated with use cases. Each task involves interaction with a user done via so called interaction points. An interaction point is represented by one or more classes which serve the following needs:
- receive and process an input from a user
- make proper requests to the business objects
- request/modify the task’s state
- send feedback to the user
The following figure illustrates the described relationships.
As we can see the interaction point responsibilities are rather vast. That is why architectural solutions such as MVC make their aim to split interaction points into simpler parts. Next we will consider various ways of splitting the interaction point (including the MVC paradigm). Though the division of the interaction point into smaller elements is preferred we will start with a case when an interaction point is represented by a single view class.
Interaction point as a single view
When the interaction point is made of a single view class it is the view which is responsible for all intermediate data processing. Such view class handles user input, makes proper calls to the business objects, analyses the data received from them, decides what to display to the user and actually displays it.
In order to demonstrate this and the other approaches let us now introduce the example we will refer to throughout the article. Consider the encyclopedia application where a user enters some term and the system gives back the explanation for it. In most cases the explanation is unambiguously found for a term. However sometimes several explanations of a term (especially if the term is an abbreviation) may apply and the system then asks the user which one to display.
With a single view approach the sequence diagram for our encyclopedia application would look as follows:
This figure reveals the drawbacks of combining all intermediate logic into a view class. The view then becomes too bloated and difficult for understanding. Moreover such approach violates the single responsibility principle by uniting in the view two conceptually different responsibilities. First is making requests to the model and deciding what to display (application logic) and the second is actually displaying (presentation mechanism). As a result if we want to make our encyclopedia work both as a windows and a web application we will have to duplicate the application logic in two view classes: one for Windows and the other for web environment.
Model-View-Controller
We have seen the downsides of the solution when the interaction point is represented by a single view class. On the contrary to the single view technique MVC does break the interaction point into three parts. These parts are: Controller, View and Presentation model.
Controller handles user input, makes subsequent calls to the business objects and manages the application flow. In particular the controller decides what to display to the user. However it is not allowed in MVC to access the view directly, instead the underlying model should be altered and the changes will be propagated to the view through the observer mechanism. Thus in order to make the view update itself the controller should change the presentation model object.
In our encyclopedia example the controller class asks the model for the proper explanations (1.1 in the figure below) and passes the returned explanations to the presentation model (1.2). Depending on how much explanations were found (one or more) the controller sets a flag in the presentation model indicating whether the user should choose the desired variant (1.3). The view then reflects the changes in the presentation model (1.3.1) and either displays dialog with several variants (1.3.1.3) or displays the only found explanation (1.3.1.4).
The main advantage of MVC is a clear distribution of responsibilities between parties. The controller drives the application flow specifying what and when should be done. The view only renders the underlying business and presentation models and presents them to the user. Since views in MVC do not contain any application logic they can be harmlessly substituted, for example there may be different view classes for Windows and web interfaces.
Nevertheless there are two major drawbacks in the traditional MVC approach. First is a higher complexity because of the observer mechanism: in order to update the view the controller must make changes to the presentation model, which will trigger the view update. Such indirect relationship may be difficult to understand. Instead the controller could simply send a message to the view, however such direct linking is not allowed in MVC.
The other shortcoming is that MVC does not conform to the modern UI programming environments where widgets themselves handle user gestures. For example form classes (either web or Windows) in .NET applications by default contain handlers for user input. Thus it would be difficult to break the common paradigm and make controllers receive the user input instead of views.
The next architectural pattern we consider was designed to eliminate the drawbacks of MVC, while preserving its separation of application logic and presentation mechanism.
Model-View-Presenter
Model-View-Presenter approach appeared in the late 1990’s and was an evolution of MVC pattern. Above we have described two typical shortcomings of the MVC pattern. Now let us look at how MVP eliminates these two.
Firstly according to MVP direct requests from the controller to the view become possible. Thus the controller itself may trigger the view updates instead of performing a round trip through the presentation model. In this sense the controller in MVP contains the presentation model from MVC. That is probably the reason why the controller in MVP is referred to as presenter (however we will continue naming it controller).
Here we must note that although the controller has a link to the view object it does not depend on the concrete view class. Instead the controller treats the view in an abstracted way by the means of a separated interface implemented by the view (see the figure above). For example the encyclopedia controller will communicate the view via the IEncyclopediaView interface with “chooseExplFrom(…)” and “displayExpl(…)” operations. Since such separated interface is a part of the application logic, the dependency runs from the presentation to the application logic but not vice versa.
Next thing that makes MVP more convenient (in contrast to MVC) is that it allows views to receive user input. Such behavior fits well modern UI environments. For instance in Windows keyboard and mouse events are handled by UI controls and forms, in ASP.NET user requests are processed by web page classes. Though the view in MVP receives the user input it should merely delegate all the processing to the controller.
The next figure demonstrates the interactions in MVP by the example of the encyclopedia application. As we can see direct calls from the controller to the view (via the IEncyclopediaView interface) make the overall picture less complicated then that in MVC. Yet the clear separation of responsibilities between the controller (application logic) and the view (presentation mechanism) still remains, in particular allowing the developer to easily support interchangeable views for web and Windows environments.
Summary
Let us sum up what we have discussed so far. Among the architectural patterns we have considered MVP seems to be the most attractive one. It allows building flexible systems without overheads peculiar to the classic MVC approach. That is why it is MVP that we are going to base our framework on.
Part 2. Implementing core MVP functionality
Introduction
In the previous article we have made our choice in favour of the Model-View-Presenter architectural pattern. Thus our final goal is the creation of the MVP framework. However we should firstly make it clear what does mean “MVP framework” by deciding what the future system will be intended for and what problems it will solve.
In this article we will start with clarifying the aim of our system and gathering some basic requirements for it. After that we will proceed to the design and implementation stages creating fundamental classes that will meet our basic requirements.
Basic requirements
The aim of our system can be formulated as follows: it should simplify the usage of the MVP pattern by developers. The system should make it easier to fulfill every common task within the MVP approach. For example it should simplify such actions as navigating between views, accessing controllers, etc. For each common operation we will analyze how our system can help in performing it and by doing so we will construct requirements to the system. We will express the requirements in a popular form of use cases, each describing the desired interactions between a user and the system.
Starting a task
To begin with let us consider how a user would start a task. Starting a task implies certain processing (registering and initializing the task) so it would be nice to delegate this work to the system (MVP framework). A user should be able to specify actions performed on a task start by implementing some OnStart method. Thus given a task descriptor the system should perform necessary processing and call the task OnStart handler.
Starting a task
User: Pass the task descriptor to the system and ask to start the task.
System: Register the task, initialize it and invoke OnStart handler.
Here a task descriptor is something that describes the task, specifically its structure and its properties. Let us decide what will be convenient to use as a task descriptor.
Since every task is a part of an application we might want to describe tasks directly in the source code of the application. A good way of defining an entity (such as task) in a source code is using a type definition construct. Therefore a task type can be used as its descriptor. Moreover a task type (class) may define OnStart handler method and an instance of this type can hold the task state at runtime. So this is how the revised version of the “Starting a task” use case looks:
Starting a task (revision 1)
User: Pass the task type to the system and ask to start the task.
System: Create a task instance, register and initialize it and invoke its
OnStart operation.
Of course there must be some framework class which processes start task requests from a user. Let us call it TasksManager:
Navigating from within a task
Every task involves a number of views with transitions possible between them. At this point let us discuss how a user would navigate to some view from a task code. Say some view should be activated in the OnStart handler code. It would be convenient if the navigation is done by the framework and the navigation logic is isolated in some Navigator class. Then each user task instance should be associated with a proper navigator instance.
Navigating from within a task
Precondition: The task is associated with a proper navigator instance.
User: Ask that associated navigator to navigate to some view.
System: Do the navigation, alter the task state.
It is important to note that the precondition requires each task to be linked to its navigator. Such linking may be done during the task start process. So here is the modified version of “Starting a task” use case:
Starting a task (revision 2)
User: Pass the task type to the system and ask to start the task.
System: Create a task and a navigator instances, initialize and link them
together. Invoke the OnStart operation on the task instance.
Using various presentation mechanisms
According to the MVP paradigm the system should make it easy to use different presentation mechanisms for example Web or Windows UI. A presentation mechanism has influence upon how switching between views is done. Therefore it seems quite reasonable to encapsulate view-switching code in a separate ViewsManager abstract class with subclasses for each specific UI kind. Then the Navigator class containing some common navigation logic will be associated with the ViewsManager class.
We can not yet formulate any use case for this requirement however the arguments above prove the need of the ViewsManager concept. Thus the domain model at the moment looks as follows:
Describing a task structure
From the previous article we know that every task consists of a number of interaction points. Each interaction point in its turn is characterized by its view, controller and possible transitions to the other interaction points (in the previous article we decided to use "Controller" notation instead of "Presenter" so do not get confused with such naming). A picture illustrating this is below:
For each linked pair of source and target interaction points the NavigationTrigger instance defines a trigger which should be called to perform the navigation. For example a trigger “Next” may cause a transition from “Step1” view to “Step2” view in a wizard-like application.
Notice that we don’t specify any view type in the InteractionPointInfo class since specific view implementations are the prerogative of view managers.
As we have decided to describe a task by its type let us find out how we can accompany a type definition in .NET with a task structural information. Interaction points can be declared in a form of constant fields inside the task type. This allows referencing interaction points in a compiler-checked way rather than using literal strings. For example one may call Navigate(MyTask.View1) instead of Navigate("View 1").
class WashDishesTask { // Below are three interaction point definitions // with the view names specified public const string SelectDishes = "Select dishes view"; public const string Dishwasher = "Dishwasher view"; public const string WashComplete = "Wash complete view"; }
Such constant field alone describes an interaction point party, specifying only the view name. We need a means to accompany such fields with controller type and navigation triggers declarations. A good way to equip language elements (fields, in particular) in .NET with some additional info is using .NET custom attributes. In our case it might look as follows:
class WashDishesTask { [InteractionPoint(typeof(SelectDishesController))] [NavTarget("Next", Dishwasher)] public const string SelectDishes = "Select dishes view"; [InteractionPoint(typeof(DishwasherController))] [NavTarget("Next", WashComplete)] [NavTarget("Previous", SelectDishes)] public const string Dishwasher = "Dishwasher view"; [InteractionPoint(typeof(WashCompleteController))] public const string WashComplete = "Wash complete view"; }
The suggested here approach for describing tasks seems to be more or less handy to start with. With it the revised version of the "Starting a task" use case looks so:
Starting a task (revision 3)
User: Add fields describing interaction points to the task type. Equip these
fields with [InteractionPoint] and [NavTarget] attributes. Then pass
the task type to the system and ask to start the task.
System: Extract the task information from its type. Create a task and a
navigator instances, initialize and link them together. Invoke the
OnStart operation on the task instance.
Accessing the controller from a view and vice versa
According to the MVP pattern views handle user gestures and then pass control to the corresponding controller (and again I recall that we are using the "controller" name instead of "presenter"). Moreover in MVP (in contrast to MVC) controllers may access their views as well. Hence it should be easy for a user to access the controller from a view code and vice versa. In MVP this is solved by linking together each view with the corresponding controller instance.
Accessing the controller from a view and vice versa
Precondition: View and its controller are linked to each other.
User: Access that associated controller/view.
For user’s convenience it should be the framework job to link views and controllers together. Later when designing classes we will discuss which class will be responsible for such linking.
Accessing the task and navigating from a controller
Controllers often need to request/modify their task state. So we may require each controller to be linked to its task.
Accessing the task from a controller
Precondition: Controller is linked to its task.
User: Access that associated task.
A controller may also need to trigger navigation to some view. This is done easily by accessing the task and then getting its navigator.
Navigating from within a controller
Precondition: Controller is linked to its task which is connected to the
navigator
User: Access the navigator through the associated task. Invoke the navigation.
We have discussed the most fundamental requirements for the future system. Based on these requirements we are going to proceed to designing classes.
Designing key classes
Above we have introduced a number of fundamental concepts such as task, navigator and others by analyzing requirements for our system. These concepts with the relationships between them make up so called analysis model of the system. Analysis classes from this model usually turn into design classes by being equipped with operations, additional attributes and other details.
Here we are going to walk through all the requirements we have formulated and to design classes based on the analysis model in order to meet these requirements.
TasksManager
First let us deal with the “Starting a task (revision 3)” use case and the TasksManager concept. According to this use case we may introduce a TasksManager class with a StartTask(taskType: Type) method. This method should create task and navigator instances, connect them to each other and invoke the task’s OnStart() method. It should also create a TaskInfo instance based on the task type. Tasks are designed by users however in order for the framework to communicate with task instances the latter should conform to some interface. Let us call it ITask. There is also a requirement we have missed: tasks should be able to access their tasks manager, that is why the TasksManager should also link the created task to itself.
public class TasksManager
{
public ITask StartTask(Type taskType)
{
TaskInfo ti = GetTaskInfo(taskType); // get TaskInfo from task type
Navigator n = new Navigator(); // create navigator
ITask t = CreateHelper.Create(taskType) as ITask; // create task
t.TasksManager = this; // link the created task to itself
n.Task = t; // connect the navigator to the task
t.Navigator = n; // and the task to the navigator
t.OnStart(); // invoke the task's OnStart()
return t;
}
}
GetTaskInfo - is a method that extracts task information from a task type. Above we suggested to describe tasks by inserting constant fields to the type definition. However there may be other ways to equip a task type with the task information. Hence different methods to extract such information may exist. We will isolate the extraction logic in a ITaskInfoProvider interface with a GetTaskInfo(taskType: Type): TaskInfo method.
public interface ITaskInfoProvider
{
TaskInfo GetTaskInfo(Type taskType);
}
It is worth keeping all configuration data including task and view descriptions in a centralized MVCConfiguration class. Then each tasks manager will be linked to its own MVCConfiguration instance:
public class TasksManager
...
public MVCConfiguration Config
public class MVCConfiguration
...
public ITaskInfoProvider TaskInfoProvider
A user may start a task of the same type more then once; and extracting the task information each time is redundant. That is why we need a repository object for all tasks configuration data. Let it be TaskInfoCollection instance.
public class MVCConfiguration
...
public TaskInfoCollection TaskInfos
If the necessary task info object already exists in the inner hash table the TaskInfoCollection will return it, otherwise it will extract a new TaskInfo object from the task type with the help of the TaskInfoProvider class:
public class TaskInfoCollection
{
private Hashtable taskInfos = new Hashtable();
private MVCConfiguration mvcConfig;
public TaskInfo this[Type taskType]
{
get
{
TaskInfo ti = taskInfos[taskType] as TaskInfo;
if (ti == null)
{
ti = mvcConfig.TaskInfoProvider.GetTaskInfo(taskType);
taskInfos[taskType] = ti;
}
return ti;
}
set { taskInfos[taskType] = value; }
}
}
Finally this is how the TasksManager.GetTaskInfo(...) method looks:
public class TasksManager
...
private TaskInfo GetTaskInfo(Type taskType)
{
return Config.TaskInfos[taskType];
}
Navigator
Now let us look into how the navigation occurs. Navigator class should have a public Navigate(..) method with a navigation trigger name passed as parameter.
For a navigator to switch to another view it needs to know the task navigation structure. Therefore it should be linked to the TaskInfo instance describing that task. Such linking can be done in the TasksManager.StartTask(...) method:
public class TasksManager
...
public ITask StartTask(Type taskType)
{
...
n.TaskInfo = ti;
...
}
Task information is not the only needed component for a navigator to do the navigation. Another important thing we have introduced in the analysis phase is the views manager concept. Its responsibility is actual view switching, with different views manager implementations capable of different presentation mechansims. The navigator will be connected to the views manager in the TasksManager.StartTask(...) method:
public class TasksManager
...
public ITask StartTask(Type taskType)
{
...
IViewsManager vm = CreateHelper.Create(Config.ViewsManagerType)
as IViewsManager;
n.ViewsManager = vm;
vm.Navigator = n;
...
}
Note that we are using the MVCConfiguration class to store the used views manager type.
Now we are ready to write code for the Navigator.Navigate(...) operation:
public class Navigator
{
...
public TaskInfo TaskInfo
...
public IViewsManager ViewsManager
...
public void Navigate(string triggerName)
{
string nextViewName = TaskInfo.GetNextViewName(Task.CurrViewName,
triggerName);
if (nextViewName == Task.CurrViewName) return;
NavigateDirectly(nextViewName);
}
public void NavigateDirectly(string viewName)
{
Task.CurrViewName = viewName;
ViewsManager.ActivateView(Task.CurrViewName);
}
}
Designing a simple views manager
Up to the moment we have roughly designed all key classes except for a views manager class. Let us build a simple IViewsManager implementation. Although simple, it will be a basis for more complicated real-life views managers.
To make our views manager as simple as possible let us assume that views are usual windows forms. Then our SimpleFormsViewsManager will be responsible for switching between those forms.
ViewInfo
In order to activate a form for the first time it needs to be created. Therefore the views manager should know the view type by its name. We will encapsulate the information about a view type in a ViewInfo class instance. Thus, given a view name, the view manager should retrieve the corresponding ViewInfo object through the intermediate ViewInfoCollection object.
public class ViewInfoCollection
...
public ViewInfo this[string viewName]
{
get { return viewInfoCollection[viewName] as ViewInfo; }
set { viewInfoCollection[viewName] = value; }
}
public class ViewInfo
...
public Type ViewType
Subsequent (second, third, etc.) view activations don’t require the view creation. Instead they require locating the already created view by its name. For this the views manager should have an association to a FormCollection class returning already created views by their names.
public class SimpleFormsViewsManager : IViewsManager
...
private Dictionary<string, Form> forms
= new Dictionary<string, Form>();
The question is where a views manager takes the view descriptions (ViewInfo objects) from. As views are parts of a task it is natural to store their descriptions within that task’s description:
public class TaskInfo
...
public ViewInfoCollection ViewInfos
This approach does not bind tasks to any specific presentation mechanism since the base ViewInfo class is independent of a specific presentation.
Next question is how a ViewInfoCollection gets populated. Obviously a user can modify the collection at runtime. However usually a task structure is known at design time, and a declarative syntax to describe it may apply. A good solution is to mark view types with a [View] attribute like this:
[View(typeof(Task1), “View1”)]
class Form1: Form
…
Here we declare that the TaskInfo object for Task1 should contain a ViewInfo instance pointing to the Form1 type. Of course there should be a class which will generate ViewInfo objects from such declarations. Let us assign this responsibility to a IViewInfosProvider interface with a GetFromAssembly(assembly:Assembly) operation. It will generate ViewInfo objects from the declarations in the input assembly:
public interface IViewInfosProvider
{
ViewInfosByTaskCollection GetFromAssembly(Assembly assembly);
}
public class DefaultViewInfosProvider : IViewInfosProvider
...
ActivateView implementation
In general the view activation mechanism is quite simple: the necessary form should be found by its name and then the Form.Show() and Form.Activate() methods should be called on it.
public class SimpleFormsViewsManager : IViewsManager
...
public void ActivateView(string viewName)
{
Form f = FindOrCreateView(viewName);
f.Show();
f.Activate();
}
The FindOrCreate operation above should create the view in case it does not exist yet. Of course a view creation implies certain initialization steps. These steps may be derived from the requirements to our system. Take a look at the “Accessing the controller from a view and vice versa” use case. It requires a view to be linked to the controller during the initialization process:
public class SimpleFormsViewsManager : IViewsManager
...
private Form FindOrCreateView(string viewName)
{
Form result;
if (!forms.TryGetValue(viewName, out result))
{
result = CreateHelper.Create(ViewInfos[viewName].ViewType) as Form;
forms[viewName] = result;
(result as IView).ViewName = viewName;
InitializeView(result as IView);
}
return result;
}
private void InitializeView(IView view)
{
view.Controller = Navigator.GetController(view.ViewName);
view.Controller.View = view;
}
In this code we make the Navigator class responsible for holding the controllers for its task. Navigator will also create and initialize controllers if needed. According to the “Accessing the task and navigating from a controller” use case a controller initialization should include its linking to the task:
public class Navigator
...
public IController GetController(string viewName)
{
IController result = controllers[viewName] as IController;
if (result == null)
{
InteractionPointInfo iPointInf = TaskInfo.InteractionPoints[viewName];
result = CreateHelper.Create(iPointInf.ControllerType) as IController;
result.Task = Task;
controllers[viewName] = result;
}
return result;
}
Manual view activation
What happens when a user himself clicks on a form and activates it? That means the user decides to do the navigation to the selected view. Thus the Navigator.Navigate(…) operation should be called in response to the manual view activation. We can implement this by handling the Form.Activated event:
public class SimpleFormsViewsManager : IViewsManager
...
void view_Activated(object sender, EventArgs e)
{
Navigator.TryNavigateToView((sender as IView).ViewName);
}
public class Navigator
...
public void TryNavigateToView(string viewName)
{
if (TaskInfo.CanNavigateToView(Task.CurrViewName, viewName))
Task.CurrViewName = viewName;
ViewsManager.ActivateView(Task.CurrViewName);
}
Navigator.TryNavigateToView(...) does the following: if navigation to the destination view is possible via any of the navigation tree ribs (i.e. CanNavigateToView returns true) then the destination view gets activated, otherwise the source view is activated. Thus if a user clicks on a view that is not accessible from the current one, then the task will remain in the current view and the views manager will switch back to the current view.
Summary
Throughout this article we have developed the core classes of the future MVP framework. These classes help users in fulfilling the main Model-View-Presenter usage scenarios, and establish a firm ground for the futher famework's growth and extension.
Note that the SimpleFormsViewsManager class sources as well as examples on using MVC# framework are bundled with MVC# sources and located in the "Examples" folder.
Part 3. Designing a Windows Forms views engine
Introduction
In the previous articles (parts 1-2) we have introduced a views manager concept for isolating the presentation mechanism of the MVP framework. This article describes the development of the real-life Windows Forms views manager and its attendant classes for our MVP framework.
The only responsibility of a views manager is switching between views. This might seem easy at first sight, however it becomes more tricky as we delve deeper into the presentation specifics and take into account pecularities of the views mechanism. For example in the previous part we have already created a simple Windows Forms views manager, however it is not able to treat user controls as views, neither it can handle dialogs or MDI forms.
That is why for creating a fully functional views manager we need to thoroughly analyze the corresponding presentation mechanism and construct requirements to that views manager. These requirements will typically include the description of possible view types, thier interrelation and so on. Thus our first step is building requirements to the constructed views manager. We will assume that the basic requirements of working with simple forms are already implemented (see the end of the previous article where we constructed a simple forms views manager) and will proceed to the more advanced demands.
Requirements
User control views
The starting point for building the first requirement will be the fact that a user might want to have more than one interaction point on his screen at a moment. Although separate, these views may be logically coupled, which discourages us from placing them onto different forms. A more plausible (and popular in modern GUIs) solution is putting views into different parts of a single form. For instance an orders view and an order lines view can be displayed as parts (e.g. upper and lower halfs) of a common window.
In .NET Windows Forms technology such views are implemented as user controls. They are designed separately, but finally are placed together and arranged on a single form. Thus in general our requirement sounds like this: UserControl class descedants may be used as views.
To express this requirement in a more precise form let us decide how a developer would mark a particular user control to be used as a view. Here two alternatives are possible: 1) Let the framework create an instance of the user control, 2) Create the user control istance manually and place it on some view. If a developer chooses 2 (to create an instance himself) then how will the framework distinguish the user control-view from an ordinary user control? The answer is quite obvious here: a user control class should implement the IView interface to make the MVP framework treat it as a view. So here is how the first use case looks:
User control views
User: Create a UserControl sublass which implements the IView interface.
Place its instance on some view. Assign a ViewName property to it.
System: Find this user control and initialize it (tie to the controller,
register in the system and so forth)
Here we have included the view name assignment to the user's actions. That is because a view initialization requires the knowledge of that view's name.
However the view registration and initialization are not the only necessary activities. We should also consider how a user control view should be activated. Unlike forms user controls cannot be activated, instead they have a Focus() method which moves in the input focus. However focusing a user control is of little use if the parent form is inactive, therefore we should also assure the parent form activation:
Activating a user control view
User: Trigger a user control view activation (via Navigator.Naviagate(.).
System: Activate the parent form and Call the Focus() method on the
user control view.
A manual user control view activation is possible too when a user clicks somewhere inside that user control. As a response the system should perform navigation to this view:
Manual user control view activation
End user: Click on a user control view (or somehow else move the focus
inside it).
System: Perform the navigation to this view.
MDI form views
Another kind of view we might want to use is the MDI form. Although slightly out of fashion nowdays, MDI forms may prove useful in various applications. That is why the next requirement will concern the usage of MDI forms as views.
Applying MDI forms in .NET is simple: the only thing needed is to specify the parent form by setting its isMdiContainer property to true and to set MdiParent property for all child forms. We could link child forms to the parent form instances by ourselves, however it is not as easy since the MVP framework and particulary the views manager itself creates forms and holds their instances. The better approach is to somehow tell the framework which views should act as MDI parents, and which ones should be their children. A good way of doing so is applying a .NET attribute with necessary parameters to the view type, like this: [WinformsView("ChildView", typeof(MainTask), MdiParent = "ParentView")]. So here is the next use case:
MDI form views
User: Equip the form type with a WinformsView attribute with MdiParent
or isMdiParent named parameters. Then at some point navigate to
the corresponding view.
System: Initialize the view as an MDI parent or child as specified.
With respect to the view activation mechanism MDI forms behave the same way as simple forms do. So let us turn to the next requirement.
Modal form views
Modal forms (dialogs) are very useful if we want a user to interact with only one form until he closes it. In .NET a form modality depends on the way how it is shown. Form.Show() displays form in an ordinary (not modal) state, while Form.ShowModal() displays it as modal. Anyway it is a job of the views manager to show forms, and we should somehow indicate it which forms to display as modal. As in previous requirement we may use a .NET attribute with a named parameter ShowModal: [WinformsView("ChildView", typeof(MainTask), ShowModal = true)]. This is the use case:
Modal form views
User: Equip the form type with a WinformsView attribute with a ShowModal
named parameter. Then navigate to the corresponding view.
System: Show the form as modal by calling Form.ShowDialog().
Different views with same type
Applying the [View("ViewName", ...)] or [WinformsView("ViewName", ...)] attribute to a view type we specify the concrete view type for the interaction point with "ViewName" view name. But what if another view should be of the same type? The answer is straightforward: allow users to specify several [(Winforms)View] attributes with different view names for a single view type:
Different views with same type
User: Equip a view type with several (Winforms)View attributes.
System: Treat this as descriptions of different views with the
same view type.
Notifications to views
It may be important for a view to react in a particular way when it gets (de)activated. For example a view may disable some controls on it when it loses focus. A view may also need to perform some initalization steps when it is activated for a first time. For this we need notifications to be sent to views whenever they are (de)activated or initialized (activated for a first time).
For handling a view (de)activation we might watch the change of the Task.CurrentViewName property. The view initialization can be done in the IView.Controller setter method. Much more straightforward, however, is to have explicit Activate(bool activate) and Initialize() operations, invoked by the views manager. These operations may be placed in a separate INotifiedView interface.
Notifications to views
User: Invoke navigation to/from a view that implements INotifiedView
interface.
System: Call Activate(true/false) on that view. Call Initialize() on
it if activated for a first time.
Base views implementations
Each form or user control we want to be a view should implement the IView interface. It would be convenient if the framework provides developers with base form and user control classes which already implement IView and even INotifiedView interfaces. For example these could be WinFormView and WinUserControlView classes.
Now we have finished with building requirements and will proceed to the implementation phase.
Implementation
General classes
First we will implement the WinformsView attribute with optional parameters as defined in the "MDI form views" and "Modal form views" use cases:
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class WinformsViewAttribute : ViewAttribute
{
...
public WinformsViewAttribute(Type taskType, string viewName)
: base(taskType, viewName) { }
public WinformsViewAttribute() { }
public bool ShowModal
...
public bool IsMdiParent
...
public string MdiParent
...
}
In the previous article we have introduced the DefaultViewInfosProvider class for processing [View] attributes. For each met [View] attribute it created a ViewInfo instance. Similarly we should treat [WinformsViewAttribute] attributes, except for that WinformsViewInfo objects should be created instead of simple ViewInfo objects.
public class WinformsViewInfosProvider : DefaultViewInfosProvider
{
protected override ViewInfo newViewInfo(Type viewType, ViewAttribute viewAttr)
{
WinformsViewInfo viewInfo = new WinformsViewInfo(viewType);
if (!(viewAttr is WinformsViewAttribute)) return viewInfo;
viewInfo.IsMdiParent = (viewAttr as WinformsViewAttribute).IsMdiParent;
viewInfo.MdiParent = (viewAttr as WinformsViewAttribute).MdiParent;
viewInfo.ShowModal = (viewAttr as WinformsViewAttribute).ShowModal;
return viewInfo;
}
}
public class WinformsViewInfo : ViewInfo
{
public WinformsViewInfo(Type viewType) : base(viewType)
{ }
public bool ShowModal
...
public bool IsMdiParent
...
public string MdiParent
...
}
Notice that "AllowMultiple = true" is specified for the WinformsViewAttribute (as well as for the ViewAttribute). By doing so we meet the "Different views with same type" requirement.
One more simple requirement we will implement before proceeding to the more complicated ones is the "Base views implementations" requirement. For that we will create Form and UserControl descedants and make them implement IView and IWinformsView interfaces in a simpliest way: with the use of virtual properties with backing fields and empty virtual methods:
public class WinFormView : Form, IView, IWinformsView
{
// IView and IWinformsView implementations with virtual
// methods and virtual properties with backing fields
...
}
public class WinUserControlView : UserControl, IView, IWinformsView
{
// IView and IWinformsView implementations with virtual
// methods and virtual properties with backing fields
...
}
\
WinformsViewsManager class
WinformsViewsManager class will inherit from ViewsManagerBase - a simpliest IViewsManager implementation:
public class WinformsViewsManager : ViewsManagerBase
...
public override void ActivateView(string viewName)
{
IView view = FindOrCreateView(viewName);
NotifyViewsOnActivation(view);
if (view is Form)
ActivateFormView(view);
else if (view is UserControl)
ActivateUserControlView(view);
}
In the ActivateView method above we do the following: get already created view from an internal hash or create a new one and activate this view in a manner depending on the view kind. Between these two steps we notify views about their (de)activation in the NotifyViewsOnActivation(...) method - this is required by the "Notifications to views" use case:
public class WinformsViewsManager : ViewsManagerBase
...
private void NotifyViewsOnActivation(IView activatedView)
{
IWinformsView prevActiveWFView = prevActiveView as IWinformsView;
if (prevActiveWFView != null) prevActiveWFView.Activate(false);
IWinformsView winformsView = activatedView as IWinformsView;
if (winformsView != null) winformsView.Activate(true);
prevActiveView = activatedView;
}
FindOrCreateView(...) method instantiates views if they do not exist yet. Type information is taken from the appropriate WinformsViewInfo object. Then, depending on the view type (form or user control), the corresponding view initialization method is invoked:
public class WinformsViewsManager : ViewsManagerBase
...
private IView FindOrCreateView(string viewName)
{
IView result = views[viewName] as IView;
if (result == null)
{
WinformsViewInfo viewInf = ViewInfos[viewName] as WinformsViewInfo;
result = CreateHelper.Create(ViewInfos[viewName].ViewType) as IView;
result.ViewName = viewName;
if (result is UserControl)
InitializeUserControlView(result as UserControl);
else if (result is Form)
InitializeFormView(result as Form, viewInf);
}
return result;
}
Below are the methods for user control and form initialization:
public class WinformsViewsManager : ViewsManagerBase
...
protected virtual void InitializeUserControlView(UserControl userControlView)
{
InitializeView(userControlView as IView);
userControlView.Enter += new EventHandler(view_ActivatedManually);
NotifyInitialize(userControlView as IView);
InitializeChildViews(userControlView);
}
protected virtual void InitializeFormView(Form form, WinformsViewInfo viewInf)
{
InitializeView(form as IView);
form.Activated += new EventHandler(view_ActivatedManually);
form.IsMdiContainer = viewInf.IsMdiParent;
string mdiParent = viewInf.MdiParent;
if (mdiParent != null)
form.MdiParent = views[mdiParent] as Form;
NotifyInitialize(form as IView);
InitializeChildViews(form);
}
Both these methods use the InitializeView(...) and NotifyInitialize(...) methods which contain common initialization steps regardless of the view type. InitializeView(...) binds together a view with its controller. NotifyInitialize(...) sends an "Initialize" message to the view accordingly to the "Notifications to views" requirement:
public class WinformsViewsManager : ViewsManagerBase
...
private void InitializeView(IView view)
{
views[view.ViewName] = view;
view.Controller = Navigator.GetController(view.ViewName);
view.Controller.View = view;
}
private void NotifyInitialize(IView view)
{
INotifiedView winformsView = view as INotifiedView;
if (winformsView != null)
winformsView.Initialize();
}
Note that the InitializeFormView(...) method contains code specific to views represented as forms: if needed, it makes a form MDI child or parent, thus satisfying the "MDI form views" requirement.
InitializeChildViews(...) - is a method that searches for user control views inside a form or another user control view. The search is done recursively, for found user control views the user control-specific InitializeUserControlView(...) method is called. By doing so we implement the "User control views" use case:
public class WinformsViewsManager : ViewsManagerBase
...
protected void InitializeChildViews(Control container)
{
foreach (Control c in container.Controls)
{
IView childView = c as IView;
if ((childView != null) && (!IsInitialized(childView)))
InitializeUserControlView(childView as UserControl);
else
InitializeChildViews(c);
}
}
By handling the Enter event of user controls we meet the "Manual user control view activation" requirement:
private void view_ActivatedManually(object sender, EventArgs e)
{
Navigator.TryNavigateToView((sender as IView).ViewName);
}
The last two methods left are the view activation methods ActivateFormView and ActivateUserControlView. The former shows and makes active a form view, taking into account that it could be configured as modal (and thus meeting the "Modal form views" requirement):
public class WinformsViewsManager : ViewsManagerBase
...
private void ActivateFormView(IView view)
{
Form form = view as Form;
WinformsViewInfo viewInf = ViewInfos[view.ViewName] as WinformsViewInfo;
if (viewInf.ShowModal)
{
if (!form.Visible) form.ShowDialog();
}
else
{
form.Show();
form.Activate();
}
}
ActivateUserControlView(...) method not only focuses the user control but it firstly activates the parent of this control, thus implementing the "Activating a user control view" use case:
public class WinformsViewsManager : ViewsManagerBase
...
private void ActivateUserControlView(IView view)
{
UserControl uc = view as UserControl;
uc.Focus();
uc.FindForm().Show();
uc.FindForm().Activate();
}
Summary
Throughout this article we have been building a comprehensive Windows Forms views engine for the Model-View-Presenter framework. We have started with a list of requirements for the future views engine and then implemented these requirements in the WinformsViewsManager class and other satellite classes. As a result these classes comprise a fully-functional views engine suitable for various MVP applications with Windows Forms-based UI. However it is not restricted to further extend this views engine tailoring it for specific needs.
Part 4. Strongly typed associations
Introduction
In the previous parts (1, 2, 3) we have constructed a quite usable and functional framework. However it still has some drawbacks. One of them is the necessity of typecasting when accessing controllers, views and tasks. The example below demonstrates such typecasting:
public class ProductsView : WebFormView, IProductsView ... private void ShowProductDetailsButton_Click(object sender, EventArgs e) { (Controller as ProductsController).ShowProductDetails(); // typecasting required }
As a system grows such typecasts may bloat code excessively decreasing its readability and leading to errors. To eliminate this drawback we need a means of explicitly specifying the type of the associated controller (or view/task). In other words we need to make associations between tasks, controllers and views strongly typed.
Solution outline
The most obvious solution is to isolate the typecasing operation in a new property of the required type:
public class ProductsView : WebFormView, IProductsView ... private new ProductsController Controller { get { return base.Controller as ProductsController; } set { base.Controller = value; } } private void ShowProductDetailsButton_Click(object sender, EventArgs e) { Controller.ShowProductDetails(); // typecasting NOT required }
Although acceptable, this solution requires several additional lines of code. More elegant solution can be constructed with a handy feature of .NET framework called Generics. Generics mechanism allows varying a class members' types by specifying those types in the class declaration. For example it is possible to adjust a return type for certain properties by writing that type in brackets in the class definition line.
Applying Generics we could strictly specify the type of the association between a view and its controller as in the code below:
public class ProductsView : WebFormView<ProductsController>, IProductsView ... private void ShowProductDetailsButton_Click(object sender, EventArgs e) { Controller.ShowProductDetails(); // typecasting NOT required }
To make the above code workable we need to extend our framework, adding the generics support.
Solution implementation
First of all we will add generic view and controller interfaces to the framework. They will extend old IView and IController interfaces with new strongly typed generic associations:
public interface IView<T> : IView where T : IController { new T Controller { get; set; } }
public interface IController<TTask, TView> : IController where TTask : ITask { new TTask Task { get; set; } new TView View { get; set; } }
These interfaces alone do provide strongly typed associations, however we also need to write some base generic implementation classes for these interfaces. So a developer will only inherit these base classes instead of implementing the interfaces above.
We will implement the properties simply with backing fields and mark them virtual so that a developer may override them in subclasses:
public class WinFormView<T> : Form, IView<T> where T : class, IController { ... protected T controller; public virtual T Controller { get { return controller; } set { controller = value; } } IController IView.Controller { get { return Controller; } set { Controller = value as T; } } ... }
public class ControllerBase<TTask, TView> : IController<TTask, TView> where TTask : class, ITask where TView : class { protected TTask task; protected TView view; public virtual TTask Task { get { return task; } set { task = value; } } public virtual TView View { get { return view; } set { view = value; } } ITask IController.Task { get { return Task; } set { Task = value as TTask; } } IView IController.View { get { return View as IView; } set { View = value as TView; } } }
Note that the non-generic
IView
and IController
interfaces are implemented as gateways to the strongly typed generic properties. This makes the access in the old non-generic manner (as done by the framework) equivalent to accessing the new strongly typed properties.
Below is an example of using the new generic features of the framework:
class MyController : ControllerBase<MyTask, IMyView> { public void MyOperation() { View.MyViewOperation(); // Typecasting to IMyView NOT required } public override MyTask Task { get { return base.Task; } set { base.Task = value; // Controller initialization here ... } } }
Summary
In the article we have developed new framework features which make it more usable and allow to avoid typecasting errors.
No comments:
Post a Comment