Click here to Skip to main content
15,881,380 members
Articles / Programming Languages / C#
Article

Building an MVP Framework for .NET. Part 3: Designing a Windows Forms Views Engine

Rate me:
Please Sign up or sign in to vote.
4.13/5 (18 votes)
12 Feb 2008CPOL9 min read 90.9K   1.6K   83   13
This article describes the development of the fully functional Windows Forms views engine for the MVP Framework being constructed.

Table of Contents

Introduction

In the previous articles (parts 1 and 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 peculiarities 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 can it 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, their 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 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 halves) of a common window.

In .NET Windows Forms technology, such views are implemented as user controls. They are designed separately, but are finally placed together and arranged on a single form. Thus in general our requirement sounds like this: UserControl class descendants 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 instance 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 subclass 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 nowadays, 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 particularly 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 in which 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 which forms to display as modal. As in the 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 initialization steps when it is activated for the first time. For this we need notifications to be sent to views whenever they are (de)activated or initialized (activated for the 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:

C#
[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.

C#
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 descendants and make them implement IView and IWinformsView interfaces in the simplest way: with the use of virtual properties with backing fields and empty virtual methods:

C#
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 - the simplest IViewsManager implementation:

C#
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:

C#
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:

C#
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:

C#
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:

C#
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:

C#
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:

C#
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):

C#
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:

C#
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.

Project Website

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Team Leader
Russian Federation Russian Federation
Oleg Zhukov, born and living in Russia is Lead Engineer and Project Manager in a company which provides business software solutions. He has graduated from Moscow Institute of Physics and Technology (MIPT) (department of system programming) and has got a M.S. degree in applied physics and mathematics. His research and development work concerns architectural patterns, domain-driven development and systems analysis. Being the adherent of agile methods he applies them extensively in the projects managed by him.

Comments and Discussions

 
QuestionGreat Article BUT how can i "in real world" take advantage of this? Pin
TheCubanP29-Sep-10 6:23
TheCubanP29-Sep-10 6:23 
AnswerRe: Great Article BUT how can i "in real world" take advantage of this? Pin
Oleg Zhukov30-Sep-10 12:50
Oleg Zhukov30-Sep-10 12:50 
QuestionWebUserControlView? Pin
boogo217-Jun-08 18:34
boogo217-Jun-08 18:34 
AnswerRe: WebUserControlView? Pin
Oleg Zhukov20-Jun-08 5:05
Oleg Zhukov20-Jun-08 5:05 
GeneralRe: WebUserControlView? Pin
Ruben Chakhmakhchyan27-Jun-08 2:56
Ruben Chakhmakhchyan27-Jun-08 2:56 
GeneralRe: WebUserControlView? Pin
Oleg Zhukov27-Jun-08 7:27
Oleg Zhukov27-Jun-08 7:27 
QuestionWhat about CAB? Pin
MeC++19-Feb-08 9:26
MeC++19-Feb-08 9:26 
AnswerRe: What about CAB? Pin
Oleg Zhukov21-Feb-08 19:32
Oleg Zhukov21-Feb-08 19:32 
AnswerRe: What about CAB? Pin
GerhardKreuzer29-Sep-08 9:10
GerhardKreuzer29-Sep-08 9:10 
GeneralSource Code Pin
Razan Paul (Raju)13-Feb-08 3:08
Razan Paul (Raju)13-Feb-08 3:08 
GeneralRe: Source Code Pin
Oleg Zhukov13-Feb-08 15:17
Oleg Zhukov13-Feb-08 15:17 
GeneralRe: Source Code Pin
camilohe14-Feb-08 4:00
camilohe14-Feb-08 4:00 
GeneralRe: Source Code Pin
Oleg Zhukov14-Feb-08 14:30
Oleg Zhukov14-Feb-08 14:30 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.