Chillisoft Solutions
Habanero


Tutorial Contents

Habanero Tutorial - for v2.0.0b

Note: Most of this tutorial is still relevant for v2.1, which adds new code generation features and generates a dual release project using a ControlFactory structure.

This tutorial demonstrates some of Habanero's features by creating a demo application in C#. You may want to build the application yourself as you read each section. It is usually best to start with the "Quick & Dirty" tutorial before doing this one.

The source code for the full sample project is available for download at the bottom of this page. Included is the script for the MySQL database. Simply use the restore option in the MySQL administration panel to create the database on your system.


Contents:





Our Story: Replace-IT

Our fictitious computer parts supply business, Replace-IT, has grown beyond all expectations and orders are raining down from all over the country.  It wasn't going to be long before our makeshift stock control system would begin to crack up.  The poor receptionist is spending hours each day logging calls from irate customers whose orders have either been wrongly fulfilled or have simply slipped through the cracks.

It's time for a customised system, and being an IT firm, we feel we have enough know-how to code our own system.  So off we go, load up the coffee machine, crank up some rock music and pull out the SQL manual for the first time.

We've decided to code the system in C# with object orientation, using the MySQL database to store the parts and sales data.  MySQL is a relational database, which means it will be good for reporting. After getting "Hello, world" working and taking fifteen minutes to celebrate, it's heads-down for the real grind.

And it all grinds to a halt.  Having drawn up a plan of our object structure, we then sit down and map out the database table structure.  It suddenly dawns on us: how do you reconcile the two?  Surely there must be a simple way?  And it sure is going to be a mission to write up all the database connection code and all the SQL statements to move the data back and forth between the database and the application. With a splatter of keyboard strokes and a tick-tick of mouse buttons, our browser calls up the Habanero architecture to solve all the world's problems ... well, except for global warming.

The Problem

The hunt is always on to find an easier, quicker, cleaner and more error-free way to achieve a programming task.  Despite continued innovation, there are still some glaring efficiency gaps, such as the jagged interface between relational databases and object-oriented code.

Habanero was developed by a group of software developers that ran into the continuous problem of having to provide the intermediate between object-oriented code and database tables.  A pattern of usage developed and Habanero was packaged together with recurring functionality from the different applications.  Furthermore, the architecture remained in use by the team for a long period before being released onto the market, considerably improving stability and ensuring that the software is relevant to real-world business needs.

What sets Habanero apart from other flavours of persistence or object-relational mapping (ORM) solutions is the ability to generate user interfaces that are automatically mapped to the object as well.  The implication is that you can build a functional database-dependent application with visual results in far less time, which is also great news for those interested in building prototype applications for their clients.

Revisiting the Business

Having decided on using Habanero, it's time to get this vehicle out of the garage and onto the information highway.  We'll build the system incrementally, getting the basic structure up and running and then adding the bells and whistles as we go.






Understanding Habanero-Think

This is a more theoretical explanation of the Habanero framework.  If you prefer a hands-on approach, you can skip this section and come back to it later.

What Does Habanero Try to Fix?

Habanero attempts to tackle two primary problems.

Firstly, there are a number of repetitive steps required to copy values from relational databases, which are not object-oriented, to objects in code.  Database connections need to be coded and managed, and the particular quirks of each database vendor need to be understood.  Then Sql statements must be written for each type of action (create, read, update, delete) and for each property of each object.  Concurrency checks need to be done, and some kind of mapping for inheritance and relationships must also be managed.  In short, there's a vast amount of work covered in these actions, and what's more frustrating is that they are largely repetitive from project to project.  Equally, making small changes to a database structure can result in a large amount of readjustment in the code as well.

Secondly, there is considerable repetition in designing a user interface and mapping each control to a property, as well as ensuring that each property provides up-to-date values that reflect the database content.

Of course, individual requirements for user interfaces can vary vastly, and it is unlikely that any one solution can automate most developers' requirements.  However, Habanero does provide a large array of functionality to create full user interfaces or components of a user interface, and then map these to properties as the developer specifies.  These properties are then kept up-to-date by the background engine as you use the components at run-time.  Depending on your application, this can present a considerable time-saving.

How Does Habanero Carry Out Mapping?

Habanero requires a set of class definitions that are contained in a single XML file. Here you specify:

  • Which classes map to which tables
  • Which properties in each class map to which fields
  • The primary key and any alternate keys
  • Property rules, types and limitations
  • Relationships between tables
  • How user interfaces can display the class data

Habanero comes with Firestarter, a GUI application that can be used to manage the class definitions for you. If you use Firestarter, much of the work in setting up and maintaining your mappings can be automated. You can simply start with an existing database structure, then use Firestarter to generate the class definitions from the database and then generate the code for these class definitions.

You can of course work directly with the class definitions XML - the XML code used to achieve common tasks in this tutorial is included. Carrying out the process manually, you would need to handle three steps, in any order. 1) Design the database. 2) Create the classes. 3) Create the XML class definitions. With regards to creating the classes, all entity classes are required to inherit from Habanero's BusinessObject. For each property value that must be mapped, the property will need get and set methods that use a Habanero-provided method to carry out the persistence. The appendices contain the full API for the class definitions.

What are Business Objects?

Business objects are entities that in some way represent different components of business logic.  For instance, in the manufacture of a car, the wheels, door, bonnet, seat and ignition all make up business objects.  An email client provides more a more abstract example: an individual email, an address book, a blocked-senders list and a preference settings list could all be business objects.  Essentially business objects are well used term in the IT industry to differentiate these kinds of components from other objects like widgets in a user interface or components of your database connection architecture.

Habanero's BusinessObject provides a range of common functionality to its inheritors.  Most importantly it hands down the tools to carry out persistence of the properties to and from the database.

What About Sql?

There is some debate at the moment on the wisdom of either hiding developers from the power of Sql or giving the developers open room to create errors through faulty Sql code.  In Habanero, both sides are catered for to some extent.

At a simple level, you will hardly write any Sql while using Habanero.  Basic persistence to and from the database is handled by Habanero, which automatically constructs the Sql needed for each property value and adapts each statement to cater for the supported database vendor you specified.

There are, however, times when you will need to use some level of Sql.  At a simple level, you can place restraints on how many objects are loaded in a collection of business objects, and how these are ordered.  These are provided using Sql criteria that will be appended to the Sql statements.  Secondly, Habanero provides support for migrations, so that you can take an updated application to an outdated client installation, and the application will automatically update the client using the Sql statements you have provided.  Finally, you can still execute a self-created Sql statement at any place in your code.  You can construct the statement with a single string, or you can safely build a SqlStatement object out of components.

Supporting Alternate Data Sources

Habanero provides the structure to support data storage other than databases. For instance, testing can be done using an in-memory database that manages the data during the lifetime of the application. The current device is stored using the GlobalRegistry.DataAccessor property.

Designing for Many UI Environments

Habanero supports UI generation for forms and grids, so that developers can automate much of their user interface design. Even better, Habanero uses a control factory structure, so that each control is an instance of an interface and is created by a control factory. With this design, you need only change the control factory and you can convert your entire UI design to run in a different UI environment.

Taking advantage Visual WebGUI by Gizmox, you can design one system to run either as a desktop application, or as an online web application. Visual WebGUI simulates Windows Forms design for the web environment, giving you the advantage of reduced development time and increased responsiveness.






Installing Habanero

System Requirements

Habanero is a .Net product, so you will need to install the .Net 2.0 (or above) framework.  You will also need to meet the .Net 2.0 minimum requirements.  As far as application requirements go, this depends heavily on the type of application you will be developing.  For client installations, you require Windows 2000 or above and will typically need a Pentium 4 1.6ghz, 256mb RAM for Windows 2000 or 512mb RAM for Windows XP.  The minimum requirements for Windows Vista applications are not yet clear.

Licensing & Installation

You can obtain all editions of Habanero through the Download section. The download file is a zip that contains two MSI installation files, one for the Habanero framework with its DLL's, documentation and source code. The other installs Firestarter, the GUI application to manager the class definitions.

The source comes with test classes for use with NUnit, which is a free testing framework - that way you will be able to check if any of your changes break other parts of the framework. Note that the source code is distributed under the GNU Lesser GPL, implying that you cannot sell the source code or any modifications to it. If you have fixed any bugs or made useful additions, it would be helpful to the community of Habanero developers if you posted the code on the Habanero forum. Some of this code may be incorporated into future versions of Habanero.

More instructions on working with the source code can be found in the appendices.

Files & Directories

The Habanero installation will include the following folders:
  • bin - contains the Habanero DLLs
  • source - contains those parts of Habanero which are open source
  • firestarter - contains the FireStarter application
  • docs - contains Habanero documentation, excluding the tutorials, which are only available online
  • lib - some additional DLLs that may be useful

If you will be using logging in your application, download and install the log4net DLL's - see Appendix II for more information.

Once you have chosen the database vendor you will be using, follow the installation instructions for that database.  Your Habanero-based application will need to reference the DLL's that come with that installation.

Technical Support

Chillisoft provides various types of support for Habanero. See the Support page for more information. There is also a brief Troubleshooting section at the end of this tutorial.






Setting up a Simple Application

NOTE: You will find some sections with different markings:

Auto - indicates that Firestarter is used to automate some tasks. This is the recommended route.

Manual - indicates how you could achieve a task by working directly with the code or XML. This is for advanced use and you can usually skip these sections.

Designing the Database

At this stage you'll have chosen your database of choice and installed it. For this tutorial we'll be using the MySQL database.  We open the database administrator and create a new schema called "replace_it".  It's time for our first table, which will hold our computer parts.  We'll call it ComputerPart - no surprises there.  The first column is "ComputerPartID" which serves as the primary key.  Code conventions for C# use PascalCase for naming, so it's easier if we use that same convention in the database too. Now there is some debate on primary keys and what format they take, but for our example, our primary key will be a Guid and will remain hidden from the user.  In MySQL, a Guid can be represented as a CHAR(38) type.

Auto Auto-Generating the Class Definitions

The class definitions are held in an XML file and are used to describe how the database maps to the objects in code. Habanero includes FireStarter, which is a GUI editor that helps you manage them visually. You can either set up the classes individually using FireStarter, or you can have them generated from the database for you.

Before you open FireStarter, you will need to copy the DLL of your database vendor (eg. MySQL.Data.dll for MySQL) into the executable directory of FireStarter in your Chillisoft/Habanero installation. Once you've opened FireStarter, create a new project in FireStarter (through the File menu) called "Replace_it". Then go to the Generate menu and select "ClassDefs from Database". The following window should appear:

First add a Connection string that specifies how to access your database. The Default Assembly indicates which project your class files will reside in. Pascal Case refers to the naming of the classes and Table Name Prefix can eliminate any prefix you have in front of your table (eg. if all your tables are tbsomething). Tick the two Populate options for now, so that we can generate user interfaces for the classes as well. Ignore the Show Primary Keys options and the Append options, which prevent a regeneration from adding columns or fields when you have previously chosen not to. Then Test the connection and Generate the definitions. ComputerPart should now be listed. If you have any error messages, check that your connection string is correct (port 3306 is often a good choice for a localhost server).

Manual Writing the Class Definitions Manually

The class definitions are held in an XML file and are used to describe how the database maps to the objects in code. A full list of the elements and options available are listed in Appendix I. Create an XML file called "ClassDefs.xml" and insert the following code.

ClassDefs.xml
<?xml version="1.0" encoding="utf-8" ?>
<classes>
  <class name="ComputerPart" assembly="Replace_it">
    <property name="ComputerPartID" type="Guid" />
    <property name="Description" />
    <property name="Price" type="Decimal" />
    <primaryKey>
      <prop name="ComputerPartID" />
    </primaryKey>
  </class>
</classes>

Later on, once you've created your solution, you will need to add the XML file to your solution. This file must be copied to the output directory of your application. Right-click on the project containing your ClassDefs.xml file, go to Build Events and add the following Post-build command: copy "$(ProjectDir)ClassDefs.xml" "$(SolutionDir)Replace_it\bin\Debug\"

By the way, you can use Firestarter to export just the XML class definitions file for you, rather than generating the whole project. Keep in mind that if you make local changes to your XML file and then export the class definitions from Firestarter, you may lose your changes. If you edit the XML directly, you may want to use Firestarter's import utility which will import an existing XML file.

Auto Auto-Generating a New Solution

When you are starting a new project from scratch, Firestarter can generate the new project for you as a solution (.sln) file that you can open in Microsoft Visual Studio. This will contain the assemblies, the business object classes, the launch classes and some additional utilities.

If you already have an application and want to take advantage of the code generation, you can take one of two approaches. Either generate a new project and transfer your old code across to the new one, or generate a new project and transfer the classes from the new project across to your old one. The first option is better, because regenerating the code after making some changes will not require the manual transfer to be repeated.

Code generation is done by Firestarter. Once you've set up your class definitions, simply go to the Generate menu and select "Generate Code". The following window should pop up:

You might need to specify which Connection the new solution will be using and you'll need to set the Solution Directory. The Habanero DLL path only needs to be supplied if you want the application to reference a different set of DLLs to that which FireStarter is currently using. As you can see, four projects are created within your solution:

  • Replace_it - The startup project that defines the environment
  • Replace_it.BO - Holds the business objects and the class definitions XML file
  • Replace_it.Logic - Holds the application's business logic
  • Replace_it.UI - Holds the user interfaces and form controller

Once you've clicked Generate you can open the project by double-clicking on the .sln file (depending on your IDE). Add your database vendor's DLL to the references of the root project (eg. MySQL.Data.dll), and ensure that Replace_it is the default startup assembly. The project should compile and run successfully.

Manual Setting up the Solution/Project Manually

Fire up your standard IDE or text editor and set up a new solution.  If you're using Visual Studio, add new projects as recommended above in the Auto solution. For this tutorial, there should be one Windows Application project and the rest would be libraries. First up we'll need to reference some DLL's that are important to our application.  If you're using Visual Studio, right click on the References folder and "Add reference".

  • Habanero*.dll - These are the DLL's you installed.
  • log4net.dll - This is the logging system that Habanero supports for outputting runtime information to a specified log.  See Appendix II for more information.
  • MySql.Data.dll - Used to manage MySql database connections (if you're using a different database, you'll need to make appropriate adjustments here)
  • Noogen.Winforms.dll - Used to generate combo-boxes

If you've created a new project in Visual Studio 2005, you'll see that two classes have been added already: "Form1.cs" and "Program.cs".  If you're using a different IDE, go ahead and create two equivalent classes for consistency. We'll also integrate Habanero at this stage - notice the "using" references, the two lines at the beginning of the "main" method and the try/catch clause which provides a neat error message box.

Program.cs
using System;
using System.Windows.Forms;
using Habanero.UI.Base;
using Habanero.UI.Win;
using Habanero.Base;

namespace Replace_it
{
    static class Program
    {
        [STAThread]
        static void Main()
        {
            HabaneroAppWin mainApp = new HabaneroAppWin("Replace_IT", "v1.0");
            if (!mainApp.Startup()) return;

            try
            {
                Application.EnableVisualStyles();
                Application.SetCompatibleTextRenderingDefault(false);
                Application.Run(new Form1());
            }
            catch (Exception ex)
            {
                GlobalRegistry.UIExceptionNotifier.Notify(ex,
                "An error has occurred in the application.",
                "Application Error");
            }
        }
    }
}

 

Form1.cs
Note: InitializeComponent() is implemented in an IDE-created class called Form1.Designer.cs
using System;
using System.Collections;
using System.Windows.Forms;

namespace Replace_it
{
	public partial class Form1 : Form
	{
		public Form1()
		{
			InitializeComponent();
		}
	}
}

Manual Configuring Database Settings

The application needs to know how to connect to the database and this is done using the "app.config" file in Visual Studio (right-click on the project, "add new item" and choose "Application Configuration File").  Once the file has been added to the project, it should be amended to look something like that below.  The options for vendor are "mysql", "access", "sqlserver", "oracle", "postgresql" and "sqlite".  Additionally, you may want to add logging support to your application, which is a simple operation - see Appendix II on how to add Log4net to the configuration file.

app.config
<?xml version="1.0"?>
<configuration>

  <configSections>
      <section name="DatabaseConfig" type="System.Configuration.DictionarySectionHandler"/>
  </configSections>

  <DatabaseConfig>
    <add key="vendor" value="Mysql"/>
    <add key="server" value="localhost"/>
    <add key="database" value="replace_it"/>
    <add key="username" value="..."/>
    <add key="password" value="..."/>
    <add key="port" value="3306"/>
  </DatabaseConfig>
</configuration>

Manual Adding Business Objects

We'll need a new class for ComputerPart that will map to its database table. As with all such objects, ComputerPart inherits from BusinessObject.  Here is the code for a simple version of the class that provides properties to manipulate the class's fields.  Note that if you're wanting to vary this for your own use, you'll only need to change the class name where appropriate and add or remove the properties for each of the fields you want to display or amend in your database table.

Just a small but important note here on properties.  When you amend a property directly in the code, you will then need to call Save() on the object to have the changes persisted.  Thus, you can make several changes or calculations and only save the changes when you are ready.

Since we're working with database values which potentially have a null value, we use nullable types (with the ? on the end) - should you need to cast a nullable type to a non-nullable type, use ".Value".

ComputerPart.cs
using System;
using Habanero.BO;

namespace Replace_it
{

public class ComputerPart : BusinessObject
{
    public Guid? ComputerPartID
    {
        get { return (Guid?)GetPropertyValue("ComputerPartID"); }
        set { SetPropertyValue("ComputerPartID", value); }
    }

    public string Description 
    {
        get { return (string)GetPropertyValue("Description"); }
        set { SetPropertyValue("Description", value); }
    }

    public decimal? Price
    {
        get { return (decimal?)GetPropertyValue("Price"); }
        set { SetPropertyValue("Price", value); }
    }
}
}

Accessing Business Objects in Code

Now that we've set the Habanero environment and have a working persistence framework, we have ready access to our business objects. Here is a sample set of code to load, edit and save a ComputerPart:

using Habanero.BO;

public SomeClass
{
    public void SomeMethod()
    {
        // Create a new object
        ComputerPart computerPart = new ComputerPart();
        computerPart.Description = "320gb Hard Drive";
        computerPart.Price = 130;
        computerPart.Save();
        
        // Load an object
        ComputerPart copy = BORegistry.DataAccessor.BusinessObjectLoader.GetBusinessObject<ComputerPart>("Price = 130");
        
        // Load and modify a collection
        BusinessObjectCollection<ComputerPart> computerParts = new 
            BusinessObjectCollection<ComputerPart>();
        computerParts.Load("Price = 130", "");
        computerParts[0].Description = "320gb Hard Disk Drive";
        computerParts.AcceptChanges();
    }
}

Note that the changes are not committed until you call Save on a BusinessObject or AcceptChanges on a collection. You can also group changes together under one Transaction - see the Wiki for more information.






Generating User Interfaces

We're about to explore a powerful feature of Habanero - generating user interfaces for the objects we are mapping. This works best when you have a class with several properties that behave fairly simply and just require information, such as contact details. When you have a complex user interface, Habanero can still generate most of the interface for you, saving you the time of mapping the controls to the properties and checking that the values supplied are valid according to the application's business rules. You can then add customisation on top of the generated controls.

Multiple User Interface Environments

A key feature of version 2.0 is the ability to design one application for multiple environments. This would have been unthinkable in the past, but with the launch of Visual WebGUI by Gizmox, Habanero now allows you to design your user interface independently of the environment, and you can switch between the environments simply by changing your configuration.

The key to achieving this is the IControlFactory class. Whenever you want to create a control, like a TextBox, Grid or Form, simply call a method in the application's ControlFactory, which will create the right control and make sure it is customised for that environment. For instance, apart from the obvious namespace issues, one key difference between designing for the desktop and designing for the web is the amount of events that are assigned. If each event causes a call to the server in your web application, you can understand that Habanero ensures that controls for the web environment sacrifice some usability in order to reduce bandwidth and lag in your web application.

The following diagram illustrates how controls are structured in Habanero, with "Win" indicating controls from System.Windows.Forms and "VWG" indicating controls from Gizmox.WebGUI.Forms.

Most controls have a manager, which groups common logic used between all the variants of a control. The current ControlFactory is stored in GlobalUIRegistry.ControlFactory - there is a default control factory initialised for you when you run StartUp on HabaneroApp. In a moment we'll see how to create controls using your ControlFactory.

Auto Managing UI Design in Firestarter

Using FireStarter, you manage which properties to display under each class's "UIs" tab. You can generate grids and/or forms, and specify which control types to use to edit the data. When you generate the class definitions from a database, FireStarter can automatically create the UI definitions as well. The following screenshot demonstrates what would have been created for a grid display of computer parts:

Manual Editing UI Class Definitions

You might want to edit the UI class definitions directly in the XML. The result as viewed above can be added to your XML as follows for the ComputerPart class:

ClassDefs.xml
<?xml version="1.0" encoding="utf-8" ?>
<classes>
  <class name="ComputerPart" assembly="Replace_it">
    <property name="ComputerPartID" type="Guid" />
    <property name="Description" />
    <property name="Price" type="Decimal" />
    <primaryKey>
      <prop name="ComputerPartID" />
    </primaryKey>
    <ui>
      <grid>
        <column property="Description" />
        <column property="Price" />
      </grid>
      <form>
        <field property="Description" />
        <field property="Price" />
      </form>
    </ui>
  </class>
</classes>

You can see from the UI section of this definition that a lot is assumed about the control types and the labels. See the Appendix on Class Definitions for more information on how you can control the layout and display of the different fields and columns.

Managing Multiple Forms

When you generate a new project with FireStarter, the main form is set as an MDI form, meaning that it can contain other windows within it. The one advantage of having several forms open at the same time is that when you switch away from a form, you should be able to return to it in its last state (unless you close it). Habanero facilitates this with the FormController class, from which we will inherit and adapt.

First we'll create a form to show our computer parts, simply adding a new file in the .UI project. In order to merge this form into the form controller, it needs to inherit from UserControlWin and IFormControl.

We'll add code to load the computer parts from the database into a collection of business objects and make them available to be read by the grid. At a later stage we'll want to use Sql criteria with the Load() method to limit just how many parts we display. We'll also instantiate a grid and add it to the provided form. There are some grid variants - this one is read-only and provides buttons to edit the selected row. Note that we use the BorderLayoutManager supplied with Habanero - this was created to cover for user interface shortfalls in .net.

ComputerPartsForm.cs
using System.Collections;
using Habanero.BO;
using Habanero.UI.Base;
using Habanero.UI.Win;
using Replace_it.BO;

namespace Replace_it.UI
{
    public class ComputerPartsForm : UserControlWin, IFormControl
    {
        public ComputerPartsForm()
        {
            BusinessObjectCollection<ComputerPart> boCollection = 
                new BusinessObjectCollection<ComputerPart>();
            boCollection.LoadAll();
            IReadOnlyGridControl grid = GlobalUIRegistry.ControlFactory.CreateReadOnlyGridControl();
            grid.SetBusinessObjectCollection(boCollection);

            BorderLayoutManager manager = GlobalUIRegistry.ControlFactory.CreateBorderLayoutManager(this);
            manager.AddControl(grid, BorderLayoutManager.Position.Centre);
        }

        public void SetForm(IFormHabanero form) {}
    }
}

You'll notice that when FireStarter generates code for a new project, a class is already created in the UI project to control the forms. You'll simply need to add each form you create to the structure already provided:

Replace_itFormController.cs
using System.Windows.Forms;
using Habanero.UI.Base;
using Habanero.UI.Forms;

namespace Replace_it.UI
{
    public class Replace_itFormController : FormController
    {
        public const string COMPUTER_PARTS = "Computer Parts";
        
        public Replace_itFormController(IFormHabanero parentForm, IControlFactory controlFactory) :
        		base(parentForm, controlFactory) { }
        
        protected override IFormControl GetFormControl(string heading) {
            IFormControl formCtl = null;
            switch (heading)
            {
                case COMPUTER_PARTS:
                    formCtl = new ComputerPartsForm();
                    break;
            }
            return formCtl;
        }
    }
}

Finally, in ProgramForm (the master application form in the root project) we tie the form controller to the main menu so that the correct form opens and so that all currently open forms are listed under the Window menu. Note that in order to integrate with Habanero's UI structure, we change the form to inherit from FormWin rather than Form itself.

ProgramForm.cs (Form1.cs) (Amended)
using System.Windows.Forms;
using Habanero.Base;
using Replace_it.UI;

namespace Replace_it
{
    public partial class ProgramForm : FormWin
    {
        private Replace_itFormController _formController;
		
        public ProgramForm()
        {
            SetBounds(0, 0, 640, 480);
            IsMdiContainer = true;
            CreateMainMenu();
            InitializeComponent();
            SetupForm();
        }

        private void SetupForm()
        {
            Text = GlobalRegistry.ApplicationName + " " + GlobalRegistry.ApplicationVersion;
        }

        private void CreateMainMenu()
        {
            _formController = new Replace_itFormController(this, GlobalUIRegistry.ControlFactory);

            MainMenu mainMenu = new MainMenu();
            MenuItem fileMenu = new MenuItem("&Data");
            fileMenu.MenuItems.Add(new MenuItem("&Computer Parts", delegate
                { _formController.SetCurrentControl(Replace_itFormController.COMPUTER_PARTS); }));
            mainMenu.MenuItems.Add(fileMenu);

            MenuItem windowMenu = new MenuItem("&Window");
            windowMenu.MdiList = true;
            mainMenu.MenuItems.Add(windowMenu);

            Menu = mainMenu;
        }
    }
}

A small note here: if you prefer using the form designer in Visual Studio to handle your menus, simply remove the CreateMainMenu method listed above and use the designer to add the menu. When you double-click on a menu item, insert the "_formController.SetCurr..." code into the newly created method.

Running the Application

Everything is in place now and if we build and run the project, we'll have an editing window something like this:

You should be able to click on the "Add" button and the newly added object should appear in the grid immediately.  If that doesn't happen then you will need to check your database setup, the app.config file, the class definitions or the ComputerPart.cs class.  Check, for instance, that you've provided the correct field names.

You may notice that the editing form doesn't have a title bar and can't be moved around. This can be amended in FireStarter by supplying a Title for the Form, under the "UIs" tab.

Note that in this application we haven't included the ComputerPartID in the grid.  This was partly a matter of choice and partly of principle.  Regarding choice, a Guid is not as useful as a description when listing parts.  Regarding principle, it's commonly held opinion that you shouldn't be changing your primary key, so we've designed the database to eliminate that possibility, keeping the primary key discrete.  Of course, we will later add a unique reference number for each computer part, but that will be in a custom format rather than as a Guid, which is admittedly difficult to read and communicate.

Our application is nicely set for expansion now, and you can see that it will be easy to scale it up as we add components. Let's try that in the next section.






Managing Changes

The ComputerPart class is very limited at the moment - we don't even know how many parts we have in stock. We simply need to add a few more fields, but just how much work is this going to be? Change management is one area where object-relational mapping begins to shine. Rather than update a whole range of SQL statements, we can manage this process fairly smoothly.

Updating the Database

We'll need to begin the change process by updating the database. After adding some more important fields, our database structure now looks like this:

Auto Updating Code Automatically

The process of updating your code is made easy with FireStarter. Simply carry out the generation steps again. Firstly, call up the menu option "Generate > ClassDefs from Database". Keep the same settings, except tick the two "Append" options, so that any new fields will also be assigned to your grid and form property lists.

To update the code, regenerate the project using the menu option, "Generate > Generate Code", and keep the same settings as before. FireStarter will add the files that are missing and will update the business objects. Re-open the solution if you have added new classes and they don't appear. Note that each business object class is split into two partial files. The first file (eg. ComputerPart.cs) is where you can add your custom code, knowing that it won't be affected by a regeneration. The second file (eg. ComputerPart.Def.cs) holds the properties and relationships and will be replaced each time you regenerate.

Manual Updating Class Definitions & Code Manually

To update the class definitions, simply add the new properties and their types. "String" is the assumed default type and the other types can be named either by their wrapper class names (eg. Int32) or their standard-use names (eg. int). (see Appendix I for a list of data types in the class definitions). If you're generating user interfaces you will also add the new properties to your UI listings where appropriate.

ClassDefs.xml (Amended)
<classes>
<class name="ComputerPart" assembly="Replace_it">
  <property name="ComputerPartID" type="Guid" />
  <property name="Description" />
  <property name="Cost" type="Decimal" />
  <property name="Price" type="Decimal" />
  <property name="Stock" type="Int32" />
  <property name="PartCode" />
  <primaryKey>
    <prop name="ComputerPartID" />
  </primaryKey>
</class>
</classes>

The ComputerPart class also needs new properties to reflect the changes.

ComputerPart.cs (Amended)
public int? Stock
{
    get { return (int?)GetPropertyValue("Stock"); }
    set { SetPropertyValue("Stock", value); }
}

public decimal? Cost
{
    get { return (decimal?)GetPropertyValue("Cost"); }
    set { SetPropertyValue("Cost", value); }
}

public string PartCode
{
    get { return (string)GetPropertyValue("PartCode"); }
    set { SetPropertyValue("PartCode", value); }
}

Check that the project compiles. If you get an error when you build at this stage, check for mistakes like wrong capitalisation, spaces between words and types that don't match up.

Running the updated project should provide an editing form something like the following:






Introducing Inheritance

We're ready to introduce orders received by customers and orders sent to suppliers. It's easy to see that they have several things in common, making this a good opportunity to explore inheritance.

Inheritance Strategies

Inheritance is one distinct feature supported by object-orientation but not by relational databases.  Habanero as an object relational mapping tool translates an inheritance structure to database tables in three different ways.  The default "ClassTableInheritance" uses one database table per class in the inheritance structure. All fields that are inherited from the parent are actually stored in the database table of the parent, while the child table only stores its ID, the parent's ID and any fields that are unique to the child.  "SingleTableInheritance" maps all fields of all classes of an inheritance structure into a single table. "ConcreteTableInheritance" uses a table for each concrete class in the inheritance hierarchy, so that the fields of an abstract parent are stored in the child rather.

In our example, supplier orders and customer orders are very similar - they can both have an order number, name, price and delivery status. These properties can all be abstracted to a parent PartsOrder class that the user will never see. ("Order" is a sql keyword)

Now to choose an inheritance strategy.  Each one obviously has its pros and cons.  The advantage of "ClassTableInheritance" is that we're not having to duplicate the fields of each inheritor in both the database tables and the class definitions - once they've been declared in the parent, they're inherited automatically.  "SingleTableInheritance" places all the data into one table, which does reduce our control over the individual tables.  "ConcreteTableInheritance" separates the data distinctly but does incur duplication.  You can imagine that in a class with many fields, this duplication can add up to a lot of extra work.  We'll go with the default for this example, although you may want to experiment with the other structures too.

Note that the inheritance format discussed below is one of several types available. For more information on other ways of implementing inheritance, see the Wiki.

Adding the Database Tables

Here are the three database tables. At the moment we're keeping it simple and not giving the child classes any fields of their own. This is just temporary, and it's easy to imagine that differences will begin to creep in. Foreign keys are created here to prevent a parts order from being deleted while the child order still exists. Out of interest, there is no requirement for database foreign keys in Habanero, because Habanero can carry out deletion prevention itself. There are two advantages of adding foreign keys though - it helps FireStarter to detect relationships when generating the definitions and it prevents accidental deletions if someone is editing the database directly.

 

 

Auto Auto-Generating the Classes

As you did in the "Managing Changes" section, use FireStarter to regenerate the class definitions. There are just some fiddly extra steps to take before you generate the code. FireStarter doesn't yet detect inheritance from a database schema, so go to the CustomerOrder and SupplierOrder classes, select Super Class and assign the PartsOrder class. Secondly, delete the PartsOrderID properties in the two child classes (since this is inherited). Finally, jump across to the UI definitions for each class and assign which properties you would like to display in those two classes. The multi-selector is an easy way of assigning the properties quickly.

FireStarter correctly picks up that the Delivered property is a boolean, but will display a TextBox by default. We'd rather use a CheckBox, so switch over to the "UIs" tab and the "Form" tab, and change the Delivered control to CheckBox.

Subsequently regenerate the code as you did before, which should create three new classes in your project. Re-open your solution if the new classes don't appear.

Manual Adding the Class Definitions

In the child class definitions, the "superClass" element is used to indicate inheritance and the PartsOrderID is left out because it's inherited. If you're generating user interfaces, you'll also need to include UI definitions for the child classes.

  <class name="PartsOrder" assembly="Replace_it">
    <property name="PartsOrderID" type="Guid" />
    <property name="OrderNumber" />
    <property name="Name" />
    <property name="TotalCharge" type="Decimal" />
    <property name="Delivered" type="Boolean" default="false" />
    <primaryKey>
      <prop name="PartsOrderID" />
    </primaryKey>
  </class>

  <class name="SupplierOrder" assembly="Replace_it">
    <superClass class="PartsOrder" assembly="Replace_it" />
    <property name="SupplierOrderID" type="Guid" />
    <primaryKey>
      <prop name="SupplierOrderID"/>
    </primaryKey>
  </class>

  <class name="CustomerOrder" assembly="Replace_it">
    <superClass class="PartsOrder" assembly="Replace_it" />
    <property name="CustomerOrderID" type="Guid" />
    <primaryKey>
      <prop name="CustomerOrderID"/>
    </primaryKey>
  </class>

Manual Adding the Classes Manually

PartsOrder.cs
using System;
using Habanero.BO;

namespace Replace_it
{
public class PartsOrder : BusinessObject
{
    public Guid? PartsOrderID
    {
        get { return (Guid?)GetPropertyValue("PartsOrderID"); }
        set { SetPropertyValue("PartsOrderID", value); }
    }

    public int? OrderNumber
    {
        get { return (int?)GetPropertyValue("OrderNumber"); }
        set { SetPropertyValue("OrderNumber", value); }
    }

    public string Name
    {
        get { return (string)GetPropertyValue("Name"); }
        set { SetPropertyValue("Name", value); }
    }

    public decimal? TotalCharge
    {
        get { return (decimal?)GetPropertyValue("TotalCharge"); }
        set { SetPropertyValue("TotalCharge", value); }
    }

    public bool? Delivered
    {
        get { return (bool?)GetPropertyValue("Delivered"); }
        set { SetPropertyValue("Delivered", value); }
    }
}
}

Thanks to inheritance, our two new child classes couldn't be simpler:

SupplierOrder.cs
namespace Replace_it
{
    class SupplierOrder : PartsOrder
    {
    }
}

CustomerOrder.cs
namespace Replace_it
{
    class CustomerOrder : PartsOrder
    {
    }
}

We could just create some user interfaces for these orders, but let's go a step further. Covering relationships will give us the tools needed to have a list of parts for each order...






Creating Relationships

Introducing Relationships

Relationships have a range of uses, especially in providing content for combo-boxes and relating a group of items that belong to a parent. Order items are a perfect example - the interesting challenge is how to describe this relationship. In database design, you create the child class and include a field that refers back to the parent, whereas in code you would probably hold a collection in the parent class. Habanero effectively links both approaches.

In addition to the relationship between a PartsOrder and OrderItems, we can also create a relationship between the OrderItem and the ComputerPart it represents. If we simply put a string for the part code field, we're opening the system up to errors. What if the part code changes format? It's far cleaner to change the part code field to an ID (in this case a Guid) that points back to the original ComputerPart table. The second advantage of this is that we can easily construct a ComboBox, so the user simply picks the part code from a list. The third advantage is that giving us the ID of the ComputerPart means we're one step away from all the other properties of that part, such as price and stock.

Relationships are usually described as 1-to-1, 1-to-many or many-to-many. Many-to-many relationships require an intermediate table, which we won't discuss here (there's plenty of web material on this subject). What concerns Habanero is the immediate view from each class. In other words, does this relationship point to single or multiple objects? For a PartsOrder, a multiple relationship points to all the items in the order, and in an OrderItem, a single relationship points to the original order. Similarly, in an OrderItem, a single relationship exists to the ComputerPart and a multiple relationship points from the ComputerPart to all the OrderItems that use it.

Just a note here on deletion prevention. Habanero provides three options for deletion prevention on multiple relationships only. Deletion prevention resolves the question of: what happens if someone accidentally deletes a ComputerPart that is being used in an OrderItem? The deletion check goes on the ComputerPart's relationship rather than on the OrderItem. Habanero thus poses the question to a multiple relationship: can I delete myself if someone is using me? This works the opposite way to foreign keys. The three options are: prevent deletion, delete related (eg. delete the order items if I get deleted) and dereference related (ie. in code, remove the reference so that the object will either continue to belong to something else that references it or be cleaned up by the garbage collector). Prevention is the default and that's our preference in this example.

Adding an OrderItem Table

Here is the table layout for the "OrderItem", including the two foreign keys:

Auto Regenerating the Code

While you regenerate the class definitions, you need to watch that your previous changes are not overwritten. For instance, the CheckBox control for the Delivery boolean (discussed under Inheritance) may revert to a Textbox. Simply turn off the two "Append" options, to prevent previous UI definitions being appended to.

FireStarter detects relationships from the foreign keys you supply in the database, so looking under the "Relationships" tab will show the relationships we have discussed.

You can then regenerate the code as well, restarting Visual Studio if no OrderItem.cs class is visible. One last step is needed after generating the code - there is no way to know which PartsOrder an OrderItem is associated with, so we can specify that in the constructor. The constructor must not be added to the OrderItem.Def.cs stub, otherwise it will be overwritten in the next regeneration.

OrderItem.cs (Amended)
    public partial class OrderItem : BusinessObject
    {
        public OrderItem() { }

        public OrderItem(PartsOrder partsOrder)
        {
            PartsOrderID = partsOrder.PartsOrderID;
        }
    }

Manual Adding Class Definitions & Code Manually

The new feature here is the "relationship" element. We'll add single-type relationships in the OrderItem class definition and multiple-type relationships on the other sides.

<class name="OrderItem" assembly="Replace_it">
  <property name="OrderItemID" type="Guid" />
  <property name="ComputerPartID" type="Guid" />
  <property name="Quantity" type="Int32" />
  <property name="PartsOrderID" type="Guid" />
  <primaryKey>
    <prop name="OrderItemID" />
  </primaryKey>
  <relationship name="PartsOrder" type="single" relatedClass="PartsOrder" relatedAssembly="Replace_it">
    <relatedProperty property="PartsOrderID" relatedProperty="PartsOrderID" />
  </relationship>
  <relationship name="ComputerPart" type="single" relatedClass="ComputerPart" relatedAssembly="Replace_it">
    <relatedProperty property="ComputerPartID" relatedProperty="ComputerPartID" />
  </relationship>
</class>

Also, add the multiple relationships for the definitions of PartsOrder and ComputerPart:

<relationship name="OrderItems" type="multiple" relatedClass="OrderItem" relatedAssembly="Replace_it">
  <relatedProperty property="PartsOrderID" relatedProperty="PartsOrderID" />
</relationship>

<relationship name="OrderItems" type="multiple" relatedClass="OrderItem" relatedAssembly="Replace_it">
  <relatedProperty property="ComputerPartID" relatedProperty="ComputerPartID" />
</relationship>

The OrderItem class will need an additional constructor to assign the ID of the parent PartsOrder and an additional method to return the parent order relating to an order item.

OrderItem.cs
using System;
using Habanero.BO;

namespace Replace_it
{
    public class OrderItem : BusinessObject
    {
        public OrderItem() { }

        public OrderItem(PartsOrder partsOrder)
        {
            PartsOrderID = partsOrder.PartsOrderID;
        }

        public PartsOrder PartsOrder
        {
            get { return Relationships.GetRelatedObject<PartsOrder>("PartsOrder"); }
        }

        public Guid? OrderItemID
        {
            get { return (Guid?)GetPropertyValue("OrderItemID"); }
            set { SetPropertyValue("OrderItemID", value); }
        }

        public Guid? ComputerPartID
        {
            get { return (Guid?)GetPropertyValue("ComputerPartID"); }
            set { SetPropertyValue("ComputerPartID", value); }
        }

        public int? Quantity
        {
            get { return (int?)GetPropertyValue("Quantity"); }
            set { SetPropertyValue("Quantity", value); }
        }

        public Guid? PartsOrderID
        {
            get { return (Guid?)GetPropertyValue("PartsOrderID"); }
            set { SetPropertyValue("PartsOrderID", value); }
        }
    }
}

PartsOrder.cs will require an additional method to return the collection of its associated order items (between the quotes we specify the relationship name as defined in the class definitions):

public IBusinessObjectCollection OrderItems
{
    get { return Relationships.GetRelatedCollection<OrderItem>("OrderItems"); }
}

Likewise for ComputerPart.cs:

public IBusinessObjectCollection OrderItems
{
    get { return Relationships.GetRelatedCollection<OrderItem>("OrderItems"); }
}

At this stage the application should compile successfully.

Developing the User Interface

There are several ways to display the corresponding order items when you click on an order. One efficient way is to have two grids, so that clicking on the customer order grid will cause the order items grid below it to show the items for that order.

The principle challenge here is to copy across the PartsOrderID when a new item is added, otherwise we have no way of knowing what the parent order is. The DefaultBOCreator creates a new object when we click "Add", so we'll simply replace it with our own, passing the parent object to the constructor. The overridden CreateBusinessObject will be called by the grid control just before the editing screen is opened.

OrderItemCreator.cs
using Habanero.Base;
using Replace_it.BO;

namespace Replace_it.UI
{
public class OrderItemCreator : IBusinessObjectCreator
{
    private PartsOrder _parentObject;

    public OrderItemCreator()
    {
        _parentObject = null;
    }

    public OrderItemCreator(PartsOrder partsOrder)
    {
        _parentObject = partsOrder;
    }

    public IBusinessObject CreateBusinessObject()
    {
        if (_parentObject != null)
        {
            return new OrderItem(_parentObject);
        }
        return null;
    }
}
}

We can now build one form to display all kinds of orders, by taking advantage of generics.

OrdersForm.cs
using System;
using System.Windows.Forms;
using Habanero.BO;
using Habanero.UI.Base;
using Habanero.UI.Win;
using Replace_it.BO;

namespace Replace_it.UI
{
public class OrdersForm<T> : UserControlWin, IFormControl where T: BusinessObject, new()
{
    private IReadOnlyGridControl _grid;
    private IReadOnlyGridControl _orderItemsGrid;

    public OrdersForm()
    {
        BusinessObjectCollection<T> boCollection = new BusinessObjectCollection<T>();
        boCollection.LoadAll();
        _grid = GlobalUIRegistry.ControlFactory.CreateReadOnlyGridControl();
        _grid.SetBusinessObjectCollection(boCollection);
        BorderLayoutManager manager = GlobalUIRegistry.ControlFactory.CreateBorderLayoutManager(this);
        manager.AddControl(_grid, BorderLayoutManager.Position.Centre);

        _orderItemsGrid = GlobalUIRegistry.ControlFactory.CreateReadOnlyGridControl();
        _orderItemsGrid.Height = 200;
        SetOrderItemsCollection();
        manager.AddControl(_orderItemsGrid, BorderLayoutManager.Position.South);

        _grid.Grid.BusinessObjectSelected += delegate { SetOrderItemsCollection(); };
    }

    public void SetForm(IFormHabanero form) { }

    private void SetOrderItemsCollection()
    {
        PartsOrder selectedPartsOrder = (PartsOrder)_grid.SelectedBusinessObject;
        if (selectedPartsOrder != null)
            _orderItemsGrid.SetBusinessObjectCollection(selectedPartsOrder.OrderItems);
        _orderItemsGrid.BusinessObjectCreator = new OrderItemCreator(selectedPartsOrder);
    }
}
}

The form controller must be updated, and you can see here how to call the orders form.

OrdersForm.cs
using Replace_it.BO;

...

public const string CUSTOMER_ORDERS = "Customer Orders";
public const string SUPPLIER_ORDERS = "Supplier Orders";

...

case CUSTOMER_ORDERS:
    formCtl = new OrdersForm<CustomerOrder>();
    break;
case SUPPLIER_ORDERS:
    formCtl = new OrdersForm<SupplierOrder>();
    break;

Finally, update the ProgramForm with two new menu items to access customer and supplier orders.

When run, the application should appear something like the following:

Note that typing a value into the part code field will crash the application, because it won't be a complete Guid.  Just hang tight on that one - we'll resolve that in the next section by adding lookup lists and a combo box.






User Interface Enhancements

Habanero has extensive support for generating user interfaces and provides access to these controls so that additional behaviour can be assigned. In this section we'll look at some particular features available to the developer, along with techniques used to achieve common tasks.

Adding Lookup-Lists in a ComboBox

In the previous section we looked at how you can specify a lookup-list using relationships. A lookup-list is simply a set of options that the user can select from for a particular field, such as a list of countries or a list of address contacts. Habanero allows you to provide values for a lookup-list from a pipe-separated string (eg. gender options: "M|F") or from a database. These lookup-lists are defined under the property in the class definitions.

Combo-boxes in a user interface are populated from three types of lookup-lists: BusinessObjectLookupList, which creates a list from a set of loaded business objects, DatabaseLookupList, which builds a list from a given sql statement, and SimpleLookupList, where a static collection can be specified in the class definitions.

A lookup-list comprises a set of display-value pairs.  The display part is shown to the user and the value part is the value actually stored. The BusinessObjectLookupList provides the display part from the class's ToString() method, while the DatabaseLookupList requires a value field and a display field in the select part of the Sql string.

To implement a combo-box for the ComputerPart in OrderItem, we'll first amend the class definitions. In FireStarter, select OrderItem, change to the "Properties" tab, select ComputerPartID and go to the "Lookup List" tab, where you can select the list type as "Business Object":

You could also have chosen the "Database" type, in which case you would have provided a sql string as "select ComputerPartID, PartCode from computerpart order by PartCode".

We also need to indicate that the control type to display the computer part will be a ComboBox. Set this under the "UIs" tab:

Finally, we'll need a revised ToString() method in the ComputerPart class which describes how to display an instance in the combo-box. Be sure to add this method in the ComputerPart.cs file and not the ComputerPart.Def.cs file.

public override string ToString()
{
    return PartCode + " - " + Description;
}

This should now run successfully, with the string display showing correctly in the grids and the combo-box, but the Guid stored in the database.

Fulfilling Orders

Where the Delivered field is set to false, we can now dispatch or receive orders and automatically amend the stock quantities. One way of doing this is to add a button on each orders grid. Add the following code at the bottom of the OrdersForm constructor:

if (typeof(T) == typeof(CustomerOrder))
{
    _grid.Buttons.AddButton("Dispatch", new EventHandler(DispatchClickedHandler));
}
else if (typeof(T) == typeof(SupplierOrder))
{
    _grid.Buttons.AddButton("Receive", new EventHandler(ReceiveClickedHandler));
}

The DispatchClickedHandler and ReceiveClickedHandler are implemented further along. The parts stock is loaded and we check that all of the stock is available before changing stock levels. Note that we call Save() on the objects in order to persist the changes to the database, otherwise our stock will still be the same when we restart the application.

private void DispatchClickedHandler(object sender, EventArgs e)
{
    PartsOrder selectedOrder = (PartsOrder)_grid.SelectedBusinessObject;
    if (selectedOrder.Delivered.Value || selectedOrder.OrderItems.Count == 0)
        return;

    BusinessObjectCollection<ComputerPart> stockAvailable = new BusinessObjectCollection<ComputerPart>();
    stockAvailable.LoadAll();

    foreach (OrderItem orderItem in selectedOrder.OrderItems)
    {
        ComputerPart matchingPart = stockAvailable.FindByGuid(orderItem.ComputerPartID.Value);
        if (matchingPart == null)
        {
            MessageBox.Show("Could not find some of the stock on order. Please double-check.");
            return;
        }
            
        if (orderItem.Quantity > matchingPart.Stock)
        {
            MessageBox.Show("Insufficient stock of part: " + matchingPart.PartCode);
            return;
        }
    }

    foreach (OrderItem orderItem in selectedOrder.OrderItems)
    {
        ComputerPart matchingPart = stockAvailable.FindByGuid(orderItem.ComputerPartID.Value);
        matchingPart.Stock -= orderItem.Quantity;
        matchingPart.Save();
    }
    MessageBox.Show("Stock dispatched and stock levels amended");
    selectedOrder.Delivered = true;
    selectedOrder.Save();
}

private void ReceiveClickedHandler(object sender, EventArgs e)
{
    PartsOrder selectedPartsOrder = (PartsOrder)_grid.SelectedBusinessObject;
    if (selectedPartsOrder.Delivered.Value || selectedPartsOrder.OrderItems.Count == 0)
        return;

    BusinessObjectCollection<ComputerPart> stockAvailable = 
        new BusinessObjectCollection<ComputerPart>();
    stockAvailable.LoadAll();

    foreach (OrderItem orderItem in selectedPartsOrder.OrderItems)
    {
        ComputerPart matchingPart = stockAvailable.FindByGuid(orderItem.ComputerPartID.Value);
        if (matchingPart == null)
        {
            MessageBox.Show("Could not find some of the stock on order. Please double-check.");
            return;
        }
    }

    foreach (OrderItem orderItem in selectedPartsOrder.OrderItems)
    {
        ComputerPart matchingPart = stockAvailable.FindByGuid(orderItem.ComputerPartID.Value);
        matchingPart.Stock += orderItem.Quantity;
        matchingPart.Save();
    }
    MessageBox.Show("Stock received and stock levels amended");
    selectedPartsOrder.Delivered = true;
    selectedPartsOrder.Save();
}

At this stage we have a basic stock management system.  Using Habanero we've achieved this with very little contact with either the user interface designer or the database connections.

Data Validation & Property Rules

This is also a good time to introduce property rules (also known as validation). In our parts business, we obviously don't want a negative stock, so we can create a rule that prevents negative integers.  A rule can be a combination of smaller rules, so that ranges can be created for instance.  Using FireStarter, the rule can be assigned for the Stock property of ComputerPart. Similar rules could of course be designed for a wide range of numbers, such as the Cost and Price.

Better Number Control

While our user interface is functional, it's not very robust, and an experimental user could quickly bring our application down to earth by entering strange characters into some of our controls. We can exercise some control over our numbers input by using NumericUpDown controls.

The control types to be used in the user interface for the ComputerPart can be amended in FireStarter as before, except that the mapper type needs to be specified. The mapper is a Habanero component used to map data back and forth between the object and the UI control. When you specify the NumericUpDown control and leave the mapper as "Auto-Select", the mapper defaults to NumericUpDownIntegerMapper. This is fine for the Stock property, but for Cost and Price you should manually specify the mapper as NumericUpDownMoneyMapper, which assigns two decimal places.

Filtering Grid Displays

Quite clearly, our grid displays could grow rather large as we begin to add computer parts or customer orders.  There are several ways to limit the number of objects being shown.  We could specify load criteria as a parameter in each collection's Load() command.  Another simple way is to add a filter control, which filters out the rows to display, so that only those rows that meet a requirement are shown.

Looking first at the computer parts, how you filter your values will depend on how you distinguish your parts.  For instance, Replace IT uses a part code made up of three letters which indicate the type of product and four numbers which serve as a sub-code for that type.  For example, one of the optical mice might have a code of MOU1534.  So our solution simply categorises items by their three first letters, doing a non-strict string comparison.

The Habanero FilterControl comes attached with the GridControl, but can also be deployed independently. You simply need to assign a set of filter controls and specify which columns they filter out. The GridControl automatically detects when a filter control has been modified by the user and filters the grid immediately. Keep in mind that the this filter only works on a pre-loaded collection, so your grid will initially contain the full collection before filtering.

In this example, the AddStringFilterComboBox method creates a combo-box that searches for all objects with that given string within the specified property. There is also a free-type TextBox filter to search on the Description field. Add the following code at the bottom of the ComputerPartsForm constructor:

string[] options = { "CDR", "KEY", "MOU", "HDD", "DVD", "MBD", "FDD", "CAS", "MON", "VID" };

grid.FilterControl.AddStringFilterComboBox("Category:", "PartCode", new ArrayList(options), false);
grid.FilterControl.AddStringFilterTextBox("Description:", "Description");

This should create the following interface:

It would be nice to do something similar with orders, where we place a checkbox that allows us to show only those orders that are still to be delivered. However, the Habanero checkbox filter is a strict "true" or "false" check, and doesn't provide inclusivity. In other words, a ticked checkbox should show only "false" values, and an unticked checkbox should show both "true" and "false" values, not just "true" values. The better approach here is a custom implementation, to be added at the bottom of the OrdersForm constructor:

ICheckBox checkBox = GlobalUIRegistry.ControlFactory.CreateCheckBox();
checkBox.Text = "Undelivered orders only";
checkBox.CheckedChanged += delegate
   {
       BusinessObjectCollection<T> col = new BusinessObjectCollection<T>();
       if (checkBox.Checked) col.Load("Delivered = 'false'", "");
       else col.LoadAll();
       _grid.SetBusinessObjectCollection(col);
   };
manager.AddControl(checkBox, BorderLayoutManager.Position.North);





Version Control

Updating Database Versions

When you start producing applications for business and industry, you'll often find requests for new features and changes streaming in.  Adding a new database field is a multi-step process.  When it comes to changing the application, Habanero helps to simplify the process, so that you need only change the class definitions, the class implementation and the user interface (if not entirely defined in the class definitions).  The result of those changes is a new set of EXE's or DLL's that can simply replace what the client has already.

When it comes to database changes, however, you can't simply replace an entire working database.  Usually, you'd have to manually run a set of Sql scripts to add or remove columns.  Updating one database is manageable, but multiple installations can be both tedious and prone to error - if you forget the update, your application will crash.  Habanero can automate this database update process for you using the IApplicationVersionUpgrader interface and the DBMigrator class, which will cause the application itself to update the database automatically, so you only need to replace the application.

Database Versioning in Practice

Back at Replace IT, we recently installed the latest version of the new system and it's been running smoothly.  The databases have rapidly become populated with new entries.  However, the accounts department have found the CustomerOrder form a little bit lacking - they're now asking for an address column.

We have already covered the change management process earlier on, but we'll change the process a bit now to explain database version control. Instead of adding the database field and doing a class definition regeneration, simply add the new Address field straight into the class definitions for CustomerOrder (either manually or through FireStarter). If you're generating user interfaces, you'll also want to amend the UI definitions so that the Address is shown in the editing form for the CustomerOrder.

Next we'll implement IApplicationVersionUpgrader in the root project, which will carry out a version check every time it runs.  If the database version is out-of-date, it will carry out any migration instructions to get it up to speed.  You'll probably need to add a reference to the Habanero.DB.dll in your project. Here is the new class:

Replace_itVersionUpgrader.cs
using Habanero.Base;
using Habanero.DB;

namespace Replace_it
{
    public class Replace_itVersionUpgrader : IApplicationVersionUpgrader
    {
        public void Upgrade()
        {
            DBMigrator migrator = new DBMigrator(DatabaseConnection.CurrentConnection);
            migrator.SetSettingsStorer(GlobalRegistry.Settings);

            migrator.AddMigration(1, "ALTER TABLE CustomerOrder ADD COLUMN Address VARCHAR(200);");

            migrator.MigrateToLatestVersion();
        }
    }
}

To extend this for each version change, simply add more AddMigration() calls, and increment the version number each time.  We'll need to point to this class right at the start of our application, by adding another line in Program.cs:

Program.cs (Amended)
HabaneroAppWin mainApp = new HabaneroAppWin("Replace_it", "v1.0");
mainApp.ApplicationVersionUpgrader = new Replace_itVersionUpgrader();
if (!mainApp.Startup()) return;

There's one final step: creating the database table that will store the version number.  HabaneroApp supports both DatabaseSettings and ConfigFileSettings, but the latter can't be written to, so we'll need a database table called "settings", with the following design:

As soon as you've created the table, add a single row with the SettingName as "DATABASE_VERSION" and the SettingValue as "0".  Our application should run now, and a quick check on the database should show a new field for the address.  Running the application again should make no change.






Managing Lookup-Lists

In the previous section on User Interface Enhancements, we introduced the concept of a lookup-list. Basically, a lookup-list provides a set of options for a control like a combobox to display. Sometimes your lookup-list will be a fixed list that doesn't change, as in the months of the year. Other times you'll want the options to be dynamic so that some administrator can edit or add to them over time, such as the countries of the world. Then again, you might allow the case where the list grows as the final users themselves add items as they need them, such as a list of customers.

Because of its emphasis on UI generation, Habanero is well-designed to cope with lookup-lists. We have already discussed how to implement the lists, but you may also want to be able to manage all the lookup-lists centrally.

Adding the Computer Part Types

Once our collection of computer parts starts to grow, we'll need a way of categorising them. To achieve this we'll simply create a new dynamic lookup-list that stores all the categories in a table. Here's the simple database table design:

In ComputerPart, we'll need a new column for ComputerPartTypeID that will hold the primary key of the computer part type. A foreign key is useful to check data integrity and for Firestarter to generate the relationships.

Adding the Class Definitions

Go ahead and generate the class definitions for ComputerPartType and ComputerPart. The three properties in ComputerPartType will be a Guid and two strings. For the ComputerPartTypeID in ComputerPart, set its lookup-list property to BusinessObjectLookupList. There should be two relationships: a single relationship from ComputerPart to ComputerPartType and a multiple relationship from ComputerPartType to ComputerPart. Also remember to add a grid UI definition for ComputerPartType. In ComputerPart, add the ComputerPartType in the form and grid - Firestarter should automatically convert the type to a ComboBox.

NOTE: Be aware that with Firestarter not detecting inheritance, when you regenerate, the existing inheritance structure might become a little corrupted. You can either choose to auto-generate the classes and repair the inheritance structure, or you can manually create the ComputerPartType class, properties, relationships and UI changes in FireStarter. Either way, it's best to use FireStarter to manage your definitions and code generation rather than editing the XML yourself, since that will save having to edit both the XML file and the classes in the code.

Adding the Classes

When you regenerate your project, the new ComputerPartType class should appear. If not, reload Visual Studio. Importantly, you need to add a ToString() method to the ComputerPart class, which determines what string will be shown to the user in the ComboBox dropdown. Displaying the Name property of the type is probably best.

Implementing a Lookup-List Editor

Our changes are almost ready to run, except for the fact that there are no computer part types available to choose from. We could easily build a new form with a grid to add some types to choose from, but you can imagine that we'd need a new form every time we add a new lookup-list. If you've had any experience with applications like these, you'll know that you use lookup-lists for a wide range of options, including customers, countries, cities, genders and many others.

A cleaner way of managing all your lookup-lists centrally is to use Habanero's StaticDataEditor, which provides a treeview on the left with all the available lookup-lists, and an editable grid on the right which allows the users to add new items. The implementation is really simple:

Replace_itLookupEditor.cs
using Habanero.BO.ClassDefinition;
using Habanero.UI.Base;
using Habanero.UI.Win;
using Replace_it.BO;

namespace Replace_it.UI
{
public class Replace_itLookupEditor : UserControlWin, IFormControl
{
    public Replace_itLookupEditor(IControlFactory controlFactory)
    {
        IStaticDataEditor staticDataEditor = controlFactory.CreateStaticDataEditor();
        BorderLayoutManager layoutManager = controlFactory.CreateBorderLayoutManager(this);
        layoutManager.AddControl(staticDataEditor, BorderLayoutManager.Position.Centre);
        staticDataEditor.AddSection("Lookup lists");
        staticDataEditor.AddItem("Computer Part Types", ClassDef.ClassDefs[typeof(ComputerPartType)]);
    }

    public void SetForm(IFormHabanero form) {}
}
}

As you can see, it's easy to add new types to this editor - simply use the AddItem method. As before you'll need to add this form to the Form Controller and include a main menu item to access it.

Adding Options on the Fly

Habanero also provides a feature where users can right-click on a ComboBox to add options immediately. This service requires a UI form definition to have been created in the XML class definitions.






Appendix I: Class Definitions

XML Class Definitions

This section covers a list of explanations, possible options and limitations for all class definitions as listed in the class definitions XML file (usually called "classdefs.xml").  The class definitions are required in order for Habanero to run, and you need to include the definitions for at least one valid class.

This is a sample structure for a class definition file.  Note that the order of elements is enforced as per the Document Type Definition (DTD).  See the specifications further along for specific options and limitations.

Sample Structure:
<classes>
<class name="class_name" assembly="assembly_name">
  <superClass class="class_inherited_from" assembly="its_assembly" />

  <property name="property_name" />
  <property name="property_name" type="Guid" compulsory="true">
    <rule name="rule name" message="message for user if rule broken" />
      <add key="min" value="0" />
      <add key="max" value="300" />
    </rule>
    <databaseLookupList sql="select display_field, value_field from table_name" />
  </property>

  <key name="keydef name" message="message to user" ignoreIfNull="true">
    <prop name="property_name" />
  </key>

  <primaryKey>
    <prop name="primary_key_name" />
  </primaryKey>

  <relationship name="rel_name" type="multiple" relatedClass="class" relatedAssembly="assembly">
    <relatedProperty property="related_prop_name" relatedProperty="related_prop_name" />
  </relationship>

  <ui>
    <grid>
      <column property="property_name" />
      <column heading="column_heading" property="property_name" width="100" />
    </grid>
    <form height="400" width="450" heading="edit_form_heading">
      <tab name="form_name">
        <columnLayout width="350">
          <field property="property_name" />
        </columnLayout>
        <columnLayout>
          <field label="label" property="prop_name" type="ComboBox" mapperType="ListComboBoxMapper">
            <parameter name="options" value="F|M" />
            <trigger target="prop_name" conditionValue="value" action="enable" value="false" >
          </field>
        </columnLayout>
      </tab>
      <tab name="form_name2">
        <field label="label" property="prop_name" type="TextBox" editable="false" />
      </tab>
    </form>
  </ui>	
</class>
</classes>

Here follows a list of all the class definition elements.  For clarification, "attribs" means attributes and the default settings are in italics.  Keep in mind that XML elements are case-sensitive.

Class Definition Specifications
 
add
businessObjectLookupList
class
classes

column
columnLayout
databaseLookupList
field

form
grid

item
key
parameter
primaryKey

prop
property
relationship

relatedProperty
rule
simpleLookupList
superClass

tab
trigger
ui
classes
Description: The root element of the xml document. Contains one class element for each class/table.
Comes under: -
Must contain: class (1..*)
Can contain: -
Compulsory attribs: -
Optional attribs: -
class
Description: Contains a definition for each class/table.  Holds property definitions, primary key definitions, relationships and how the data is displayed in the user interface.
Comes under: classes
Must contain: property (1..*)
primaryKey (1)
Can contain: superClass (1)
key (1..*)
relationship (1..*)
ui (1..*)
Compulsory attribs: name (name of the class in the assembly/project)
assembly (name of the assembly/project)
Optional attribs: table (table name in database, if omitted then the table name is assumed to be the class name)
displayName (the text to use in labels or headings where this class is shown or edited)
superClass
Description: Indicates which class this class inherits from and the type of inheritance used. ClassTableInheritance uses one database table per class in the inheritance structure. SingleTableInheritance maps all fields of all classes of an inheritance structure into a single table. ConcreteTableInheritance uses a table for each concrete class in the inheritance hierarchy.
Comes under: class
Must contain: -
Can contain: -
Compulsory attribs: class (the class name from which the class inherits)
assembly (the assembly/project containing the super-class)
Optional attribs: orMapping =ClassTableInheritance/SingleTableInheritance/ConcreteTableInheritance (the type of inheritance used)
id (in ClassTableInheritance, the property in the child class that has a copy of the parent's ID, use empty string if child has no primary key and just inherits parent's ID, null value assumes child has a field with the name of the parent's ID that is not the child's ID)
discriminator (in SingleTableInheritance, sets the column in the parent's db table that holds the type of the class in that row)
property
Description: Defines an individual property or field name in a class.  Specifies what the data type is, whether there is a specific list of options to choose from or whether there are any property rules or limitations on the property values. At the very least, the property to be used for the primary key definition needs to be defined.
Comes under: class
Must contain: -
Can contain: rule (1)
databaseLookupList (1)
businessObjectLookupList (1)
simpleLookupList (1)
Compulsory attribs: name (the name of the property as listed in the code)
Optional attribs: type =String/Guid/Int32/Boolean/Decimal/DateTime/etc (the data type at code level, standard names like 'int' are also valid, you can add your own types if desired, Habanero.Util also provides LongText and ByteString to convert Clobs and Blobs to strings)
assembly =System (the assembly to which the property type belongs)
databaseField (if the field name is different to the property name)
default (any default value to be assigned)
compulsory =true/false (whether assigning a value is compulsory, will make the field's label bold and provide error messaging to the user if not completed)
displayName (the text to use in labels or headings where this value is shown or edited)
description (the text to use where additional information is shown about the property, such as a tooltip)
keepValuePrivate (masks the value in a user control with the standard password mask character)
readWriteRule =ReadWrite/ReadOnly/WriteOnce/WriteNotNew/WriteNew (limitations on how often the value can be read or written to, see the API for more info)
auto-incrementing =true/false (whether this property is an auto-incrementing primary key, isObjectID must be false on the primary key)
length (for strings, the maximum length the string can be)
rule
Description: Sets specific rules that a string value must adhere to.  This element holds the name of the rule and a message to display to the user if the rule is broken.  A single rule can therefore be a combination of several cases.  You can also add rules of your own by creating a class that inherits from Habanero.BO.PropRuleBase, and specifying this class here. See the 'add' element for a list of rules currently available.
Comes under: property
Must contain: add (1..*)
Can contain: -
Compulsory attribs: name (the name of the rule, for the user's reference)
Optional attribs: message (to display to the user if the rule is broken, if this is left out, a default explanation will be provided)
class (a class that carries out checking against a custom rule)
assembly (the assembly holding the above class)
add
Description: A key-value pair that sets a single rule that the property value must adhere to.
Comes under: rule
Must contain: -
Can contain: -
Compulsory attribs: key (the name of the rule, eg. "max")
value (the value applicable to the rule, eg. "300")

The following options are available, depending on the property type you specified:

string minLength (minimum number of characters)
maxLength (maximum number of characters)
patternMatch (a regular expression to match to)
DateTime min (earliest date allowed)
max (latest date allowed)
decimal/int min
max
Optional attribs: -
databaseLookup-
List
Description: Provides a list of potential values for the property, using the objects loaded from the database with the given sql statement.  Two fields must be provided to form a display-value pair - the first is the value to store and the second is the value to display to the user (eg. in a ComboBox or grid), which is useful when you are using Guids.  The sql statement needs to be in the form of: "select valueField, displayField from tableName".  An order-by clause can also be appended.  The class and assembly will seldom need to be specified.
Comes under: property
Must contain: -
Can contain: -
Compulsory attribs: sql (the select sql statement to load the objects to populate the list, use escape characters for symbols like <,>)
Optional attribs: timeout =10000 (the period in milliseconds after which the cached copy expires)
class
(the class for the objects being loaded)
assembly (the assembly/project for the objects being loaded)
simpleLookupList
Description: Provides a list of potential values for the property.  You can specify a list of options in the "options" attribute, separated by pipes, and/or you can include sets of display-value pairs in "item" elements, that have the advantage of displaying a different value to the one actually stored (eg. store a Guid in the database, but display a string in a ComboBox or grid).
Comes under: property
Must contain: -
Can contain: item (0..*)
Compulsory attribs: -
Optional attribs: options (a list of options, eg. "Male|Female|Not-specified")
item
Description: A display-value pair that serves as one option in a simpleLookupList.  The display attribute appears to the user (eg. in the ComboBox or grid), while the value attribute is the value stored.  This is a useful option when a Guid is being stored.
Comes under: simpleLookupList
Must contain: -
Can contain: -
Compulsory attribs: display (the value that will appear to the user in a form or grid)
value (the value that will be stored for the property)
Optional attribs: -
businessObject-
LookupList
Description: Provides a list of potential values for the property, using the objects loaded from a particular business object type. The primary key value will be the actual value stored, while the value displayed to the user (eg. in the ComboBox or grid) will be that obtained from the object's ToString() method. You can also set criteria to limit which objects are loaded.  This list is sorted by default, but you can specify an alternative sort column or direction with the sort attribute.
Comes under: property
Must contain: -
Can contain: -
Compulsory attribs: class (the business object class for the objects to be loaded)
assembly (the assembly holding the above class)
Optional attribs: criteria (a sql segment to be attached to a where-clause that limits which objects are loaded)
sort (which property of the business object to sort on, format is: "property", "property asc", "property desc")
key
Description: Specifies a unique key for the class/table, which is a property or combination of properties which must be unique for each instance/row.
Comes under: class
Must contain: prop (1..*)
Can contain: -
Compulsory attribs: -
Optional attribs: name (a name that describes the key)
message (to explain why a write operation violates the key, if not specified then a default message is provided)
ignoreIfNull =true/false (whether to ignore the uniqueness check if any of the properties making up the key are null)
primaryKey
Description: Describes the primary key for the class, using properties that have been defined in the property elements. The "prop" sub-elements list the properties that the primary key is comprised of (a composite primary key will have more than one prop element).
Comes under: class
Must contain: prop (1..*)
Can contain: -
Compulsory attribs: -
Optional attribs: isObjectID =true/false (whether this key has a single Guid property that is the object's ID, false for composite keys or non-Guids)
prop
Description: Specifies the existing property definition being referred to in the composition of a primary key or alternate key
Comes under: primaryKey, key
Must contain: -
Can contain: -
Compulsory attribs: name (the name of the existing property definition being referred to)
Optional attribs: -
relationship
Description: Describes a relationship between this class and another class (equivalent to foreign keys in a database). Note that the relationship is mapped between a particular property in one class and a primary key property in another class.  For the delete action, DeleteRelated deletes all related objects when one is deleted, DereferenceRelated dereferences them instead, and Prevent prevents the object from being deleted when other objects related to it.
Comes under: class
Must contain: relatedProperty (1..*)
Can contain: -
Compulsory attribs: name (the name of the relationship)
type =single/multiple (if this class relates to one or many of the other class)
relatedClass (the related class name)
relatedAssembly (the assembly/project containing the related class)
Optional attribs: keepReference =true/false (keep a reference in memory to the class, can set this to false if your application is very memory-intensive)
orderBy (the field name(s) to append to the sql order-by clause)
deleteAction =DeleteRelated/DereferenceRelated/Prevent/DoNothing (what action to take when the related object is deleted)
relatedProperty
Description: Defines the properties on which relationship matching takes place.
Comes under: relationship
Must contain: -
Can contain: -
Compulsory attribs: property (the property in this class which relates to the other class)
relatedProperty (the property in the other class being related to)
Optional attribs: -
ui
Description: Defines how the class is displayed in user interfaces like grids and editing forms.  If you will only use one set of UI definitions, you do not need to assign a "name" attribute.  If you want to be able to choose between different sets of definitions at different times, assign different names and then use constructors in the IGridDataProvider implementations to choose the name of the definition to use.
Comes under: class
Must contain: grid (1)
form (1)
Can contain: -
Compulsory attribs: -
Optional attribs: name =default (the name of the set of definitions)
grid
Description: Defines how the class properties are displayed in a user interface grid, with one column element for each column to display.
Comes under: ui
Must contain: column (1..*)
Can contain: -
Compulsory attribs: -
Optional attribs: sortColumn (the column on which the grid is sorted, use "PropertyName" or "PropertyName asc" or "PropertyName desc")
column
Description: Defines one column of data in a user interface grid.
Comes under: grid
Must contain: -
Can contain: parameter (1..*)
Compulsory attribs: property (the name of the property being shown)
Optional attribs: heading (the column heading as seen by the user, the property name will be used if this is not specified)
type
=DataGridViewTextBoxColumn (the type of column to use, other standard types include DataGridViewCheckBoxColumn, DataGridViewComboBoxColumn, DataGridViewNumericUpDownColumn for decimals and DataGridViewDateTimeColumn, you can also specify custom types)
assembly (of the control type, only specify this if you are using a custom type)
editable =true/false (whether the property can be edited directly on the grid, irrelevant if grid is read-only)
width =100 (in pixels)
alignment =left/right/center/centre
form
Description: Defines how the class properties are displayed in a user interface editing form.  Here you can list a set of tab, column or field elements, but not a combination of the three at the same level.
Comes under: ui
Must contain: -
Can contain: tab (1..*)
columnLayout (1..*)
field (1..*)
Compulsory attribs: -
Optional attribs: title (the window heading as it appears to the user)
width =300 (in pixels)
height =250 (in pixels)
tab
Description: Creates a tab in the editing form window. Several tabs can be created, but if there is only one tab, then you don't need to include this element.  Under this element you can have either columnLayout or field elements but not a combination of the two.
Comes under: form
Must contain: -
Can contain: columnLayout (1..*)
field (1..*)
Compulsory attribs: name (the name of the tab as it appears to the user)
Optional attribs: -
columnLayout
Description: Holds a column of controls in an editing form.  If you will only have one column, then you don't need to include this element.
Comes under: tab, form
Must contain: field (1..*)
Can contain: -
Compulsory attribs: -
Optional attribs: width (in pixels)
field
Description: Defines a control to edit a field/property in an editing form.  "parameter" elements can be used to add further specifications for the control, and "trigger" elements can trigger a range of events (list all parameter elements before triggers). Note that there are default control types assigned when not explicitly specified - these are determined by the UI environment of the ControlFactory.
Comes under: columnLayout, tab, form
Must contain: -
Can contain: parameter (1..*), trigger (1..*)
Compulsory attribs: property (the property to edit)
Optional attribs: label (the label to appear next to the control on the form, the property name will be used if this is not specified)
editable =true/false
type =TextBox (the class name of the type of control)
assembly (the assembly of the control type)
mapperType =TextBoxMapper (the class used to map the control to the business object property, Habanero provides mappers for the standard controls, you can leave this off for TextBox, ComboBox, CheckBox, DateTimePicker, ListView and NumericUpDown)
mapperAssembly (specify this if you are using a custom mapper)
toolTipText (the text to display in a tooltip for this control)
parameter
Description: Adds additional definitions for a control being displayed in a form or grid, such as the number of decimal places to show. There is no error-checking on unsupported types, so this is a fully flexible system that can be used for any purpose you wish - simply use the GetParameterValue method on UIFormField or UIGridColumn to access the value. Parameter names are case-sensitive.
Comes under: field, column
Must contain: -
Can contain: -
Compulsory attribs: name (the name of the type of setting - see below for examples)
value (the values to apply to the above setting - see below)

The following options are available for a form field element:
options pipe-separated list of choices in a ComboBox (specify the mapperType as "ListComboBoxMapper")
- eg. value="M|F"
numLines number of lines in a TextBox
alignment text alignment, only applies to TextBox and NumericUpDown, options are: left, right, center and centre
isEmail if so, user can double-click on a valid email and open in default mail client, value="true"
colSpan the number of columns for the field control to span across
rowSpan the number of rows for the field control to span across
decimalPlaces the number of decimals to allow when NumericUpDownMoneyMapper is used
rightClickEnabled whether a user can right-click on the ComboBox to add additional options
dateFormat the format of date displays, in the style of DateTime.ToString() (eg. "dd MM yy"), including shortcuts, such as "d", which uses the short date format of the culture of the user's machine

The following options are available for a grid column element:
dateFormat the format of date displays, in the style of DateTime.ToString() (eg. "dd MM yy"), including shortcuts, such as "d", which uses the short date format of the culture of the user's machine
Optional attribs: -
trigger
Description: Carries out some action when a field control has its value changed. For instance, a trigger could cause a method to be executed or another field to be disabled. A trigger usually has a source control (triggeredBy) that fires the trigger when its value changes and a target control that is affected by the change. The trigger can be declared either on the source or the target and multiple triggers are also permitted.
Comes under: field
Must contain: -
Can contain: -
Compulsory attribs: action (the type of action to carry out when the trigger fires)

The following actions are available for a form field element:
assignLiteral Assign the given literal in "value" to the target property. The value will be converted to the type of the property.
assignProperty Assign the value returned by a property to the target field. The property must be listed in the class of the general BusinessObject being displayed on the form. List the name of the property in the "value" attribute.
execute Execute a parameterless method found on the class of the general BusinessObject being displayed on the form. No target field required.
filter As an example, if you have a Country ComboBox as your source and a City ComboBox as your target, selecting the Country will filter City, showing only those Cities with a CountryID matching the CountryID of the selected Country. The target must use a BusinessObjectLookupList and must have a property with the same name as the source's ID.
filterReverse As with "filter", but with Country as the target - if you select a City, the specific Country for that City will be selected. Both source and target properties must use a BusinessObjectLookupList and the source must have a property with the same name as the target's ID.
enable Enables or disables the target property - the "value" attribute must be "true" or "false" respectively.
enableOnce As with "enable", but will only carry out the enabling or disabling once, regardless of any further value changes in the source.
Optional attribs: triggeredBy (the property name of the field that causes the trigger when its value changes, don't specify this if you are listing the trigger under the source field)
target (the property name of the field that is affected by a fired trigger, not required if you are listing the trigger under the target field or for actions like "execute")
conditionValue (a string literal value that the source control must have in order for the trigger to fire, the value will be converted to the type of the field's property, in the case of ComboBoxes this must be the string value as shown in the drop-down)
value (a field used by "action", see "action" above for further information)





Appendix II: Logging & Errors

Log4net is a system used by Habanero for logging output from your application.  Log files give you assistance in several ways, including analysing system faults remotely, tracking system usage, tracing the process that led to a breakdown and understanding where the system failed when you don't have any other visual response.

Habanero logs output to this logging system, and you can use logging calls in your code to add to this output.  Log4net is highly flexible, allowing you to specify the log output medium (eg. text file), the output format and the output detail level (eg. errors only, or all debug information).

Setting Up Logging Support

The settings are laid out in the app.config file (or your equivalent config file) between the configuration tags.  First declare the "log4net" settings type in "configSections" as follows:

<configSections>
  <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net"/>
</configSections>

Then specify the "log4net" settings - here is an example which you could use:

<log4net>
  <appender name="FileAppender" type="log4net.Appender.FileAppender">
    <param name="File" value="log.txt"/>
    <param name="AppendToFile" value="true"/>
    <layout type="log4net.Layout.PatternLayout">
      <param name="ConversionPattern" value="%d [%t] %-5p %c [%x] - %m%n"/>
    </layout>
  </appender>
  <root>
    <level value="INFO"/>
    <appender-ref ref="FileAppender"/>
  </root>
</log4net>

The settings above specify the log output file name, that the existing file will append new data, the output format and that all output of type "INFO" or more serious will be logged.  "DEBUG" is the lowest level and will display all types of logging output.

Using Logging in Your Own Code

To use logging in your own code, you'll need to setup a logger in each class where you want to send logging output.  Include "log4net" as follows:

using log4net;

Then add the logging object to your class fields like this:

private static readonly ILog log = LogManager.GetLogger("yourClassName");

Finally you can send log output anywhere in the code of that class like this:

log.Info("The application is starting...");
log.Debug("System variable set to: xyz");
log.Error("An error has occurred in the startup section.");

For more information on log4net, visit the homepage at: http://logging.apache.org/log4net
You can also get configuration options at: http://logging.apache.org/log4net/release/manual/configuration.html

User-Friendly Error Form

The HabaneroAppWin class has an ExceptionNotifier property, which allows you to specify a standard form to display error messages (do this before calling Startup).  This form must implement IExceptionNotifier.  By default, Habanero supplies its own error display form, which hides the messy details from a final user, but still allows a developer to view the stack trace.  While the form will automatically popup during the program launch, during normal run-time, you will need to enclose the code in a try-catch block.  Here is a sample implementation in Program.cs, the root class.

using Habanero.Base;

And in the Main() method:

try
{
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);
    Application.Run(new Form1());
}
catch (Exception ex)
{
    GlobalRegistry.UIExceptionNotifier.Notify(ex,
    "An error has occurred in the application.",
    "Application Error");
}

You can also mask the exception so that naive users aren't hit with the full details of a complicated error message:

GlobalRegistry.UIExceptionNotifier.Notify(
    new UserException("An error has occurred in the application.", ex),
    "An error has occurred in the application.",
    "Application Error");





Appendix III: Habanero Source Code

Included with the Habanero download is the source code for the framework.  Source is included for the Habanero components, but not for FireStarter.  All the source code components can be reached from the "Habanero.sln" solution file.

Licensing

The source code is distributed under the GNU Lesser GPL. The full license is included with the installation, or you can access it at http://www.gnu.org/licenses/. Under this license, any changes you make to the source should be accessible to others and you may not sell the code you have modified. We recommend that you post changes or bug fixes on the Habanero Forum for the benefit of other users. The Habanero development team may in turn include those changes in future releases of Habanero. If you do make changes, you can include your name in the copyright listing at the top of the file where the changes were made.

Running the Tests

Habanero includes a set of unit tests that can be run to make sure your changes don't break existing code accidentally. Make sure the tests run successfully before you make changes, and then run them again after each change.

  • Habanero uses nUnit to carry out unit testing, so install nUnit as necessary.
  • The tests can be run centrally from the "Habanero.nunit" file.
  • The tests use a MySQL database to carry out persistence checks. You will need to install MySQL and restore the test database (located in the "docs" directory). You will also need to provide a connection string for the database.
  • The connection string is hardcoded to (localhost, 3306, root, root) - if you have a different connection string, amend the Habanero.Test.MyDBConnection class





Troubleshooting

Troubleshooting FAQ

  • Application is not working
    • Is the code correct?
    • Do you have an app.config file that is correctly configured?
    • Do you have a classdefs.xml file and is it accessible in the output folder with the executables?
    • Does the class definition have the required basic elements?
    • Is there a class element for each class, a class for each class element and a database table for each class element?
    • Do all the table names and field names match up case-sensitively with both the database field names and the properties in the class? (If in doubt, add a table attribute to your class element)
    • Are their any property names misspelt in the rest of the class definitions?
    • Are all the data types correct in your classes, the class definitions and the database?
  • "Object reference not set to an instance of an object."
    • This is a .Net error thrown when a null reference is being encountered.  This is a common exception that can be thrown for a wide range of activities, so you'll need to do some further research to try and isolate the code that is throwing the exception.
  • Class definitions
    • Classdefs.xml not found
      • While the class definitions may be stored under your project or assembly, they need to be copied to your execution directory as well (eg. bin, debug, output).  Either add a build command to copy the file at run-time or use a feature like Visual Studio's where you can set the properties on "classdefs.xml" so that it is copied to the output directory.
    • Can't find control, mapper or assembly for a field/grid control
      • Apart from checking for the obvious (does the type or mapper exist in the assembly as specified?), check that the assembly you're referring to has been referenced in the relevant project (try referencing it in the other projects if you've got multiple ones).
  • Database issues
    • Changes not being saved to database
      • If you edit an object in a default form, the changes should be committed automatically, but if you edit values at code level, you need to call Save() on the object to commit the changes.
      • You cannot edit a primary key, Habanero blocks this edit.  For this reason, once an object has been created, you should either not make the field available for editing, or set the field's "editable" attribute to false.
    • Null values being saved in not-null fields
      • Empty strings are not null.  Ensure that you have catered for the possibility of empty strings - some counter strategies include adding a unique key constraint on the field or placing a "minLength" rule on a property.
    • Initial values show in form or grid, but they save as null
      • Because controls like CheckBoxes have no way of showing null, it is up to the developer to add a default value to the property in the class definitions, using the "default" attribute in the "property" element.
  • User interface issues
    • ComboBox displaying wrong information
      • There are two means to display data in a ComboBox using the class definitions.  You can specify a set list of options using "attribute" elements (see Appendix I), or you can set the list to be dynamically loaded when the form comes up, using the lookup-list source options.  These need a pair of strings and Guids, with the strings displayed to the user and the Guids stored in the property values.  Your property type therefore needs to be a Guid.
    • No objects appear in the grid
      • When you create a business object collection to pass to the grid, you need to load the objects in the collection using the collection's Load() or LoadAll() command.
    • Can't edit a date
      • You are unable to edit a DateTime property in a textbox.  Rather use a DateTimePicker (along with the DateTimePickerMapper), which are easier to use and have better error checking.
    • Why do I get an empty form?
      • If you've created an form-based application and nothing is showing in your form, then you need to decide what to show in the form, such as a grid containing your data or a menu that opens other grids or forms.  See the tutorial on examples for displaying data in a form using Habanero.
      • If you've set a grid to display and have no data showing in the grid, even though there is a data in the database, ensure that you called the Load() command on the business object collection, or that you didn't use criteria that the data couldn't adhere to.
  • Naming issues
    • Dashes in the assembly name
      • In Visual Studio, putting a dash in the assembly name will result in an underscore in the namespace.  This will cause failures with Habanero - while we create a workaround for this problem, we suggest you avoid using dashes in the assembly names.

(this list is a work in progress)












Please send tutorial feedback to habanero@chillisoft.co.za.

Download tutorial source code Download demo with VWG


Chillisoft Solutions

Habanero Stats |  Changes Log |  Legal Disclaimer |  Privacy Policy
© 2007 Chillisoft Solutions. All rights reserved.