Saturday, 12 November 2011

System generated and managed user input forms

Most of my system work so far has been purely architectural - establishing the way the system is to be constructed, and the overall structure of the system.  This work has led to a style of programming that separates out interfaces and their implementation. My system runs until it needs data from the user; at which point it runs an input form, and retrieve data from it. It was obviously going to be trivially easy to go from the architecture to implemention of a running system. This was a statement of the blindingly obvious that proved not to be so.

My final implementation eventually proved simple, but the route to the solution was long and tortuous. I eventually investigated the Delphi concept of component ownership; the nature of Windows processing loop; the behaviour of modal forms; how to implement callback functions; and how to implement an interface based system controlling Delphi forms.

My initial approach was a naive, direct implementation of the architecture, The code illustrates that naive approach. The only problem is that it doesn't work! As explained by one of the answers I obtained from StackOverflow:
it doesn't allow the user to provide any input before it retrieves the content of TForm6.Edit1.Text. You basically say:
Create the form
Show it to the user
Immediately read whatever was set at designtime in the form's Edit1.Text
In addition, this basic formulation is not exactly what I wanted in terms of the architecture. The secondary form is called from the main form; I want it to be called from a unit rather than a form. It does not separate out the interfaces from the implementation in the manner I want, and if the example were to be scaled up, there would be a memory leak on each invocation of a new form.

So I started investigating and experimenting. My first exploration was the above question to StackOverflow. The preliminary response to this led me to investigating the use of modal forms rather than non-modal. I came to the conclusion that this was not an option - this has the side effect that a modal form blocks all keystrokes to the application, which blocks the application from creating new tasks. I did come up with a workaround to this particular problem. The use of modal forms also did not seem to meet my inferred requirements from the architecture I have derived.

My intended system
An grossly simplified overview of my overall system structure is:
A number of tasks can be run by the application. Each task applies a particular tool to an object. In order to obtain any needed user input the task may run one or more user interfaces (UI). These UIs are of varying complexity from a simple selection form to a multiple page wizard type interface taking a very substantial degree of information and user interaction. Each task needs the full information from its UI before it can proceed or complete. Each task may be sufficiently complex that the user may need to start another task before completing the first one, in order to get relevant information or provide updates to existing objects. The application does have its own UI, but this is minimal, providing a task/object selection only. All the work within the application is delegated to the individual tasks.

Example  
Using my example modules, one might wish to parse an XML version of a document concerning the Russian trip schedule. When selecting the document it is evident that the trip schedule needs editing. In order to correctly edit the trip schedule, the user needs to view the Moscow hotel booking.

I am currently programming one of the tasks, to prove that the overall structure is feasible, and to act as a template for subsequent development and code generation.

I am trying to produce the overall system in an extremely DRY (Don't Repeat Yourself) manner (perhaps I should call it ARID (Architectural Responsibilities In Design/Development)). As such I expect much of my eventual system to be described in domain specific languages, which I am also developing, and be produced by generators and templates which again form part of my overall development.

The UIs will be created, destroyed and managed by the task. In this respect, the manager is acting as a controller of the UI. The application needs to be able to run other tasks concurrently.

The above considerations have lead me adopt an programming approach based on interfaces, implemented by objects in Delphi - in particular, the UI as a Delphi interface, implemented by a TForm.

The point is still, though, that you have to allow the user a chance to provide input before you can retrieve it.

Other Approaches
As I continued to investigate I encountered other approaches. In no particular order they were:
  1. Use Show and have the form pass back information (via some mechanism like PostMessage or an event handler when the user clicks a button to let you know there's data to retrieve.
  2. Build reference counting into an enclosing TInterfacedObject object that includes the form.
  3. Build reference counting into a TForm descendant.
  4. Build each task as a separate process - which I understand is costly in terms of system resources and performance, on a Windows system.
  5. Have the implementation object be a wrapper to TForm and implement the event handlers for the TForm in the wrapper. Incorporate a delay loop function in the event handlers for all the relevant events.
  6. Use callback functions in the unit controlling the form.
  7. Use delegate as a keyword in the interface declaration.
Problems and obstacles
  1. Callbacks - the parameters of a callback function have to be derived from TObject rather than integers, booleans, interfaces, or other strucutured types.
  2. Callbacks - most of the resources I could find explaining callbacks used examples that had the callback function called from the code that initalized the callback. It took me a while to realise I had to split these two parts - set up the callback, run the form, and then call the callback function.
  3. Callbacks and Interface wrappers - I kept tripping over the the cyclic reference problem of Unit A needing to call Unit B which needed to call Unit A. This repeated in various ways as I experimented with callbacks.
  4. Interface wrappers - I hit an infinite regress: - if I seperated the form from its interface, then I needed a callback from the form; and if that was seperated out......
  5. Unit controller - trying to switch control of the form from another form to a unit shows that the controlling object needs to be derived from TComponent. This ensures that the TForm descendant is owned by something that will hook into the form management routines correctly.
  6. Windows processing loop - In order to use Delphi's implementation of the window's processing loop, all forms need to be ultimately owned by TApplication. This requires use of TApplication.CreateForm - either directly in Application; or indirectly via ownership by a TComponent, which is itself owned by Application (using TComponent.Create(Appliction)).
  7. TForm creation - the procedures used for communication with the form have to be  published rather than public - if they are only public the form will not display - for reasons I have not fully investigated.
My solution
My solution defines
  1. Two interfaces: IResult - defined to carry the required data from the form to UserInput; and     IUserInput - obtain user input from a form in an IResult. 
  2. Two creator functions - one for each Interface
  3. Implementation of the interfaces - one object for each interface, and one for the actual user input form.
Example code for this solution is attached. The example has not been extended to ensure that clean up is done correctly, and the form event that makes the information available is the closing of the form. In a real situation it would probably be a user triggered event.

The UserInput interface is a little odd since the sole property of the interface, UserInput, is a write only property - the intention being that property is filled by the user input; and implies that the appropriate setter needs to have been called before use is made of the interface. I am still not completely comfortable with this, though it does seem to be a solution to the issues I faced.

No comments:

Post a Comment

Post a Comment