Properly refresh DBGrid without loosing current position

This article on delphi.about.com shows one way: http://delphi.about.com/od/delphitips2008/qt/dbgrid_row_pos.htm

However, if you consider the following scenario, you’ll notice that one is not working right:

– initial recordset has 10 records

– position on the 9th

– delete first 7 records

– cursor should now be on the 2nd  row.

Same goes if we add records in front of the current record.

So, how to do this correctly? The first impuls is to do something like:


b := ds.GetBookmark;

try

finally

if (b<>nil) and ds.BookMarkValid(b) then

try

ds.GotoBookMark(b);

finally

ds.FreeBookMark(b);

end;

end;

That looks all nice and dandy but:

– this will always position the selected row in the center of the grid. It’s pretty annoying if you do repetitive refreshing (like in a timer)

– if the current record is deleted … nothing happens. Of course if you happen to reposition the cursor (like using locate) in the code, then you will have some random re-positionings. Again, not nice.

So lets start with problem no 2.


RecNo := ds.RecNo;

b := ds.GetBookmark;

try

finally

if (b<>nil) and ds.BookMarkValid(b) then

try

ds.GotoBookMark(b);

finally

ds.FreeBookMark(b);

end

else if (recno<ds.RecordCount) and (recno<>ds.RecNo) then
begin
ds.First;
ds.MoveBy(Max(0, recno-1));
end;

Ok, so this solves the case when the current record is deleted. Still we have to fix the centering of the GotoBookMark.

Now, upon inspection of that function, we see that it actually does a Resync and passes rmCenter to it, which is our problem. So let’s simulate the GotobookMark without centering:


THackDataSet=class(TDataSet) // a nice "hack" so we can access protected members
end;

RecNo := ds.RecNo;

b := ds.GetBookmark;

try

finally

if (b<>nil) and ds.BookMarkValid(b) then

try

//      ds.GotoBookMark(b);

ds.CheckBrowseMode;
THackDataSet(ds).DoBeforeScroll;
THackDataSet(ds).InternalGotoBookmark(b);
ds.Resync([rmExact{, rmCenter}]);
THackDataSet(ds).DoAfterScroll;
finally

ds.FreeBookMark(b);

end

else if (recn<ds.RecordCount) and (recno<>ds.RecNo) then
begin
ds.First;
ds.MoveBy(Max(0, recno-1));
end;

Ok, now this almost solves the problem. Now, the row is repositioned to the last in the grid. Still not what we want. So what we can still do here is force the resync to take the active record the right one and not the one set by the InternalGotoBookmark.


THackDataSet=class(TDataSet) // a nice "hack" so we can access protected members
end;

THackDBGrid=class(TDBGrid)
end;

Row := THackDBGrid(grid).Row;// or THackDataSet(ds).ActiveRecord

RecNo := ds.RecNo;

b := ds.GetBookmark;

try

finally

if (b<>nil) and ds.BookMarkValid(b) then

try

//      ds.GotoBookMark(b);

ds.CheckBrowseMode;
THackDataSet(ds).DoBeforeScroll;
THackDataSet(ds).InternalGotoBookmark(b);
if THackDataSet(ds).ActiveRecord <> Row - 1 then
THackDataSet(ds).MoveBy(Row - THackDataSet(ds).ActiveRecord - 1);
ds.Resync([rmExact{, rmCenter}]);
THackDataSet(ds).DoAfterScroll;
finally

ds.FreeBookMark(b);

end

else if (recno<ds.RecordCount) and (recno<>ds.RecNo) then
begin
ds.First;
ds.MoveBy(Max(0, recno-1));
end;

This is almost perfect. There is still the same issue of positioning on the first/last record when the current record is deleted. I can’t confirm this yet as my test scenarios haven’t gotten that far but I *think* it will happen. And if it does, you can simply apply the same ActiveRecord thing to calculate the correct position for the MoveBy.

I’ll update this post if and when I get to kick this scenario.

http://delphi.about.com/od/delphitips2008/qt/dbgrid_row_pos.htm

Related posts

2 Responses to “Properly refresh DBGrid without loosing current position”

  1. ciuly says:

    Obviously, issues appeared 🙂
    I’m using TClientDataSet as standalone memory table and it seems the bookmarks are no longer valid if you add/delete data to the clientdataset.

    So, I ditched the bookmarks and used locate instead on the primary key. Locate also behaves like GotoBookMark so the issue persists and the solution is the same: use the ActiveRecord to reposition correctly.

    THackClientDataSet=class(TClientDataSet) // a nice "hack" so we can access protected members
    end;
     
    THackDBGrid=class(TDBGrid)
    end;
     
    Row := THackDBGrid(grid).Row;// or THackDataSet(ds).ActiveRecord
     
    RecNo := ds.RecNo;
     
    v := VarArrayOf(lst of field values that compose the key); - use single variant if only one field - also set to NULL if no records
     
    try
     
    finally
     
    if not VarIsNull(v) then 
      THackClientDataSet(ds).DoBeforeScroll;
    if (not VarIsNull(v)) and (THackClientDataSet(ds).LocateRecord(list of key fields, v, [], True)) then
    begin
            if THackClientDataSet(ds).ActiveRecord <> Row - 1 then
              THackClientDataSet(ds).MoveBy(Row - THackClientDataSet(ds).ActiveRecord - 1);
            ds.Resync([rmExact{, rmCenter}]);
            THackClientDataSet(ds).DoAfterScroll;
    end
     
    else if (recno<ds.RecordCount) and (recno<>ds.RecNo) then
    begin
    ds.First;
    ds.MoveBy(Max(0, recno-1));
    end;
    
  2. Avnir says:

    Many thanks ciuly, I implemented this method in C++ builder. If anyone’s interested, here’s what I did:

    create a class inheriting TClientDataSet

    [Code]class MyCDS: public TClientDataSet
    {
    public:
    __fastcall MyCDS( TComponent* aOwner)
    : TClientDataSet( aOwner)
    {
    };
    void __fastcall DoRefresh( String PKFieldName);
    };[/Code]

    Then create the method, DoRefresh
    [Code]void __fastcall MyCDS::DoRefresh( String PKFieldName)
    {
    TLocateOptions Opt;
    Opt.Clear();
    TResyncMode syncMode( rmExact);

    int MyActiveRec = ActiveRecord;
    int MyRecNo = RecNo;
    Variant V = FieldByName( PKFieldName)->Value;

    DisableControls();
    DoBeforeScroll();

    if( !V.IsNull() && LocateRecord( PKFieldName, V, Opt, true))
    {
    if( ActiveRecord != MyActiveRec)
    MoveBy( MyActiveRec – ActiveRecord);
    Resync( syncMode);
    }
    else if( MyRecNo < RecordCount && MyRecNo != RecNo)
    {
    First();
    MoveBy( std::max( 0, MyRecNo));
    }

    DoAfterScroll();
    EnableControls();
    }[/Code]

    To use this, instanciate MyCDS instead of TClientDataSet and whenever refresh is required, call DoRefresh. This works brilliantly.

Leave a Reply

This blog is kept spam free by WP-SpamFree.