TFNCGrid + DatabaseAdapter - access violation

I am using a TFNCGrid, TTMSFNCGridDatabaseAdapter for a restaurant point of sale. I have a tblTicketItem1 (DBISAM) data table connected to the FNCGrid using the Database Adapter. I also have a tblTicketIem2 pointing to the same data table, that is used for doing most of the work to create the data records. When an item is added to or edited on the ticket, multiple records will be added/edited using the tblTicketIem2 component. Then I call the Refresh method of the first table (tblTicketItem1) that is connected to the FNCGrid so that the changes will be reflected in the FNCGrid.

This works fine as long as all the records for that item are visible in the grid before editing. When editing an item that comprises of multiple records and the records are only partially displayed. For example: If I have a pizza that include 10 toppings I would have one record for the pizza and 10 records, one for each topping. If only 5 of the records are visible in the grid when I edit; then after making changes and updating the data records I call tblTicketItem1.Refresh to update the grid's display and I get an access violation.

PS. I wish I had a better understanding of the relationship of TFNCGrid, TTMSFNCGridDatabaseAdapter to the TDataset; and when using multiple data cursors (TTables) and when synchronization is needed (like what we do in the GetCellLayout event). For example: is synchronization needed also in the OnSelectedEvent?

I hope my description is not too confusing.

Hi,

Can you provide more info on the access violation?
Can you test by adding TMSFNCGrid1.BeginUpdate & TMSFNCGrid1.EndUpdate?
Where are you calling tblTicketItem1.Refresh?

Pieter,

Can you provide more info on the access violation?
See attached images.
Screen Shot 2020-12-15 at 10.44.41 AM.png

Screen Shot 2020-12-15 at 10.44.51 AM.png

Can you test by adding TMSFNCGrid1.BeginUpdate & TMSFNCGrid1.EndUpdate?
I added the Begin/End Update. no change.

Where are you calling tblTicketItem1.Refresh?

After any changes are done to tblTillItems2 I do a refresh of tblTicketItems1.

I have code in GetCellLayout of the grid to change the appearance of each cell appropriately. I created a Helper class for TFNCGrid to make it easier to replace the old grid with FNCGrid in my program.

I noticed that if I "remarked-out" my code in the GetCellLayout the AV error goes away, so I think it is something that I don't have quite right. To do the "synchronization" in that method I call my helper functions SyncDBegin and SyncDEnd. I have included my helper class (FNCGridHelper.pas) and my GetCellLayout code.

The AV error appears to only happen when I have enough order items on the ticket that exceed the display area and I edit the item (which includes any number of records) on the ticket.

FNCGrid AV Error.zip (5.32 KB)

Hi,

Looking at the error it tries to access a field with an incorrect index or name. This line might be where the problem lies (inside the SyncDBegin function):

result := aDBAdapter.Columns[nColumn].Field;

Internally this tries to retrieve the field based on the field name. I suggest to replace this line with access to the correct field directly on your dataset for example:

result := aDBAdapter.DataLink.DataSet.Fields[nColumn];

I tried

result := aDBAdapter.DataLink.DataSet.Fields[nColumn];

It did not work because the data table contains all of its field objects (20 count) and I manually created only four of the field columns to display in the grid. So my grid columns do not match the DataLink field columns.

I would need a way to take the display column number and map it to the DataLink field position.

Or you could use the FieldName, regardless of the index

I have tried the suggestions above, including using the FieldName. Using the ACol or FieldName both work the same, and get the AV in the same way. I was about to create a demo program to duplication the issue with TClientDataSets, but the TClientDataSets appears to behave differently concerning two TDataSets pointing to the same data table. In trying the several options, I think it has more to do the the Row, rather than the column. Here are the conditions and symptoms:

  • an FNCGrid connected to a data table (TDBISAMTable I'll call T1) using the FNC data adapter.
  • a second data table (TDBISAMTable I'll call T2) that references the same physical table. Both tables have the same "SetRange" in place.
  • Records are added, changed, deleted using T2. After each change a T1.Refresh is called.
  • One or more records are added at one time, then the T1.Refresh happens.
  • If the grid has 34 visible rows; I can start with no records; then add one or more records with no AV error. When I surpass the 34 visible, that is when the AV error happens.
  • I think... The AV error happens when the grid is repainting itself. It is hard to tell because there is no Call Stack that traces from my code to the place where the error is happening.

If you have access to DBISAM I could try to create a sample program to duplicate the problem.

I am using FNCGrid because the application is a touch screen VCL app. I haven't found another grid that has that feature.

I'm not sure what to do with this. Perhaps my implementation of the above suggestions was not correct.

Sorry, to keep coming back with this. I appreciate your time and consideration.

Hi,

It could potentially be a refresh issue or out of sync issue. It just tries to set the active record based on the buffer count, but if the buffer count is not in sync then the issues rise. A couple of questions:

  • Did you use DisableControls & EnableControls with each sync to the dataset?
  • Did you use BeginUpdate & EndUpdate to update the grid?
  • Can you try by implementing the OnGetRecordCount event and then return the total number of records instead of relying on the buffer count getting set to the visible number of rows?

T1 is connected to the grid; T2 is not connected to any controls. All table changes are done with T2, then I call T1.Refresh.

Did you use DisableControls & EnableControls with each sync to the dataset?
I tried this around the T1.Refresh and in the gridTicketGetCellLayout event = no change. T2 is not connected to any controls. Since no data changes are done with T1 directly, there really is no other place to put these.

Did you use BeginUpdate & EndUpdate to update the grid?Same answer as above.

Can you try by implementing the OnGetRecordCount event and then return the total number of records instead of relying on the buffer count getting set to the visible number of rows?
I'm not sure what this means. I never set the visible rows, it depends on the size of the users screen.

In the gridTicketGetCellLayout, after SetActiveRecord, I use T1 to access another field value. If I comment this out, no AV.

We'll further investigate this here as soon as possible

Hi,

We have tried to reproduce this here but haven't been able to trigger the access violation. Unfortunately as this is something specific I want to ask you to try to reproduce the issue with a TClientDataSet, simulating the issue. The TClientDataSet can be programmatically filled with data. This way, we'll know exactly what is going wrong and potentially look out for a fix.

I will create a program to reproduce the issue. I'm not sure if TClientDataSet will work because I would need to use the CloneCursor to point to TDataSet to the same memory table. My understanding is that TClientDataSet keeps things in sync between cloned cursors. I may look into TFDMemTable or may have to use a disk based table to reproduce.

Here is a program that duplicates the AV Error. It uses TFDMemTable.

FNCGridTest.zip (60 KB)

Thank you for the sample. The issue I think is that the ActiveRecord and Record that should match with tblOrderItemsForGridOnly is out of sync. The grid is using a BufferCount to make sure that it does not load all items available in the dataset, only the visible amount. When changing the buffer count to load all records available, the issue disappears. So for now I suggest to make this change inside VCL.TMSFNCGridDatabaseAdapter:

function TTMSFNCGridDatabaseAdapter.GetBufferCount: Integer;
begin
  Result := 0;
  if Assigned(Grid) then
  begin
    Result := Grid.VisibleRowCount + 1;
    if Result < ((Round(Grid.Height) div Round(Grid.DefaultRowHeight)) - Grid.FixedRows + 1) then
      Result := ((Round(Grid.Height) div Round(Grid.DefaultRowHeight)) - Grid.FixedRows + 1)
  end;
end;
function TTMSFNCGridDatabaseAdapter.GetBufferCount: Integer;
begin
  Result := GetRecordCount;
 end;

I'll make the GetBufferCount virtual, so you can create a descendant class and then make the above implementation, without needing to change the source code. There are alternatives:

  1. Limit the other table in the same way by setting a buffercount as you would do when using a grid database adapter, but then you'll need to sync the active record as well.
  2. Remove the grid database adapter and simply manually loop through the dataset using for next statements and then the grid will be completely filled with data, without using a buffercount.

Questions:
Using the GetBufferCount change : For the place where I'm having the problem this change would work fine (the total number of items is for one order only). I have other implementations in other windows where the record count may be the entire file, would those other grids then suffer in performance? If so, I would need to be able to switch between the VisibleRowCount or GetRecordCount according to my needs. Of course, making GetBufferCount virtual would give me that control.

Remove the grid database adapter and simply manually loop through the dataset using for next statements and then the grid will be completely filled with data, without using a buffercount.

This idea sounds interesting. So then I would write my own code to manage the putting and getting the data to the grid. How could I keep the rows in the grid in sync with the data, is there a place in the grid to store what database record number belongs to each row? When I make a change in the database, I would need to update the affected rows in the grid.

Hi,

The next update will have GetBufferCount virtual. The performance depends on the number of records in the database. You can experiment by using the switch technique between the inherited implementation or the GetRecordCount and see what performance you have. When using a single table, you can keep the current implementation, and only use the GetRecordCount when you use a secondary table. Alternatively, you can also limit the second table based on the number of records in the buffer, but that can be complex. The next update is planned for next week, but you can already change the implementation, make it virtual and create a class descendant?

I guess we can keep this as an alternative solution just in case the first technique doesn't work and work out the details when it's necessary.