Working with Continuous Forms at Run Time

Working with Continuous Forms at Run Time

Continuous forms have certain characteristics that require special attention on the part of the developer. In a continuous form, a band may be repeated many times when the form is generated at run time, depending on the PageSize property and the number of records in the form's data source. Code in event handlers needs to be aware not only of the control that triggered the event, but also of the specific band instance in which the event occurred. Another consideration relates to the way that data binding is implemented. With continuous forms, field values are written directly into the form's HTML code. Navigating to another page of records (for example, by clicking on the Next Page button in the form's navigation bar) requires that the form's HTML code be reloaded, which can have implications for code which modifies the form's variables or controls. This article seeks to cover the most important aspects of programming in the context of continuous forms.



Working with Band Instances in Event Handlers

Continuous forms, like all Morfik forms, are comprised of bands. At design-time, a form generally has a header, a footer and a detail band. In databound forms, additional bands can be added to allow records to be grouped by field value. The header and footer bands, if present, are included in the generated form exactly once (at the top and bottom of the form). The detail and group header/footer bands, however, are repeated as many times as required, depending on the form's PageSize property and the number of records in the form's data source. This one-to-many relationship between design-time bands and run-time band instances necessitates the concept of BandIndex. For a control which resides in a repeated band, the BandIndex property indicates which instance of the control is being referred to. BandIndex=0 refers to the control in the first instance of the band, BandIndex=1 refers to the control in the second instance of the band, and so on.


Example 1 - Using BandIndex in an OnClick event to target another control in the same band instance

Let's look at the BookCollector project. The form 'frmBookAlbum' is a continuous form with Page Size of 2 and Column Count of 2. At design-time it looks like this:


File:CtsFormsRunTimeFig1.jpg


At run time two instances of the Detail band are displayed (provided there are sufficient records). Because it is a multi-column form, the two detail band instances are displayed side-by-side:


File:CtsFormsRunTimeFig2.jpg


Suppose we want to display the book's price in an alert dialog when the user clicks on its image. In the On Click event of Image1 (the Image control which displays the book's cover), we first set the BandIndex property of PriceLabel (the TextLabel which contains the book's price) to be the same as that of Image1. This ensures that we are referring to the instance of PriceLabel residing in the same band as the image clicked on by the user. PriceLabel's Caption property can then be accessed as normal:

FX Code

Procedure frmBookAlbum.Image1Click(Event: TDOMEvent);
Begin
    PriceLabel.BandIndex := Image1.BandIndex;
    ShowMessage('This book costs ' + PriceLabel.Caption);
End;


The same technique is applicable when working with controls in group header/footer bands. Suppose that in frmBookAlbum, we want to group the records by the GenreName field. We make the following settings in the Sorting and Grouping dialog:


File:CtsFormsRunTimeFig3.jpg


We then place the following controls on the GenreName header band: a textlabel GenreNameLabel with DataField=GenreName, and a Button Button1.


File:CtsFormsRunTimeFig4.jpg


Suppose we want to display the book's Genre in an alert dialog when the user clicks on Button1. This is accomplished by adding the following code to Button1's OnClick event:

FX Code

Procedure frmBookAlbum.Button1Click(Event: TDOMEvent);
Begin
    GenreNameLabel.BandIndex := Button1.BandIndex;
    ShowMessage(GenreNameLabel.Caption);
End;


Example 2 - Using BandIndex to update a control in all band instances

Another situation where BandIndex is often used is when the developer wants to perform a given operation on a control in all run-time band instances. An example of this can be seen in the implementation of the fade-in effect in frmBookAlbum of the BookCollector example. The form's On Before Show event contains the following code:

FX Code

Procedure frmBookAlbum.WebFormBeforeShow(Var Show: Boolean);
Var
    i : Integer;
Begin
    GetIndex.Button1.Down := True;
    SetCtrlOpacity(Container1, 0);
    Timer1.Enabled := True;
End;


The first line selects Button1 (the Home "tab") in frmBookAlbum's owner form 'Index'. The second line contains the function call SetCtrlOpacity(Container1, 0), which hides all controls on Container control Container1. (The function SetCtrlOpacity is explained below.) The third line starts the Timer 'Timer1'. Timer1's On Timer event contains the following code:

FX Code

Procedure frmBookAlbum.Timer1Timer(Event: TDOMEvent);
Begin
    SetCtrlOpacity(Container1, Container1.Attrs.Opacity + 10);
    Timer1.Enabled := Container1.Attrs.Opacity < 100;
End;


Each time this event fires, the opacity of Container1 is increased by 10%. When 100% is reached, the controls on Container1 are fully visible, and Timer1 is disabled. Finally we come to the implementation of the SetCtrlOpacity function:

FX Code

Procedure frmBookAlbum.SetCtrlOpacity(Ctrl : TWebControl; O : Integer);
Var
    i     : Integer;
    Style : THTML_CSSStyleDeclarationExt;
Begin
    If Ctrl = Nil Then Exit;
    If O > 100 Then O := 100 Else
    If O < 0   Then O := 0;
 
    Ctrl.Attrs.Opacity := O;
 
    For i := 0 To Ctrl.DOMHandleArray.Handles.Count - 1 Do
    Begin
        Style := Ctrl.DOMHandleArray.Handle[i].Style;
        If Style <> Nil Then
        Begin
            Style.Opacity    := FloatToStr(Ctrl.Attrs.Opacity / 100);
            Style.MozOpacity := FloatToStr(Ctrl.Attrs.Opacity / 100);
            If (Browser.EngineID = 'MS') Then
                Style.Filter :=  'alpha(opacity=' + IntToStr(Ctrl.Attrs.Opacity) + ')';
        End;
    End;
End;


The first few lines perform some basic validation on the arguments passed to the function, and then assign Ctrl.Attrs.Opacity the specified value. The For loop is what actually effects the change to the control's appearance. DOMHandleArray contains a list of "handles" or pointers to the browser's DOM data structure for each run-time instance of the control. By applying the opacity change to each of these handles in a loop, the appearance of Container1 in every run-time band instance is updated. Because we are working directly with the browser's DOM data structures at this point, which vary slightly from one browser vendor to another, it is necessary in this example to update three different properties: Style.Opacity, Style.MozOpacity and Style.Filter.


Example 3 - Highlighting alternate rows of data in different colors

One technique that is sometimes used to improve readability in lists is to highlight alternate rows in different colors. Suppose we have a continuous databound form 'Form1'. The desired effect could be achieved by attaching the following code to the Detail band's server-side On Before Print event:

FX Code

Procedure Form1.DetailBeforePrint(Sender: TWebControl; Canvas: TWebCanvas; Var Print: Boolean);
Var
    S : String;
Begin
    If LineNo Mod 2 = 1 Then
        Theme_GetThemedValue(ThemeName, '#THEME#Colors.Color1.1', S)
    Else
        Theme_GetThemedValue(ThemeName, '#THEME#Colors.Color1.2', S);
    Sender.Color := S.ToInteger();
End;


In server-side code the BandIndex property is not available. Instead, LineNo, a method of the form class, is used. It returns the zero-based index of the detail band instance that is currently being "printed" or processed. The If statement above results in the temporary variable S being assigned either the theme color 1.1 (if LineNo is odd) or theme color 1.2 (if LineNo is even). The final line then assigns the color stored in S to the Color property of Sender (Sender is a pointer to the control that triggered the event; in this case, the detail band).



Identifying the "source" control of an event

Sometimes when writing an event handler it is necessary to be able to identify the control that triggered the event. This is particularly useful if the developer wants to be able to use the same event handler code for multiple controls.


Example 4 - Hiding a control when its DataField is null

Let's suppose that we have a continuous databound form 'Form1'. In the data source of Form1, certain fields can be NULL, in which case we do not want to display the associated control at run time. This could be accomplished by linking the following server-side code to the OnBeforePrint event of all affected controls:

FX Code

Procedure Form1.ControlBeforePrint(Sender: TWebControl; Canvas: TWebCanvas; Var Print: Boolean);
Begin
    Print := Not Sender.DBField.IsNull;
End;


The OnBeforePrint event is called immediately before the control's HTML code is generated. The Sender parameter points to the control whose HTML code is being generated ("printed"). The Print parameter is true by default, but can be set to False to suppress the output of HTML code for the control.


Example 5 - Implementing a custom tab control interface

Let's look at the MorfikCRM project, which is available for download from the Morfik website. In the footer band of the form 'frmMain' is a row of four textlabels with captions 'Contacts', 'Opportunities', 'Assets' and 'Notes'. Clicking on any of these textlabels displays relevant content in the space underneath the textlabel. At run time the XApp looks like this:

File:CtsFormsRunTimeFig5.jpg


The four textlabels reside on a container 'Container1'. Concealed underneath Container1 are the tabs of a TabControl 'TabControl1'. The four TabSheets of TabControl1 each contain a SubForm which displays the appropriate content. All four textlabels have their OnClick event bound to the following event handler:

FX Code

Procedure frmMain.lblHeaderClick(Event: TDOMEvent);
Var
    C : TWebControl;
    i : Integer;
Begin
    For i := 0 To Container1.ChildCount - 1 Do
        TextLabel(Container1.ChildCtrls[i]).FontStyle := [fsBold];
 
    C := GetEventSource(Event);
    C.FontStyle := [fsBold,fsUnderline];
 
    TabControl1.ActivePage := C.Caption + 'TabSheet';
End;


The For loop sets the FontStyle of all four textlabels to [fsBold]. In the following line, the function call GetEventSource(Event) retrieves the textlabel that the user clicked on. This textlabel then has its FontStyle property set to [fsBold,fsUnderline]. This results in the effect seen in the above screenshot, where the textlabel corresponding to the active tab has its caption underlined. Finally, the ActivePage property of TabControl1 is set to the appropriate tabsheet, bringing the appropriate content to the foreground.



Data Navigation

The simplest way of allowing the user to navigate through the records displayed in a form is to add a navigation bar. This may be done by enabling the Navigation Bar property of the header or footer bands of the form at design time. However, it is also possible to perform data navigation operations in browser-side code. It is important to understand that with continuous forms, the field values are included in the form's HTML code, rather than being sent in a separate HTTP request as with single forms. This means that the form's HTML code must be downloaded again when the user navigates to another set or "page" of records (for example, by clicking on the Next Page button in the navigation bar). The developer must be aware that modifications made to the form's variables do not persist when the user switches to a new data page.


Example 6 - Implementing a custom navigation bar

Suppose that we want to use our own buttons for data navigation instead of using the built-in navigation bar. This could be for a number of reasons: perhaps we want to restrict the operations that the user can perform, or perhaps we simply want greater control over the form's appearance than can be achieved using the standard navigation bar. We begin by placing buttons FirstPageButton, PreviousPageButton, NextPageButton, LastPageButton and RefreshButton on the form. The OnClick event handler of each of these buttons then needs to be linked to the corresponding event handler in the following code listing:

FX Code

Procedure Form1.FirstPageButtonClick(Event: TDOMEvent);
Begin
    FirstPage(Event);
End;
 
Procedure Form1.PreviousPageButtonClick(Event: TDOMEvent);
Begin
    PreviousPage(Event);
End;
 
Procedure Form1.NextPageButtonClick(Event: TDOMEvent);
Begin
    NextPage(Event);
End;
 
Procedure Form1.LastPageButtonClick(Event: TDOMEvent);
Begin
    LastPage(Event);
End;
 
Procedure Form1.RefreshButtonClick(Event: TDOMEvent);
Begin
    RefreshPage(Event);
End;


The methods FirstPage, PreviousPage, NextPage, LastPage and RefreshPage called in the above event handlers are standard methods of the form class. Next we add a textlabel CurrentPageLabel to the form, to allow the user to see where he/she is in the dataset. Finally, we add the following code to the form's OnReady event:

FX Code

Procedure Form1.WebFormReady(Var Ready: Boolean);
Var
    CurrentPage, PageCount     : Integer;
Begin
    PageCount                  := Ceil(RecordCount    / PageSize);
    CurrentPage                := Ceil(StartingOffset / PageSize);
    CurrentPageLabel  .Caption := CurrentPage.ToString();
 
    NextPageButton    .Enabled := CurrentPage < PageCount - 1;
    PreviousPageButton.Enabled := CurrentPage > 0;
End;


The first three lines calculate the total number of data pages and the index of the current data page, which is then displayed in the relevant textlabels. The last two lines ensure that the next/previous page button is greyed out if there is no next/previous data page to navigate to.

Example 7 - Implementing an option to hide images in the BookCollector project

Let's suppose we want to give the user the option to hide the images of the book covers in form frmBookAlbum of the BookCollector sample project.

First we add a Checkbox ShowImagesCheckBox to frmBookAlbum's header band, and set its Checked property to True. We then put the following code in its OnClick event:

FX Code

Procedure frmBookAlbum.ShowImagesCheckBoxClick(Event: TDOMEvent);
Begin
    SetImageVisibility(ShowImagesCheckBox.Checked);
End;


The function SetImageVisibility uses a For loop to set the Visible property of Image1 in all run-time band instances:

FX Code

Procedure frmBookAlbum.SetImageVisibility(Value : Boolean);
Var
    i : Integer;
Begin
    For i := 0 To Image1.DomHandleArray.Count - 1 Do
    Begin
        Image1.BandIndex := i;
        Image1.Visible   := Value;
    End;
End;


Unchecking the ShowImagesCheckBox at run time hides the image controls. However, as soon as the user navigates to a new data page using the << or >> buttons in the Index form, the checkbox will be reset to its original (checked) state, and the images will be displayed again. In order to have the state of the show/hide images option persist from one data page to the next, we need to use a form parameter. We open the form Parameters Dialog, and add a parameter DisplayThumbnails, with a default value of True. In the form's OnReady event, we add the following code:

FX Code

Procedure frmBookAlbum.WebFormReady(Var Ready: Boolean);
Begin
    ShowImagesCheckBox.Checked := DisplayThumbnails.ToBoolean();
    SetImageVisibility(DisplayThumbnails.ToBoolean());
End;


This sets ShowImagesCheckBox.Checked according to the value of the DisplayThumbnails parameter, and then shows/hides the images of the book covers accordingly.

Finally, we need to add one more line to ShowImagesCheckBoxClick to save the state of the checkbox into the DisplayThumbnails parameter:

FX Code

Procedure frmBookAlbum.ShowImagesCheckBoxClick(Event: TDOMEvent);
Begin
    SetImageVisibility(ShowImagesCheckBox.Checked);
    DisplayThumbnails := ShowImagesCheckBox.Checked.ToString();
End;