Even if the new application is the result of a migration from a current VCL application which is already multi-user and client/server, a uniGUI application will serve multiple users within the same process.
A multi-user client/server VCL application usually has multiple instances of the client application running on several computers, each one of them connects to a shared database server. Almost all database servers support multiple connections by implementing row/table locking when needed.
That kind of VCL application is a typical 2-tier application. A similar uniGUI application is a 3-tier application, because the user interface (presentation layer) runs in the client browser, most of the previous application code runs on the server (now serving multiple users/sessions), and it shares the same database server.
The fact that the new non-visual code of the uniGUI application serves multiple users requires handling concurrent access to any shared resources. This concurrent access will occur in a multi-threaded manner where each request comes from a different thread.
ServerModule
Use the ServerModule singleton for global and probably read/only information.
Most web applications require access to a database server. Sometimes it will use data access controls like TFDConnection, but it is also possible to use some middleware. In any case, the information about how to connect to the database server should be available to all user sessions. If using a TFDConnection or similar, the program will need to build the correct connection string or instantiate the corresponding components.
As mentioned previously, if using FireDAC we should add the TFDManager, the driver for the chosen database, etc. If using a TADOConnection it is enough to have the connection string.
After creating the ServerModule, it should only expose read/only properties to the user sessions.
As the ServerModule is a singleton, created when the server starts, introducing read/write variables will block the use of any load balancing technology. uniGUI plans to release its Load Balance Server and it assumes no affinity between users and servers. If a session needs to be restarted, it should be able to run in any available server. Any read/write variable with the right to be shared on the ServerModule should be moved to the shared database (which should take care of the concurrent access by itself).
If, despite the previous warning, there is some very important reason to expose here a read/write property, it must be protected by making sure that only one session at a time can modify it. The obvious solution for this kind of protection is to use a critical section.
In this case, the connection string was assigned when creating the ServerModule, and it is a read/only property. However, the SharedVariable is a read/write property, and we must make sure that only one user can write to it. We are enforcing the single access to the variable by using a critical section. Even more, we are hiding this fact in the SetSharedVariable method so that it is not necessary to know, acquire, nor release the TCriticalSection object when accessing the property. Of course, by allowing to modify that property, we also need to control the readings. Any read/write property will slow down the application (depending on how frequently it accesses the variable).
It is important to note that while you can use writable shared variables in ServerModule as defined above, it is not recommended to rely on it as an application-wide global variable. In future each uniGUI application can be divided into many several smaller processes to implement load-balancing and other advanced feature. In this case any shared global variable in ServerModule will be visible to its owner process only, not to the other processes which will serve the same application in a pool of processes. For this, it is recommended to keep such global variables in a database table instead of keeping them in memory.
Do not rely on writable shared variables in ServerModule for application-wide state. Store such global variables in a shared database table so they remain visible across processes in a load-balanced environment.
MainModule
Use the MainModule for user session variables.
Using RAD Studio for Delphi development generates correct code for a single-user VCL application, but some of that code will create issues in a multi-user or multithreaded environment. Below are examples showing why it will fail and how to fix it.
After asking to create a new VCL form we get this code:
Then another unit might use that global variable like this:
Form1 is accessible to any user session, and several users could be using it. Imagine what will happen if two users create the same variable (causing a memory leak of one form and an invalid reference when releasing the surviving form).
2
Corrected usage (no global variables)
A simple fix avoids that situation:
In uniGUI we don't need to keep an instance of the form in a global variable. Delete all automatically generated global variables and create your own private instance. This is a good habit for VCL applications and it requires fixing forms and data modules.
Example of an auto-generated data module with a global variable (don't use):
uniGUI is more than just a set of components for rendering the presentation layer; it is also a framework supporting the best practices for web application development. Among the supported best practices is the absence of automatically generated global variables and the automatic memory management of forms and data modules attached to a user session.
Instead of the global variable Delphi creates for forms and data modules, uniGUI creates functions which return the session instance based on the previously registered type. Let's examine MainModule as a typical example.
Notice the call to RegisterMainModuleClass during initialization. When needing access to the session's MainModule, it is enough to use the function UniMainModule. Another session function, UniApplication, will return the instance of the registered MainModule corresponding to the current session.
The same kind of solution applies to session forms and modules. If we use the uniGUI Wizard for creating a new form, and select Application Form, we will get this:
uniGUI also manages the lifetime of the form. Example of usage from a main form:
In the previous VCL code we needed to create the temporary instance of the form, open it using ShowModal, and release it (we used try ... finally to avoid a memory leak). This code only needs a call: UniForm1.ShowModal. Why?
UniForm1 creates a temporary form which is automatically released upon reaching the end of its lifetime, but it will be available until that moment.
What about showing a non-modal form? uniGUI forms are automatically released when closed.
If you need just a few data modules, you can use the MainModule for sharing your connection and other session variables, and create these data modules as "Application Data Modules". They will be attached to the session and managed by uniGUI. The code is very similar to the previous form.
If you have many forms or the data modules consume many resources, any form could handle its own data module manually by creating it on FormCreate and releasing it on FormDestroy. Still, by creating the data module as a uniGUI data module, no global variable will be created, it won't be necessary to create it in FormCreate, and if the developer forgets to release the data module in FormDestroy, it will be automatically released when closing the user session.
As the MainModule is automatically created for each new user, all variables attached to a session should be accessible through it. Never use global variables.
Free Forms and DataModules
In uniGUI you can also create free forms and datamodules. They are referred as free because their lifetime is managed by the developer, not the framework.
Once the MainModule exists, uniGUI runtime will check if a login form was registered. You can create the login form using the uniGUI Wizard for creating forms and selecting "Login Form". The only difference with any other application form is that it inherits from TUniLoginForm and it will be automatically displayed when a new session is started.
When implementing the functionality of this form, it is important to remember that it can only use the ServerModule, DataModule, and any other temporary form using them, but not the MainForm. In fact, no form should ever access the MainForm, but the MainModule.
Using Global Form and DataModule references
In uniGUI Application Forms, Application DataModules and MainModule are referenced with global functions which return an instance of that object for the current session.
For forms, this function replaces the standard variable created in VCL applications. It allows keeping the same syntax for accessing the forms:
UniForm1.Show;
Calling the UniForm1 function will create an instance of TUniForm1 or return the existing instance (making it a singleton per session). It can be used to easily create and display forms. One important detail is that these functions should not be used as global references for those forms or data modules. It is not recommended to call a form's global function from other forms or data modules for any purpose other than displaying that form.
The uniGUI framework manages the lifetime of all previous "application" objects (forms and datamodules). Each uniGUI session/MainModule keeps a list of managed forms and datamodules. A call like UniMainModule.GetFormInstance(TUniForm1) asks for a singleton of that specific form (TUniForm1). If that instance doesn't exist, it will be created. If the instance was already created, it will be returned.
In addition to the singleton factories, uniGUI also tries to optimize memory usage. The application datamodules will be automatically released when closing the session (because there is no way to know if they are in use), but forms are released as soon as they are not in use. For example, after exiting from a call to ShowModal, and receiving the result, that form can be safely released.
Using functions for accessing a form instead of global variables provides two advantages:
Each session has access to its own form, and only one (like a singleton per session).
Application forms are managed by the uniGUI framework, becoming on-demand resources.
unit Unit1;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants,
System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs;
type
TForm1 = class(TForm)
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
end.
unit Unit2;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants,
System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs,
Vcl.StdCtrls;
type
TForm2 = class(TForm)
Button1: TButton;
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form2: TForm2;
implementation
uses Unit1;
{$R *.dfm}
procedure TForm2.Button1Click(Sender: TObject);
begin
Form1 := TForm1.Create(Self);
try
Form1.ShowModal;
finally
Form1.Free;
end;
end;
end.
procedure TForm2.Button1Click(Sender: TObject);
begin
with TForm1.Create(Self) do
try
ShowModal;
finally
Free;
end;
end;
unit Unit3;
interface
uses
System.SysUtils, System.Classes;
type
TDataModule1 = class(TDataModule)
private
{ Private declarations }
public
{ Public declarations }
end;
// this is a global variable and can not be used in uniGUI
var
DataModule1: TDataModule1;
implementation
{$R *.dfm}
end.
unit MainModule;
interface
uses
uniGUIMainModule, SysUtils, Classes;
type
TUniMainModule = class(TUniGUIMainModule)
private
{ Private declarations }
public
{ Public declarations }
end;
function UniMainModule: TUniMainModule;
implementation
{$R *.dfm}
uses
UniGUIVars, ServerModule, uniGUIApplication;
function UniMainModule: TUniMainModule;
begin
// returns the correct instance of MainModule for current session
Result := TUniMainModule(UniApplication.UniMainModule)
end;
initialization
RegisterMainModuleClass(TUniMainModule);
end.
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics,
Controls, Forms, uniGUITypes, uniGUIAbstractClasses,
uniGUIClasses, uniGUIForm;
type
TUniForm1 = class(TUniForm)
private
{ Private declarations }
public
{ Public declarations }
end;
function UniForm1: TUniForm1;
implementation
{$R *.dfm}
uses
MainModule, uniGUIApplication;
function UniForm1: TUniForm1;
begin
// returns the correct instance of UniForm1 for current session
Result := TUniForm1(UniMainModule.GetFormInstance(TUniForm1));
end;
end.
unit Main;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics,
Controls, Forms, uniGUITypes, uniGUIAbstractClasses,
uniGUIClasses, uniGUIRegClasses, uniGUIForm, uniGUIBaseClasses, uniButton;
type
TMainForm = class(TUniForm)
btnShow: TUniButton;
btnShowModal: TUniButton;
procedure btnShowClick(Sender: TObject);
procedure btnShowModalClick(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
function MainForm: TMainForm;
implementation
{$R *.dfm}
uses
uniGUIVars, MainModule, uniGUIApplication,
uniGUIDialogs,
Unit1;
function MainForm: TMainForm;
begin
// returns the correct instance of MainForm for current session
Result := TMainForm(UniMainModule.GetFormInstance(TMainForm));
end;
procedure TMainForm.btnShowClick(Sender: TObject);
begin
UniForm1.Show;
end;
procedure TMainForm.btnShowModalClick(Sender: TObject);
begin
UniForm1.ShowModal; // Synchronous mode
ShowMessage('Result = ' + IntToStr(UniForm1.ModalResult));
end;
initialization
RegisterAppFormClass(TMainForm);
end.
unit Unit2;
interface
uses
SysUtils, Classes;
type
TDataModule2 = class(TDataModule)
private
{ Private declarations }
public
{ Public declarations }
end;
function DataModule2: TDataModule2;
implementation
{$R *.dfm}
uses
UniGUIVars, uniGUIMainModule, MainModule;
function DataModule2: TDataModule2;
begin
// returns the correct instance of DataModule2 for current session
Result := TDataModule2(UniMainModule.GetModuleInstance(TDataModule2));
end;
initialization
RegisterModuleClass(TDataModule2);
end.
unit LoginFormUnit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics,
Controls, Forms, uniGUITypes, uniGUIAbstractClasses,
uniGUIClasses, uniGUIRegClasses, uniGUIForm;
type
TUniLoginForm1 = class(TUniLoginForm)
private
{ Private declarations }
public
{ Public declarations }
end;
function UniLoginForm1: TUniLoginForm1;
implementation
{$R *.dfm}
uses
uniGUIVars, MainModule, uniGUIApplication;
function UniLoginForm1: TUniLoginForm1;
begin
Result := TUniLoginForm1(UniMainModule.GetFormInstance(TUniLoginForm1));
end;
initialization
RegisterAppFormClass(TUniLoginForm1);
end.