monolith documentation index

What is monolith?

monolith is an application framework which generates web-based applications. It differs from existing web tools because it doesn't work in terms of "pages" and "CGI scripts". Instead it allows you to build applications using reusable widgets (buttons, labels, etc.) which you arrange into windows. This makes it much more like building a traditional application in Windows/MFC, Java/JFC, Tcl/Tk, Motif, etc.

You can also use the basic widgets to build reusable "super-widgets" which you can then embed in other applications, give away or sell. An example might be a discussion system "widget" which can be placed anywhere in another application.

Of course, there are limitations in the web and browsers which means that you can't do everything you might do in a normal application. But you can do most things.

monolith programs and widgets are written in C or C++ (actually I've never used C++ with monolith, but you are welcome to try).

CGI vs. rws's shared object scripts vs. monolith applications

(This section is from the rws documentation page).

Shared object scripts are the direct analogy to CGI scripts, the only difference being that CGI scripts are usually written in very high level languages like Perl and PHP, and shared object scripts are loaded into the server process for efficiency. (Perl CGI scripts can also be loaded into the Apache server process using mod_perl, and this is done for similar reasons of efficiency).

monolith programs are entire applications, the sort of thing which normally would be written using dozens of cooperating CGI scripts. In the case of monolith, however, the entire application compiles down to a single .so file which happens to be (you guessed it) a shared object script.

Imagine that you are going to write yet another web-based email client. For some reason you want to write this in C (please don't try this at home: I wrote one in Perl at my last job and that was hard enough). Here are three possible approaches using C and rws:

  1. Write forty or so shared object scripts. Each displays a single frame of the application, one might generate the frameset, a couple of dozen to implement specific operations like emptying trash or moving a message between folders.

    This is very much the normal way of writing CGI-based applications.

  2. Write a monolith application. This will probably be in lots of C files, but will compile down and be linked into a single .so file (eg. email.so) which is dropped into the so-bin directory.
  3. Write a monolith email super-widget. This is going to exist in a shared library called /usr/lib/libmyemail.so with a corresponding header file defining the interface called myemail.h.

    Write a tiny monolith application which just instantiates a window and an email widget, and embeds the email widget in the window. This will compile into email.so (it'll be very tiny) which is dropped into so-bin.

    The advantage of this final approach is that you can reuse the email widget in other places, or indeed sell it to other monolith users.

So monolith is good when you want to build applications from widgets as you would if you were building a Java/Swing, Windows MFC, gtk, Tcl/Tk graphical application. It's also good if code re-use is important to you. Shared object scripts are good when you are familiar with CGI-based techniques to build websites.

Of course, the same rws server can serve shared object scripts, multiple monolith applications, flat files, and directory listings, all at the same time.

Compiling and installing monolith programs

monolith programs are C programs which compile into .so files, called shared object scripts. The rws webserver runs these directly.

To compile, probably the simplest thing to do is write a Makefile which does:

gcc -Wall -Werror -c prog.c -o prog.o
gcc -shared -Wl,-soname,prog.so prog.o \
		-lmonolithcore -lpthrlib -lc2 -lm -o prog.so

The last step generates the actual binary. Copy this into rws's /so-bin directory, and make sure it is mode 0755 (-rwxr-xr-x).

To create an /so-bin directory, add this to your rws hosts file:

alias /so-bin/
        path:           /path/to/your/so-bin
        exec so:        1
end alias

Now test it out by going to http://your.webserver/so-bin/prog.so

If it doesn't work, here is a checklist before you email me:

I have quite successfully used gdb on a running server to debug and diagnose problems in monolith programs. However note that by default gdb may have trouble loading the symbol table for the monolithcore library and your program. Use the sharedlibrary monolith; sharedlibrary script.so command to load symbols instead.

Tutorial

This tutorial begins with the basics of monolith application design, and then goes through some of the examples which you will find in the examples/ directory in the source distribution.

A simple "hello, world" program

The simple "hello, world" program is useful because it lets us (a) make sure our development environment is really working, and (b) sort out the boilerplate code that every monolith application needs. You can find the full program in the source distribution as doc/hello.c.

Start by including some necessary headers:

#include <pool.h>

#include <monolith.h>
#include <ml_window.h>
#include <ml_text_label.h>

Then there is some standard boilerplate code that needs to go in (once only) into every monolith application. You don't need to worry about what it does: it's just glue between rws's shared object scripts and the monolith core code:

/*----- The following standard boilerplate code must appear -----*/

/* Main entry point to the app. */
static void app_main (ml_session);

int
handle_request (rws_request rq)
{
  return ml_entry_point (rq, app_main);
}

/*----- End of standard boilerplate code -----*/

Then the "hello, world" program itself:

static void
app_main (ml_session session)
{
  pool pool = ml_session_pool (session);
  ml_window w;
  ml_text_label text;

  /* Create the top-level window. */
  w = new_ml_window (session, pool);

  /* Create the text widget. */
  text = new_ml_text_label (pool, "Hello, World!");

  /* Pack the text widget into the window. */
  ml_window_pack (w, text);
}

app_main is the entry point into the application. It has one parameter, the opaque ml_session object, which is explained below. Every monolith program needs a top-level window, so we create this using new_ml_window(3). Now we need to put something into the window, otherwise it'll appear completely blank. In this case we're going to put a text label widget inside this window containing the familiar greeting. We have to tell the window that it contains the text widget (otherwise they'd be just two completely separate variables), so we call ml_window_pack(3) to place the text label inside the window.

Now you should compile and run this example (in doc/hello.c) using the instructions above and verify that it runs.

Design: sessions, session pools, shared data, session data

The app_main function has just one argument passed to it, the opaque ml_session object. What is this used for?

Think of a normal GUI application written in C or C++ (or another language if you like). A user starts up the application. The application interacts exclusively with that one user. Global variables in the application belong entirely to that application and that user. The application continues to be used until the user closes it down. We will define this process of the user starting up the application, using the application and then finally closing it down, as a session.

Web-based applications are slightly different in that the same code running in the same Unix process can be used by many users at the same time. For the benefit of programmers, monolith automatically keeps all of these users' sessions separate for you, maintaining a different opaque ml_session object for each session.

One implication of this is that app_main is called with a different ml_session object each time, because a session only starts once. (The same user might come back later and use the same application, but that would be a different session).

So a session is different from a user. A session is also different from an HTTP request. Typically during a session a user might fill in a few forms, press some buttons, browse through tables and so on. Each of these operations probably involves an HTTP request. So in monolith HTTP requests are very short-lived (of the order of 1ms - 1s), but sessions can be quite long (hours of activity).

monolith allocates a separate session pool for each session, and most applications are expected to allocate most of their data on this session pool. Of course when a user finishes their session, the session pool will be deleted. This normally results in all of the widgets and stuff created during the session being nuked, and this is generally a good thing.

C static variables (ie. globals and variables in functions declared using static) are shared between all sessions. This can be useful under some circumstances, but if what you really want is persistent data, then you are far better off using a database of some sort at the back-end. This is because static variables will obviously be trashed if the monolith application is unloaded or the webserver crashes.

c2lib's global_pool is also shared between all sessions (but don't use it directly: create a subpool in your _init function and delete the subpool in your _fini function).

If C static variables are shared, how do we keep a separate set of variables for each session? Generally the easiest way to do this is to allocate a per-session structure once in app_main and pass it around. This is called the session data structure. We'll see this being done in example 01 below.

Design (for CGI programmers): what are widgets?

A short aside: what are widgets? If you have any experience of using a traditional GUI library or GUI-builder tool, like Tcl/Tk, gtk, Windows MFC, Motif, Java Swing, etc., then you'll probably already know what a widget (or "control" in MS-speak) is, so skip to the next section. This section is for people coming from a non-graphical or purely CGI background.

In traditional GUI environments, applications are not built up using pages, forms, CGI scripts and so on, but are instead built up using small reusable objects called widgets. A typical basic widget might be a push button, a label, or an image. There are also compound widgets which store other widgets inside themselves. In monolith for example, a table layout can be used to arrange other widgets into a table. A table layout is itself a widget, and you can use a table layout (populated with widgets inside) any place you would use a basic widget. This is important because complex layouts are often constructed from several layers of compound and basic widgets (buttons and labels inside table layouts inside other table layouts inside windows, etc.)

Here is an example form constructed using nested widgets:

Window Form Table layout
Label
Label Form input
Label Form input
Empty Form submit

Example 01: Label and button

The first example, examples/01_label_and_button.c, is very simple. It displays a label and a button. Clicking on the button increments the number on the label. Try this now. Also try running it from two different browsers and machines. Notice how the label starts counting up from 0 independently on each machine (demonstrating that each session is really independent because this demo uses a session data structure).

As before we begin by including some necessary headers and the same boilerplate code as in our "hello, world" example above. I won't repeat that here, because it's identical. Then we declare our session data structure and a few private functions:

struct data
{
  ml_label lb;			/* Label. */
  int count;			/* Count of number of button presses. */
};

static void increment (ml_session, void *);
static void update_label (pool pool, ml_label lb, int count);

We keep (a pointer to) the label widget and the count of button presses in our session data structure. In theory we could keep more here, but in fact it's not necessary. These are the only two variables that we need to make "global" to the session, because these are the only two variables which our callback function will need when it comes to update the label. Our callback function is going to be called when the user clicks on the button, as we'll see in a moment.

The main entry point to our application is called app_main. It's called at the beginning of the session:

static void
app_main (ml_session session)
{
  pool pool = ml_session_pool (session);
  struct data *data;
  ml_window w;
  ml_flow_layout lay;
  ml_label lb;
  ml_button b;

Notice that we use ml_session_pool(3) to get the current session pool, where we are going to make all of our allocations.

data will point to our session data structure, but we have to allocate and initialise it first:

  /* Create the private, per-session data area and save it in the
   * session object.
   */
  data = pmalloc (pool, sizeof *data);
  data->count = 0;

Next we create the window, label and button. We're going to pack the label and button into a flow layout which is the simplest sort of compound widget: it just displays the widgets inside itself one after another.

  /* Create the top-level window. */
  w = new_ml_window (session, pool);

  /* Create the flow layout widget which will be packed into the window. */
  lay = new_ml_flow_layout (pool);

  /* Create the label and button. */
  data->lb = lb = new_ml_label (pool, 0);
  update_label (pool, data->lb, 0);

  b = new_ml_button (pool, "Push me!");
  ml_button_set_callback (b, increment, session, data);

  /* Pack the label and button into the flow layout widget. */
  ml_flow_layout_pack (lay, lb);
  ml_flow_layout_pack (lay, b);

  /* Pack the flow layout widget into the window. */
  ml_window_pack (w, lay);
}

Notice the call to ml_button_set_callback. When the button is pressed, the increment function will be called like this: increment (session, data). (Recall that data is our session data pointer).

This is the definition of increment:

static void
increment (ml_session session, void *vp)
{
  struct data *data = (struct data *) vp;

  update_label (ml_session_pool (session), data->lb, ++data->count);
}

It increments data->count and calls update_label which is the function which actually changes the message on the label.

update_label is defined as:

static void
update_label (pool pool, ml_label lb, int count)
{
  ml_label_set_text (lb,
		     psprintf (pool,
			       "Button pressed: <b>%d</b><br>",
			       count));
}

(If you are unfamiliar with the function psprintf then you should read the c2lib documentation).

That's the end of our first significant monolith application! At around 31 lines of code, it's considerably smaller than the equivalent CGI script.

Design: Applications and widgets

This section talks about one of the more fundamental design considerations you need to think about when first designing a new monolith application. Namely when to build application code and when to build reusable widgets.

The label and button example above is a complete monolith application. What happens however if we needed to embed this label and button combo in another program (not a very likely scenario, I'll admit, but let's imagine that you've developed a calendar program or something else quite substantial).

The answer is to turn your application into a reusable widget. Widgets are often composed of many other more fundamental widgets, and in this case it is possible to turn the label and button combo into a full-blown widget. This widget could be used just like any of the core widgets which monolith provides.

Turning an application into a widget isn't too hard, depending on the complexity of the application itself, but it's better when designing the application if you first of all work out whether the application as a whole -- or parts of the application -- can be designed as reusable widgets.

If you were designing a calendar program in monolith then you might decompose the design like this:

Component Specification Can be used as a widget?
Whole application Calendar: A tool for storing and tracking daily events, providing appointments, backed up in a database Yes. By writing the whole calendar as a reusable widget, we can include the calendar widget in an Outlook-style personal information manager (PIM).
New event form Form which appears when the user adds a new event. No. Quite specific to this calendar, so reuse doesn't make much sense.
Date selector Form input which allows the user to choose a date and automatically verifies it. Yes. This is applicable on many different forms in other applications.

Example 03: Toy calculators

Example 03 will demonstrate how to write a simple reusable widget. In fact the reusable widget that we're going to write is the same as the application in example 02 (not covered here, but supplied with the source in the examples/ directory). So if you want you can study the process of converting a whole application into a reusable widget.

The source code for example 03 is divided into three files:

It's helpful at this point if you run the example. You should see four calculators. Try doing some sums. Notice how each calculator acts completely independently of the others.

Let's start with the application code. This is very simple. It just creates four toy_calculator objects and populates a table layout widget with them:

static void
app_main (ml_session session)
{
  pool pool = ml_session_pool (session);
  ml_window w;
  ml_table_layout tbl;
  toy_calculator calcs[4];

  /* Create the top-level window. */
  w = new_ml_window (session, pool);

  /* Create a table layout widget to arrange the calculators. */
  tbl = new_ml_table_layout (pool, 2, 2);

  /* Create the calculators and pack them into the table layout. */
  calcs[0] = new_toy_calculator (pool, session);
  ml_table_layout_pack (tbl, calcs[0], 0, 0);
  calcs[1] = new_toy_calculator (pool, session);
  ml_table_layout_pack (tbl, calcs[1], 0, 1);
  calcs[2] = new_toy_calculator (pool, session);
  ml_table_layout_pack (tbl, calcs[2], 1, 0);
  calcs[3] = new_toy_calculator (pool, session);
  ml_table_layout_pack (tbl, calcs[3], 1, 1);

  /* Pack the table into the window. */
  ml_window_pack (w, tbl);
}

The header file toy_calculator.h defines the interface. It's very simple, because there's only one thing you can do with a toy_calculator at the moment, and that's create one by using the new_toy_calculator function. Notice the cdoc documentation in the comments.

#ifndef TOY_CALCULATOR_H
#define TOY_CALCULATOR_H

#include <pool.h>
#include <monolith.h>

struct toy_calculator;
typedef struct toy_calculator *toy_calculator;

/* Function: new_toy_calculator - toy calculator widget
 *
 * @code{new_toy_calculator} creates a new reusable toy calculator
 * widget.
 */
extern toy_calculator new_toy_calculator (pool, ml_session);

#endif /* TOY_CALCULATOR_H */

The actual implementation of the toy_calculator widget is complicated, so we'll take it step by step here. However remember that if all you want to do is to use a toy_calculator, then you needn't worry about the implementation at all. You only need to look at the header file and use it just like you would any other widget.

toy_calculator.c begins by including many header files.

#include <string.h>

#include <pool.h>
#include <pstring.h>
#include <pthr_cgi.h>

#include <monolith.h>
#include <ml_window.h>
#include <ml_table_layout.h>
#include <ml_text_label.h>
#include <ml_box.h>
#include <ml_button.h>
#include <ml_widget.h>

#include "toy_calculator.h"

Every widget has an associated structure, which is where it stores all its private data. For callers, the widget structure is opaque (notice how it is defined in the toy_calculator.h header file above). The toy_calculator object we've been passing around above is in fact a pointer, typedef'd to struct toy_calculator *. (This is a common coding convention in c2lib and pthrlib). In the actual implementation, obviously we need to see the private members.

Moreover, every widget structure must begin with a pointer to struct ml_widget_operations because the toy_calculator object "inherits" from the abstract base class ml_widget.

Here is the definition of struct toy_calculator:

static void repaint (void *, ml_session, const char *, io_handle);

struct ml_widget_operations toy_calculator_ops =
  {
    repaint: repaint
  };

struct toy_calculator
{
  struct ml_widget_operations *ops;
  pool pool;			/* Pool for allocations. */
  ml_text_label disp;		/* The display. */
  char digits[16];		/* Display digits (only 10+1 used). */
  double reg;			/* Hidden register. */
  int op;			/* Operation: PLUS, MINUS, TIMES, DIVIDE. */
  ml_box box;			/* The top-level box. */
};

repaint is going to be the function which actually draws our widget, but we'll see that a little bit later.

The most important function we need to define is new_toy_calculator which creates new toy_calculator widgets. I won't show this function in full because it's quite long (it needs to create one ml_button object for every button on the calculator, and there are 18 of them in all!). But here's the important outline.

We start by allocating and initialising a new struct toy_calculator. A pointer to this is stored in w.

toy_calculator
new_toy_calculator (pool pool, ml_session session)
{
  toy_calculator w;
  ml_box box;
  ml_table_layout tbl;
  ml_button b[18];
  ml_text_label disp;

  w = pmalloc (pool, sizeof *w);
  w->ops = &toy_calculator_ops;
  w->pool = pool;
  strcpy (w->digits, "0");
  w->reg = 0;
  w->op = 0;

Then some code creates the actual widgets contained inside the calculator, the top level being a box widget:

  /* Create the box surrounding the calculator. */
  box = new_ml_box (pool);

  /* A table layout widget is used to arrange the buttons and the screen.
   * There are 6 rows, each with 4 columns.
   */
  tbl = new_ml_table_layout (pool, 6, 4);

  /* Create the numeric buttons. */
  b[0] = new_ml_button (pool, "0");
  ml_button_set_callback (b[0], press_button_0, session, w);
  ml_button_set_key (b[0], 1);
         :      :     :
         :      :     :
         :      :     :

Finally we pack everything up and return the widget pointer (w):

         :      :     :
         :      :     :
         :      :     :
  /* Pack the table into the box. */
  ml_box_pack (box, tbl);

  /* Save the display widget in the widget structure so that the
   * callback functions can update it.
   */
  w->disp = disp;

  /* Save the box, so we can repaint. */
  w->box = box;

  return w;
}

When a button is pressed, one of the appropriate callback functions is called. Because there are 18 buttons, there are 18 separate callback functions, but we only reproduce a few here. Notice how the toy_calculator pointer is passed as the second argument (the void *), so we need to cast this back before using it:

static void press_button_N (toy_calculator, int);

static void
press_button_0 (ml_session session, void *vw)
{
  toy_calculator w = (toy_calculator) vw;

  press_button_N (w, 0);
}

         :      :     :
         :      :     :
         :      :     :

static void
press_button_N (toy_calculator w, int n)
{
  int len;

  if (strcmp (w->digits, "0") == 0)
    w->digits[0] = '\0';

  len = strlen (w->digits);

  if ((strchr (w->digits, '.') && len < 11) || len < 10)
    {
      w->digits[len] = '0' + n;
      w->digits[len+1] = '\0';
      ml_text_label_set_text (w->disp, w->digits);
    }
}

Here's the callback function which runs when the [AC] button is pressed:

static void
press_button_AC (ml_session session, void *vw)
{
  toy_calculator w = (toy_calculator) vw;

  strcpy (w->digits, "0");
  w->reg = 0;
  ml_text_label_set_text (w->disp, "0");
}

Finally our widget must know how to repaint (redisplay) itself. monolith will call the repaint function at the appropriate moment, and it must generate the HTML corresponding to the widget. In the case of this widget, it's a compound widget built up entirely out of core monolith widgets. The top-level widget inside the calculator is the box (w->box), so we just call the repaint function for that:

static void
repaint (void *vw, ml_session session, const char *windowid, io_handle io)
{
  toy_calculator w = (toy_calculator) vw;

  ml_widget_repaint (w->box, session, windowid, io);
}

That's all. Our reusable toy calculator is now complete.

Example 06: Forms

Example 06 shows the various input controls available when using forms. The example is straightforward, albeit rather long because of the number of different controls demonstrated. We begin with an unusually long list of includes:

#include <string.h>

#include <pool.h>
#include <pstring.h>
#include <pthr_cgi.h>

#include <monolith.h>
#include <ml_window.h>
#include <ml_text_label.h>
#include <ml_button.h>
#include <ml_table_layout.h>
#include <ml_flow_layout.h>
#include <ml_form.h>
#include <ml_form_submit.h>
#include <ml_form_text.h>
#include <ml_form_textarea.h>
#include <ml_form_password.h>
#include <ml_form_select.h>
#include <ml_form_radio_group.h>
#include <ml_form_radio.h>
#include <ml_form_checkbox.h>
#include <ml_form_textarea.h>

The private session data structure contains pointers to the form input widgets so that our callback functions are able to read their contents:

/* Private per-session data. */
struct data
{
  ml_window win;

  /* The form input fields themselves. */
  ml_form_text familyname, givenname;
  ml_form_password password;
  ml_form_select dd, mm, yyyy;	/* Date of birth. */
  ml_form_radio_group gender;
  ml_form_radio m, f;		/* Gender. */
  ml_form_checkbox eating, drinking, sleeping; /* Interests */
  /*ml_form_file photo;            File upload, not yet implemented. */
  ml_form_select dept;
  ml_form_textarea comments;
};

app_main allocates the session data structure and calls create_form which actually creates the initial form:

static void
app_main (ml_session session)
{
  pool pool = ml_session_pool (session);
  struct data *data;

  /* Create the private, per-session data area and save it in the
   * session object.
   */
  data = pmalloc (pool, sizeof *data);

  /* Create the top-level window. */
  data->win = new_ml_window (session, pool);

  create_form (session, data);
}

create_form is quite a long, but not very complex function. It creates an input control of each type. Notice first the visual structure of the form:

Window Form Table layout
Label Form input
Label Form input
etc.
Empty Form submit

I will not reproduce all of the form inputs here:

static void
create_form (ml_session session, void *vp)
{
  pool pool = ml_session_pool (session);
  struct data *data = (struct data *) vp;
  ml_form form;
  ml_table_layout tbl;
  ml_text_label text;
  ml_form_submit submit;
  ml_flow_layout flow;
  int i;

  /* Create the form. */
  form = new_ml_form (pool);
  ml_form_set_callback (form, submit_form, session, data);

  /* Create the table. */
  tbl = new_ml_table_layout (pool, 10, 2);

  /* Create the contents of the form. */
  text = new_ml_text_label (pool, "Family name / surname");
  ml_table_layout_pack (tbl, text, 0, 0);
  data->familyname = new_ml_form_text (pool, form);
  ml_table_layout_pack (tbl, data->familyname, 0, 1);

         :      :     :
         :      :     :
         :      :     :

  /* Submit button. */
  submit = new_ml_form_submit (pool, form, "Submit");
  ml_table_layout_pack (tbl, submit, 9, 1);

  /* Pack it all up. */
  ml_form_pack (form, tbl);
  ml_window_pack (data->win, form);
}

Notice that we set a callback function on the form:

ml_form_set_callback (form, submit_form, session, data);

When the form is submitted by the user, submit_form (session, data) will be called. submit_form, reproduced next, can read the value that the user entered into each form field by calling ml_form_input_get_value:

static void
submit_form (ml_session session, void *vp)
{
  pool pool = ml_session_pool (session);
  struct data *data = (struct data *) vp;
  ml_text_label text;
  ml_table_layout tbl;
  ml_button button;
  const char *str;

  str = psprintf
    (pool,
     "Form submitted.\n"
     "\n"
     "Family name: %s\n"
     "Given name: %s\n"
     "Password: %s\n"
     "Date of birth: dd = %d, mm = %d, yyyy = %d\n"
     "Gender: %s\n"
     "   M is checked: %d  F is checked: %d\n"
     "Interests: Eating = %d, Drinking = %d, Sleeping = %d\n"
     "Dept fields checked: [ %s ]\n"
     "Comments:\n"
     "--start--\n"
     "%s\n"
     "--end--\n",
     ml_form_input_get_value (data->familyname),
     ml_form_input_get_value (data->givenname),
     ml_form_input_get_value (data->password),
     1 + ml_form_select_get_selection (data->dd),
     1 + ml_form_select_get_selection (data->mm),
     1900 + ml_form_select_get_selection (data->yyyy),
     ml_form_input_get_value (data->gender),
     ml_form_radio_get_checked (data->m),
     ml_form_radio_get_checked (data->f),
     ml_form_input_get_value (data->eating) ? 1 : 0,
     ml_form_input_get_value (data->drinking) ? 1 : 0,
     ml_form_input_get_value (data->sleeping) ? 1 : 0,
     pjoin (pool,
	    pvitostr (pool, ml_form_select_get_selections (data->dept)), ", "),
     ml_form_input_get_value (data->comments));

  tbl = new_ml_table_layout (pool, 2, 1);
  text = new_ml_text_label (pool, str);
  ml_table_layout_pack (tbl, text, 0, 0);
  button = new_ml_button (pool, "Back to form");
  ml_button_set_callback (button, create_form, session, data);
  ml_table_layout_pack (tbl, button, 1, 0);

  ml_window_pack (data->win, tbl);
}

Notice how we have added a button called Back to form which calls create_form.

The end of this tutorial

That's the end of this tutorial. You should be able to go and write monolith applications and widgets now. Full manual pages are included below.

The monolith class hierarchy

Of course monolith is written in C, so there are no classes per se, but this is the general class hierarchy of monolith widgets and windows:

ml_window: window or frameset

ml_session: user session

ml_widget
    |
    |
    +--	ml_box: draws a box around another widget
    |
    +--	ml_button: simple button
    |
    +--	ml_dialog: ask the user a question
    |
    +--	ml_flow_layout: layout widgets one after another
    |
    +--	ml_form: surrounds a collection of form inputs
    |
    +--	ml_form_input
    |	    |
    |	    |
    |	    +--	ml_form_checkbox: checkbox (tickbox)
    |	    |
    |	    +--	ml_form_file: file upload input [not implemented]
    |	    |
    |	    +--	ml_form_password: single line password input
    |	    |
    |	    +--	ml_form_radio_group: group of radio buttons
    |	    |
    |       +--	ml_form_radio: radio button
    |       |
    |	    +--	ml_form_select: drop-down list of options
    |	    |
    |	    +--	ml_form_submit: submit button
    |	    |
    |	    +--	ml_form_text: single line text input
    |	    |
    |	    +--	ml_form_textarea: multi-line text input
    |
    +-- ml_image: display an icon or image
    |
    +-- ml_label: display arbitrary HTML
    |
    +-- ml_table_layout: powerful table layouts of widgets
    |
    +-- ml_text_label: single line or paragraphs of plain text
    |
    +--	ml_toggle_button: toggle button

Inheritance is faked using a technique very similar to the vtables used by C++.

Other Notes

Can I theme monolith applications?

Yes, you can. At the moment, the easiest way to change the look and feel is to edit the default stylesheet (default.css). This way you can make extensive changes to how your application looks from a single file.

If you don't like the idea of editing default.css, then copy it and make your own. Call ml_window_set_stylesheet on all your application windows to point to your new stylesheet.

You can even provide different themes to different users (so-called "skinning" - ugh I hate that term). Once a user has logged into the app, call ml_window_set_stylesheet with the appropriate theme for that user.

Frames considered harmful

Although monolith supports frames and pop-up windows, care should be taken because these do not work in the way that most users expect.

The problem arises when one frame tries to update the state of another frame (the same problem applies to two separate windows, but I'll just use the generic term "frame" here). For example, imagine the following simple frameset:

Left frame

[Button 1]

[Button 2]

[Button 3]

Right frame

It's common to want the buttons in the left hand frame to change the contents displayed in the right hand frame, and a naive way to do this would be to set the callback for each button to a function like this:

static void
update_right_frame (ml_session session, void *vp)
{
  /* Get private per-session data. */
  struct data *data = (struct data *) vp;

  pool pool = ml_session_pool (session);

  ml_text_label label = new_ml_text_label (pool, updated content);

  /* Change the contents of the right hand frame. */
  ml_window_pack (data->right_frame, label);
}

The problem is that this doesn't work at all. Pressing the buttons will not update the right hand frame.

For seasoned HTML programmers, it will be obvious why this happens. For people used to traditional application development, it is confusing and seems like a bug.

In future, we will add features to Monolith to allow careful developers to use frames in situations such as above. However at the moment, the advice is:

  1. Consider each frame/window/pop-up to be a completely separate entity, almost like a separate session. Don't expect that updating properties in another frame/window/pop-up will work (it almost certainly will have no effect).
  2. Avoid using framesets if possible. Table layouts are often a better substitute.

Notes on forms

You cannot nest forms. (This is a limitation of HTML.)

If you have several forms on the same page, you can run into problems. A common problem happens when you have two forms on the same dialog like this:

First form
[ --- input #1 --- ] [submit]
Second form
[ --- input #2 --- ] [submit]

If the user types something in input field #1, then types something in input field #2, and presses the second submit button, then the contents of input field #1 will disappear.

To avoid this, either only use one form on a dialog, or design your dialogs so that it is clear that the first submit button must be pressed after filling out the first form.

Another problem with forms (and again, a limitation of HTML) is that they are not interactive. The server cannot read the value of a form input field until the [Submit] button has been pressed.

If you are using forms, then only use form input widgets inside them. Toggle buttons and so on are not form input widgets, and cannot be part of a form.

Links to manual pages


Richard Jones
Last modified: Sat Sep 7 14:45:50 BST 2002