User Interface

<< Click to Display Table of Contents >>

Navigation:  Developer's Guide > Best Practices >

User Interface

uniGUI conveniently integrates with the RAD Studio VCL designer to leverage the knowledge of current developers, but the designer is incapable of rendering the exact user experience of the real application running in a web browser.

 

As was already mentioned before, VCL components are incompatible with uniGUI, but the uniGUI components keep several VCL properties which provides better compatibility when migrating VCL applications to uniGUI applications. However, uniGUI components render ExtJS components which are specifically designed and optimized for the web. That being said, even standard uniGUI components like TUniPanel take advantage of web features, while other components don't have an equivalent in the VCL. uniGUI components become Sencha Ext JS components running in the client browser, and Sencha is continuously improving their components and creating new components.

 

The best practice when creating the uniGUI user interface is to configure the components for the web, and to use the best components optimized for the web.

 

Let's show how to apply this best practice by making a comparison between VCL alignment and client-side alignment, and learning how to use TUniFieldSet and TUniFieldContainer.

 

VCL alignment vs web alignment

 

VCL alignment happens server-side, while web alignment executes it immediately client-side. If the developer uses a TPanel control with a TSplitter to adjust the panel width, dragging the TSplitter will create a sequence of requests to the server which will send updates to the client to render the changes. It is costly and makes the user interface less responsive.

 

VCL

 

Some of the VCL control properties are:

Height

Width

Top

Left

Align - alNone, alTop, alBottom, alLeft, alRight, alClient

 

Windows, and the VCL, which is an object oriented, component-based, encapsulation of Windows visual objects, are both based on screen physical coordinates (it is the reason for our current troubles with high resolution displays using much higher DPI).

 

The World Wide Web must cope with very different client devices, and it is based on relative positions.

 

uniGUI keeps the same VCL properties, but it also allows to override the VCL behavior and take advantage of the more powerful and flexible web properties.

 

Web

 

Alignment in Containers

 

Some of the Web properties are:

AlignmentControl - uniAlignmentClient, uniAlignmentServer

Layout - absolute, accordion, anchor, auto, border, fit, form, hbox, vbox, table, column

LayoutAttribs

oAlign - top, middle, bottom, stretch, stretchmax

oColumns

oPack - start, center, end

oPadding

LayoutConfig

oAnchor

oBodyPadding

oColSpan

oColumnWidth

oDockWhenAligned

oFlex

oHeight

oIgnorePosition

oMargin

oPadding

oRegion - north, south, east, west, center (equivalent to alTop, alBottom, alRight, alLeft, alClient)

oRowSpan

oSplit - To enable an automatic client-side splitter

oWidth

 

LayoutProperties

 

In the folder Demos there are several projects which show how to use the previous properties:

Clientside Alignment - Dock and Align

Clientside Alignment - Layout Accordion

Clientside Alignment - Layout Anchor

Clientside Alignment - Layout Border

Clientside Alignment - Layout Column

Clientside Alignment - Layout Fit

Clientside Alignment - Layout Form

Clientside Alignment - Layout HBox

Clientside Alignment - Layout Percentage

Clientside Alignment - Layout VBox

Clientside Alignment - Layout Table

Clientside Alignment - Layout Table Span

Clientside Alignment - Features Demo

 

Panels

 

UniGUI panels are also collapsible. The corresponding properties are:

Collapsed

CollapseDirection - cdBottom, cdDefault, cdLeft, cdRight, cdTop

Collapsible

 

The demo Collapsible Panels shows several collapsible panels.

 

Collapsible Panels with server-side alignment

Collapsible Panels with server-side alignment

 

This demo starts with some collapsed panels and all four of them can be collapsed or expanded using the corresponding buttons. You will notice that each collapse/expand request requires a trip to the server to render the new layout. Try changing that behavior by changing the form AlignmentControl to uniAlignmentClient and configuring the controls using the previously described web properties. Confirm that the new application executes layout changes much faster (there is no further need to ask the server for a new render).

 

FieldSets

 

TUniFieldSet

 

The uniGUI control TUniFieldSet contains fields (uniGUI controls/editors) which should be arranged in one column. This container will render each field according to common properties:

FieldLabel

FieldLabelAlign - alLeft, alRight, alTop

FieldLabelFont

FieldLabelSeparator - default ':'

FieldLabelWidth

 

Among the uniGUI controls which could be included in a TUniFieldSet is the TUniFieldContainer, which allows to create hierarchical forms like:

 

TUniFieldSet and TUniFieldContainer(s) - UniFieldContainer example

TUniFieldSet and TUniFieldContainer(s) - UniFieldContainer example

 

TUniFieldContainer

 

The container arranges the fields according to the web layout requested. In the previous example, FieldContainer3 is using layout table with 3 columns.

 

FieldContainer3

FieldContainer3

 

The TUniFieldContainer renders the fields in the order they were created. If that order is or becomes incorrect, the property CreateOrder should be used to indicate the desired order. The default value is zero, meaning that they should be the last fields. The desired order starts with one, and the fields will be saved on the .DFM file in that order.

 

Separate User Interface from Business Logic

 

Delphi is a RAD environment which encourages development speed. It is easy to create a small application just by dropping a few components on a form, changing some properties, adding a few event handlers, and implementing the real code which was the original goal of the application. In a few minutes, you could have a working application. It is true; RAD Studio makes it possible. But it is also true that most of the time, that application will not be a good foundation for a bigger project. Merging the user interface with the business logic, adding visual controls and data access components, having all kind of event handlers in the same unit is a good recipe for failure.

 

There are many solutions for disconnecting the presentation layer and the business logic. Everyone knows about MVC (Model-View-Controller), MVP (Model-View-Presenter), MVVM (Model-View-ViewModel), and similar design patterns. While some of these patterns are generic and could be implemented in several languages, development environments, and supporting frameworks, some of them are better for specific scenarios. For example, MVVM was specifically designed to take advantage of WPF (Windows Presentation Foundation), XAML, and event-driven programming.

 

Despite the Delphi RAD environment, several of its features allow achieving the previous goal, not necessarily following strict patterns.

 

Use interfaces, not forms

 

Let's start by showing how to apply one of the more important best practices: code against interfaces, not concrete classes.

 

Our program could run as a desktop application or as a Web application, but our business logic will require user interaction. If we need to ask the user for some information, we will need to use some visual artifact, but it doesn't mean that our business logic needs to know unnecessary details about it. Even better, it should be possible to write the business logic without having a concrete implementation of the artifact (form, message dialog, whatever it is).

 

It will be better to explain this principle with a simple example.

 

clip0131

 

unit _MyForm;
 
interface
 
uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;
 
type
  TMyForm = class(TForm)
    edtSomeText: TEdit;
    btnOk: TButton;
    btnCancel: TButton;
    lblSomeText: TLabel;
  private
    function GetSomeText: string;
    procedure SetSomeText(const Value: string);
  public
 
    property SomeText : string read GetSomeText write SetSomeText;
  end;
 
  function GetOrModifySomeText(var aText : string) : boolean;
 
implementation
 
{$R *.dfm}
 
function GetOrModifySomeText(var aText : string) : boolean;
var
  MyForm: TMyForm;
begin
  MyForm := TMyForm.Create(Application);
  try
    MyForm.SomeText := aText;
 
    Result := MyForm.ShowModal = mrOk;
 
    if Result then
      aText := MyForm.SomeText;
  finally
    MyForm.Free;
  end;
end;
 
{ TMyForm }
 
function TMyForm.GetSomeText: string;
begin
  Result := edtSomeText.Text;
end;
 
procedure TMyForm.SetSomeText(const Value: string);
begin
  edtSomeText.Text := Value;
end;
 
end.

 

The business logic only needs access to a function GetOrModifySomeText which will return true if the text passed as parameter was modified. It doesn't need to know how that is done. If the only way of using the function is to make an explicit reference to the unit implementing the business logic, we will be adding a dependency to a VCL form and, even worse, to a particular implementation of it.

 

The same form, this time implemented as a uniGUI free form, requires a similar code.

 

unit _MyForm;
 
interface
 
uses
  System.Classes,
  Vcl.Controls,
  Vcl.Forms,
  uniGUIBaseClasses,
  uniGUIClasses,
  uniLabel,
  uniGUITypes,
  uniGUIAbstractClasses,
  uniGUIForm,
  uniGUIApplication,
  uniEdit,
  uniButton;
 
type
  TMyForm = class(TUniForm)
    edtSomeText: TUniEdit;
    btnOk: TUniButton;
    btnCancel: TUniButton;
    lblSomeText: TUniLabel;
  private
    function GetSomeText: string;
    procedure SetSomeText(const Value: string);
  public
 
    property SomeText : string read GetSomeText write SetSomeText;
  end;
 
  function GetOrModifySomeText(var aText : string) : boolean;
 
implementation
 
{$R *.dfm}
 
function GetOrModifySomeText(var aText : string) : boolean;
var
  MyForm: TMyForm;
begin
  MyForm := TMyForm.Create(UniGUIApplication.UniApplication);
  MyForm.SomeText := aText;
 
  Result := (MyForm.ShowModal = mrOk);  // Synchronized mode
 
  if Result then
    aText := MyForm.SomeText;
end;
 
{ TMyForm }
 
function TMyForm.GetSomeText: string;
begin
  Result := edtSomeText.Text;
end;
 
procedure TMyForm.SetSomeText(const Value: string);
begin
  edtSomeText.Text := Value;
end;
 
end.

 

The only difference here is that we don't need to explicitly release the uniGUI form.

 

Now, what about being able to write the business logic without having a concrete implementation of the form, or even better, how to target both VCL and uniGUI with the same code?

 

Let's do some refactoring.

 

Extract an interface exposing the form behavior

 

type
 
  IMyForm = interface
    ['{80CDB092-DDA0-49A0-9FFF-F8D69E18777C}']
 
    function GetSomeText: string;
    procedure SetSomeText(const Value: string);
    function _ShowModal : integer;
 
    property SomeText : string read GetSomeText write SetSomeText;
  end;

 

Modify both forms to implement the interface

 

unit _MyForm;
 
interface
 
uses
  System.Classes,
  Vcl.Controls,
  Vcl.Forms,
  uniGUIBaseClasses,
  uniGUIClasses,
  uniLabel,
  uniGUITypes,
  uniGUIAbstractClasses,
  uniGUIForm,
  uniGUIApplication,
  uniEdit,
  uniButton,
  _MyFormIntf;
 
type
 
  TMyForm = class(TUniForm, IMyForm)
    edtSomeText: TUniEdit;
    btnOk: TUniButton;
    btnCancel: TUniButton;
    lblSomeText: TUniLabel;
  private
    function GetSomeText: string;
    procedure SetSomeText(const Value: string);
    function _ShowModal : integer;
  public
 
  end;
 
implementation
 
{$R *.dfm}
 
{ TMyForm }
 
function TMyForm.GetSomeText: string;
begin
  Result := edtSomeText.Text;
end;
 
procedure TMyForm.SetSomeText(const Value: string);
begin
  edtSomeText.Text := Value;
end;
 
function TMyForm._ShowModal : integer;

begin

  Result := ShowModal;

end;

 
end.

 

Don't reference the forms, but the interface

 

function GetOrModifySomeText(MyForm: IMyForm; var aText : string) : boolean;
begin
  MyForm.SomeText := aText;
 
  Result := MyForm._ShowModal = mrOk;
 
  if Result then
    aText := MyForm.SomeText;
end;

 

Once the form was created, it can be passed around as the interface it implements. Notice that we added _ShowModal to the interface because ShowModal is different for VCL and uniGUI.

 

Don't access forms, except for creating and releasing instances

 

A Delphi form is a "view" or one of the possible implementations of the presentation layer. It should always implement one or several interfaces to clarify its purpose. Any access to the form should be done by using one of the interfaces it implements, hiding any implementation detail and avoiding future errors when modifying internal resources.

 

Accessing forms through interfaces avoids breaking the code using the form when the form changes. It also opens the possibility of creating different forms/views implementing the same interface. For example:

The application could share most of the business logic while targeting the Delphi VCL desktop and Delphi uniGUI (also desktop-like).

A web application built with uniGUI could provide two different user interfaces, one for desktop users (having mouse and keyboard), other for touch devices (phones and tablets).

 

Never introduce business logic code in forms, take advantage of Delphi features

 

Every line of code expressing business logic and embedded in a form creates an unnecessary dependency and impedes creating new views. A typical web application provides access to a database server through a business logic layer. The equivalent Delphi objects are forms, data modules, optional business classes, and the database connections.

 

Let's analyze how those Delphi objects relate to the components of the MVC pattern.

 

Model View Controller

Model View Controller

 

A Delphi implementation of MVC is something like this:

The View is a TForm, TUniForm, TUnimForm.

oIt is created at design-time and rendered at run-time.

oSome properties could be updated through its interface (still a good behavior)

oMost of the updates will be automatically propagated through the database components (connected during creating)

oAny user action could trigger database-related updates (perfect), while others could trigger actions (also good). Other acceptable good behavior could be to execute required/expected actions like creating its own data module and releasing it.

The Controller usually is the data module used by the form, and most of the time it contains part of all the Model (at least, the database components).

oIn a small application, Controller and Model will be implemented in the MainModule.

oBigger applications could use a data module for some forms, in addition to the MainModule.

 

The Model-View-Presenter pattern suggests something similar.

 

Model View Presenter

Model View Presenter

 

In this pattern, the Presenter is the mechanism implemented in VCL or uniGUI for handling the "communication" between the Model (data module and business classes) and the View (form). It is worth mentioning that this View is passive (or as passive as possible), which means that it won't include any business logic.

 

In the next section we will show how to build an application targeting both uniGUI platforms (desktop and touch), but it is important to list the Delphi features that application will use.

 

Database controls and its events

 

As the goal is to keep the business logic code out of the View, we will never use data access components in a form. Instead, all these components will be hosted by data modules. In a VCL application, that data module will be automatically created and assigned to the global variable. In uniGUI the approach is different, as the MainModule will be accessible through the function uniMainModule. In both cases, any form can link its data access controls at design-time (to the corresponding data sources and datasets).

 

Following the principle of consuming resources when needed, always on-demand, each form requiring database access should announce/request the data module to activate/deactive its resources (that is, open and close datasets). These actions should be done in OnCreate and OnDestroy.

 

In bigger applications, it could be necessary to create and destroy any additional data module used by the form.

 

In any case, by hosting the data access controls in the data module, any event will be implemented in the same place (or it could ask some business class defined out of the data module).

 

By following these practices, no business logic is added to the View. As we will mention later in this chapter, it is even possible to inject these minimal dependencies.

 

Requesting actions from the View without writing code (platform-independent)

 

What about using a menu (MainMenu or PopupMenu), pressing a button for requesting a global action or an action focused on some selected dataset record?

 

In a data-centric application, most of the requested actions affect the active records or some selection of records. Other actions are global and could be requested in several places in the user interface.

 

Delphi supports actions and action lists for a long time. Each button in a toolbar can be linked to an action. Menu items from any menu can also be linked to actions. Other buttons share that capability. It the action list is hosted by the same data module, every time the user triggers an action, the action's Execute method will be able to query the context (for example, the selected record of a dataset) and implement the requested business logic.

 

Of course, some actions could require interacting with the user through the same user interface it came from. While using interfaces allows the developer to access a View without knowing how it is implemented or the platform it is targeting, in this case we will need to instantiate the View for the current platform.

 

Let's see two possible solutions to the previous question.

 

Create an interface as a facade to platform-dependent services

 

When the VCL application or uniGUI session starts, whatever is the current platform can be saved and used later for instantiating the correct Views. Of course, the VCL application will always be VCL, but a uniGUI application could be targeting two platforms: desktop and touch. If the user session started from a touch device, any further interaction would rely on touch forms and messages.

 

The easiest solution for small projects is to define an interface for all the "services" (forms and messaging) required by the business logic, implement them for both targets, and instantiate the correct one according to the user session. After that, every time a form is needed, the concrete implementation, available through the interface, will be used.

 

Use Dependency Injection for registering multiple implementations of the same form interface

 

Big projects can have hundreds or even thousands of forms, making the previous solution tedious, time-consuming, and error-prone. The correct solution is to leave that task to a Dependency Injection framework like Spring4D.

 

It is out of the scope of this documentation, but we will show how to modify our demo to do it.