-2

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.
  • This is exactly the same issue as your last question. Same solution. Remove `MyString`, or keep it in sync with the other references. Consider the following code, where `x` and `y` are of type `Integer`. If I write `x := 1`, then `y := x`, and then `x := 2`, what value do you expect to see in `y`? – David Heffernan May 21 '18 at 16:04
  • I can't remove myString. In my real code it is a property of another object. How can I use this kind of construct (or another) to use a tEdit descendant to update a property of another object? – costateixeira May 21 '18 at 16:07
  • Then you need to keep the two instances synced. When you modify one, modify the other. I already said that at the dupe. Now, can you answer my question. Does `y` have the value 1 or 2? – David Heffernan May 21 '18 at 16:08
  • I just don't know how to do that - My idea is that TMyEdit automatically updates whatever instance it is associated with. I will have several TMyEdits for many TMyString properties - and I wanted to simply associate each TMyEdit with a property and with this ensure synchronicity to minimize code. – costateixeira May 21 '18 at 16:11
  • Please answer my question. I need to know whether you understand this or not. – David Heffernan May 21 '18 at 16:13
  • I did not understand the question (but i see it relates to passing references) . In the question you asked, I would expect y to be 1, but what I am trying to do is a way that y would be 2 - that is exactly my problem. – costateixeira May 21 '18 at 16:19
  • @DavidHeffernan While both this and the linked question are essentially tackling the same problem I would not consider them duplicates. In first question that you consider this one to be duplicate off OP is asking of how to link two objects so that one object has needed reference to another. But in this question OP is actually asking of how he can modify data from another object that is linked to the first one. And I must say that this second question is much more clear of what OP is trying to achieve. – SilverWarior May 21 '18 at 16:37
  • That shows that you understand the issue at your last question. And that you understand that my answer is correct even if you unaccepted it. This question probably isn't a duplicate I guess, but it's way too confusing for me to get in to. – David Heffernan May 21 '18 at 16:37
  • 1
    ... Any way if the question is reopened I can provide the Op with the needed answer in which I would explain exactly how he can achieve his goal. I have done what Op is trying to several times now and I'm even decided to call this approach of mine "Property forwarding". – SilverWarior May 21 '18 at 16:39
  • @Silver I agree. It's not a dupe because we now have the references pointing at the same instance. But it's still not a great question. – David Heffernan May 21 '18 at 16:40
  • @DavidHeffernan True it is not a great question but it is good enough so that it can be answered. Somehow I feel that OP programming knowledge is not good enough so he would be able to write a great question. – SilverWarior May 21 '18 at 16:44
  • @SilverWarior I did not know this required a novel approach. Too many years away from Delphi and programming in general, i guess. Looking forward. And yes, I do not know how to ask it better. Thanks both for trying to understand. Looking forward for some light. – costateixeira May 21 '18 at 16:51
  • @DavidHeffernan i unaccepted because I asked a question with more detail and it was considered a duplicate, which could indicate there was already a solution. The discussion was interesting and made me produce an example so cheers for that. – costateixeira May 21 '18 at 16:54
  • Never mind. I know how this pans out. – David Heffernan May 21 '18 at 19:03
  • For me to ask better next time: why would the question be downvoted? – costateixeira May 24 '18 at 20:23

1 Answers1

1

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.

SilverWarior
  • 7,372
  • 2
  • 16
  • 22
  • Thank you. This worked. I did need to add a couple properties to get the object, and not the string - so besides `function GetMyExternalObjectStr: String; procedure SetMyExternalObjectStr(AValue: String);` I had `function GetMyExternalObject: TMyExternalObject; procedure SetMyExternalObject(AValue: TMyExternalObject);` – costateixeira May 24 '18 at 18:09