Click here to Skip to main content
Click here to Skip to main content

The Application Automation Layer - Using XML To Dynamically Generate GUI Elements--Forms And Controls

, 22 Jun 2003
Rate this:
Please Sign up or sign in to vote.
Exploring the issues of runtime form and control generation as specified by XML files.

Part I
Part II
Part III
Part IV

Table Of Contents

Introduction
Managing Forms
A Plethora Of Forms
The Bootstrap Process
The Schemas
Form Definition Schema (FormType)
Control Set Schema (ControlInfoType)
Header Schema (HeaderType)
Column Schema (ColumnType)
The Basic Object Model
.NET's Form vs MFC's Frame/View
Basic Forms
SDI Forms
Dialog Forms
MDI Forms
Basic Controls
TextBox: A Simple Control Example
Label: A Customized Control Example
Things That Are Both Controls And Forms
Panels
TabControls
Discussion: Attributes
Data Management
Event Management
The AAL In Action: Photo Album Prototype
XML Files
Look Ma, No Hands!
Discussion: Using XML
The Distribution File
Conclusion

Introduction

This article demonstrates, within the growing AAL framework, the use of XML to control the visual layout of forms, controls within forms, and the management of events.  As in the previous article, which discussed XML driven menus generated at runtime, this article focuses on XML driven GUI elements.  Within the context of the .NET environment, this includes forms (MDI, SDI, dialog), controls contained within forms, form/control event specification, and data container association.

Keep in mind that the work here represents a prototype in many ways--the XML schema, the form object model, the features (and lack thereof) that's been implemented, etc.  This code will most likely undergo considerable refinement as case studies using the AAL are developed.  Also, the much promised SourceForge site has not happened yet except for getting approval for the AAL project and some tinkering.  I am definitely interested in finding talented people to work on this project, especially in the area of GUI-based design and modeling tools to support Form generation, component connectivity, and data modeling.  The point being, this is a work in progress and it can be fun to see how something evolves over time and to see that there's a lot of room for others to contribute to the design and implementation.

Previous Installments

Introduction And Design
Bootstrap Loader And Component Manager (beware, requires considerable rework at this point)
The Data Hub
XML Driven Menus

The Benefits Of GUI Automation

A brief summary the benefits of GUI automation:

  1. Simplifies re-use;
  2. Clear separation of front-end from application specific, business, and data management layers;
  3. Event instrumentation;
  4. Provides a mechanism for automation in testing;
  5. GUI changes can be made without exiting, editing, and recompiling the application;
  6. Early prototyping;
  7. Easily change between SDI, MDI, and dialog implementations;
  8. Re-use common form elements across many forms.

Managing Forms

A large application will have numerous forms implemented through single/multiple document interfaces, dialog boxes, and/or tab views.  Creating a form, such as the one used above by a boatyard client, results in reams of form designer generated code.  If, as Taka once said (but points out that he is not the author of this witticism), "every line of code is a liability", at some point the number of lines of code that generates an application specific GUI exceeds the number of lines it takes to generate a GUI from an XML specification (or any other specification, for that matter).  The liability incurred by the .NET framework in the generation of GUI's is considerable.  Even when considering the MFC framework and resource (.RC) files, there is a considerable amount of application specific code required for setting initial control state, presentation options not supported by the form wizard, initial value, and data management.  When the issues of data binding, SQL statements, data set and data adapter management is added to the equation, a considerable amount of code is generated simply to manage the flow of data through the GUI.  Using automation, form elements and data flow can be generalized into a set of definitions and relationships between those definitions, eliminating significant amounts of custom coding.

As an application grows in scope, there is also a tendency to couple GUI elements directly together.  For example, in the above screenshot, it is tempting to directly connect elements of the Receiving / Process Vendor Invoice tabs to other tabs involved in the receiving and inventory management aspects of the application, or vice versa.  This dependency ultimately results in the product becoming rigid--impossible to add features and requiring expensive refactoring/rewriting.  Although it can be avoided with good discipline (which breaks down as deadlines approach), why not instead use a framework that eliminates the temptations of this kind of dependency-building?  The cost benefit is substantial when you consider all the factors involved--early prototyping, the ability to perform unit testing, simplified debugging, elimination of refactoring, etc.

As a point of interest, the GUI in the above screenshot is specified by an XML file which was created by using the MFC version of the AAL to generate the XML code during the GUI script processing.

A Plethora Of Forms

Managing numerous forms, even with XML (or is that especially with XML?) can be cumbersome.  A simple step to address this issue is to break apart form definitions on the physical boundary of the form itself.  In the AAL implementation, this includes tab pages, dialogs, and forms.  A simple schema provides the ability to specify separate form files:

 

From an implementation point of view:

public object LoadForms(EventData eventData)
{
	formSpec+="<NewDataSet><NewDataSet>\r\n";
	string formList;
	eventData.UnMarshal("FormList", out formList);
	XmlDataDocument formDoc=new XmlDataDocument();
	StreamReader tr=new StreamReader("formList.xsd");
	formDoc.DataSet.ReadXmlSchema(tr);
	tr.Close();
	formDoc.Load(formList);
	DataTable dt=formDoc.DataSet.Tables["Form"];
	foreach (DataRow row in dt.Rows)
	{
		string formName=(string)row["Name"];
		Dbg.WriteLine("Loading form: "+formName);
		LoadFormSpecifications(formName);
	}
	formSpec+="</NewDataSet></NewDataSet>\r\n";
	SyncDataSet();
	return null;
}

the separate forms are concatenated into a single string which is the loaded into an XmlDataDocument object.

The Bootstrap Process

To make an application truly independent of its implementation, the entire form loading process needs to be externalized:

  1. Specifying the form list
  2. Specifying the application's form style (SDI, MDI, Dialog)
  3. Specifying the top-level form or starting form
  4. Specifying the initial menu associated with the form
  5. Specifying the application icon (at least, on the caption bar)

This is handled nicely by, yes you guessed it, an "application info" schema:

 

The bootstrap loader reads in the associated application specific XML file, as specified in the command line, and interfaces with the Menu Manager and Form Manager components to create the GUI:

DataTable dt=Lib.Xml.GetTable("appInfo.xsd", args[0], "//AppInfo");
string formListFile=dt.Rows[0]["FormFile"] as string;
string mainFormName=dt.Rows[0]["MainFormName"] as string;
string menuFile=dt.Rows[0]["MenuFile"] as string;
string appType=dt.Rows[0]["AppType"] as string;
string appIcon=dt.Rows[0]["AppIcon"] as string;

EventData ev=EventData.Marshal(new MED[] {new MED("FormList", formListFile)});
icm.Invoke("WindowFormManager.LoadForms", ev);

ev=EventData.Marshal(new MED[] {new MED("MenuDefFile", menuFile)});
icm.Invoke("MenuMgr.LoadMenus", ev);

ev=EventData.Marshal(new MED[] {new MED("FormName", mainFormName), new MED("AppIcon", appIcon)});
switch(appType)
{
	case "Dialog":
		icm.Invoke("FormMgr.CreateDlgApp", ev);
		break;

	case "Single Document":
		icm.Invoke("FormMgr.CreateSDIApp", ev);
		break;

	case "Multi Document":
		icm.Invoke("FormMgr.CreateMDIApp", ev);
		break;
}

ev=EventData.Marshal(new MED[] {new MED("FormName", mainFormName), new MED("MenuName", "MainMenu")});
icm.Invoke("MenuMgr.AssignMenu", ev);
icm.Invoke("FormMgr.ShowForm", ev);

Incidentally, looking at the above code, the switch statement is a good candidate for refactoring, possibly by passing the application type directly to the Form Manager.  This is why the code presented here is considered a prototype!

The XML file that specifies all of the startup conditions is set in the properties page of the BootstrapLoader project:

Plug-In Architecture

A brief note about the bootstrap loader and the plug-in architecture.  The bootstrap loader is the EXE that is run to load a specific application solution.  The idea here is that a specific application solution is derived at entirely through XML specification, scripting, and where applicable, application specific assemblies.  The bootstrap loader is continually evolving to achieve this.  Currently, the only component is loads is the Component Manager assembly.  The remaining components are loaded and initialized by the Component Manager itself.  Currently, the bootstrap loader also deals with reading in the GUI specification.  This is for convenience only, and will shortly need to be migrated out of the bootstrap loader and into the Form Manager.  For those interested, the XML file components.xml is  specifies the components and their public interface points (methods) along with parameters.  The interface point information is used by the Component Manager to set up some hashtables to quickly locate the MethodInfo data associated with a method.  This is a key plug-in architectural concept and will be used extensively in the development of the script engine as it provides a central dispatching mechanism capable of invoking synchronous and asynchronous methods with instrumentation and profiling.

As I note below, I'm still learning the capabilities of .NET.  The entire issue of specifying parameter information in an XML file is completely unnecessary, as this information can be extracted at runtime.  While it's nice to have the information in a "documented" manner, this "documentation" can itself be generated at runtime also, so the documentation issue is moot.  I will migrate the Component Manager implementation over to this self-discovery technique soon.  Remember: prototype!

The Schemas

XML driven forms consists of several schemas all residing in a single XSD.  These schemas are:

  • A form schema, defining features of the form
  • A control set schema, defining form controls and their features
  • A header schema, used to define header information for list type controls (combo boxes, list boxes, list views, and tabs)
  • A column schema, used for defining special controls such as the Outlook Bar control (OK, this is a kludge, I think).

I am not to concerned regarding whether the schema is going to change any time soon.  The schema is read into internal structures that can pretty much stay constant while the schema implementation might undergo some improvements.

Form Definition Schema (FormType)

 

The form type defines the following features of a form (I hesitate to use the word "attribute" because of its XML connotations):

Name A unique identifier for the form used to reference all operations performed on the form by the application.
Caption The form caption.  In the case of a tab form, this is the text appearing on the tab.
DataDomainName The identifier specifying the container domain for all control data within the form.
PreCreateEvent The event invoked before the form is created and controls are placed on the form surface.
PostCreateEvent The event invoked after the form is created and all controls are placed on the form surface.
OnCloseEvent The even that is invoked prior to the form being closed and removed from the Form Manager's form list.
FormDimensions The position and size of the form.  This includes special values for auto-centering and auto-sizing.
ControlSetName The XML data set defining all controls to be placed on the form surface.
Icon The form's icon, displayed in the caption.

This is a good set of starting features and can be easily expanded.

Noteworthy Features

  1. Automatic autosizing if only one FormDimensions record is provided (this is sort of an annoying implementation, as it is based on "missing" information.  I think it would be better to explicitly state such things as auto-sizing, auto-centering, starting form state (minimized, normal, maximized), etc.  Well, this is a prototype;
  2. The same form definition supports SDI, MDI, and dialog forms;
  3. Decouples a form from its controls.

Control Set Schema (ControlInfoType)

A control set can be associated with any form.  The schema:

defines several attributes of a control.  Again, this is an incomplete definition but quite sufficient for a starting point:

Name The internal unique identifier for the control
Control The type of control (the list is defined in the schema)
ControlDimensions Position and size of the control
VarName The variable name containing the control's scalar data
ListName The variable name containing the control's matrix data
Justification Left, right, or center.  Applies primarily to text
RW Read/write or read/only
Header For matrix controls, such as comboboxes and lists, this defines the columns
Bitmap Specifies an associated bitmap
Parent Probably obsolete--specifies the parent control
Tooltip The tooltip text
Font The control font.  This overrides the form font, which defaults to MS Sans Serif, 8 point
Options Additional options, for example, showing grid lines
OnLeftClick The left click event handler
OnDoubleClick The double click event handler
OnRightClick The right click event handler
OnLoseFocus The lose focus event handler (hey, where's OnGainFocus???)

Noteworthy Features

  1. Controls can be moved to a different dialog or other form without refactoring the class names, event handlers, data collections, etc.  I do this a lot as a product is evolved and enhanced.
  2. A control can specify another control set.  This provides a mechanism to build GUI's by re-using existing GUI elements--design once, use many times.  In the screen shot of the boatyard software, there are several GUI subsets that exist on different tabs.  Please note that this feature is not yet implemented!
  3. The list control, as currently implemented, only supports "report" mode.  Personal bias.  I have never, ever, needed to use a list control (nor wanted to) in any other mode.

VarName And ListName

A control is either scalar, such as a label, groupbox, button, text box, radio button, or checkbox, or a matrix, such as a combo-box, list box, or grid.  Actually, in the case of matrix controls, there is also a scalar element--the selected row.  These two elements of the control define the name for the scalar and matrix (if required) variables.  When the control is created, the Form Manager instructs the DataHub to create slots for these variables within the DataSet specified by the associated form (if no data set name is provided in the associated form, then the form name is used).  The reader may observe that instead of this mechanism, the structures and containing data could be dynamically created at runtime through a variety of mechanisms.  One such mechanism is Activator.CreateInstance.  I have chosen not to go this route for two reasons, primarily--the DataHub provides important instrumentation, event, and conversion capabilities, and the implementation is less language/framework dependent.  The DataHub can be modified though, if you are particularly motivated or interested in this capability.

Header Schema (HeaderType)

The header schema specifies display information regarding the columns in a matrix control, such as a combobox, list control, etc.  The header is also used to specify the tabs in a tab control (probably not the best implementation, eh?)

Name A placeholder name identifying the column
Width The width of the column (autosizing is currently not supported)
Justification The justification, which affects both the header (for list controls) and the text in the column
Visible Whether the column is visible or not.  This is an excellent mechanism to hide information, such as ID's
ReadOnly For editable cells (not yet implemented), controls whether the entire column is read-only
Options Freeform options
ColumnInfo Specifies column specific information, currently used exclusively by the Outlook Bar control

Noteworthy Features

One of the most valuable techniques in managing data transactions (the user interface and the database) is the ability to hide columns of data.  These columns usually contain primary key ID's or other information that is useful for managing data transactions.  The ListView and ComboBox controls do not natively support this capability.  Besides facilitating data transactions (which some might argue can be handled by the data binding capability of .NET, this technique is also useful for supporting other user-interface related features in which additional hidden information drives other elements of the GUI.  This is all achieved by using a single matrix as the data source for both display information and control/transaction information.

Although currently not implemented, the header information will be expanded at a later date to specify additional editing capability found in several of the grid controls published here on The Code Project.

Column Info Schema (ColumnInfoType)

The column info schema supports the Outlook Bar and ToolBar controls.

CI_Icon The item's icon
CI_OnClickEvent The event handler when the item is clicked
CI_Caption The item's caption
Association For ToolBars, the associated menu item tag

Noteworthy Features

This is admittedly a kludge that, while it supports the current requirements of two controls, will need to be revisited.

The Basic Object Model

You will note from this object model that the interface between the AAL control implementation and .NET's control implementation is through a "has a" relationship instead of a "is a kind of" (derived class) relationship.  Similarly for forms.  This is done intentionally (and many may disagree) to prevent direct access to the .NET framework implementation.  Part of the purpose of the AAL is to manage interaction with system objects, and this is the mechanism used to achieve this.

Basic Forms

.NET's Form vs. MFC's Frame/View Architecture

In MFC, the concept of a form is actually implemented as a frame and a view.  After working with .NET forms for a while and discovering some of the odd behaviors when adding toolbars and status bars, I realized that there is a good reason to have a similar frame-view architecture.  The AAL implicitly implements this architecture by creating a panel control that lives in the extents of the form, making a sort of quasi frame-view architecture.  When controls are added to the form, in actuality they are added to the panel's control list.  The exceptions to this (so far) are the status bar and the tool bar which are added to the form itself.  Internally, the form resizes the panel (i.e., the view) to accommodate the status bar and toolbar, as illustrated by this code:

public void AddControl(AALControl ctrl)
{
	if (ctrl.GetType().Name=="AALStatusBar")
	{
		// add to form, not view
		Controls.Add(ctrl.sysctrl);
		statusBarHeight+=ctrl.sysctrl.Size.Height;
		view.Size=new Size(ClientSize.Width, ClientSize.Height-toolBarHeight-statusBarHeight);
	}
	else if (ctrl.GetType().Name=="AALToolBar")
	{
		// add to form, not view
		Controls.Add(ctrl.sysctrl);
		toolBarHeight+=ctrl.sysctrl.Size.Height;
		view.Location=new Point(0, toolBarHeight);
        view.Size=new Size(ClientSize.Width, ClientSize.Height-toolBarHeight-statusBarHeight);
	}
	else
	    ... add the control to the panel ...

There are also other points of confusion in the .NET framework related.  One is with regards to splitters, and the other is with regards to the sizing of containers, such as panels, that are docked/anchored to the outer panel, which itself is docked/anchored.  These issues are worthy of a completely separate article.

SDI Forms

An SDI Form has the simplest implementation, implementing the form and menu helper interfaces:

namespace AAL
{
	internal class AALSDIForm : AALForm
	{
		public AALSDIForm(FormInfo fi, FormMgr formMgr) : base(fi, formMgr)
		{
			sysform=new SmartForm(formMgr, this, fi.pos);
			sysform.Text=fi.caption;
			sysform.ClientSize=fi.size;
		}

		public override void Show()
		{
			sysform.View.Visible=true;		// show the form's panel
			sysform.Show();
			base.Show();
		}
	}
}

The SmartForm implements the pseudo-frame/view architecture:

public SmartForm(FormMgr formMgr, AALForm aalForm, Point p)
{
	this.formMgr=formMgr;
	this.aalForm=aalForm;

	// the view remains hidden in MDI forms
	view=new Panel();
	view.Visible=false;
	view.Location=new Point(0, 0);
	view.Size=ClientSize;
	view.BorderStyle=BorderStyle.None;
	view.Anchor=AnchorStyles.Left | AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Right;
	Controls.Add(view);
	pos=p;
	menuHelpPanel=null;
	statusBarHeight=0;
	toolBarHeight=0;
}

Dialog Forms

The dialog form implementation differs from the SDI Form implementation only in the configuration of the form properties and how the form is invoked.  Note that a dialog form can have menus, toolbars, statusbars, etc.  The only difference is that it acts as a modal form:

namespace AAL
{
	internal class AALDlgForm : AALForm
	{
		public AALDlgForm(FormInfo fi, FormMgr formMgr) : base(fi, formMgr)
		{
			sysform=new SmartForm(formMgr, this, fi.pos);
			sysform.Text=fi.caption;
			sysform.Location=fi.pos;
			sysform.ClientSize=fi.size;
			sysform.ShowInTaskbar=true;
			sysform.MaximizeBox=false;
			sysform.MinimizeBox=false;
			sysform.FormBorderStyle=FormBorderStyle.Sizable;
		}

		public override void Show()
		{
			sysform.View.Visible=true;
			sysform.ShowDialog();
			base.Show();
		}
	}
}

MDI Forms

MDI Forms, or Multiple Document Interface, consist of an application frame with zero or more document windows.  To create an MDI application, the main form's IsMdiContainer property is set to true:

public AALMDIForm(FormInfo fi, FormMgr formMgr) : base(fi, formMgr)
{
	activeChild=null;
	childListByName=new Hashtable();
	childListByForm=new Hashtable();
	sysform=new SmartForm(formMgr, this, fi.pos);
	sysform.Text=fi.caption;
	sysform.ClientSize=fi.size;
	sysform.IsMdiContainer=true;
}

Note that the MDI form tracks the active child:

public void SetActiveChild(AALMDIChildForm form)
{
	Dbg.Assert(childListByForm.Contains(form), new DbgKey("UnkForm"), "SetActiveChild failed");
	activeChild=form;
	string name=childListByForm[form] as string;
	EventData ev=EventData.Marshal(new MED[] {new MED("FormName", name)});
	formMgr.ICM.Invoke("MenuMgr.SetActiveWindow", ev);
}

This is done primarily to ensure that an MDI menu is properly selected and that the child window is checked in the MDI child list of a menu. I found that the events Activated and MdiChildActivated did not work as expected. Therefore, the GotFocus event handler is used instead (but requires a kludge with the deleted flag, because the GotFocus event fires after the form manager has removed the form from its master list.

public AALMDIChildForm(AALForm parent, FormInfo fi, FormMgr formMgr) : base(fi, formMgr)
{
	deleted=false;
	Dbg.WriteLine("Creating MDI Child: "+fi.name);
	this.parent=parent;
	sysform=new SmartForm(formMgr, this, fi.pos);
	sysform.Text=fi.caption;
	sysform.ClientSize=fi.size;
	sysform.MdiParent=(Form)parent.IForm.Form;
	((AALMDIForm)parent).AddChild(fi.name, fi.caption, this);

	// Notes:
	// Activated does not work
	// MdiChildActivated does not work
	sysform.GotFocus+=new EventHandler(Activated);
}
. . .
public void Activated(object sender, EventArgs e)
{
	// Need to test here, because OnFocus message is not processed until after the form is 
	// closed.  This is probably a workaround required because the "Activated" and "MdiChildActivated"
	// event handlers don't appear to work.  Maybe they do in VS.NET 2003.
	if (!deleted)
	{
		Dbg.WriteLine("Activating MDI Child: "+formInfo.name);
		((AALMDIForm)parent).SetActiveChild(this);
	}
}

Basic Controls

In all cases, a control derives from the AALControl class which encapsulates (a "has a" relationship) the .NET control. In certain cases, the .NET control is insufficient and a Smart... control is created with a "is a kind of" relationship. That is, it is derived from the .NET control.  I will illustrate this architecture with several examples.  The base class AALControl also provides a wrapper for the ControlInfo class, which is a hodgepodge collection of information extracted from the XML control specification and some common functionality for all controls, such as the ability to control font, docking, color, and other options.  Now, I will not say that this is the best architecture, and in practical usage it often requires casting and seemingly unnecessary layers of objects to get to the native .NET control.  I do in particular like the "has a" relationship as this gives the AAL a lot more control (ha ha ha) over the application's interface with the .NET's implementation of GUI controls.  In other words, it prevents the application from directly accessing control properties, methods, and events.  This is necessary so that the programmer does not wander from the AAL architecture!

TextBox: A Simple Control Example

In this basic control, the TextBox is instantiated directly:

namespace AAL
{
	internal class AALTextBox : AALControl
	{
		public AALTextBox(ControlInfo ci) : base(ci)
		{
		}

		public override void Create()
		{
			TextBox tb=new TextBox();
			sysctrl=tb;
			sysctrl.Location=ctrlInfo.pos;
			sysctrl.Size=ctrlInfo.size;
			sysctrl.Visible=true;
			tb.KeyUp+=new KeyEventHandler(OnKeyEvent);
		}
		...

Label: A Customized Control Example

The AALLabel implementation demonstrates deriving a class from .NET's definition to provide some additional functionality, in this case, the ability to set the justification for the text in the label.

internal class AALLabel : AALControl
{
	public AALLabel(ControlInfo ci) : base(ci)
	{
	}

	public override void Create()
	{
		// property TextAlign is useless because it aligns to an adjacent control, which
		// is not what we want, so we have to create a custom label class and override the
		// CreateParams property.
		uint style=0;
		switch (ctrlInfo.justification)
		{
			case "Center":
				style=1;
				break;

			case "Right":
				style=2;
				break;
		}

		sysctrl=new SmartLabel(style);
		sysctrl.Text=ctrlInfo.caption;
		sysctrl.Location=ctrlInfo.pos;
		sysctrl.Size=ctrlInfo.size;
		((Label)sysctrl).FlatStyle=FlatStyle.System;
		sysctrl.Visible=true;
	}
}
This class creates the control. Because the Label control is missing the ability to set its justification (as far as I can tell), a SmartLabel control is created instead which overrides the CreateParams method, enabling the AAL version to set the justification:
internal class SmartLabel : Label
{
	private long style;

	public SmartLabel(uint style)
	{
		this.style=style;
	}

	protected override CreateParams CreateParams 
	{
		get 
		{
			CreateParams cp = base.CreateParams;
			cp.Style=(int)((uint)cp.Style | style);
			return cp;
		}
	}
}

Things That Are Both Controls And Forms

There are several control types that are both controls and implemented as forms in the AAL scheme of things. Currently, these are the Panel and the TabPage control.

Panels

Panels are treated as containers for forms, and are thus derived from the IForm interface.  Because panels do not have menus or status bars, the IMenuHelper interface is omitted. The core control, AALPanel, is derived from AALControl as usual, and it instantiates a "smart" Panel derived object.

internal class SmartPanel : Panel
{
	private AALPanel ctrl;

	public SmartPanel(AALPanel ctrl)
	{
		this.ctrl=ctrl;
	}

	protected override void Dispose(bool b)
	{
		ctrl.RemoveForm();
		base.Dispose(b);
	}		
}

internal class AALPanel : AALControl
{
	public AALPanel(ControlInfo ci) : base(ci)
	{
	}

	public override void Create()
	{
		sysctrl=new SmartPanel(this);
		((Panel)sysctrl).BackColor=System.Drawing.SystemColors.Window;
		sysctrl.Location=ctrlInfo.pos;
		sysctrl.Size=ctrlInfo.size;

		if (ctrlInfo.HasOption("3DBorder"))
		{
			((Panel)sysctrl).BorderStyle=BorderStyle.Fixed3D;
		}
		else
			if (ctrlInfo.HasOption("Border"))
		{
			((Panel)sysctrl).BorderStyle=BorderStyle.FixedSingle;
		}

		sysctrl.Visible=true;

		// use the panel name as the form name
		FormInfo fi=ctrlInfo.formMgr.GetFormInfo(ctrlInfo.name);
		ctrlInfo.formMgr.CreatePanelForm(fi, sysctrl);
	}
	...
}

Note that:

  1. after creating the panel control, the Form Manager is instructed to create a panel form
  2. the SmartPanel overrides the Dispose method to ensure that the associated form is properly cleaned up
  3. the object model could be simplified further by having the AALPanelForm contain the Panel control.  However, as a design decision, I believe it is better to be consistent in the manner in which controls are handled, especially when there is no significant performance or resource penalty associated with being consistent, as opposed to hyper-optimization.

TabControls

Note the similarities between this implementation and the Panel implementation. The control is similarly created:

internal class AALTabControl : AALControl
{
	public AALTabControl(ControlInfo ci) : base(ci)
	{
	}

	public override void Create()
	{
		sysctrl=new SmartTabControl(this);
		sysctrl.Location=ctrlInfo.pos;
		sysctrl.Size=ctrlInfo.size;
		sysctrl.Visible=true;
		LoadHeader();
	}
	...

but there are now multiple TabPage forms that are created based on the XML header definition for the TabControl:

...
public void LoadHeader()
{
	foreach (DataRow row in header.Rows)
	{
		string tabPageName=row["Name"] as string;
		FormInfo fi=ctrlInfo.formMgr.GetFormInfo(tabPageName);
		AALTabPageForm page=ctrlInfo.formMgr.CreateTabPageForm(fi);
		((TabControl)sysctrl).TabPages.Add((TabPage)page.tabPageControl.sysctrl);
	}
}

The AALTabPageForm maintains the "has a" relationship style used throughout the AAL. It instantiates an AALTabPageControl:

public class AALTabPageForm : AALForm, IForm
{
	public AALTabPageControl tabPageControl;

	public AALTabPageForm(FormInfo fi, FormMgr formMgr) : base(fi, formMgr)
	{
		tabPageControl=new AALTabPageControl(fi.caption);
	}
	...

which "has a" TabPage:

public class AALTabPageControl : AALControl
{
	public AALTabPageControl(string caption)
	{
		ctrlInfo.caption=caption;
		sysctrl=new TabPage(ctrlInfo.caption);
		sysctrl.BackColor=Color.White;
		sysctrl.Visible=true;
	}
	...
}

Discussion: Attributes

Now I will happily admit that I come from the old school of C++/MFC programming, so the concept of programming properties and attributes dynamically just hasn't sunk in yet for me. The reader will note that there are a lot of different properties that can specified for controls and forms, but the AAL currently implements an extremely (emphasis on extremely) limited set, in which XML specified properties are passed to the control. Ideally, and in hindsight as I write this article, a generic XML specified property-value pair might be a better solution.  Using .NET's dynamic type capability, the values can be set for the specific control as desired.  I'll be considering this design change in the future and possibly migrate the implementation to this approach.

Data Management

All controls use the AAL architecture for data management (where implemented at this point--remember, prototype!).  This means that the controls interface with the Data Hub.  Each form is associated with a specific workflow domain.  If no domain is specified in the XML definition for the form, a default domain of the same name as the form is created.  The issue of managing the dynamic creation of MDI child forms and potential naming contention is blissfully ignored for now. 

Most controls, like a text box, radio button, or check box, have at least a scalar variable.  Some controls, like a combo-box, list control or tree control have both an index variable (a scalar) and a list (a matrix).  Note again that multiple-selection combo-boxes, lists, and tree controls are all happily ignored for the time being.  All controls use a common data representation for their values and translate between this common data representation and the specific needs of the control.  This ensures that a uniform data representation is available for all components that are part of an application built using the AAL.

Each form implements the ability to update all the controls/storage on a form bi-directionally or a specific control on a form.  Additionally, the ability to update a list container for a specific control is provided.  Note that this is a unidirectional update--from data container to control only:

public void UpdateStorage(string ctrlName)
{
	Dbg.Assert(ctrlList.Contains(ctrlName), new DbgKey("UnkCtrl"), ctrlName);
	((AALControl)ctrlList[ctrlName]).UpdateStorage();
}

public void UpdateAllStorage()
{
	IEnumerator iter=ctrlList.Values.GetEnumerator();
	while (iter.MoveNext())
	{
		AALControl ctrl=iter.Current as AALControl;
		ctrl.UpdateStorage();
	}
}

public void UpdateControl(string ctrlName)
{
	Dbg.Assert(ctrlList.Contains(ctrlName), new DbgKey("UnkCtrl"), ctrlName);
	((AALControl)ctrlList[ctrlName]).UpdateControl();
}

public void UpdateControlList(string ctrlName)
{
	Dbg.Assert(ctrlList.Contains(ctrlName), new DbgKey("UnkCtrl"), ctrlName);
	((AALControl)ctrlList[ctrlName]).UpdateControlList();
}

public void UpdateAllControls()
{
	IEnumerator iter=ctrlList.Values.GetEnumerator();
	while (iter.MoveNext())
	{
		AALControl ctrl=iter.Current as AALControl;
		ctrl.UpdateControl();
	}
}

A typical data transfer between the control and the data hub, using the TextBox as an example, appears as (and yes, there are too many layers--the IDH should be a global object in each assembly):

public override void UpdateControl()
{
	((TextBox)sysctrl).Text=ctrlInfo.formMgr.IDH.GetDatum(ctrlInfo.VarName).ToString();
}

public override void UpdateStorage()
{
	ctrlInfo.formMgr.IDH.SetDatum(ctrlInfo.VarName, ((TextBox)sysctrl).Text);
}

Each control is responsible for implementing the Update... methods and for correctly translating to and from the Data Hub representation. This is pretty simple in .NET because of the implicit awareness each object has of its own type and the ability to convert from an object to a different type.

Please refer to the previous articles in this series (listed at the top of the page) for further information regarding data domains and Data Hub.

Event Management

All events (and again the implementation is very bare-bones currently) utilize the Component Manager's Invoke method. Events in the AAL architecture are instrumented, meaning that an audit trail is automatically created. Handling control events directly within the application breaks this design criteria. Since events often trigger workflows, it is also appropriate that the event mechanism that the .NET framework provides be tied in with the scripted workflow mechanism that the AAL provides. Lastly, the AAL attempts to enforce a highly modularized approach to application design and coding, and this mechanism ensures that events comply with this architecture.

A typical implementation for handling an event is demonstrated in this code from the Button control (similarly with the IDH, the ICM should also be a global object in each assembly):

private void OnLeftButtonClick(object sender, EventArgs e)
{
	ctrlInfo.form.UpdateAllStorage();
	ctrlInfo.formMgr.ICM.Invoke(ctrlInfo.onLeftClick, null);
}
Please refer to the previous articles in this series (listed at the top of the page) for further information regarding event management and the Component Manager.

The AAL In Action: Photo Album Prototype

This prototype application is provided with the source code (which is hardwired to load the configuration files for this application in the bootstrapLoader.cs file).

Having perused both freeware, shareware, and expensive-ware products, I have yet to find something that lets me organize my thousands of digital images (and growing!) in:

  • a dynamic manner--I want keys that are not predefined by somebody else;
  • quickly--I don't want a lot of other overhead associated with cataloging the image
  • easily--I want to be able to easily create new keys and populate them with selection fields and be able to easily select one or more of those fields from one or more keys for each picture
  • relationally--for lookup, I want to be able to specify a complex combination of keys and fields

After some research, and like Matt Gullett's "The Life of a Project" pointed out for something different, I simply haven't found something that meets my needs in terms of simplicity, elegance, and capability.  Besides, this is a great little project to prove the concepts behind the AAL.

XML Files

The application and its function are currently defined entirely through the use of XML.  Three examples are illustrated next.

PhotoAlbumAppInfo.xml:

This file specifies the list of forms that comprise the application, the menu, the data domain information, and startup information such as the application icon, document model, and starting form.

<NewDataSet>
 <AppInfo>
  <FormFile>PhotoAlbumFormList.xml</FormFile>
  <MenuFile>PhotoAlbumMenus.xml</MenuFile>
  <AppType>Single Document</AppType>
  <MainFormName>PhotoAlbumMainForm</MainFormName>
  <DataDomainFile>PhotoAlbumDataDomain.xml</DataDomainFile>
  <AppIcon>ka.ico</AppIcon>
 </AppInfo>
</NewDataSet>

PhotoAlbumFormList.xml

The "list of forms" currently specifies only one form.

<NewDataSet>
 <Form>
  <Name>PhotoAlbumMainForm.xml</Name>
 </Form>
</NewDataSet>

PhotoAlbumDataDomain.xml:

The current (and limited!) functionality of the program is achieved by specifying "on update" handlers for three controls:

  1. the TextBox control in which the user can enter a file path directly;
  2. the FolderTreeView control, which has a "show this path" variable and an internal "path selected" variable;
  3. The PictureBox control, which displays the selected image.

Observe how the workflow domain names are the same as the form names found in the "PhotoAlbumMainForm.xml" file (too large to show here).

<NewDataSet>
 <WorkflowDomain>
  <WorkflowDomainName>CatalogTab</WorkflowDomainName>
  <DataSet>
   <DataSetName>CatalogTab</DataSetName>
   <Datum>
    <DatumName>path</DatumName>
    <UpdateEvent>DataHub.Copy(DockingPanel.path, CatalogTab.path)</UpdateEvent>
   </Datum>
  </DataSet>
  <SingletonDatum />
  <InitialLoadSet>
  <InitialLoad />
  </InitialLoadSet>
 </WorkflowDomain>
 <WorkflowDomain>
  <WorkflowDomainName>DockingPanel</WorkflowDomainName>
  <DataSet>
   <DataSetName>DockingPanel</DataSetName>
   <Datum>
    <DatumName>path</DatumName>
    <UpdateEvent>FormMgr.UpdateControl(DockingPanel, FileView)</UpdateEvent>
   </Datum>
   <Datum>
    <DatumName>fullPath</DatumName>
    <UpdateEvent>DataHub.Copy(BitmapPanel.image, DockingPanel.fullPath)</UpdateEvent>
   </Datum>
  </DataSet>
  <SingletonDatum />
  <InitialLoadSet>
  <InitialLoad />
  </InitialLoadSet>
 </WorkflowDomain>
 <WorkflowDomain>
  <WorkflowDomainName>BitmapPanel</WorkflowDomainName>
  <DataSet>
   <DataSetName>BitmapPanel</DataSetName>
   <Datum>
    <DatumName>image</DatumName>
    <UpdateEvent>FormMgr.UpdateControl(BitmapPanel, PictureBox1)</UpdateEvent>
   </Datum>
  </DataSet>
  <SingletonDatum />
  <InitialLoadSet>
  <InitialLoad />
  </InitialLoadSet>
  </WorkflowDomain>
</NewDataSet>

Look Ma, No Hands!

The Photo Album currently does not implement any application specific code.  The current functionality:

  • Navigate the directory structure by directly keying in a path;
  • Navigate the directory structure via the tree;
  • Clicking on an image file and displaying the image;

is done entirely by events triggering off of data updates.  More complicated workflow's can be scripted when the script engine is completed.

Discussion: Using XML

Personally, I've found that using XML to specify form layout and data events is cumbersome and awkward.  In raw editing mode, the tags and rapid growth of the file makes it difficult to navigate, read, and modify an XML file.  Using my XML Data Editor helps, but because it's a generic editor, there are features I'd like to see that it simply doesn't support and navigation is still awkward.  Some custom tools and layout editors would be the best solution for this kind of work.  Any takers?

The Distribution File

The distribution file is an MSI file that installs the source code to the specified target directory.  The project files are VS.NET 2003 format.  The Photo Album prototype currently resides in the "template" directory that is populated off of the target directory, and if no "app info" xml file is specified in the command line, the bootstrap loader defaults to loading the Photo Album app info file.

The code installs as the "Application Automation Layer" in the "Add or Remove Programs" utility, and must be removed in order to install a new version (some of the options when setting up an MSI don't seem to work).  The MSI creates the following directories (for example, I overrode the default target directory during installation and specified "c:\temp\aal" instead:

The project solution file is found in the AAL directory.  After building the solution (ignore the two warnings in the DataAccessLayer assembly), run the "BootstrapLoader.exe" application found in the "template" directory.  To run the application from the IDE or to debug it, change the command line arguments and working directory for the BootstrapLoader project (a screenshot of this appeared at the beginning of the article in "The Bootstrap Process" section).

Conclusion

What else is there to say? Onward and upward!

(However, note that as the CP2 Scripting Framework Project project takes shape, I may have to completely rethink the component method interface mechanism I'm currently using, especially since the next article should really delve into workflow management so more interesting things can be done with the Photo Album).

References

FolderTreeView Control:
    http://www.codeproject.com/cs/miscctrl/FolderTreeView.asp

Using A Manifest:
    http://www.codeproject.com/useritems/xptheme.asp
    http://www.codeproject.com/csharp/dotnetvisualstyles.asp

MDI Forms:
    http://www.codeproject.com/csharp/mdiformstutorial.asp
 

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

Share

About the Author

Marc Clifton

United States United States
Marc is the creator of two open source projets, MyXaml, a declarative (XML) instantiation engine and the Advanced Unit Testing framework, and Interacx, a commercial n-tier RAD application suite.  Visit his website, www.marcclifton.com, where you will find many of his articles and his blog.
 
Marc lives in Philmont, NY.

Comments and Discussions

 
GeneralWoooooow PinmemberMeisi23-Jun-03 17:11 

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

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

| Advertise | Privacy | Mobile
Web03 | 2.8.140821.2 | Last Updated 23 Jun 2003
Article Copyright 2003 by Marc Clifton
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid