6

I like to disable controls when it's pointless for the user to click on them.

One special case is a set of custom menu buttons that emulate the first, prior, next and last buttons of a standard TDBNavigator.

When the user clicks on the first button, the first and prior buttons are both disabled.

When the user then clicks the next and prior buttons the underlying TDataSet is positioned on the same record as before, but the first and prior buttons are both still enabled.

The current implementation looks like this:

NavigationFirstButton.Enabled := not DataSet.IsEmpty and not DataSet.Bof;
NavigationPriorButton.Enabled := not DataSet.IsEmpty and not DataSet.Bof;
NavigationNextButton.Enabled  := not DataSet.IsEmpty and not DataSet.Eof;
NavigationLastButton.Enabled  := not DataSet.IsEmpty and not DataSet.Eof;

Bof and Eof are not the right way to disable the buttons, because I have to know beforehand if the current record is going to be the first/last record.

So I thought of rewriting this using a IsFirstRecord and IsLastRecord method:

function IsFirstRecord(ADataSet: TDataSet): Boolean;
begin
    Result := ADataSet.RecNo = 0;
end;

function IsLastRecord(ADataSet: TDataSet): Boolean;
begin
    Result := ADataSet.RecNo = ADataSet.RecordCount - 1;
end;

I don't think that this is a good idea, since I've seen cases where for the first record RecNo = 0 is not true. (i.e. A filtered TADSQuery)

What is a reliable implementation for IsFirstRecord and IsLastRecord? Is it even possible using the current TDataSet architecture?

Jens Mühlenhoff
  • 14,565
  • 6
  • 56
  • 113
  • `RecNo` is optional (may be non-implemented) for SQL datasets. `RecordCount` is bad for SQL dataset, because it requires to fetch all the data. Imagine SQL SELECT request, that generates 10GB of information. Usually user would only analyze first screen of data (or maybe 3 screens or 10 screens) and then close the form, ignoring the rest of much less relevant data. By calling .RecordCount you demand that 1: SQL Server would read in proper order all those 10GB from discs 2: Network would transfer all those 10GB of data 3: client would cache in RAM all those 10 GB of data. – Arioch 'The Oct 31 '13 at 13:41
  • So it would be stress-test for server and network (slowing down all other work) and out of memory crash on client. And all that for the sake of infomation that would probably never will be needed. So `RecNo` approach is proper thing for ISAM databases (DBF, Paradox, etc). For SQL databases proper way is to use `TDataSet.EOF` and `TDataSet.BOF` functions – Arioch 'The Oct 31 '13 at 13:43
  • 1
    PS. by definition DataSet.Empty == DataSet.EOF and DataSet.BOF so that check is redundant. I guess you meant something `DataSet.Active and not DataSet.Bof;` – Arioch 'The Oct 31 '13 at 13:44
  • 1
    Recordcount is not always available (think dynamic datasets) – whosrdaddy Oct 31 '13 at 14:09
  • @whosrdaddy just FetchAll and someday soon after eternity... :-) – Arioch 'The Oct 31 '13 at 14:10
  • That would defeat the purpose of dynamic datasets ;) – whosrdaddy Oct 31 '13 at 14:11
  • @Arioch'The Ok, thanks for the `RecordCount` info, this makes my initial idea even more wrong ;). So you think that what I'm trying to do is not possible in an efficient way? – Jens Mühlenhoff Oct 31 '13 at 14:15
  • 1
    When you check for the state of the record (lets say back/up): disable controls; save a bookmark; Prior; if BOF then it's the first record; restore bookmark; enable controls; (and you are correct TDBNavigator is doing it wrong) – kobik Oct 31 '13 at 14:27
  • @JensMühlenhoff i can only repeat: For SQL databases proper way is to use TDataSet.EOF and TDataSet.BOF functions. Look into TDBNavigator sources and do not try to outsmart VCL designers in what those methods they designed for – Arioch 'The Oct 31 '13 at 14:30
  • @kobik - it looks like he has some broken database or TDataSet implementation and now he wants to design a broken VCL, so that two bugs would annihilate each other. While sometimes counter-bug is next to the only solution, that way never was "right" nor "efficient" – Arioch 'The Oct 31 '13 at 14:33
  • 1
    There's no need to actually retrieve record data to have a record count for an sql dataset. In fact I presume most dataset descendants would try to avoid that. `TSQLDataSet`, for instance, tries to run a `select count` query. `TADSQuery`, also, [looks like](http://devzone.advantagedatabase.com/dz/webhelp/advantage9.0/mergedProjects/ade/sec7/recordcount.htm) doing the sane thing and counting the records at the server side. – Sertac Akyuz Oct 31 '13 at 23:20
  • 2
    @SertacAkyuz that means `RecordCount` is unreliable with ReadCommitted isolation level. You may get 100, but actually fetching racords soon after found there are 105 or 95 of those – Arioch 'The Nov 01 '13 at 07:18

1 Answers1

6

You could try something like this:

function IsFirstRecord(ADataSet: TDataSet): Boolean;
var
  BmStr: TBookmarkStr;
begin
  Result := not ADataSet.IsEmpty;
  if not Result then Exit;
  Result := ADataSet.Bof;
  // if ADataSet is already at BOF there is no point to continue
  if not Result then
  begin
    ADataSet.DisableControls;
    try
      BmStr := ADataSet.Bookmark;
      try
        ADataSet.Prior;
        Result := ADataSet.Bof;
      finally
        ADataSet.Bookmark := BmStr;
      end;
    finally
      ADataSet.EnableControls;
    end;
  end;
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  if IsFirstRecord(ADODataSet1) then
    ShowMessage('First')
  else
    ShowMessage('Not First');
end;

For IsLastRecord implementation simply replace:

ADataSet.Prior -> ADataSet.Next
ADataSet.Bof -> ADataSet.Eof
kobik
  • 21,001
  • 4
  • 61
  • 121