Creating a Plug-in Framework


Applications implementing an MDI user interface can display any number of child forms. Therefore, there should not necessarily be a reason to limit the type of forms to only those packaged with the application. Creating a plug-in framework can extend the functionality of an enterprise far beyond its original intent. In many cases, supporting customization within an enterprise application further extends its reach and adoption rate. This is where the MVC architecture pulls everything together. The controller directs the application flow, the model retrieves and updates the data, and the view presents everything to the user.

Creating the Controller

The controller is the most complex element of an MVC architecture. Its responsibilities must include managing multiple viewers and exchanging business objects, all based on user interaction.

As Figure 8-6 illustrates, the implementation of the controller puts it in the middle of the application. Its function is based on configuration information that identifies what business objects and what actions trigger specific views to be displayed. For example, an Issue business object and a View action might display one form while a User object and a New action display a completely different form. This outlines a specific need for mapping the relationships between business objects, actions, and forms.

click to expand
Figure 8-6: The role of the controller in an MVC architecture

Mapping Objects to Actions

The controller is only as strong as its mappings. To keep the controller flexible, it should not know any specifics about what business objects it is working with or which forms should be displayed. It should only know that a combination of business objects and actions results in the display of forms. It is expected that the forms will know what to do with the business objects they receive.

The first step is to add a new Class Library project to the IssueTracker solution, named ApplicationFramework . Controller- related code needs to reside in a separate assembly because references can only be made to .dll files, not .exe files. Because the controller will need to manage a mapping of objects and actions to forms, there needs to be a structure for capturing such information. Listing 8-2 outlines the ObjectMappingEntry class, which represents a single action mapping. The controller will manage an array of ObjectMappingEntry objects to select the correct form in response to user action.

Listing 8-2: The ObjectMappingEntry Class
start example
 public class ObjectMappingEntry {     string _Viewer;     string _BusinessObjectName;     Controller.ControllerActions _Action;     public string Viewer {         set         {              _Viewer = value;         }         get         {              return _Viewer;         }     }     public string BusinessObjectName     {         set         {              _BusinessObjectName = value;         }         get         {              return _BusinessObjectName;         }     }     public Controller.ControllerActions Action     {         set         {              _Action = value;         }         get         {              return _Action;         }     } } 
end example
 

The ObjectMappingEntry class is essentially a simple structure of three data members and supporting get and set accessor methods . Each data member is needed to help the controller take action. The BusinessObjectName property specifies which business object this mapping entry applies to, such as Issue or UserCollection. The Action property is based on the ControllerActions enumeration defined within the Controller object. It specifies which action is being taken. The Viewer property simply labels which MDI child form needs to be displayed in response to a specific business object and action.

Note  

Because viewers may potentially be loaded from foreign assemblies, the Viewer property must be fully qualified with its namespace, such as WinUI.FormIssueSummary.

Implementing the Controller

With the ObjectMappingEntry class defined, you can implement the Controller class to take action based on entry values. Listing 8-3 outlines the code that implements the Controller object and directs the application flow based on user actions.

Listing 8-3: The Controller Object
start example
 public class Controller {     //definition of action types     public enum ControllerActions     {         New = 1,         View = 2,         Edit = 3,         Delete = 4     }     //reference to the controller MDI parent     static private Form _ParentForm;     //container for all of the individual object mappings     static ArrayList _ObjectMappings = new ArrayList();     //property accessor to set parent form     public Form ParentForm     {         set         {              _ParentForm = value;         }     }     public Controller()     {         //hard-code new issue mapping         ObjectMappingEntry entry;         entry = new ObjectMappingEntry();         entry.Action = ControllerActions.View;         entry.BusinessObjectName = "IssueCollection";         entry.Viewer = "WinUI.FormIssueSummary";         _ObjectMappings.Add( entry );         entry = new ObjectMappingEntry();         entry.Action = ControllerActions.View;         entry.BusinessObjectName = "Issue";         entry.Viewer = "WinUI.FormIssueDetails";         _ObjectMappings.Add( entry );         entry = new ObjectMappingEntry();         entry.Action = ControllerActions.New;         entry.BusinessObjectName = "Issue";         entry.Viewer = "WinUI.FormIssueDetails";         _ObjectMappings.Add( entry );         return;     }     public static void Process( object argObject, ControllerActions argAction )     {         //based on mapping, display specific form         foreach( ObjectMappingEntry objMapping in _ObjectMappings )         {             //find the right business object             if( argObject.GetType().Name.CompareTo(                 objMapping.BusinessObjectName ) == 0 )             {                 //find the right action                 if( objMapping.Action == argAction )                 {                     //start the viewer                     Type typeViewer = Assembly.GetExecutingAssembly().GetType(                         objMapping.Viewer );                     Form formViewer = (Form)Activator.CreateInstance(typeViewer);                     formViewer.MdiParent = _ParentForm;                     formViewer.Show();                     break;                 }             }         }         return;     } } 
end example
 

First, the Controller class implements the ControllerActions enumeration. This describes what action is being taken by the user against a specific business object or collection. In most cases, New, View, Edit, and Delete is sufficient, but you can add additional actions as necessary.

Next, the Controller's properties are implemented. The ParentForm property specifies the MDI parent that contains the controller. This is necessary so that the Controller can assign the MDI parent to new child forms that are created and displayed. The _ObjectMappings array is a collection of ObjectMappingEntry items. When the Controller is asked to take action, this ArrayList will be searched for an applicable mapping to use.

Next, the code implements the Controller's class constructor. This is currently where hard-coded object-action mappings are being added. Later, these mappings will migrate into an external configuration file. Table 8-1 describes three object mappings.

Table 8-1: Sample Summary of Object to Action to Forms

BUSINESS OBJECT

CONTROLLER ACTION

CHILD FORM

IssueCollection

View

WinUI.FormIssueSummary

Issue

View

WinUI.FormIssueDetails

Issue

New

WinUI. FormIssueDetails

Finally, the code implements the Process method to take the action. This method accepts two parameters: a business object to process and an action to take. The method begins by iterating through the ArrayList, inspecting object mappings. The business object's data type compares against business object labels stored within the ObjectMappingEntry. If it finds a matching object name, it also checks the ControllerActions for a match. If it also finds a matching action, then the Controller object can display the appropriate form.

Again, the Controller object has no awareness of any specific form, and there is no FormIssueSummary variable available to create a new instance. So, the Controller object turns to the assembly. The Assembly.GetExecutingAssembly method points to the assembly running the active code ”in this case, WinUI.exe. Because the FormIssueSummary object is defined within the same assembly, you can use the GetType method to return a Type object for the named object. The Activator.CreateInstance method uses the Type object to instantiate the class and present the child form.

Applying the Controller to the MDI Parent

Now that you have implemented the Controller object, you can put it to use. The IssueTracker application begins by displaying the FormIssueSummary child form. A mapping has already been defined that binds an IssueCollection object and a View action to the FormIssueSummary child form. In Listing 8-4, the Form_Load event handler only needs to populate an IssueCollection object and pass it along to the Controller.

Listing 8-4: The MDI Application's Form_Load Event Handler
start example
 private void FormMain_Load( object sender, System.EventArgs e ) {     IssueCollection myColl = new IssueCollection();     Controller.Process( myColl, Controller.ControllerActions.View );     return; } 
end example
 

In this case, the IssueCollection is empty. Later, you will populate it with actual data. The collection and a ControllerAction will be passed to the static Process method. The Process method will reference its collection of mappings and display the appropriate child form.

Applying the Controller to the MDI Child

Using the Controller object need not be limited to MDI parents. Child forms, or any other object class that needs to direct form actions, can also use the Controller class. In Listing 8-5, the FormIssueSummary class uses the Controller class to display the FormIssueDetails child form in response to double-clicking a row.

Listing 8-5: Using the Controller Class to Process User Actions
start example
 private void listView1_DoubleClick( object sender, System.EventArgs e ) {     Issue myIssue = new Issue();     myIssue.IssueID = 101;     Controller.Process( myIssue, Controller.ControllerActions.View ); } 
end example
 

Essentially, the code functions the same way. When the user double-clicks a row in the list view control, its event handler creates a new Issue object, sets its properties, and passes it to the Controller object's Process method along with a ControllerAction value set to View.

Creating the Model

The model element of the MVC architecture already exists with the abstractions that the Business Facade and Business Rules projects provide. The child forms use these projects and their methods to set and get values from the model. Listing 8-6 demonstrates a child form's use of the model by implementing the Form_Load event handler for the FormIssueSummary form.

Listing 8-6: Filling a List View with Summary Data
start example
 private void FormIssueSummary_Load(object sender, System.EventArgs e) {     IssueManager mgrIssues = new IssueManager();     IssueCollection collIssues = mgrIssues.GetAllIssues();     foreach( Issue issueItem in collIssues )     {         listView1.Items.Add( issueItem.Summary );     }     return; } 
end example
 

The event handler begins by instantiating an IssueManager object provided by the Business Facade project. The IssueManager returns a collection of Issue objects that abstract values stored in the database. This collection is then iterated to add rows to the list view control. Figure 8-7 shows the final outcome.

click to expand
Figure 8-7: The summary view of all issues

The Form_Load event handler is similar for the FormIssueDetails form. You can even apply data binding to child form controls, as shown in Listing 8-7.

Listing 8-7: Data Binding with Form Controls
start example
 private void FormIssueDetails_Load(object sender, System.EventArgs e) {     IssueManager managerIssue = new IssueManager();     Issue issue = managerIssue.GetIssue( m_intIssueID );     txtEntryDate.DataBindings.Add( "Text", issue, "EntryDate" );     cboType.DataBindings.Add( "Text", issue, "TypeID" );     cboStatus.DataBindings.Add( "Text", issue, "StatusID" );     cboPriority.DataBindings.Add( "Text", issue, "PriorityID" );     txtSummary.DataBindings.Add( "Text", issue, "Summary" );     txtDescription.DataBindings.Add( "Text", issue, "Description" );     return; } 
end example
 

Again, you have put IssueManager to use. This time, you have invoked the GetIssue method along with a specific Issue ID. You then used .NET's data binding to bind the populated Issue object each relevant form control. Figure 8-8 shows the filled FormIssueDetails form.

click to expand
Figure 8-8: Data binding form fields to a data collection

So far, the controller and model elements of the MVC architecture have largely completed the application. The application starts with a Form_Load event handler that uses the Controller class to display a summary form. The DoubleClick event handler in that form uses the Controller class to display a details form.

Simple MDI child forms have worked out well to this point, but to round out a solid application framework, you need to attend to the view area.

Creating Extendable Viewers

So far, the standard MDI user interface methodology has worked out well to implement an MVC architecture. However, to turn MDI into an extendable solution ”rather than a flexible solution ”you need to put some additional elements into place, including consistent viewer implementation, the ability to load viewers dynamically, and the ability to retrieve controller settings from an external file. You can fulfill these three basic needs with the help of three additional .NET concepts: forms inheritance, external assembly sharing, and application configuration files.

Loading Forms from an External Library

Another characteristic of application extensibility is to load viewers at runtime that might become available after the enterprise application has already been deployed. The .NET Framework's reflection capabilities radically simplify this. Listing 8-8 revisits the Controller object's Process method with an approach to instantiating form objects from a separate assembly. In this implementation, the Controller class can load a form even if it exists in a separate assembly file.

Listing 8-8: An Improved Process Method for the Controller Class
start example
 public static void Process( object argObject, ControllerActions argAction ) {     //based on mapping, display specific form     foreach( ObjectMappingEntry objMapping in _ObjectMappings )     {         //find the right business object         if( argObject.GetType().Name.CompareTo(             objMapping.BusinessObjectName ) == 0 )         {             //find the right action             if( objMapping.Action == argAction )             {  Assembly asm = Assembly.Load( objMapping.Viewer.Substring( 0,   objMapping.Viewer.IndexOf( '.' ) ) );   Type typeViewer = asm.GetType( objMapping.Viewer );  FrameworkViewer formViewer =                     (FrameworkViewer)Activator.CreateInstance( typeViewer );                 formViewer.MdiParent = _ParentForm;                 formViewer.Show();             }         }     }     return; } 
end example
 

The most significant difference is that the form object's Type is not obtained from the application via the GetExecutingAssembly method but rather from a foreign assembly via the Assembly.Load method. The Assembly.Load method loads a named assembly into the application's domain by name. In this case, a substring operation pulls the namespace from the fully qualified name. Otherwise, there is little or no difference from the original implementation.

Note  

This version of the Process method will no longer display forms defined within the same executable file. In general, a separate assembly should manage all forms, including those deployed with the application. Product upgrades may be as simple as replacing the assembly.

Extending Windows Forms Through Inheritance

Just like Web forms, Windows forms should also adhere to standards set by an application framework. If there is to be any sort of plug-in framework, it will only be possible through implementation consistency. One effective way to enforce that consistency is through published interfaces. Another way to enforce consistency is through forms inheritance.

Forms inheritance enables a new Windows form to be created by inheriting from an existing form class. This is helpful in building a plug-in framework because you can create a base form that implements basic operational tasks , such as registering forms, returning menu information, and returning a toolbar icon. Rather than implementing this functionality over and over, the inherited form can benefit from the functionality defined in the base class.

Add a new Windows form to the ApplicationFramework project by selecting it in the Solution Explorer and choosing Add ˜ Add Windows Form. Next, enter FrameworkViewer.cs for the new filename. This will become the base class from which future application viewers will inherit. All that remains is to add the necessary viewer functionality to this form.

To demonstrate possible functionality that might appear in the base class, consider the following: The Controller class has identified four different states that viewers might operate in: New, View, Edit, and Delete. The application framework guidelines might decide that the only difference between a viewer in a New state and a viewer in an Edit state is that data fields are read-only when in the View state. If all viewers in the View state are read-only, then it makes sense to disable all fields in one central location. Listing 8-9 outlines the FrameworkViewer base class, which includes the functionality that evaluates the View mode of the viewer and disables all data entry fields as needed.

Listing 8-9: Implementing the FrameworkViewer Base Class
start example
 public class FrameworkViewer : System.Windows.Forms.Form {     private Controller.ControllerActions _ViewMode;     public Controller.ControllerActions ViewMode     {         set         {              _ViewMode = value;         }     }     public FrameworkViewer()     {         InitializeComponent();     }     private void InitializeComponent()     {         this.Load += new System.EventHandler(this.FrameworkViewer_Load);     }     private void FrameworkViewer_Load(object sender, System.EventArgs e)     {         if( ViewMode == Controller.ControllerActions.View )         {             //set all edit and list controls to read only             foreach( Control controlItem in Controls )             {                 if( controlItem.GetType().Name.CompareTo( "TextBox" ) == 0                      controlItem.GetType().Name.CompareTo( "ComboBox" ) == 0                      controlItem.GetType().Name.CompareTo( "ListBox" ) == 0 )                 {                     controlItem.Enabled = false;                 }             }         }         return;     } } 
end example
 

The code behind the FrameworkViewer class appears like many other form classes. The first element added is a ControllerActions member that identifies the state of the form as New, View, Edit, or Delete. Next, a property accessor allows the controller to set that value. The default constructor that follows is essentially the one provided by Visual Studio. The InitializeComponent method, however, includes only a single statement that designates a Form_Load event handler. This event handler is implemented in the method that follows . The event handler iterates through the form's collection of controls and evaluates their Type values. If a control has a Type value that matches a textbox, combo box, or list box, then the control is disabled. This has the effect of rendering any viewer that is initialized in the View state as read-only.

To take advantage of this functionality, the Controller class needs to set the View mode when the view is initially created. Make the following changes to Listing 8-8 to support this new framework feature:

 Type typeViewer = asm.GetType( objMapping.Viewer ); FrameworkViewer formViewer =     (FrameworkViewer)Activator.CreateInstance( typeViewer );  formViewer.ViewMode = argAction;  formViewer.MdiParent = _ParentForm; formViewer.Show(); 

Turning a Child Form into a Viewer

Turning any MDI child form into a FrameworkViewer is an easy step. The only change necessary is in the class declaration of the form's code-behind page. The class declaration for the FormIssueDetails class now looks like the following:

 public class FormIssueDetails : FrameworkViewer //System.Windows.Forms.Form 

The code compiles normally, and you can still edit the form within the Visual Studio form designer. Once converted, the viewer has access to all the benefits that the application framework provides. Figure 8-9 shows the FormIssueDetails viewer as it appears in its View state.

click to expand
Figure 8-9: The completed FormIssueDetails in its View state

In the case of third-party integrators who do not have code or project access, add a new project reference to the framework assembly by clicking Add Reference, selecting the Projects tab, clicking the Browse button, and pointing to the ApplicationFramework file. Next, add a Windows form to the project and change its base class to FrameworkViewer as specified earlier.

After saving the changes to the source file, preview the form in the form designer. All form code including properties, placed controls, and events are inherited. Like all other objects, the rules of object-oriented inheritance apply when it comes to private, public, and protected methods and attributes.

Retrieving Settings from the Application Configuration File

Earlier, Table 8-1 outlined the application's object-action-form mappings, which defined how the Controller object should respond to a specific user action. Listing 8-3 hard-coded this information directly into the Controller object's constructor. This is far from flexible, and you should certainly externalize it into a configuration file. Coincidentally, the .NET Framework provides services for storing application information into an Extensible Markup Language (XML) file. Create a text file named after the application executable file, app.config, and enter the information outlined in Listing 8-10. Next, save the app.config file to the WinUI project folder and add it as an existing item to the WinUI project. Finally, update the following statement within the AssemblyInfo.cs file:

 [assembly: AssemblyConfiguration("app.config")] 
Listing 8-10: Storing Application Configuration Settings in the app.config File
start example
 <?xml version="1.0" encoding="utf-8" ?> <configuration>     <configSections>         <sectionGroup name="Viewers">             <section name="Include" />         </sectionGroup>     </configSections>     <Viewers>         <Include BusinessObjectName="IssueCollection" Action="2"             Viewer="ApplicationFramework.FormIssueSummary" />         <Include BusinessObjectName="Issue" Action="2"             Viewer="ApplicationFramework.FormIssueDetails" />         <Include BusinessObjectName="Issue" Action="1"             Viewer="ApplicationFramework.FormIssueDetails" />         <Include BusinessObjectName="Object" Action="3"             Viewer="ApplicationFramework.FormDataManager" />     </Viewers> </configuration> 
end example
 

The app.config file contains the same information that was hard-coded into the constructor of the Controller class. With the migration to an external file, you can update the constructor to look more like Listing 8-11.

Listing 8-11: Accessing the app.config File for Controller Settings
start example
 public Controller() {     ObjectMappingEntry entry;     XmlDocument xmldoc = new XmlDocument();     xmldoc.Load( "WinUI.exe.config" );     XmlNode root = xmldoc.DocumentElement;     try     {         XmlNodeList xnodelist =             root.SelectNodes( "/configuration/Viewers/Include" );         foreach( XmlNode xnode in xnodelist )         {              //create a new entry object              entry = new ObjectMappingEntry();              //translate the integer into a ControllerAction              switch( int.Parse(xnode.Attributes["Action"].Value) )              {                  case 1:                      entry.Action = ControllerActions.New;                      break;                  case 2:                      entry.Action = ControllerActions.View;                      break;                  case 3:                      entry.Action = ControllerActions.Edit;                      break;                  case 4:                     entry.Action = ControllerActions.Delete;                      break;              }              //set the BusinessObjectName              entry.BusinessObjectName =                  xnode.Attributes["BusinessObjectName"].Value;              //set the viewer name              entry.Viewer = xnode.Attributes["Viewer"].Value;              //add this mapping to the collection              _ObjectMappings.Add( entry );         }     }     catch( Exception x )     {         EventLog systemLog = new EventLog();         systemLog.Source = "IssueTracker";         systemLog.WriteEntry( x.Message, EventLogEntryType.Error, 0 );     }     return; } 
end example
 

The AppSettingsReader object is a useful tool for pulling custom application attributes from an app.config file. Its only downside is that it expects all values to be represented by a key name-value pair. For representing collections of related values, you can use XPath to navigate to specific nodes and extract specific values. First, XPath navigates to the node containing all the viewer entries. Next, a new ObjectMappingEntry object is created and each child node is iterated to retrieve the necessary viewer attributes. Values are pulled that indicate the action, business object name, and viewer name for each mapping that is stored into the ArrayList.




Developing. NET Enterprise Applications
Developing .NET Enterprise Applications
ISBN: 1590590465
EAN: 2147483647
Year: 2005
Pages: 119

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net