0

To reproduce: Set up a TControlList with LiveBindings to a DataSet and assign an OnClick event that just does:

DataSet.Edit;
DataSet.Post;

Now when you click on a cell it will scroll that cell to be in the last row of the list (assuming you've already scrolled down far enough).

This is probably a bug, but is there a way I can prevent the repositioning of the visible items in the list?

Using Delphi 10.4.2

Alister
  • 6,527
  • 4
  • 46
  • 70
  • Post is not even necessary to have strange scrolling. Actually I don't understand what it really does. The clicked item sometimes stay where it is and sometimes it scroll toward the end of view. – fpiette Mar 30 '21 at 08:06
  • I think you should provide an [MCVE](https://stackoverflow.com/help/mcve) for a q like this so that readers can paste it into Delphi and try it without having to guess the exact details of your app. Especially with LiveBindings, which is quirky at the best of times. – MartynA Mar 30 '21 at 10:43

2 Answers2

2

After investigating this quite thoroughly, I've come to the conclusion that the behaviour you've noted is a consequence of the TControlList's LiveBinding mechanism "working as designed", so there is unlikely to be a clean way of avoiding it (though there is one obvious work-around, see below).

Please try the test project below by creating a new VCL project, adding a TClientDataSet, a TStringGrid and a TControlList containing 2 TLabels, then adjust the height of the TStringGrid to about 10 rows and the TControlList to about 5. No manual setting up of LiveBindings is necessary, since it is all done in the code below.

Then, set the main form's code as shown below, compile and run.

At run-time, the ClientDataSet creates one hundred numbered records with two fields and displays them in the StringList and ControlList.

The behaviour I see is that the ControlList behaves as expected unless its current row is its last one

  • if it is then the ControlList scrolls so that the clicked row is shifted up one row. The only simple way I could find to restore it to being the last row is to click the upper thumb of the ControlList's vertical scrollbar, which as noted in the code's comments could be the basis of a (not implemented and, I suspect, brittle) work-around.

Note that the DoSomething method, which is executed when the ControList is clicked, has a $define which determines whether the ClientDataSet does an Edit; Post or a Next; Prior. It doesn't seem to make any difference which of these pairs of operations is executed, the behaviour is the same. This is what prompted me to take a look at what happens when the ClientDataSet scrolls.

If you put a disabled breakpoint on the Caption := 'Scrolled line in the AfterScroll handler, run the project and then enable the bp just before clicking on the ControlList, you'll see that when it trips, the scroll event has been triggered by MakeValidRecNo in Data.Bind.Scope which is ultimately invoked by TLinkObservers.PositionLinkPosChanged in System.Classes. It's this fact which makes me think there is very unlikely to be any clean way of avoiding the problem behaviour short of persuading EMBA to change TControlList's behaviour.

Btw, @fpiette's comment that even doing an Edit in the OnClick handle is sufficient to provoke the behaviour and the reason for that is that it will ultimately cause the ClientDataSet to scroll, triggering the behaviour noted above.

Code:

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ControlList, Data.DB,
  Datasnap.DBClient, Vcl.Grids, Data.Bind.Components, Data.Bind.DBScope, Data.Bind.Grid,
  Data.Bind.EngExt, Vcl.Bind.DBEngExt, Vcl.Bind.Grid, System.Rtti,
  System.Bindings.Outputs, Vcl.Bind.Editors, Vcl.Bind.ControlList, Vcl.ExtCtrls,
  Vcl.DBCtrls;

type
  TForm1 = class(TForm)
    ControlList1: TControlList;
    Button1: TButton;
    StringGrid1: TStringGrid;
    ClientDataSet1: TClientDataSet;
    BindingsList1: TBindingsList;
    Label1: TLabel;
    Label2: TLabel;
    DBNavigator1: TDBNavigator;
    DataSource1: TDataSource;
    Button2: TButton;
    procedure FormCreate(Sender: TObject);
    procedure ControlList1Click(Sender: TObject);
    procedure Button1Click(Sender: TObject);
    procedure ClientDataSet1AfterScroll(DataSet: TDataSet);
  private
    procedure DoSomething;
  public
    ItemIndex : Integer;
    ARect : TRect;
    BindSourceDB1 : TBindSourceDB;
    LinkGridToDataSourceBindSourceDB1 : TLinkGridToDataSource;
    LinkPropertyToFieldCaption1: TLinkPropertyToField;
    LinkGridToDataSourceBindSourceDB2: TLinkGridToDataSource;
    LinkPropertyToFieldCaption2: TLinkPropertyToField;
  end;

[...]

procedure TForm1.DoSomething;
var
  Pt : TPoint;
  Rows,
  Row : Integer;
begin
  //Exit;
  Pt := Mouse.CursorPos;
  Pt := ControlList1.ScreenToClient(Pt);
  Rows := ControlList1.ClientHeight div ControlList1.ItemHeight;
  if Pt.Y > Rows * ControlList1.ClientHeight then
    Row := Rows + 1
  else
    Row := Pt.Y div ControlList1.ItemHeight;
  Inc(Row);  // to make it 1-based
  Caption := Format('Row: %d', [Row]);

{.$Define DoEdit}
{$IfDef DoEdit}
  ClientDataSet1.Edit;
  ClientDataSet1.Post;
{$Else}
  ClientDataSet1.Next;
  ClientDataSet1.Prior;
{$Endif}

  if Row >= Rows then begin
    //  The observed behaviour is that if the clicked row was the last one in the grid
    //  the ClientDataSet operations above will have moved the current record's data
    //  will now be in the row above the bottom row.  A work-around would be to simulate
    //  clicking the upper thumb of ControlList1's vertical scrollbar, as this shifts the
    //  current ror back down to where it was, in the last row of the grid.

  end;
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  DoSomething;
end;

procedure TForm1.ClientDataSet1AfterScroll(DataSet: TDataSet);
begin
  Caption := 'Scrolled';
end;

procedure TForm1.ControlList1Click(Sender: TObject);
begin
  DoSomething;
end;

procedure TForm1.FormCreate(Sender: TObject);
var
  AField : TField;
  i : Integer;
begin

  AField := TIntegerField.Create(Self);
  AField.FieldName := 'Field1';
  AField.FieldKind := fkData;
  AField.DataSet := ClientDataSet1;

  AField := TStringField.Create(Self);
  AField.FieldName := 'Field2';
  AField.Size := 20;
  AField.FieldKind := fkData;
  AField.DataSet := ClientDataSet1;

  ControlList1.ClientHeight := ControlList1.ItemHeight * 5;
  BindSourceDB1 := TBindSourceDB.Create(Self);
  BindSourceDB1.DataSet := ClientDataSet1;

  LinkGridToDataSourceBindSourceDB1 := TLinkGridToDataSource.Create(Self);
  LinkGridToDataSourceBindSourceDB1.DataSource := BindSourceDB1;
  LinkGridToDataSourceBindSourceDB1.GridControl := StringGrid1;

  LinkGridToDataSourceBindSourceDB2 := TLinkGridToDataSource.Create(Self);
  LinkGridToDataSourceBindSourceDB2.DataSource := BindSourceDB1;
  LinkGridToDataSourceBindSourceDB2.GridControl := ControlList1;

  LinkPropertyToFieldCaption1 := TLinkPropertyToField.Create(Self);
  LinkPropertyToFieldCaption1.DataSource := BindSourceDB1;
  LinkPropertyToFieldCaption1.FieldName := 'Field1';
  LinkPropertyToFieldCaption1.Component := Label1;
  LinkPropertyToFieldCaption1.ComponentProperty := 'Caption';

  LinkPropertyToFieldCaption2 := TLinkPropertyToField.Create(Self);
  LinkPropertyToFieldCaption2.DataSource := BindSourceDB1;
  LinkPropertyToFieldCaption2.FieldName := 'Field2';
  LinkPropertyToFieldCaption2.Component := Label2;
  LinkPropertyToFieldCaption2.ComponentProperty := 'Caption';

  ClientDataSet1.IndexFieldNames := 'Field1';
  ClientDataSet1.CreateDataSet;

  for i := 1 to 100 do
    ClientDataSet1.InsertRecord([i, 'Row ' + IntToStr(i)]);
  ClientDataSet1.First;

end;
MartynA
  • 30,454
  • 4
  • 32
  • 73
2

I couldn't find a "nice" solution to this, so I went down the RTTI route. We need to access the private FScrollPos field, remember what it is before we change a record in the dataset, then reset it back.

procedure TForm7.ControlList1Click(Sender: TObject);
var
  OldScrollPos : integer;
begin
  inherited;
  OldScrollPos := TRttiContext.Create.GetType(TControlList).GetField('FScrollPos').GetValue(Sender).AsInteger;
  DataSet.Edit;
  DataSet.Post;
  TRttiContext.Create.GetType(TControlList).GetField('FScrollPos').SetValue(Sender, OldScrollPos);
end;

This removes the "scroll item to the bottom row" behaviour when we alter a record. You will need to add System.RTTI to your uses.

Alister
  • 6,527
  • 4
  • 46
  • 70