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: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.
Create the form
Show it to the user
Immediately read whatever was set at designtime in the form's Edit1.Text
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:
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.
As I continued to investigate I encountered other approaches. In no particular order they were:
- 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.
- Build reference counting into an enclosing TInterfacedObject object that includes the form.
- Build reference counting into a TForm descendant.
- Build each task as a separate process - which I understand is costly in terms of system resources and performance, on a Windows system.
- 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.
- Use callback functions in the unit controlling the form.
- Use delegate as a keyword in the interface declaration.
- Callbacks - the parameters of a callback function have to be derived from TObject rather than integers, booleans, interfaces, or other strucutured types.
- 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.
- 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.
- 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......
- 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.
- 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)).
- 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 defines
- 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.
- Two creator functions - one for each Interface
- Implementation of the interfaces - one object for each interface, and one for the actual user input form.
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.