5

I'm using a TVirtualStringTree to store pointers to records.

Originally there is a TList that contains the list of records.

I'm using the OnInitNode event to iterate through the TList and assign each records' data to the tree's nodes.

However, when retrieving the data associated with a node in the OnNewText event handler, the pointer returned has a different address than the one originally stored in the tree.

Further, I can see through debugging that the pointer (to the record data) retrieved from the node is not the same as the one originally stored in the node. I need to save changed data to a database and need to reference the record with the changed data. It should be simple as referencing the pointer, but the problem is that the pointer is not the same.

I'm not sure what I'm doing wrong and hope someone can help me fix this.

Thanks in advance.

Here is my code:

Data structure and declarations:

  TTherapData = record
    TherapID: Integer;
    TherapName: String[120];
    TherapInstr: String[120];
    Selected_DB: Byte;
    Selected: Byte;
  end;

  PTherapData = ^TTherapData;

  FTherapDataList: TList<PTherapData>;

  FTherapDataListAsg_Iter: Integer;

  vstRxList_Asg: TVirtualStringTree;

Loading the data into the TList, and then into the tree:

procedure TfmPatient_Conslt.LoadTherapList(const ADBLoad: Boolean = False);
var
  TherapData: PTherapData;
  d, x: Integer;
begin
    datamod.uspLKTHERAP_S.First;
    while not datamod.uspLKTHERAP_S.Eof do
    begin
      New(TherapData);

      TherapData^.TherapID := datamod.uspLKTHERAP_SROW_ID.AsInteger;
      TherapData^.TherapName := datamod.uspLKTHERAP_SIMPRTHERAP.AsString;
      TherapData^.TherapInstr := EmptyStr;
      TherapData^.Selected := 0;
      TherapData^.Selected_DB := 0;

      FTherapDataList.Add(TherapData);

      datamod.uspLKTHERAP_S.Next;
    end;


    datamod.uspCONSLT_RX_S.First;
    while not datamod.uspCONSLT_RX_S.Eof do
    begin
      d := datamod.uspCONSLT_RX_SRX_ID.AsInteger;
      TherapData := FTherapDataList[TherapDataList_GetIndexOfID(d)];
      TherapData^.TherapInstr := datamod.uspCONSLT_RX_SRX_INSTRUCTION.AsString;
      TherapData^.Selected := 1;
      TherapData^.Selected_DB := 1;

      datamod.uspCONSLT_RX_S.Next;
    end;

  x := TherapDataList_CountSelectedItems;

  FTherapDataListAsg_Iter := 0;
  vstRxList_Asg.NodeDataSize := SizeOf(TTherapData);
  vstRxList_Asg.RootNodeCount := 0;
  vstRxList_Asg.RootNodeCount := x;
end;

procedure TfmPatient_Conslt.vstRxList_AsgInitNode(Sender: TBaseVirtualTree; ParentNode, Node: PVirtualNode;
  var InitialStates: TVirtualNodeInitStates);
var
  TherapData: PTherapData;
begin
  TherapData := Sender.GetNodeData(Node);

  while (FTherapDataList[FTherapDataListAsg_Iter]^.Selected <> 1) do
    Inc(FTherapDataListAsg_Iter);

  TherapData^.TherapID := FTherapDataList[FTherapDataListAsg_Iter]^.TherapID;
  TherapData^.TherapName := FTherapDataList[FTherapDataListAsg_Iter]^.TherapName;
  TherapData^.TherapInstr := FTherapDataList[FTherapDataListAsg_Iter]^.TherapInstr;

  { TherapData := FTherapDataList[FTherapDataListAsg_Iter]; } //  
  { TherapData^ := FTherapDataList[FTherapDataListAsg_Iter]^; } //  

  Inc(FTherapDataListAsg_Iter);
end;

procedure TfmPatient_Conslt.vstRxList_AsgGetText(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex;
  TextType: TVSTTextType; var CellText: string);
var
  TherapData: PTherapData;
begin
  TherapData := Sender.GetNodeData(Node);
  if Assigned(TherapData) then
    if (Column = 0) then
      CellText := TherapData^.TherapName
    else if (Column = 1) then
      CellText := TherapData^.TherapInstr;
end;

procedure TfmPatient_Conslt.vstRxList_AsgEditing(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex;
  var Allowed: Boolean);
begin
  Allowed := (Column = 1);
end;

Retrieving the data. I noticed the problem here:

procedure TfmPatient_Conslt.vstRxList_AsgNewText(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex;
  NewText: string);
var
  TherapData: PTherapData;
begin

  if (Column = 1) then
  begin
    TherapData := Sender.GetNodeData(Node);
    if Assigned(TherapData) then                 // <---- There is a debug breakpoint here 
                                                 // and the watch window screen-shot 
                                                 // is taken here
      TherapData^.TherapInstr := NewText;

    // Showmessage(Format('%p', [TherapData]));  // <---- The pointer value is not the same
                                                 //       as that originally stored !

  end;

end;

This is where I save the list data to database, and the reason that I need the tree to change the original data and not a copy:

procedure TfmPatient_Conslt.SaveRxListToDB;
var
  TherapData: PTherapData;
begin

  for TherapData in FTherapDataList do
  begin
    if (TherapData^.Selected = 1) and (TherapData^.Selected_DB = 0) then
    begin
      // Add new entries to DB
      // :ROW_ID, :CONSLT_ID, :RX_ID, :RX_INSTRUCTION
      datamod.uspCONSLT_RX_I.ParamByName('ROW_ID').AsInteger := 0;
      datamod.uspCONSLT_RX_I.ParamByName('CONSLT_ID').AsInteger := FConsultationID;
      datamod.uspCONSLT_RX_I.ParamByName('RX_ID').AsInteger := TherapData^.TherapID;
      datamod.uspCONSLT_RX_I.ParamByName('RX_INSTRUCTION').AsString := TherapData^.TherapInstr;
      datamod.uspCONSLT_RX_I.PrepareSQL(False);
      datamod.uspCONSLT_RX_I.ExecProc;

      TherapData^.Selected_DB := 1;
    end
    else if (TherapData^.Selected = 1) and (TherapData^.Selected_DB = 1) then
    begin
      // Update existing DB entries
      // :CONSLT_ID, :RX_ID, :RX_INSTRUCTION
      datamod.uspCONSLT_RX_U.ParamByName('CONSLT_ID').AsInteger := FConsultationID;
      datamod.uspCONSLT_RX_U.ParamByName('RX_ID').AsInteger := TherapData^.TherapID;
      datamod.uspCONSLT_RX_U.ParamByName('RX_INSTRUCTION').AsString := TherapData^.TherapInstr;
      datamod.uspCONSLT_RX_U.PrepareSQL(False);
      datamod.uspCONSLT_RX_U.ExecProc;
    end
    else if (TherapData^.Selected = 0) and (TherapData^.Selected_DB = 1) then
    begin
      // Delete removed entries from DB
      // :CONSLT_ID, :RX_ID
      datamod.uspCONSLT_RX_D.ParamByName('CONSLT_ID').AsInteger := FConsultationID;
      datamod.uspCONSLT_RX_D.ParamByName('RX_ID').AsInteger := TherapData^.TherapID;
      datamod.uspCONSLT_RX_D.PrepareSQL(False);
      datamod.uspCONSLT_RX_D.ExecProc;

      TherapData^.Selected_DB := 0;
    end;
  end;

end;

Here's a screenshot of the from the Debug->Watch List window:

enter image description here

Ian Boyd
  • 246,734
  • 253
  • 869
  • 1,219
Steve F
  • 1,527
  • 1
  • 29
  • 55
  • 1
    Oh, finally we got [`to c`](http://stackoverflow.com/questions/26633391/how-to-assign-data-to-node-of-virtualstringtree-in-initnode-event/26636017#comment41883233_26636017) :) – TLama Nov 06 '14 at 16:28
  • @TLama :) Yes. What a long strange trip its been.. – Steve F Nov 06 '14 at 16:29

1 Answers1

5

To answer to the question in the title, I would say yes, your coding approach is wrong.

The mistake is that you don't save a pointer to your data record to the VT's node, you're allocating whole (separate) TTherapData record for each node! So the "mistake" is the line

vstRxList_Asg.NodeDataSize := SizeOf(TTherapData);

in the TfmPatient_Conslt.LoadTherapList method. What you probably want is an additional record which holds pointer to the data record:

type
  TVTNodeData = record
    TherapData: PTherapData;
  end;
  PVTNodeData = ^TVTNodeData;

and use that record as node data record:

vstRxList_Asg.NodeDataSize := SizeOf(TVTNodeData);

and node init becomes something like

procedure TfmPatient_Conslt.vstRxList_AsgInitNode(Sender: TBaseVirtualTree; ParentNode, Node: PVirtualNode; var InitialStates: TVirtualNodeInitStates);
var
  NodeData: PVTNodeData;
begin
  NodeData := Sender.GetNodeData(Node);

  while (FTherapDataList[FTherapDataListAsg_Iter]^.Selected <> 1) do
    Inc(FTherapDataListAsg_Iter);

  NodeData^.TherapData := FTherapDataList[FTherapDataListAsg_Iter];

  Inc(FTherapDataListAsg_Iter);
end;

and using the data in other tree events like

procedure TfmPatient_Conslt.vstRxList_AsgGetText(Sender: TBaseVirtualTree; Node:PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType; var CellText: string);
var
  NodeData: PVTNodeData;
  TherapData: PTherapData;
begin
  NodeData := Sender.GetNodeData(Node);
  if Assigned(NodeData) and Assigned(NodeData.TherapData) then begin
    TherapData := NodeData.TherapData;
    if (Column = 0) then
      CellText := TherapData^.TherapName
    else if (Column = 1) then
      CellText := TherapData^.TherapInstr;
  end;
end;
ain
  • 22,394
  • 3
  • 54
  • 74
  • 1
    Thanks. This has enlightened me for real. – Steve F Nov 06 '14 at 17:31
  • Do I need to free the memory allocated for the nodes (NodeData) in OnFreeNode event handler? If so, how? – Steve F Nov 06 '14 at 18:53
  • 1
    No, the VT manages memory of it's data record. However, VT doesn't finalize the user data record so if your node data record (TVTNodeData) would contain managed types like string or interface you would need to finalize those, ie set string fields to '' (empty string), interfaces to `nil` etc. But in your case there is only one pointer field which doesn't need any finalization (assuming you free the memory allocated for the `PTherapData` records when you free the `FTherapDataList`). – ain Nov 06 '14 at 20:13
  • Thanks, again. I am freeing PTherapData records when freeing the FTherapDataList. – Steve F Nov 06 '14 at 21:05