Build an AJAX Content Management System with Visual WebGUI: Creating a Framework

he Mini CMS system begun in the first article in this series needs to have some base components that represent the actual workplace. These will support loading and showing modules inside the workplace, passing control between modules, and unloading them when their tasks are complete. Modules should be able to call other modules and pass some parameters. The called module should be able to return control back to the calling module and also return an exit status (if the user saved changes or canceled), so the calling module can update itself accordingly.

To make the framework flexible, it’s best to define its components through interfaces. That will prevent binding the implementation to specific existing controls and allow future enhancements or implementation changes.

The Workplace and Module Components

?
Figure 1. IWTWorkplace and IWTModule Interfaces: These two core framework interfaces support loading, unloading, and showing modules.

The Workplace and Module components are the foundational bricks for the application. The Workplace is actually a container, which can load functional modules. Those loaded modules handle the real work of the application.

The Workplace and the Module expose the following interfaces:

  • IWTWorkplace: Defines the interface for a Workplace component, as shown in Figure 1. Components implementing this interface implement an actual workplace, and allow loading and showing modules, check if an existing module is loaded, hide all modules, removing a module. Table 1 shows its properties and methods.
  • IWTModule: Defines the interface for a Module component (see Figure 1). All modules must implement this interface. The properties and methods are described in Table 2.
Table 1. IWTWorkplace Interface: This table shows the properties and methods exposed by the IWTWorkplace interface.
Property Description
Control BodyPanel{ get; } Returns a reference to a control that act as a workplace container. In Visual WebGUI, all components derived from Control expose a Controls property (a ControlsCollection), which keeps track of all the contained controls.
string Caption
??{ get; set;}
Sets or gets the caption value for the container. By implementing the IWTWorkplace as a HeaderedPanel or GroupBox, you can set the title label of the GroupBox or the header of the HeaderedPanel.
Method Description
IWTModule LoadModule(
IWTModule module,
IWTModule parentModule)
Loads a module as the active module and returns a reference to it. The second parameter, parentModule, is a reference to the parent IWTModule (the module this one was started from). The parentModule parameter lets you control which module will became active after the newly loaded module is closed. parentModule can be null, in which case the current module has no parent. This is useful for the very first module in IWTWorkplace, or when there are no dependencies on any other previously loaded module.
IWTModule LoadModule(
??IWTModule module,
??IWTModule parentModule,
??bool loadVisible);
Same as previous method, except that it supports loading hidden modules?which is useful when the program needs to perform additional tasks on a loaded module before showing it.
void ShowModule(
??IWTModule module);
Activates a loaded module.
bool CheckModuleExists(
??IWTModule module);
Check whether the specified module has been loaded into the IWTWorkplace.
void RemoveModule(
??IWTModule module);
Removes the specified module from the IWTWorkplace.
void HideAllModules(); Hides all modules.

Table 2. IWTModule Interface Properties and Methods: Each Module component must implement this interface.
Properties Description
IWTModule CallerModule
??{ get; set; }
Gets or sets a reference to the calling module. Can be used to access properties or methods of the calling module.
IWTWorkplace HostWorkplace
??{ get; set; }
Reference to workplace component where the module is loaded.
Method Description
void Refresh(
??WTCancelGenericEventArgs e)
This method should be called by the IWTWorkplace component when a module is closed and control should pass back to the calling module. The closing module raises the Close event, which receives arguments of type WTCancelGenericEventArgs. The workplace passes these to the calling module, by calling the Refresh method.
event WTCancelGenericEventHandler Close; The IWTModule fires this event when it closes.

The interaction between IWTModule and IWTWorkplace is driven by the WTWorkplaceController class. This class contains only static methods (see Table 3), which handle the plumbing between modules and workplaces.

Table 3. WTWorkplaceController Static Methods: The WtWorkplaceController exposes only static methods.
Method Description
public static
?IWTModule LoadModule(
???IWTWorkplace workplace,
???IWTModule module,
???IWTModule parentModule,
???bool loadVisible)
Loads a module into a workplace, and lets you specify a parent module (the module that launched the current module) and whether the new module will be loaded as visible or hidden.
public static
?void ShowModule(
???IWTWorkplace workplace,
???IWTModule module)
Shows a module already loaded into a workplace.
public static
?void RemoveModule(
???IWTWorkplace workplace,
???IWTModule module)
Removes a module from the workplace.
public static
?void HideAllModules(
???IWTWorkplace workplace)
Hides all modules from a workplace.
public static
?bool CheckModuleExists(
???IWTWorkplace workplace,
???IWTModule module);
Checks whether a module is loaded into a workplace.

Using the Workplace and Module Classes

The LoadModule method takes care of loading a module into the workplace container and binding appropriate events to allow passing control between the calling and called modules:

public static IWTModule LoadModule(IWTWorkplace hostPanel,   IWTModule module, IWTModule parentModule, bool loadVisible){  Control control = module as Control;  if (control == null)    module = null;  else {    Control hostBody = hostPanel.BodyPanel;    // assign a unique name to the module we add,     // based on existing controls already loaded    int currentControlCnt = hostBody.Controls.Count + 1;    control.Name = hostBody.Name + "_ctrl" + currentControlCnt;    control.Location = new System.Drawing.Point(1, 1);    control.Dock = DockStyle.Fill;    hostBody.Controls.Add(control);    // if we don't have to show the control right now     // then just don't do anything    if (!loadVisible)      control.Visible = false;    else       ShowModule(hostPanel, module);    // set reference to caller module    module.CallerModule = parentModule;    // finally, set reference to workplace panel    module.HostWorkplace = hostPanel;    // bind close event to the controller, so it can get notified     // when a module needs to close, and perform required actions    module.Close +=new WTCancelGenericEventHandler(Module_Close);  }  return module;}

The handler for the Close event removes the module from the workplace, and optionally passes control back to the calling module, along with arguments sent by the called module:

static void Module_Close(object sender, WTCancelGenericEventArgs e) {  IWTModule module = sender as IWTModule;  if (module != null)  {    // ret reference to caller module and the workplace    IWTModule parentModule = module.CallerModule;    IWTWorkplace workplace = module.HostWorkplace;    // and removes the module from workplace to unload it    workplace.RemoveModule(module);    if (parentModule != null)    {      // if I have reference to parent module      // check if the module was not canceled, and if so      // call Refresh method of the caller module passing       // it received arguments, so it can update itself      if (!e.Cancel)        parentModule.Refresh(e);      // get reference to workplace the parent is loaded into      workplace = parentModule.HostWorkplace      // and activate the parent      workplace.ShowModule(parentModule);    }  }}

Two interesting things in the preceding code are WTCancelGenericEventHandler and WTCancelGenericEventArgs. WTCancelGenericEventHandler is a delegate designed to work in conjunction with WTCancelGenericEventArgs, which implements a cancelable generic event-based mechanism that you can use to pass essentially any data as EventArgs between modules. You’ll see more about these a little later.

With the interfaces and the WTWorkplace controller in place, you can create the actual components. Both the workplace and module inherit from UserControl, and they implement IWTWorkplace and IWTModule, respectively. Here’s the code for the WTWorkplace component:

public partial class WTWorkplace : UserControl, IWTWorkplace {   protected Control m_container;   public WTWorkplace(){      InitializeComponent();      m_container = (Control)this;   }   public string Caption {      get { return this.Text; }      set {          m_container.Text = value;      }   }   public virtual Control BodyPanel {      get {         if (m_container == null)            m_container = this;         return m_container;       }   }   public IWTModule LoadModule(IWTModule module,       IWTModule parentModule) {      return WTWorkplaceController.LoadModule(         this, module, parentModule, true);   }   public IWTModule LoadModule(IWTModule module,       IWTModule parentModule, bool loadVisible) {      return WTWorkplaceController.LoadModule(         this, module, parentModule, loadVisible);    }   public void ShowModule(IWTModule module) {      WTWorkplaceController.ShowModule(this, module);   }   public void RemoveModule(IWTModule module) {      WTWorkplaceController.RemoveModule(this, module);   }   public void HideAllModules() {      WTWorkplaceController.HideAllModules(this);   }   public bool CheckModuleExists(IWTModule module) {      return WTWorkplaceController.CheckModuleExists(this, module);   }}

The code is simple and straightforward. All the methods are just wrappers around similar methods defined in the WTWorkplaceController class.

In the downloadable code for this article, you will also find the WTHeaderedWorkplace class.

Author’s Note: Due to rapid Visual WebGUIdevelopment, the current version is 6.3.8a. The downloadable code was tested with that version. To build and run the sample solution from the file, you need to download and install Visual WebGUI Professional Edition, and register it as demo/trial. You can test the code with the Express edition, which is free, but to do that you would have to reorganize the code, because Visual Studio Express does not allow web projects, so the code must be organized as a web site.

The WTHeaderedWorkplace class is a workplace component implemented as a HeaderedPanel, with a simple implementation:

  • Create a new component named WTHeaderedWorkplace that inherits from WTWorkplace.
  • Add a HeaderedPanel to it and use the following code for its constructor:
public partial class WTHeaderedWorkplace : WTWorkplace {    public WTHeaderedWorkplace() {InitializeComponent();        headeredPanel.Dock = Gizmox.WebGUI.Forms.DockStyle.Fill;        m_container = headeredPanel;    }}

The code for WTModule derives from UserControl and implements IWTModule. Again, the members of IWTModule are very straightforward; they are simply getters/setters for two private fields that hold references to the workplace and parent modules:

    // ?    private IWTWorkplace m_hostPanel;    private IWTModule m_callerModule;    public IWTWorkplace HostWorkplace {        get { return m_hostPanel; }        set { m_hostPanel = value; }    }    public IWTModule CallerModule {        get { return m_callerModule; }        set { m_callerModule = value; }    }

The Refresh method is implemented as a virtual method, so you must override it in the application’s modules. It performs the task of updating the calling module, when the called module was closed with data to save:

public virtual void Refresh(WTCancelGenericEventArgs e){ }

WTModule contains also three important versions of the protected CloseModule method, each with a different signature:

// CloseModule should be called when the module needs to be closed// it fires Close event, which is intercepted by WTWorkplaceController    protected void CloseModule() {   CloseModule(false);}protected void CloseModule(bool cancel) {   CloseModule(new WTCancelGenericEventArgs(cancel));}protected void CloseModule(WTCancelGenericEventArgs e){   if (Close != null)      Close(this, e);}               

The module must call one of these methods when it needs to close. Call the no-argument version when the module is no longer relevant and needs to be closed. Call the second version, CloseModule(bool cancel), in the following situations:

  • When the module closes due to a Cancel button click, and pass true as the parameter
  • When the module is closing but no data needs to be saved; pass false as the parameter
  • When the module is closing with some data saved, but the calling module doesn’t need any data

Finally, call the third method when the module is closing but some data needs to be passed to the calling module, as exemplified in the ModuleName and Dashboard modules in the downloadable sample application:

// in called module, in the event handler for Save buttonprivate void buttonSave_Click(object sender, EventArgs e) {    WTCancelGenericEventArgs evt = new WTCancelGenericEventArgs();    evt.AddEventData("name", textName.Text);    CloseModule(evt);}// . . .// in caller module, the code for Refresh methodpublic override void Refresh(WTCancelGenericEventArgs e){    if (e.Cancel)        statusBar1.Text = "Name was canceled";    else {        if (e.ExistsEventData("name"))            statusBar1.Text =                 string.Format("Welcome, {0}",                     e["name"].ToString());    }}

That’s all the required code. Now you need to register and set a startup form.

Registering and Configuring Startup Forms

The WTWorkplaceController handles the plumbing between the calling and called modules. In the example Form1 file, you can see examples with Workplaces and Modules. Form 1 has two IWTWorkplaces. Into the first one it loads an IWTModule (Dashboard) that acts as a menu where the menu items launch various sample modules, either in the same Workplace, or in the right-hand workplace. You must set Form1 as the startup form for the VWG project.

In VWG, you can designate more than one form as a start point for an application; each one is considered a different application. Each form can have an associated name using the .wgx extension, which can differ from the actual name of the form.

To configure the start points, first build the project, and then open the Project Properties dialog, and select the Registration tab (Registration is a special tab that appears only for Visual WebGUI projects, as shown in Figure 2). If you do not see the Registration tab, you need to enable Visual WebGUI for the project (see the first article in this series for more information).

?
Figure 2. Application Configuration: You can set several different forms as application entry points. You configure these in the Registration tab of a Visual WebGUI Project.

?
Figure 3. Startup Form for Visual Studio: Set the Start Action to “Specific Page,” and then choose the form where you want the application to start in Visual Studio.

In the Registration section, Applications table, you can configure various starting points for the solution. As Figure 2 shows, the top button lets you select a form defined in the solution, and register it as an Application (a start point). You can unregister Applications with Delete or rename them with the bottom button. That last button is the way you change the associated application name for a form. For example, you may want to name your form frmHumanResources.frm, but want to access it as HumanResources.wgx. You can do this by changing the Application name for the frmHumanResources.frm.

While you can start the application with any form when it is deployed on the web server, and you access the application through a browser, when you start it from within Visual Studio, you, must still specify a single startup web page. You do this in Project properties, in the Web section. Set the Start Action to “Specific Page” and enter the application name of one of the forms you registered as VWG applications in the Registration section (see Figure 3).

Designing a Flexible Event-Based Infrastructure

To facilitate flexible communication between modules, the example uses two delegated and corresponding generic EventArgs classes, which let you pass any data between modules. Figure 4 shows the EventArgs class and its delegates.

?
Figure 4. Generic Events: Here’s the class definition for the generic event arguments.

The WTGenericEventHandler delegate is designed to work in conjunction with WTGenericEventArgs, while the WTCancelGenericEventHandler works with WTCancelGenericEvent.

The WTGenericEventArgs class lets you add custom, arbitrary data, which makes it easy to exchange data between objects that fire events and objects that subscribe to those events. Table 4 shows the class’s properties and methods.

Table 4. WTGenericEventArgs Properties and Methods: You can use this class to pass arbitrary data between objects during event-handling.
Property Description
string EventType
??{ get; set; }
This is as an optional identifier or name for the event. Event handlers can decide what to do with the event data based on this value. For example, an event dispatcher that monitors events from multiple components can use the EventType to determine which target components should receive events.
bool IsEventDataAvailable
??{ get; }
Returns true if the event argument has any custom data available (e.g. the Hashtable implementing the EventData member is not null; it contains some objects.)
Method Description
void AddEventData(
??string key,
??object data)
Add some key/value data to the event argument. The data can be retrieved later in the event handler.
void RemoveEventData(
??string key)
Removes data with the specified key from the event argument.
void ClearEventData() Clears all event data.
bool CheckEventDataExists(
??string dataKey)
Detemines whether event data exists for a specific key.

The WTGenericEventArgs class implements an indexer property so you can set and read event data easily, using the following code:

// . . .WTCancelGenericEventArgs evt = new WTCancelGenericEventArgs();evt["Name"] = textName.Text;evt["Age"] = textAge.Text;evt["City"] = textCity.Text;string userName = evt["Name"];         // return the name // if data for a certain key was not set// returns  nullstring userCountry = evt["Country"];   // userCountry is null

It also implements the IEnumerable interface, so you can use foreach to retrieve all the data associated with an event.

WTCancelGenericEventArgs derives from WTGenericEventArgs, and implements a Cancel property. The IWTModule Close event handler is of type WTCancelGenericEventHandler. In addition to sending event data from the called to the calling module, it allows the calling module to detect whether the called module was closed with Cancel. That’s convenient, so it does not have to refresh, and can perform additional tasks to handle the Cancel situation.

Playing with Workplaces and Modules

You’ve seen how to build a sample application that uses workplaces and modules. The main form holds two workplaces; one appears on the left and one on the right side of the form. The left workplace loads a Dashboard module, which can launch other modules that then appear either in the right-hand workplace, or replace the Dashboard in the left-hand workplace. Figure 5 and Figure 6 show examples.

?
Figure 5. Sample Application: Here’s the sample application with the Dashboard module loaded into the left workplace, and a browser module loaded into the right workplace.

?
Figure 6. Sample Application with Data Entry Module: In this view of the sample application, you can see the browser module in the right-hand workplace, and a Data Entry module (launched from the Dashboard) loaded into the left-hand workplace.

Using the WTWorkplace and WTModule classes is simple. The workplaces in the sample application are named mainPanel (the left workspace) and workPanel (the right workspace). The Dashboard module derives from WTModule, and adds one new property, TargetWorkplace, which initially holds a reference to the right-hand workplace, workPanel.

Here’s the code that loads the Dashboard into the main form:

private void Form1_Load(object sender, EventArgs e) {    Dashboard d = new Dashboard();    mainPanel.LoadModule(d, null);    d.TargetWorkplace = workPanel;}

Now the dashboard can load any of the modules either in its own workplace (replacing itself), or in the right workplace. For example:

// Dashboard loads Browser module in right workpanel TargetWorkplace.LoadModule(new BrowserModule(), this);// . . .// Dashboard loads data entry module in own workpanel HostWorkplace.LoadModule(new ModuleName(), this);

When a called module needs to close, it calls the CloseModule(?) method. If it needs to pass some data back to its calling module, it can be pass that data via the WTCancelGenericEventArg object that CloseModule gets as an argument, as in the following code:

// in called module, in the event handler for Save buttonprivate void buttonSave_Click(object sender, EventArgs e) {    WTCancelGenericEventArgs evt = new WTCancelGenericEventArgs();    // add data to pass back to calling module    Evt["FirstName"]= textFirstName.Text;    Evt["LastnName"]= textLastName.Text;    Evt["Age"]= textAge.Text;    CloseModule(evt);}

The calling module can access the data by key or by using foreach to iterate through all the data fields:

// . . .// in caller module, the code for Refresh methodpublic override void Refresh(WTCancelGenericEventArgs e){    // if cancel is true, the called module was closed with cancel button    if (e.Cancel)        statusBar1.Text = "Name was canceled";    else {        ContactBO contact = new ContactBO();        Contact.FirstName = e[?FirstName?];        Contact.LastName = e[?LastName?];        statusBar1.Text =             string.Format("Welcome, {0}",                 e["FirstName"].ToString());    }}

The downloadable code contains sample solutions for both Visual Studio 2005 and 2008. You just need to set Web.Config to the correct version for your development platform.

Share the Post:
Share on facebook
Share on twitter
Share on linkedin

Overview

Recent Articles: