1
votes

The dynamic form creation subject has been covered many times, but I could not find something to solve my problem, so here I am... again... :-)

My previous questions lead me to think that my application would start faster if not all forms were created on startup, but created dynamically when needed. And that is mostly true, the startup is much faster when I only have my main Form and a Datamodule created.

On button click, here is the code I use to create and free forms on demand (mostly inspired by what I found here from answers by Jerry Dodge and Craig Young, thanks to them for their assistance) :

procedure TfrmWelcome.BtKeywordsClick(Sender: TObject);
  var
    F_Keywords : TfrmKeywords;
  begin
    F_Keywords := Tfrmkeywords.Create(nil);
      try
        F_Keywords.ShowModal;
      finally
        F_Keywords.Free;
      end;
end;

Again, this works fine but, on creation of frmKeywords, a main tablegrid is supposedly filled by a FDQuery that fires upon displaying the form. Of course (or I would not be here), adding

frmKeywords.FDQuery1.Open;

in the FormShow or FormCreate event ends up with an "access violation error".

So I modified my creation code and it now looks like :

procedure TfrmWelcome.BtKeywordsClick(Sender: TObject);
  var
    F_Keywords : TfrmKeywords;
  begin
    F_Keywords := Tfrmkeywords.Create(nil);
      try
        F_Keywords.FDQuery1.Open;
        F_Keywords.ShowModal;
      finally
        F_Keywords.FDQuery1.Close;
        F_Keywords.Free;
      end;
end;

(I'm not even sure that the FDQUery1.Close is useful in the finally block).

Great, now my Form shows up and the main datagrid is filled with data.

Problem is that, when the user clicks in the DBGrid1, the database id of the selected record is passed as a parameter to a secondary FDQuery which in return fills a secondary DBGrid with data (master data in DBgrid1, child data in DBGrid2)

This is done like :

procedure TfrmKeywords.DBGridEh1CellClick(Column: TColumnEh);
var
  kwid : Integer;
begin
  frmKeywords.FDQuery2.Close; //Closing secondary query
  kwid := FDQuery1.FieldByName('id').AsInteger; //Assigning kw_id according to selected row
  frmKeywords.FDQuery2.ParamByName('kw_id').AsInteger := kwid; //Linking query2 parameter to kwid
  frmKeywords.FDQuery2.Open; //Reopening query2 to display assets
end;

And there, again like previously, "access violation error". Like FDQuery does not exist maybe ?

So my question is : When you dynamically create a Form, are all visual and non visual components of that Form automatically created ? Dbgrids appears on my Form and seems to work because data is displayed (at least in one of them), but the second FDQuery just does not want to work. I am obviously missing something here. I ruled out the FDConnection that is on the datamodule because the FDQuery1 works, so I'm out of ideas...

Thanks in advance

Math

2

2 Answers

4
votes

The problem is that you are accessing the predeclared global variable frmKeywords which is nil at the time you are trying to access it. Instead you are instantiating your form and storing a reference in a local variable, which is fine by itself, what is not is accessing that unassigned variable. So, just modify your code like so:

procedure TfrmKeywords.DBGridEh1CellClick(Column: TColumnEh);
var
  kwid: Integer;
begin
  { do not access the frmKeywords variable here; if you want to explicitly
    hint yourself about accessing the current form instance, you can write
    Self.FDQuery2.Close; but that Self is not necessary, e.g. the following
    lines refer to the current form instance as well }
  FDQuery2.Close;
  kwid := FDQuery1.FieldByName('id').AsInteger;
  FDQuery2.ParamByName('kw_id').AsInteger := kwid;
  FDQuery2.Open;
end;

About explicit closing dataset before it's released, you don't need to explicitly Close dataset before it's released. That happens internally.

One last note, you don't have to reopen dataset for refreshing data view when changing parameter value. It's actually not wanted. You setup an SQL query, open the dataset which prepares the query on DBMS and then you are just modifying parameter(s) and calling Refresh to refresh the view, so in your case code can be simplified to (don't forget to Open that FDQuery2 somewhere before this occurs, but only once):

procedure TfrmKeywords.DBGridEh1CellClick(Column: TColumnEh);
begin
  { dataset must be opened here, which means that FDQuery2.Open method has
    been called before (but only once for the query) }
  FDQuery2.ParamByName('kw_id').AsInteger := FDQuery1.FieldByName('id').AsInteger;
  FDQuery2.Refresh;
end;

Or take a look at Master-Detail Relationship (M/D) topic to see how to do what you want without any code (it's applicable for you because you are using DB aware control).

1
votes

If you Ctrl + Click on frmKeywords you'll be taken to the default global definition of that identifier that Delphi automatically generates(imho unhelpfully) for you.

Notice that when you create your form at runtime you're assigning the newly created form to an entirely different reference: F_Keywords := Tfrmkeywords.Create(nil);. It's very important to note that this doesn't set global variable frmKeywords. If you debug any of the access violations you were getting as follows:

  • Put a breakpoint on the offending line.
  • Run in Debug mode.
  • When you hit the breakpoint, check the value of frmKeywords and you'll notice it's nil.
  • This is what trigger the AV: you're trying to access members of "nothing".

TIP: Although forms and and data modules have some special features, they're still "normal objects". So they behave exactly like other objects:

  • Multiple instances can be created.
  • References still have to be explicitly assigned to be used.
  • Object methods can easily reference their own members.

(You may know the above, but as you'll see, the conscious reminder is important.)


So you might be thinking you could simply change your runtime creation to frmKeywords := Tfrmkeywords.Create(nil);. Yes that would work, but is a bad idea. You've done well to not use the global variable. So you should rather delete the global variable Delphi created. At which point you'll discover that your application no longer compiles because you still have a number of references to the global variable. E.g.

procedure TfrmKeywords.DBGridEh1CellClick(Column: TColumnEh);
var
  kwid : Integer;
begin
  frmKeywords.FDQuery2.Close;
  kwid := FDQuery1.FieldByName('id').AsInteger;
  frmKeywords.FDQuery2.ParamByName('kw_id').AsInteger := kwid;
  frmKeywords.FDQuery2.Open;
end;

3 lines above would produce compiler errors when the global variable is deleted. The irony is that those references are completely unnecessary, because FDQuery2 is a member of TfrmKeywords. They would also result in incorrect behaviour if you have multiple instances of the form. Apart from a possible AV: even if frmKeywords did reference a valid instance, the OnCellClick event handler would likely modify the wrong form's queries!

Victoria has already explained the simple change to the above method that fixes those problems. I'd like to just point out a few other problems associated with using global references (apart from those you've already experienced).

  • Global references mean that the objects can be accessed from anywhere within your program.
  • This makes it much more difficult to assess the impact of changes that interact with the globals. A change in one unit can have unintended consequences in a seemingly totally unrelated area of your application.
  • Global variables make it much more difficult to modularise your program, because the global effectively ties your separate units together.

The problems associated with global variables has been well researched and there is plenty of information on the subject. In short they seriously hamper maintainability. You'd do well to simply delete all the global variables Delphi generates for you. Imho, it's unfortunate that these are created, there are trivial alternatives. And I feel the chosen approach leads to bad habits for beginning programmers.