Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Access violation assigning autocomplete strings to

I am modifying the edit control with autocomplete from here: Auto append/complete from text file to an edit box delphi

I want to load autocomplete strings from DB. I declared new properties on autocomplete control descendant:

FACDataSource : TDataSource;
FACFieldName : string;

I call this to load autocomplete strings:

procedure TAutoCompleteEdit.ReadSuggestions;
begin
  FAutoCompleteSourceList.Clear;

  if (not Assigned(FACDataSource)) or (not Assigned(FACDataSource.DataSet))       or (not ACEnabled) then
      exit;

    with FACDataSource.DataSet do
    begin
       if Active and (RecordCount > 0) and (FACFieldName <> '') then
       begin
         First;
         while not EOF do
         begin
                       FAutoCompleteSourceList.Add(FACDataSource.DataSet.FieldByName(FACFieldName).AsString);
    Next;
  end;
  if FAutoCompleteSourceList.Count > 0 then
    ACStrings := FAutoCompleteSourceList;
end;

end; end;

However, I get AccessViolation when assigning FAutoCompleteSourceList to ACStrings. The setter for ACStrings is:

procedure TAutoCompleteEdit.SetACStrings(const Value: TStringList);
begin
    if Value <> FACList.FStrings then
      FACList.FStrings.Assign(Value);
end;

I get AccessViolation in the line: FACList.FStrings.Assign(Value); (READ of address XXXYYY). Value is defined and not garbage at that point (e.g. in I can view the string list in the debugger). 'FStrings' is an empty stringlist.

It works fine when the control is dropped on the form. But doesn't if I place it within a custom inplace editor shown when user enters a DBGridEH cell.

The inplace editor is like this:

    unit UInplaceAutoCompleteEditor;

    interface
    uses UDBAutoComplete, UMyInplaceEditor, classes, windows, Controls, Buttons,     DB;

    type TInplaceAutoCompleteEditor = class(TMyInplaceEditor)
      private
       FEditor : TAutoCompleteEdit;
    FButton : TSpeedButton;
    FShowButton : boolean;
    procedure SetShowButton(value : boolean);
    public
        constructor Create(AOwner : TComponent); override;
        procedure SetFocus; override;
        destructor Destroy; override;
     protected
    procedure EditorKeyDown(Sender : TObject; var Key : Word; Shift : TShiftState);
    function GetACDataSource : TDataSource;
    procedure SetACDataSource(value : TDataSource);
    function GetACFieldName : string;
    procedure SetACFieldName(value : string);
    procedure SetACEnabled(value : boolean);
    function GetACEnabled : boolean;
  published
    property Editor : TAutoCompleteEdit read FEditor;
    property ACDataSource : TDataSource read GetACDataSource write SetACDataSource;
    property ACFieldName : string read GetACFieldName write SetACFieldName;
    property ACEnabled : boolean read GetACEnabled write SetACEnabled;
    property Button : TSpeedButton read FButton;
    property ShowButton : boolean read FShowButton write SetShowButton;
end;

  procedure Register;

implementation

  procedure Register;
  begin
    RegisterComponents('nikolaev', [ TInplaceAutoCompleteEditor ]);
  end;

{ TInplaceAutoCompleteEditor }



constructor TInplaceAutoCompleteEditor.Create(AOwner: TComponent);
begin
  inherited;
  FEditor := TAutoCompleteEdit.Create(self);
  FEditor.Parent := self;
  FEditor.Align := alClient;
  FEditor.Visible := true;
  FEditor.WantTabs := true;
  FEditor.OnKeyDown := EditorKeyDown;

  FButton := TSpeedButton.Create(self);
  FButton.Parent := self;
  FButton.Align := alRight;

  self.FOwnHeight := -1;
  self.FOwnWidth := -1;

  SetShowButton(false);
end;

destructor TInplaceAutoCompleteEditor.Destroy;
begin
  Feditor.Destroy;
  FButton.Destroy;
  inherited;
end;

procedure TInplaceAutoCompleteEditor.EditorKeyDown(Sender: TObject;
  var Key: Word; Shift: TShiftState);
begin
  if Key in [ VK_Return, VK_Tab ] then
  begin
    self.Value := FEditor.Text;
    Key := 0;
    ConfirmValue;
  end;

  if Key = VK_Escape then
  begin
    Key := 0;
    CancelValue;
  end;

  inherited;
end;



function TInplaceAutoCompleteEditor.GetACDataSource: TDataSource;
begin
  Result := FEditor.ACDataSource;
end;

function TInplaceAutoCompleteEditor.GetACEnabled: boolean;
begin
  Result := FEditor.ACEnabled;
end;

function TInplaceAutoCompleteEditor.GetACFieldName: string;
begin
  Result := FEditor.ACFieldName
end;

procedure TInplaceAutoCompleteEditor.SetACDataSource(value: TDataSource);
begin
  FEditor.ACDataSource := value;
end;

procedure TInplaceAutoCompleteEditor.SetACEnabled(value: boolean);
begin
  FEditor.ACEnabled := value;
end;

procedure TInplaceAutoCompleteEditor.SetACFieldName(value: string);
begin
  FEditor.acfieldname := value;
end;

procedure TInplaceAutoCompleteEditor.SetFocus;
begin
  inherited;
  FEditor.SetFocus;
end;

procedure TInplaceAutoCompleteEditor.SetShowButton(value: boolean);
begin
  if value <> FShowButton then
  begin
    FShowButton := value;
    FButton.Visible := value;
  end;
end;

end.

This inplace editor inherits from an abstract class like this:

unit UMyInplaceEditor;

interface
uses Windows, classes, types, dbGridEh, ExtCtrls, Controls;

type TMyInplaceEditor = class (TWinControl)

  private
    FOnValueConfirmed : TNotifyEvent;
    FOnCanceled : TNotifyEvent;
    FWantTabs : boolean;
    procedure AdjustPosition;
  protected
    FOwnHeight, FOwnWidth : integer;
    FValue : Variant;
    function GetIsEditing : boolean;
    procedure SetIsEditing(value : boolean); virtual;
    procedure ConfirmValue;
    procedure CancelValue;
    procedure SetValue(val : Variant); virtual;
  public

    property OnValueConfirmed : TNotifyEvent read FOnValueConfirmed write FOnValueConfirmed;
    property OnCanceled : TNotifyEvent read FOnCanceled write FOnCanceled;
    property Value : Variant read FValue write SetValue;


    property IsEditing : boolean read GetIsEditing write SetIsEditing;
    procedure SetPosition(parentControl : TWinControl; rect : TRect); virtual;

    function ColumnEditable(column : TColumnEH) : boolean; virtual;

    constructor Create(AOwner : TComponent); override;
    property WantTabs : boolean read FWantTabs write FWantTabs;
end;
  procedure Register;

implementation

  procedure Register;
  begin
    RegisterComponents('nikolaev', [TMyInplaceEditor]);
  end;

constructor TMyInplaceEditor.Create(AOwner : TComponent);
begin
  inherited Create(AOwner);
  self.AutoSize := false;
  self.Visible := false;
  self.FOwnHeight := -1;
  self.FOwnWidth := -1;
end;

procedure TMyInplaceEditor.AdjustPosition;
var xOffset, yOffset : integer;
begin
  xoffset := self.Left + self.Width - self.Parent.Width;
  if xOffset > 0 then
    self.Left := self.Left - xOffset;

  yOffset := self.Top + self.Height - self.Parent.height;
  if yOffset > 0 then
    self.Top := self.Top - yOffset;

end;

function TMyInplaceEditor.GetIsEditing : boolean;
begin
  Result := self.Visible;
end;

procedure TMyInplaceEditor.SetIsEditing(value: Boolean);
begin
  self.Visible := value;
  self.BringToFront;
  {if Visible then
    self.SetFocus;}
end;

procedure TMyInplaceEditor.SetPosition(parentControl : TWinControl; rect: TRect);
begin
  self.Parent := parentControl;
  self.Top := rect.Top;//parentControl.Top;
  self.Left := rect.Left;//parentControl.left;
  if self.FOwnWidth = -1 then
    self.Width := rect.Right - rect.Left
  else
    self.Width := self.FOwnWidth;

    if self.FOwnHeight = -1 then
    self.Height := rect.Bottom - rect.Top
  else
    self.Height := self.FOwnHeight;
  AdjustPosition;
end;

function TMyInplaceEditor.ColumnEditable(column : TColumnEH) : boolean;
begin
  Result := true;
end;

procedure TMyInplaceEditor.ConfirmValue;
begin
  if Assigned(FOnValueConfirmed) then
    FOnValueConfirmed(self);
end;

procedure TMyInplaceEditor.CancelValue;
begin
  if Assigned(FOnCanceled) then
    FOnCanceled(self);
end;

procedure TMyInplaceEditor.SetValue(val : Variant);
begin
  FValue := val;
end;

end.

The InplaceEditor is used in a descendant from DBGridEH. I override ShowEditor and HideEditor to show / hide my editor in certain cases.

Again, the autocomplete control only throws exception when embedded in the inplaceeditor control.

What causes access violation?

like image 471
AunAun Avatar asked Jan 21 '15 10:01

AunAun


1 Answers

The problem is that the code you are using mis-handles interface reference counting. Here are the relevant extracts:

type
  TEnumString = class(TInterfacedObject, IEnumString)
  ....

Note that this class is derived from TInterfacedObject and so it manages its lifetime using reference counting.

Then the code goes on like this:

type
  TAutoCompleteEdit = class(TEdit)
  private
    FACList: TEnumString;
  ....

So we are going to hold a reference to the object rather than the interface. That looks dubious already.

Then we do this:

constructor TAutoCompleteEdit.Create(AOwner: TComponent);
begin
  inherited;
  FACList := TEnumString.Create;
  ....
end;

destructor TAutoCompleteEdit.Destroy;
begin
  FACList := nil;
  inherited;
end;

There's nothing here to keep the object alive. At other points in the code we take a reference to the IEnumString interface. But then as soon as that reference is released, the object thinks that there are no references left. And so it is deleted. Then, later on, the code refers to FACList which now points at an object that has been destroyed.


A simple way to fix this would be to make sure that the TAutoCompleteEdit control always holds a reference to the interface:

type
  TAutoCompleteEdit = class(TEdit)
  private
    FACList: TEnumString;
    FEnumString: IEnumString;
....
constructor TAutoCompleteEdit.Create(AOwner: TComponent);
begin
  inherited;
  FACList := TEnumString.Create;
  FEnumString := FACList;
  ....
end;

And with this change you can then remove the destructor for TAutoCompleteEdit since the object behind FEnumString will get destroyed by the reference counting mechanism.


Another way to fix this would be to change TEnumString to disable automatic reference counting. That would look like this:

type
  TEnumString = class(TObject, IInterface, IEnumString)
  private
    function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
    function _AddRef: Integer; stdcall;
    function _Release: Integer; stdcall;
    ....
  end;

function TEnumString.QueryInterface(const IID: TGUID; out Obj): HResult;
begin
  if GetInterface(IID, Obj) then
    Result := 0
  else
    Result := E_NOINTERFACE;
end;

function TEnumString._AddRef: Integer; 
begin
  Result := -1;
end;

function TEnumString._Release: Integer; 
begin
  Result := -1;
end;

And then you'd need the TAutoCompleteEdit destructor to look like this:

destructor TAutoCompleteEdit.Destroy;
begin
  FACList.Free;
  inherited;
end;

And a final option would be to avoid holding a TEnumString at all and instead only hold an IEnumString reference. Let the reference counting manage lifetime as in the first solution. But then you'd need to implement another interface that allowed the TAutoCompleteEdit to obtain the TStrings object.

like image 163
David Heffernan Avatar answered Sep 27 '22 00:09

David Heffernan