Delphi firemonkey - Component that holds a referen

2019-09-22 05:13发布

I'm trying to have a TEditDescendant that contains a reference to another object (MyString), so that when I edit the TEdit text, MyString gets updated and vice-versa. This works when MyString is already existing. However, if I need MyEdit to be able to create a MyString, it does not work.

The code below is a minimal example:

  • btnCreateMyEdit creates the myEdit and gives it some text.
  • btnCreateMyString creates an instance of myString btnCheck shows the issues:

    • If btnCheck is created AFTER myString exists (i.e. after clicking btnCreateMyString), no issue.
    • If btnCheck is created Before myString exists (i.e. before clicking btnCreateMyString), it shows MyString was not created.

I'm using Delphi Tokyo.

unit Unit1;

interface

uses
  System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants,
  FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs, FMX.Controls.Presentation, FMX.Edit, FMX.StdCtrls;


type
  tMyString= class(TObject)
  private
  fvalue:string;
    property value : string read fvalue write fvalue;
  end;


  TMyEdit = class(TEdit)
    constructor Create(AOwner: TComponent); override;
  private
    fMyString: tMyString;
    fOnChange : TNotifyEvent;
    procedure MyOnChange(Sender : TObject);
  public
    procedure setMyString(prop:tMyString);
    property MyProperty: tMyString read fMyString write setMyString;
    procedure load;
    property OnChange : TNotifyEvent read fOnChange write fOnChange;
  end;

  TForm1 = class(TForm)
    Button1: TButton;
    Button2: TButton;
    Button3: TButton;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure Button3Click(Sender: TObject);
  end;

var
  Form1: TForm1;
  Myedit1:TMyEdit;
  Mystring:TMyString;

implementation

{$R *.fmx}

constructor TMyEdit.Create(AOwner: TComponent);
begin
 inherited Create(AOwner);
 Inherited OnChange := MyOnChange;
end;

procedure TMyEdit.MyOnChange(Sender : TObject);
begin
  if (text<>'') and (fMyString=nil) then fMyString:=TMyString.Create;
  fMystring.value:=self.text;
end;

procedure TMyEdit.load;
begin
  if fMyString<>nil then
  text:=fMyString.value;
end;


procedure TMyEdit.setMyString(prop:tMyString);
begin
  fMyString:=prop;
  load;
end;

procedure TForm1.Button1Click(Sender: TObject);

begin
  Myedit1:=TMyEdit.Create(Form1);
  MyEdit1.Parent:=Form1;
  Myedit1.MyProperty:=MyString;
  MyEdit1.Text:='any text';
  Myedit1.MyOnChange(self);
end;

procedure TForm1.Button2Click(Sender: TObject);
begin
  button2.Text:=Myedit1.myproperty.value;
  button2.Text:= Mystring.value;
end;

procedure TForm1.Button3Click(Sender: TObject);
begin
  mystring:=tmystring.Create;
  mystring.value:='123';
end;

end.

1条回答
一纸荒年 Trace。
2楼-- · 2019-09-22 05:52

When you wish to modify data from an external object that is linked to your object (you have a reference to it stored) I think it might be best to use approach which I named "Property forwarding".

Now basically what you do in such approach is to define a property in your base object that is making use of both getter and setter methods for reading and writing data. But instead of reading data from or writing data to some internal field as these getter and setter methods most commonly doe you actually define then in a way that instead they are reading data from or writing data to some external object either by directly accessing that objects field or use external object own property for accessing its data. I suggest using the latter because this way you make it easier to modify the external object without the need to modify every other object that is reading data from or writing data to this external object.

Here is short and hopefully well commented code example that will show you how such approach works:

type
  //Our external object for storing some data
  TMyExternalObject = class (TObject)
  private
    //Field for storing some string value
    fStrValue: String;
  public
    //Property for accessing the value of fStrValue field
    property StrValue: String read fStrValue write fStrValue;
  end;

  //Out base object that will use external object for storing additionall data
  TMyBaseObject = class (TObject)
  private
    //Field for storing reference to our external object
    fMyExternalObject: TMyExternalObject;
  protected
    //Getter method that we will use to forward some data from our external object
    function GetMyExternalObjectStr: string;
    //Setter method that we will use to store some data into our external object
    procedure SetMyExternalObjectStr(AValue: String);
  public
    //Modified constructor which accepts additional optional parameter so that we can
    //set the reference to our external object upon creation of our base class
    //If this parameter is omitted when calling constructor the default value of nil will
    //be set to AMyExternalObject
    constructor Create(AMyExternalObject: TMyExternalObject = nil);
    //Property declaration that uses custom made getter and setter methods
    property ExternalObjectStr: String read GetMyExternalObjectStr write SetMyExternalObjectStr;
  end;

implementation

{ TMyBaseObject }

//Modified constructor which can set fMyExternalObject reference to the object that is passed
//as constructor parameter
constructor TMyBaseObject.Create(AMyExternalObject: TMyExternalObject);
begin
  inherited Create;
  //Set the reference of external object to the object that was passed as AMyExternalObject parameter
  //If parameter was omitted the default value of nil which was defined in constructor will be used
  fMyExternalObject := AMyExternalObject;
end;

//Getter method used to forward data from our external object
function TMyBaseObject.GetMyExternalObjectStr: string;
begin
  //Always check to se if fMyExternalObject reference is set to point to existing object otherwise you
  //will cause AccessVialation by trying to read data from nonexistent object
  if fMyExternalObject <> nil then
  begin
    //Simply assign the returned value from our external object property to the result of the method
    result := fMyExternalObject.StrValue;
  end
  else
  begin
    //If fmyExternalObject field does not reference to an existing object you will sill want to return
    //some predefined result. Not doing so could cause code optimizer to remove this entire method from
    //the code before compilation.
    //Delphi should warn you about possibility that function might not have a defined result
    result := 'No external object attached';
  end;
end;

//Setter method used to store some data to external object.
//This method also takes care of creating and linking the external object if one hasn't been linked already
procedure TMyBaseObject.SetMyExternalObjectStr(AValue: String);
begin
  //Check to see if fMyExternalObject already references to an existing external object.
  //If it does not create external object and set fMyExgternalObject to point to it
  if fMyExternalObject = nil then
  begin
    //Create the external object and set fMyExternalObject field to point to it
    fMyExternalObject := TMyExternalObject.Create;
  end;
  //Write our data to external object
  fMyExternalObject.StrValue := AValue;
end;

Note this code example does not have proper error checking (would need several try..except blocks there. I purposely omitted them in order to make code more readable.

Also my code is written to work with classes and not components. So you will have to modify it to work with your derived TEdit component. So you will have to modify the constructor declaration in a way that it won't hide default parameters of TEdit's constructor.

NOTE: While my code example will allow you to have multiple TEdit boxes reading and modifying the same string value that is stored in external object it will not cause all of those TEdit boxes to automatically update their text when the external objects string value is changed. Thre reason for this is that my code example does not have any mechanism for notifying the other TEdit boxes to redraw themselves and thus show the new updated text.

For this you will have to design a special mechanism which will notify all of the TEdit components so that they need to update themselves. Such mechanism would also require your external object to store the reference to all the TEdit components that are linking to it. If you decide to go and implement such system pay special attention because such system would be causing circular referencing and could prevent Automatic Reference Counting to properly free up the objects when there are no longer needed.
Here it might not be bad to go read some more about component notification system and how it works. Why? Because the purpose of component notification system is to provide such functionality that allows you to notify multiple component of some events.

WARNING: Since the above code example is creating these external objects when necessary you will have to make sure there is also proper code for destroying those created external objects otherwise you risk leaking them.
Now if you have just one TEdit box connecting to such external object you can destroy it in TEdit destructor. But if you plan on connection multiple TEdit components to same external object you will have to devise some other mechanism to track the life of these external objects.

I hope my answer will come helpful to you.

Any way I recommend you read more about using of getter and setter methods. They can be pretty powerful when used correctly.

PS: This approach is no novelty. I'm pretty sure it was used many times before. Also the name "Property forwarding" is how I named it. It is quite possible that it has some different name that I don't know off.

查看更多
登录 后发表回答