Interacting with Forms in Delphi – Part 4: Initialization Methods

In the previous post, we examined four traditional ways of initializing objects such as forms in Delphi. There are two more approaches we’ll examine that aren’t used as much but result in much less coupling than the others: initialization methods, and call-backs. In this installment we’ll look at the former, and in the next we’ll look at the latter.

Remember that we’re discussing how to initialize objects here, including forms. It’s not always the case that you have data available in an easily accessible database table, so we’re doing this manually, which is not all that uncommon. In fact, I maintained a rather large application a while back that used a NOSQL database where we had to do the data queries outside the forms and pump all of the data into and out of the forms exactly like this, which is where I got the inspiration for this series of articles. In that code, I found virtually all of these approaches used to interact with their forms; there was not a lot of consistency throughout the project. (Actually, this is all leading somewhere inasmuch as I hope to present a simplified way of interacting with forms that can reduce the amount of “plumbing” code required to get data in and out of them.)

One of the problems with Constructor Injection is that you can easily end up with an excessively long list of arguments to the Constructor. To solve this, I suggested something I called a conveyance object and Seeman calls a Facade Service. While there’s nothing wrong with such an approach, form initialization can often represent a distinct pattern from the initialization of other kinds of objects because you often have a bunch of data that needs to get loaded into the form when it’s created, whereas most typical objects only need to have one set of data loaded in. That’s what we’re dealing with here.

When you’ve got a bunch of data, property injection isn’t any better. You essentially want to dump the data into some kind of structured list and hand the whole thing over to the form with one reference — hence the term “conveyance object”. In that case, Constructor Injection starts to look feasible. There’s one drawback with this: you immediately become dependent on the type of object being passed into the constructor. For example, in Delphi, it’s common for someone to grab a TStringList to hold the data we want. Then the constructor might look like this:

  constructor Create( AOwner : TComponent; AInitData : TStringList ); reintroduce;

Of course, the recommended way to deal with this is to use an Interface specification, rather than a class. But TStringList has no corresponding IStringList, or even IStrings definition. Which means we’ll need to find something else. There’s still the problem that we’re breaking the inheritance heirarchy with a constructor that requires a ‘reintroduce‘ tag on it.

IMPLEMENTATION DETAILS

Let’s go back to the initial selection form we’re using for selecting an NFL team.

2014-08-13_23-25-37

In the current example, we note that each of the Items in a TRadioGroup has a corresponding slot in the Objects[] property where we can save an object (derived from TObject) related to it. So we’re creating a new TStringList and saving it to the Objects[i] property for each of the Items[i] League buttons. Then we put the team names for each league in that league’s corresponding stringlist. (League_rgrp.Items is just another TStrings object as well, so we can use IndexOf to match the League name with the corresponding radio button.) If we wanted to define the League names dynamically, we could easily do that here by adding a new TRadioButton item to the League_rgrp if the name wasn’t found.

We need to initialize this form with the following list of data:

*:AFC-NORTH
Baltimore Ravens:BAL
Cincinatti Bengals:CIN
Cleveland Browns:CLE
Pittsburgh Steelers:PIT
*:NFC-NORTH
Chicago Bears:CHI
Detroit Lions:DET
Green Bay Packers:GB
Minnesota Vikings:MIN
*:AFC-SOUTH
Houston Texans:HOU
Indianapolis Colts:IND
Jacksonville Jaguars:JAX
Tennessee Titans:TEN
*:NFC-SOUTH
Atlanta Falcons:ATL
Carolina Panthers:CAR
New Orleans Saints:NO
Tampa Bay Buccaneers:TB
*:AFC-EAST
Buffalo Bills:BUF
Miami Dolphins:MIA
New England Patriots:NE
New York Jets:NYJ
*:NFC-EAST
Dallas Cowboys:DAL
New York Giants:NYG
Philadelphia Eagles:PHI
Washington Redskins:WAS
*:AFC-WEST
Denver Broncos:DEN
Kansas City Chiefs:KC
Oakland Raiders:OAK
San Diego Chargers:SD
*:NFC-WEST
Arizona Cardinals:ARZ
San Francisco 49ers:SF
Seattle Seahawks:SEA
St. Louis Rams:STL

I put this data into a TListBox on my main form and pasted this data into it using the Items property editor in the IDE. Then in that form’s FormCreate method, I create a TStringList as a private class variable (named team_data) and assign the TListBox.Items data to it; then I clear it from the TListBox.

procedure TfrmMain.FormCreate(Sender: TObject);
begin
  team_data := TStringList.Create();
  team_data.Assign( lbox1.Items );
  team_data.NameValueSeparator := ':';
  lbox1.Items.Clear;
end;

Note where I assign ‘:’ to team_data.NameValueSeparator. This makes it really easy to split the data into name:value pairs. I could have stuck with the default ‘=’ but the ‘:’ seemed to make more sense in this context. This approach is an easy way of editing data at design-time that’s ultimately intended to be used as data at run-time. It’s also a heck of a lot easier than initializing this much stuff in code!

INITIALIZING THE FORM USING A METHOD

So now, instead of Constructor or Property injection, let’s try a different approach. Let’s add an AddTeam() method to the form’s public interface like this:

interface
type
  TNFLTeamSelection_form = class( TForm )
    League_rgrp : TRadioGroup;
  . . .
  public
    procedure AddTeam( aLeague : string; aTeam : string );
  . . .
implementation 

//-----------------------------------------------------------
function GetOrAddStringlist( aSL : TStrings; aNdx : integer ) : TStringList;
begin
  Result := TStringList(aSL.Objects[aNdx]);
  if (Result = NIL) then
  begin
    Result := TStringList.Create();
    aSL.Objects[aNdx] := Result;
  end;
end;
//-----------------------------------------------------------
procedure TNFLTeamSelection_form.AddTeam(aLeague, aTeam: string);
var  i : integer;
  sl : TStringList;
begin
  i := League_rgrp.Items.IndexOf( aLeague );
  if (i >= 0) then
  begin
    sl := GetOrAddStringlist( League_rgrp.Items, i );
    sl.Add( aTeam );
  end;
end;

Then after we create an instance of the form, we’ll call this method to pass over the team league data. Here’s the code (in the main form) that I use to create and initialize the selection form following this approach, which is in a method called by the button’s OnClick event handler. Note that I’ve obscured the fact that we’re using the selection form by hiding it inside of a function called GetTeamSelection( team_data ). There’s a reason for this that I’ll explain shortly. I’m also passing in the team data stringlist.

interface
type
  TRosterList = TObjectList;

  TfrmMain = class( TForm )
    . . .
  end;

implementation

//----------------------------------------------------------------
procedure TfrmMain.ShowTeamSelectionBox_btnClick(Sender: TObject);
var
  selected_team : string;
  myRoster : TRosterList;
begin
  selected_team := GetTeamSelection( team_data );   // <-- HIDDEN PLUMBING

  if (selected_team = '') then
  begin
    ShowMessage( 'Nothing was selected' );
    Exit;
  end;

  // Add the team's name to lbox1 for quick reference, then ask
  // if the user wants to load the roster for that team now.
  i := lbox1.Items.Add( selected_team );
  if (MessageDlg( 'You selected: '+ selected_team +#10#13+'Load their roster?',
                  mtConfirmation, [mbYes,mbNo], 0 ) = mrYes) then
  begin
    // If so, then load the roster and attach it to the Objects[] list
    // for the newly-added item (the team name).
    myRoster := LoadTeamRoster( selected_team );   // <-- HIDDEN PLUMBING
    lbox1.Items.Objects[i] := myRoster;            // <-- CACHE (temporary)
    // Finally, display the roster
    DisplayRoster( selected_team, myRoster );
  end;
  lbox1.ItemIndex := lbox1.Items.Count-1;   // highlight the team that was just added
end;
//----------------------------------------------------------------
function TfrmMain.GetTeamSelection( aTeamData : TStringList ) : string;
var
  i : integer;
  selectionForm : TNFLTeamSelection_form;
  league : string;
  team : string;
begin
  Result := '';
  // create the form dynamically
  // can be done this way:
  //   Application.CreateForm(TNFLTeamSelection_form, theForm {NFLTeamSelection_form});
  // or this way:
  selectionForm := TNFLTeamSelection_form.Create(self);
  try
    // Initialize the teams using data in aTeamData, which is a TStringList.
    // The League names start with '*' in the first col, then a ':' and
    // the league name. The Team names follow, and have a ':' followed by 2-3
    // letter code, used with the URL to get more data. By setting
    // team_data.NameValueSeparator := ':' after it was created, we can use
    // it instead of '=' for name:value pairs.

    for i := 0 to aTeamData.Count-1 do
    begin
      team := aTeamData.Names[i];
      if (team = '*') then
        league := aTeamData.ValueFromIndex[i]
      else
        selectionForm.AddTeam( league, team );   // <-- PLUMBING (goes into form)
    end;

    // Show the form, then extract the selection if OK was clicked.
    if (selectionForm.ShowModal = mrOK) then
      Result := selectionForm.GetSelection();  // <-- PLUMBING (comes out of form)

  finally
    selectionForm.Free();
  end;
end;

PLUMBING

Notice we have two bits of plumbing going in and coming out of the selection form. In this case, they’re a couple of public methods inside the form:

  • procedure selectionForm.AddTeam( league, team : string )
  • function selectionForm.GetSelection() : string

AddTeam() is used to convey the league+team data into the form for initialization needs and is called after the form has been created but before it has been shown.  Then GetSelection() is used extract the user’s selection from the form after they’ve clicked the OK button. No need for either Constructor or Property injection going in, nor properties for data coming out.

This approach is close to ideal because it serves to almost totally decouple the form from clients that use it. The only real dependency is a couple of form-specific methods to call. In particular, we are dependent on the type of data going in and out, which in this case are all fundamental types (strings) rather than any kind of objects.

I mentioned that I’m obscuring the presence of a form inside of the GetTeamSelection() method.  Part of the reason I did this is that it allows us to replace the form later on for testing purposes. Forms are a pain to test with Unit Testing, but when it comes to selection forms, they only need to be replaced with a stub that selects one of the available values and returns it without asking the user.

In summary, we’re initializing the form with several “records” (like database records) that are composed of two “fields” each. We call AddTeam() repeatedly with however many records we’ve got and stop when we’re finished. We can assume the form deals with this however it needs to deal with it.

In the end, we get back one of these Team’s names, which is the whole purpose of a selection form: show the user a list of items within a meaningful context from which to choose, then wait for his selection and process it.

Even if you had a dozen or more such fields per record, this would work fine. The form can worry about how it contains the data without the client having to be concerned with it, and if the container needs change, the client is totally unaffected.

Also note that the “current record”, ie., the one visible to the user when the form is initially displayed, is controlled exclusively by the form. It does this inside of either the FormShow or (less likely) the FormActivate methods. This further decouples the form’s behaivor from the clients that use it.

Remember, this is a selection form — it’s role in life is to present the user with a list of options from which to select one that s/he wants to work with. It’s not for data entry or editing. We’ll discuss that in a later installment.

HOW TO REGARD THIS DATA

Note that in this particular example, the data is relatively static; that is, it’s not likely to change very frequently — it might only get updated every couple of years, in fact.

For purposes of this example, pretend that’s not the case; that is, imagine that this data represents, say, something more complex, like patient visit data for a hospital’s emergency room. It can get updated virtually in real-time. So if you open the form now, and then 10 minutes from now, this list of initial data has changed. The “Leagues” could be “Departments” and the “Teams” could be “Doctors”. Finally, “Team Rosters” could be “Patients”. Use whatever analogy you prefer. The point is, I’m using this example to illustrate some ways to deal with a problem that will make a lot more sense for data that’s far more time-sensitive than NFL Team data.

Obviously, “Departments”, like “NFL Leagues”, are the least likely to change on a day-to-day basis. So we could easily hard-code them into the form (in the top TRadioGroup).

The “Doctors” serving in each “Department”, say in a Hospital ER setting, will change composition several times a day as doctors rotate in and out. So we want to be able to load this data into the form dynamically. (Please forgive me, but it’s far easier to find realistic data for NFL teams than for hospitals, especially when it comes to detailed team rosters!)

Leave a Reply

Your email address will not be published. Required fields are marked *