Sunday, October 22, 2006

Lit Window Library status

The Lit Window Library project is definitely alive and development continues.

Abstract

Summary: Building a database front end with Lit Window Library…

Audience: Software Developers

Level: Beginner

Introduction

Dear Diary!

I am sitting in my favorite café in Erlangen, sipping a Latte Macchiato and am very happy. Yesterday I visited my customer and showed off the latest version...

Yes, yesterday was a great day for me. After more than a year of development my customer began entering live business data yesterday. I sat a desk away, ready to intervene and fix any last minute crashes and show stoppers, but there were none.

And only today it slowly dawns on me that this is a major, major milestone for the Lit Window Library as well. No, the project is not dead. Far from it. I was simply too busy in the last months to bother with any kind of publicity. Now, as my workload hopefully lessens a little, I intend to publish more again.

What works *today*…

I have written a lot about grand ideas, design schemes and how great all of this is going to be - once the library has actually been implemented. But things have progressed a lot since my first note over at slashdot and in today's blog entry I will write only about what is working here and now.

The program I installed yesterday is a tool to help plan and organize business information, from financial data to individual project management issues. Different bits and pieces of information, each needing a table in a database and a form on a graphical user interface. And many interdependencies.

The application I wrote is built upon a (closed source) framework called ‘Mikado’, which I’ve been developing as the successor for my bug tracking tool BugLister. Mikado is a database front end framework which uses ODBC to connect to a database server. Its a little bit like MS-Access, written in C++ for wxWidgets and the Lit Window Library. A core component of the Mikado framework is the record_form control, a single C++ class that houses an arbitrary form (a wxPanel loaded from XRC), handles the user interface and load/save operations and also calls DLL plugins to allow implementation of specific business logic that goes beyond the capabilities of the framework. (Now imagine Python as a script language and a decent WYSIWYG form editor... But I promised I'd write only about what is working today. ;)

To explain how this works, I am going to walk through the steps to add a new kind of record ‘Project’. My client develops leading-edge communication equipment. A ‘Project’ record shall describe the financial planning aspect of such a development project. It has a unique project name, start date, customer(s), planned cash flow and required development resources. Other records will reference it. Invoices, for example, will be assigned a project, so the project ‘contains’ a list of invoices and we’d like to show all invoices written for a specific project in a list on the project’s form.

CREATE TABLE project

The first step is to define a database table. Here is the shortened version of the SQL statement. We are using postgres, by the way, but I've tested Mikado with MS-SQL, MS-Access and Oracle as well.

CREATE TABLE projects

(

id serial NOT NULL, -- unique id

name varchar(32) NOT NULL, -- project name

start_date date, -- start of the project

customer int4, -- the customer paying for the project

status int2, -- status

notes text, -- some notes

planscript text, -- financial planning information

sys__parent int4, -- a parent project

CONSTRAINT pk_projects_id PRIMARY KEY (id),

-- foreign keys omitted for brevity

);

Create the user input form for projects

Get DialogBlocks, if you haven’t already. It’s the best GUI designer for wxWidgets that I know and is written by Julian Smart. If you are using Visual Studio, get wxVisualSetup as well, as it not only integrates wxWidgets into the Visual Studio IDE. It also helps developing the Lit Window Library, because I am the one who gets the money. Okay, enough advertising ;)

A form is actually a wxPanel with sizers and widgets inside. To create the form, I add a wxPanel to the list of documents and add a wxFlexGridSizer.

id

I first add a wxStaticTextCtrl and set the label to “Id:”. This is the label the user sees so he knows that the number next to it is the id of the project. The id itself is read only. The user should not be able to change it, so I add another wxStaticTextCtrl next to the label. wxWidgets has one HUGE advantage over other GUIs I know, such as the MFC. In wxWidgets, every widget/control can have a name. The Lit Window Library data binding mechanism builds upon this. I give the new wxStaticTextCtrl a name: “id”. Note that the column in the database table has the same name.

start_date, name, notes

The next field I add to my form is the project “start_date”. I add another wxStaticTextCtrl as the label and set its value to “Start date:”. The column “start_date” is of type “date”. The Mikado framework converts this to a wxDateTime object. I add a wxDatePickerCtrl to the form, next to the label, and give it the name “start_date”. Again the control has the same name as the column in the database. I repeat these steps for “name” with a wxTextCtrl singleline and “notes” with a wxTextCtrl multiline.

customer, status

The “customer” column is an enumeration. It contains only a number, not the actual name of the customer. This number references the corresponding entry in a “customers” table in a database. We need a control that lets a user select one entry out of a list, 1 of n. That’s what a wxComboBox is for. I add one and name it “customer”. The Mikado framework does the rest. At program start, it loads the definition of the “projects” table and also resolves foreign key references. When it encounters a wxComboBox in a form, it fills the combo box with names from the “customers” table and binds the selection to the “customer” column of the “project” record. The result: the user sees all customers in the list and the current selection is read from and written to the “customer” column.

I add another wxComboBox for the foreign key column "status", a list with values such as “open”, “completed”, “possible”, “obsolete”.

automatic text completion

Actually I am not using a wxComboBox here, but a custom control "lwAutoCompleteTextCtrl", part of the Mikado framework. This control works like the autocomplete text controls you see when you are using a web browser and start typing an URL in the address bar. The list of customers is much too long to be using a combo box. lwAutoCompleteTextCtrl lets you type a few characters and the list is narrowed down immediately. But the underlying mechanisms – foreign key table and current selection – are exactly the same as for wxComboBox.

sys__parent

“sys__parent” is an interesting one. In Mikado, columns starting with “sys__” always have a special meaning. The “sys__parent” column defines a hierarchy. For table “projects”, “sys__parent” specifies the parent project of the current record, pointing to another row in the “projects” table. I wrote another custom control lwAutoCompleteTreeCtrl, which works like lwAutoCompleteTextCtrl, except that it shows a tree instead of a list. Adding the lwAutoCompleteTreeCtrl to the form lets the user enter the parent project by typing its first few letters. Or, if the user presses F4 to drop down the tree control, he can see the entire project hierarchy and select the parent project.

planscript

This text column contains the actual planning, written in a small scripting language which we designed specifically for that purpose. The parser that handles this is contained in a plugin and reacts to events sent by the framework. I am not going into more details here and simply add a multiline wxTextCtrl for it.

saving the form

The form (wxPanel) itself, like all the controls inside, also has a name property. I name my form “f_projects” and save everything to an .XRC file. The .XRC files are XML files containing the GUI layout. Here is a short code snipped of how you would use them in your C++ code.

// load XRC file into memory

wxXmlResource::Get()->Load(filename);

// create a wxPanel from the “f_projects” definition

wxPanel *panel=wxXmlResource::Get()->LoadPanel(“f_projects”);

This would return a wxPanel object containing all sizers and widgets and all that I designed with DialogBlocks above.

Registering table and form

Now that I have a database table and an XRC form, I need to register them with the framework. To register the table, I add the value “projects” to a special table “l_types”. “l_types” contains the list of the database tables that Mikado knows about. At startup, when Mikado reads it and finds the value “projects”, it will query the database server for the definition of “projects” and build a column and key catalog for it. This is the moment when the framework discovers that column “customer” references another table and that wxComboBoxes later need to be filled with values from “customers”.

I also need to tell Mikado that the form “f_projects” is the right form for “project” records and I also want a human readable name for the form. To do that I add a record “f_projects”, “projects”, “Project folder” to another special table “v_views”. This table contains a list of all forms the user can choose from. The GUI shows this list on the left side of the frame, like the folders list of your email program.

Voíla…

To test it I start Mikado. It shows an entry “Project folder” in the view list, which I click on. Mikado then shows me a list of records from the “projects” table in the database. I select one. Mikado loads the form “f_projects” (a wxPanel containing controls, remember?) from the XRC file, shows it on the screen, loads the record I selected from the database and fills the form with values from the record.

That last part is handled entirely by Lit Window’s RapidUI object, which I wrote so much about already. The framework calls AddWindow() to add the form to the RapidUI object. It then calls AddData() to add the record. After that the RapidUI object creates default rules: “for each data element” - search for a window element of the same name and if one exists, create a simple rule “window.element = data.element”. Together, these default rules bind all columns to their controls.

The code inside Mikado’s “record_form” is pretty simple, thanks to the Lit Window Library:

// load the form, init the RapidUI object

void record_form::prepare_for(wxString type_name, wxString form_name)

{

// m_working_record is of type mikado_record

// create a new, empty record of type ‘type_name’

// also changes the type of m_working_record

m_working_record.create(type_name);

// add the data to the RapidUI mediator

m_rapidUI.AddData(m_working_record);

// load the form from the XRC file

wxPanel *m_form=wxXmlResource::Get()->LoadPanel(form_name);

// add the form to the RapidUI object

m_rapidUI.AddWindow(*m_form);

// start the RapidUI object

m_rapidUI.Start();

}


// load a record from the table

void record_form::load(long bookmark)

{

// create a rowset (similar to wxDbTable)

mikado_rowset rows(m_connection);

// bind the current working record to the rowset

rows.bind(m_working_record);


// set the SQL where clause to load a particular record

// ([in]bookmark) is OLEDB syntax to specify a named parameter

// ‘bookmark’. Mikado’s ODBC layer uses the same syntax.


rows.set_where(“id=?([in]bookmark)”);


// bind the parameter to the actual variable


rows.bind(bookmark, “bookmark”);


// open the table


rows.open();


// fetch the first entry

// this stores the values in the m_working_record


rows.fetch();


// now tell the library that m_working_record has changed

// this triggers the RapidUI rules system and will update

// everything in the form


litwindow::NotifyChanged(m_working_record);

}


void record_form::test_project_form()

{

// first prepare everything for objects of type

// “projects” and load the form “f_projects”


prepare_for(“projects”, “f_projects”);


// now load record with id==5


load(5);

}

That’s all. And the point is, I’ve written this code once and will never have to write it again, because it works with all kinds of records. This is the ultimate goal of the Lit Window Library, a library of building blocks at a significantly higher level of abstraction that other libraries.

High level library of building blocks

The “record_form” is actually a mediator class (search google for mediator design pattern). It connects a record and a form and handles everything in between. A “record_form” can load, create and save arbitrary database records using arbitrary forms. Written once, used many times. If you need to show a single database record, use the “record_form” mediator from the library. Want to display a list of records and handle select/edit/insert/delete? Use a “select/edit/insert/delete” mediator object in combination with the “record_form” mediator. Instantiate them, connect a list control, a form and some action triggers (buttons and menu entries) for the insert/delete/save actions. Everything I have implemented so far is only a means to write generic mediator classes such as “record_form”.

Conclusion

When my customers started entering project information for the first time yesterday, we hit a minor snag immediately. Some projects did not have a start date yet, but the form required an entry. To fix this, I loaded the form in DialogBlocks and set the SP_ALLOWNONE flag for the “start_date” wxDatePickerCtrl. I saved the XRC file and ran Inno Setup to create a new setup.exe. They installed it and the problem was gone. This took less than five minutes and I didn't even have to recompile the application.

They also wanted to categorize projects by region (Asia, Europe, US). While they were waiting I

  • added a new column “region” to the projects table definition

  • added a simple table “regions”

  • added a simple form to be able to add regions and

  • added another lwAutoCompleteTextCtrl to the projects form

and created a new setup.exe. All of it without recompiling the application. I stress this because the chance of breaking something is a lot higher when you change actual C++ code. Mikado gives me a lot more confidence to make changes “on the fly”, while the customer is waiting.

Outlook

I’ve saved the juiciest part for my next blog entry: showing the list of invoices attached to a project in the projects form. In that entry I will add a lwMikadoDBGridCtrl and connect its properties and actions to the main form. These "connections" use the Lit Window Library rules language and are contained in a simple text field in the XRC file. Like above, changes to that connection can be made without recompiling the application.

Here is the code from the “LITWINDOW_RULES” field in the XRC file. This is actual, working code, except that I have changed the identifier names because of Copyright issues.

// set the invoice_grid database connection

// to the form’s database connection


invoice_grid.Connection:=form.connection;


// tell the invoice grid to reload itself

// when the current_bookmark of the form changes

// the invoice will no longer show all invoices,

// it will only show invoices assigned to an object

// ‘current_bookmark’


invoice_grid.ParentBookmark:=form.current_bookmark;


// tell invoice to save changes when the

// form.IsSaving trigger fires


invoice_grid.SaveAction:=form.IsSaving;

// tell form to mark data as dirty when the

// invoice_grid.IsDirty trigger fires


form.DirtyAction:=invoice_grid.IsDirty;


// this is not c++ code, these are lit window

// library rules (or constraints), compiled and

// evaluated at runtime

That’s all for now. I hope you found it interesting reading. If you did, please help spread the word and send the link to my blog to your colleagues. And as always I welcome your comments. Do you want more details? Less? Are these blog entries too long to read? Comments on the library?


Best regards

Hajo Kirchhoff

2 comments:

Anonymous said...

Is the new, boostified, LitWindow library available somewhere? The SF archive is rather old.

/Pavel

Hajo Kirchhoff of Lit Window Productions said...

Hi Pavel,
I'm willing to send you, and everyone else, a copy directly. The problem is, I haven't had time since at least a year to clean up things enough to feel comfortable to put it in SF cvs again. The code is clean enough, but the build system is in shambles.
Right now you have to have VisualStudio 2005 to be able to build it or you have to create your own projects/build files.

Anyone willing to help and create a solid boost bjam build system? That would be great.