Can the object of (TObjectList) know when some values of (TMyObject) was changed?
Some example:
TMyObject = class
oName: string;
end;
TMyObjectList = class(TObjectList<TMyObject>)
end;
procedure Form1.Button1.Click(Sender: TObject);
var
Obj: TMyObject;
List: TMyObjectList;
Begin
List:= TMyObjectList.Create;
Obj:= TMyObject.Create;
List.Add(Obj);
List[0].oName:= 'Test'; // here a want to know from var (List) when this object (Obj or List[0]) changed his value..
end;
Thanks for any help.
I just added the TObservableList<T> type to Spring4D (feature/observablelist branch). It is mostly modeled after .NET and uses the INotifyPropertyChanged interface to attach its event handler to any objects that support it. This class has been part of DSharp for quite some time and is used in production. It might change a bit in the future and become full part of the library.
Here is a small example how to use it so you get an idea:
program Project60;
{$APPTYPE CONSOLE}
uses
Spring,
Spring.Collections,
SysUtils;
type
TNotifyPropertyChangedBase = class(TInterfaceBase, INotifyPropertyChanged)
private
fOnPropertyChanged: Event<TPropertyChangedEvent>;
function GetOnPropertyChanged: IPropertyChangedEvent;
protected
procedure PropertyChanged(const propertyName: string);
end;
TMyObject = class(TNotifyPropertyChangedBase)
private
fName: string;
procedure SetName(const Value: string);
public
property Name: string read fName write SetName;
end;
TMain = class
procedure ListChanged(Sender: TObject; const item: TMyObject;
action: TCollectionChangedAction);
end;
{ TNotifyPropertyChangedBase }
function TNotifyPropertyChangedBase.GetOnPropertyChanged: IPropertyChangedEvent;
begin
Result := fOnPropertyChanged;
end;
procedure TNotifyPropertyChangedBase.PropertyChanged(
const propertyName: string);
begin
fOnPropertyChanged.Invoke(Self,
TPropertyChangedEventArgs.Create(propertyName) as IPropertyChangedEventArgs);
end;
{ TMyObject }
procedure TMyObject.SetName(const Value: string);
begin
fName := Value;
PropertyChanged('Name');
end;
{ TMain }
procedure TMain.ListChanged(Sender: TObject; const item: TMyObject;
action: TCollectionChangedAction);
begin
case action of
caAdded: Writeln('item added ', item.Name);
caRemoved, caExtracted: Writeln('item removed ', item.Name);
caChanged: Writeln('item changed ', item.Name);
end;
end;
var
main: TMain;
list: IList<TMyObject>;
o: TMyObject;
begin
list := TCollections.CreateObservableList<TMyObject>;
list.OnChanged.Add(main.ListChanged);
o := TMyObject.Create;
o.Name := 'o1';
list.Add(o);
o := TMyObject.Create;
o.Name := 'o2';
list.Add(o);
list[1].Name := 'o3';
Readln;
end.
There is nothing built in that can do what you ask. You will need to implement a notification mechanism yourself. This is the classic scenario for the Observer Pattern.
There are many implementations of this pattern already in existence. One obvious choice would be to use the implementation in Spring4D. Nick Hodges recent book, More Coding in Delphi, includes a chapter on this pattern which I would recommend.
Found the way, how to call method of TObjectList from TMyObject. Using TNotifyEvent in base Object.
Example:
TMyClass = class(TObject)
private
FName: string;
FOnNameEvent: TNotifyEvent;
procedure SetName(value: string);
public
property Name: string read FName write SetName;
property OnNameEvent: TNotifyEvent read FOnNameEvent write FOnNameEvent;
end;
procedure TMyClass.SetName(value: string);
begin
FName := value;
if Assigned(FOnNameEvent) then
FOnNameEvent(Self);
end;
procedure MyNameEvent(Sender: TObject);
var
i: Integer;
begin
for i := 0 to MyListOfMyClassObjects.Count -1 do
if Sender = MyListOfMyClassObjects.Item[i] then
begin
MessageBox(0, PChar(TMyClass(MyListOfMyClassObjects.Item[i]).Name), nil, MB_OK);
break;
end;
end;
procedure MyProc;
var
MyObject: TMyClass;
begin
MyObject := TMyClass.Create;
MyObject.OnNameEvent := MyNameEvent;
MyListOfMyClassObjects.Add(MyObject);
end;
Related
I modified #Stefan Glienkes example from Notify the TObjectList when Object changed to use IList, since I am using interfaced objects in my list.
In the event handler, I can handle caAdded and caRemoved events, but caChanged is not signaled.
Is this by design or am I making a mistake somewhere?
This example shows the behavior:
program Project61;
{$APPTYPE CONSOLE}
uses
Spring,
Spring.Collections,
SysUtils;
type
TNotifyPropertyChangedBase = class(TInterfaceBase, INotifyPropertyChanged)
private
fOnPropertyChanged: Event<TPropertyChangedEvent>;
function GetOnPropertyChanged: IPropertyChangedEvent;
protected
procedure PropertyChanged(const propertyName: string);
end;
IMyInterface = interface(IInterface)
['{D5966D7D-1F4D-4EA8-B196-CB9B39AF446E}']
function GetName: String;
procedure SetName(const Value: String);
property Name: String read GetName write SetName;
end;
TMyObject = class(TNotifyPropertyChangedBase, IMyInterface)
private
FName: string;
function GetName: string;
procedure SetName(const Value: string);
public
property Name: string read GetName write SetName;
end;
TMain = class
procedure ListChanged(Sender: TObject; const item: IMyInterface;
action: TCollectionChangedAction);
end;
{ TNotifyPropertyChangedBase }
function TNotifyPropertyChangedBase.GetOnPropertyChanged: IPropertyChangedEvent;
begin
Result := fOnPropertyChanged;
end;
procedure TNotifyPropertyChangedBase.PropertyChanged(
const propertyName: string);
begin
fOnPropertyChanged.Invoke(Self,
TPropertyChangedEventArgs.Create(propertyName) as IPropertyChangedEventArgs);
end;
{ TMyObject }
procedure TMyObject.SetName(const Value: string);
begin
FName := Value;
PropertyChanged('Name');
end;
function TMyObject.GetName: string;
begin
Result := FName;
end;
{ TMain }
procedure TMain.ListChanged(Sender: TObject; const item: IMyInterface;
action: TCollectionChangedAction);
begin
case action of
caAdded:
Writeln('item added ', item.Name);
caRemoved, caExtracted:
Writeln('item removed ', item.Name);
caChanged:
Writeln('item changed ', item.Name);
end;
end;
var
main: TMain;
list: IList<IMyInterface>;
o : IMyInterface;
begin
list := TCollections.CreateList<IMyInterface>;
list.OnChanged.Add(main.ListChanged);
o := TMyObject.Create;
o.Name := 'o1';
list.Add(o); // triggering caAdded
o := TMyObject.Create;
o.Name := 'o2';
list.Add(o); // triggering caAdded
list[1].Name := 'o3'; // not triggering caChanged
list.Remove(o); // triggering caRemoved
Readln;
end.
The lists created by TCollections.CreateList, TCollections.CreateObjectList or TCollections.CreateInterfaceList don't support INotifyPropertyChanged.
You see that TCollections.CreateObservableList which I used in my example is contraint to only hold objects as these are typically candidates for implementing property change notification as PODOs are imo usually bad candidates to be used as interfaces.
You can probably still code your own version of that list that accepts interfaces and queries them for INotifyPropertyChanged.
I have a TRttiProperty variable named aRttiProperty, that points to the property below:
Tsubscription = class(TMyObject)
private
fBilling: TMyObject;
public
property billing: TMyObject read fBilling; // << aRttiProperty point to this member
end;
Now, how can I extract the fBilling object pointer from aRttiProperty?
I try to do it like this, but it is not working:
function Tsubscription.getfBillingObj(const aRttiProperty: TRttiProperty): TMyObject
begin
Result := aRttiProperty.GetValue(Self).AsType<TMyObject>;
end;
It's returning the parent TSubscription object instead of the fbilling field object.
The code you showed in your question is perfectly fine (provided you fix your Tsubscription class declaration to include the getfBillingObj() method). The getfBillingObj() code you showed returns the correct object pointer, as demonstrated by the following code:
uses
System.Rtti;
type
TMyObject = class
public
Name: string;
constructor Create(const aName: string);
end;
Tsubscription = class(TMyObject)
private
fBilling: TMyObject;
public
constructor Create(const aName: string);
destructor Destroy; override;
function getfBillingObj(const aRttiProperty: TRttiProperty): TMyObject;
property billing: TMyObject read fBilling;
end;
constructor TMyObject.Create(const aName: string);
begin
inherited Create;
Name := aName;
end;
constructor Tsubscription.Create(const aName: string);
begin
inherited Create(aName);
fBilling := TMyObject.Create('bill');
end;
destructor Tsubscription.Destroy;
begin
fBilling.Free;
end;
function Tsubscription.getfBillingObj(const aRttiProperty: TRttiProperty): TMyObject;
begin
Result := aRttiProperty.GetValue(Self).AsType<TMyObject>;
end;
var
Ctx: TRttiContext;
prop: TRttiProperty;
sub: Tsubscription;
bill: TMyObject;
begin
sub := Tsubscription.Create('sub');
try
prop := ctx.GetType(Tsubscription).GetProperty('billing');
bill := sub.getfBillingObj(prop);
// bill.Name is 'bill' as expected...
finally
sub.Free;
end;
end;
That being said, it is not necessary to use RTTI in this situation since TSubscription has direct access to its own internal fields:
function TSubscription.getfBillingObj: TMyObject
begin
Result := fBilling;
end;
But even that is redundant since the billing property is public. Any caller can just use the billing property as-is:
var
sub: Tsubscription;
bill: TMyObject;
begin
sub := Tsubscription.Create('sub');
try
bill := sub.billing;
// bill.Name is 'bill' as expected...
finally
sub.Free;
end;
end;
I have class with 2 events: OnConnect and OnDisconnect:
type
TEvent = reference to procedure;
TConnection = class
private
fOnConnect: TEvent;
fOnDisconnect: TEvent;
public
procedure SomeBehavior(aChoice: Boolean);
property OnConnect: TEvent read fOnConnect write fOnConnect;
property OnDisconnect: TEvent read fOnDisconnect write fOnDisconnect;
end;
implementation
{ TConnection }
procedure TConnection.SomeBehavior(aChoice: Boolean);
begin
if aChoice then
fOnConnect
else
fOnDisconnect;
//im not cheacking Assign(Events) to make example simple
end;
now I would like to do same thing but in more object style.
I mean use interfaces and observer pattern from String4D. And i made this:
interface
uses
Spring.DesignPatterns;
type
IObserver = interface
procedure ReactToConnect(aText: String);
procedure ReactToDisconnect(aTimeoutInMs: Integer);
end;
IConnection<T> = interface(IObservable<IObserver>)
procedure SomeBehavior(aChoice: Boolean);
end;
implementation
uses
System.SysUtils;
type
TConnection = class(TObservable<IObserver>, IConnection<IObserver>)
public
procedure SomeBehavior(aChoice: Boolean);
end;
{ TConnection }
procedure TConnection.SomeBehavior(aChoice: Boolean);
var
procOnConnect: TProc<IObserver>;
procOnDisconnect: TProc<IObserver>; // what if i want no parameters?
someText: String;
someNumber: Integer;
begin
someText := RandomText;
procOnConnect := procedure(aObserver: IObserver)
begin
aObserver.ReactToConnect(someText);
end;
someNumber := RandomInt;
procOnDisconnect := procedure(aObserver: IObserver)
begin
aObserver.ReactToDisconnect(someNumber);
end;
if aChoice then
Self.NotifyListeners(procOnConnect)
else
Self.NotifyListeners(procOnDisconnect);
end;
im doing it fisrt time and just want to ask if its proper way? or im doing somethink heretical here?
I have a Delphi generic class that exposes a function with an argument of the generic type. Inside this function, I need to pass an instance of the generic type on to another object expecting a Variant type. Similar to this:
type
IMyInterface = interface
DoStuff(Value: Variant);
end;
TMyClass<T> = class
FMyIntf: IMyInterface
procedure DoStuff(SomeValue: T);
end;
[...]
procedure MyClass<T>.DoStuff(SomeValue: T);
begin
FMyIntf.DoStuff((*convert SomeValue to Variant here*));
end;
I tried using Rtti.TValue.From(SomeValue).AsVariant. This worked for integral types, but blew up for Booleans. I don't quite see why, since normally I'd be able to assign a Boolean value to a Variant...
Is there a better way to make this conversion? I only need it to work for simple built-in types (excluding enumerations and records)
I think there is no direct way to convert generic type to variant because variant cannot hold all the possible types. You must write your specific conversion routine. E.g.:
interface
//...
type
TDemo = class
public
class function GetAsVariant<T>(const AValue: T): Variant;
end;
//...
implementation
uses
Rtti,
TypInfo;
//...
{ TDemo}
class function TDemo.GetAsVariant<T>(const AValue: T): Variant;
var
val: TValue;
bRes: Boolean;
begin
val := TValue.From<T>(AValue);
case val.Kind of
tkInteger: Result := val.AsInteger;
tkInt64: Result := val.AsInt64;
tkEnumeration:
begin
if val.TryAsType<Boolean>(bRes) then
Result := bRes
else
Result := val.AsOrdinal;
end;
tkFloat: Result := val.AsExtended;
tkString, tkChar, tkWChar, tkLString, tkWString, tkUString:
Result := val.AsString;
tkVariant: Result := val.AsVariant
else
begin
raise Exception.Create('Unsupported type');
end;
end;
end;
Because TValue.AsVariant handles most of the type conversions internally, this function can be simplified. I will handle enumerations in case you could need them later:
class function TDemo.GetAsVariant<T>(const AValue: T): Variant;
var
val: TValue;
begin
val := TValue.From<T>(AValue);
case val.Kind of
tkEnumeration:
begin
if val.TypeInfo = TypeInfo(Boolean) then
Result := val.AsBoolean
else
Result := val.AsOrdinal;
end
else
begin
Result := val.AsVariant;
end;
end;
Possible usage:
var
vValue: Variant;
begin
vValue := TDemo.GetAsVariant<Boolean>(True);
Assert(vValue = True); //now vValue is a correct Boolean
Looks like in my Delphi version 10.2 the Boolean problem is gone and TValue.From<T>(FValue).AsVariant is enough.
Here an example with some other helpful things like comparing the generic type:
TMyValue<T> = class(TPersistent)
private
FValue: T;
procedure SetValue(const AValue: T);
function GetAsVariant: Variant; override;
public
procedure Assign(Source: TPersistent); override;
property Value: T read FValue write SetValue;
property AsVariant: Variant read GetAsVariant;
end;
function TMyValue<T>.GetAsVariant: Variant;
begin
Result:= TValue.From<T>(FValue).AsVariant;
end;
procedure TMyValue<T>.SetValue(const AValue: T);
begin
if TEqualityComparer<T>.Default.Equals(AValue, FValue) then Exit;
FValue:= AValue;
//do something
end;
procedure TMyValue<T>.Assign(Source: TPersistent);
begin
if Source is TMyValue<T> then Value:= (Source as TMyValue<T>).Value
else inherited;
end;
Another way (tested XE10)
Var
old : variant;
val : TValue;
Begin
val := TValue.FromVariant(old);
End;
Lets say I have a Treeview, and it contains items with Object pointers. How can I determine from the selected item what the Object is, so I can access it?
Here is a basic example of some classes and code to give an idea:
Note: TChildObject1 and TChildObject2 inherit from TMyObject.
type
TMyObject = class
private
FName: string;
public
property Name: string read FName write FName;
constructor Create(aName: string);
end;
type
TChildObject1 = class(TMyObject)
private
FSomeString: string;
public
property SomeString: string read FSomeString write FSomeString;
constructor Create(aName: string);
destructor Destroy; override;
end;
type
TChildObject2 = class(TMyObject)
private
FSomeInteger: integer;
public
property SomeInteger: integer read FSomeInteger write FSomeInteger;
constructor Create(aName: string);
destructor Destroy; override;
end;
Lets say they were created and added to a TTreeview like so:
procedure NewChild1(aTreeView: TTreeView; aName: string);
var
Obj: TChildObject1;
begin
Obj := TChildObject1.Create(aName);
try
aTreeView.Items.AddObject(nil, Obj.Name, Obj);
finally
Obj.Free;
end;
end;
procedure NewChild2(aTreeView: TTreeView; aName: string);
var
Obj: TChildObject2;
begin
Obj := TChildObject2.Create(aName);
try
aTreeView.Items.AddObject(nil, Obj.Name, Obj);
finally
Obj.Free;
end;
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
// add the items to the tree
NewChild1(TreeView1, 'Child Object 1');
NewChild2(TreeView1, 'Child Object 2');
end;
Now, when I select a Node in the Treeview, how can I determine which Object class the pointer leads to? I tried this, which is not working:
Note: This does not error, but it does not return the correct value (ie, does not pick up the correct object)
procedure TForm1.TreeView1Click(Sender: TObject);
var
Obj: TMyObject;
begin
if TreeView1.Selected <> nil then
begin
Obj := TMyObject(TreeView1.Selected.Data);
if Obj is TChildObject1 then
begin
Edit1.Text := 'this node is a child1 object';
end else
if Obj is TChildObject2 then
begin
Edit1.Text := 'and this node is child 2 object';
end;
end;
end;
I could do it something like below, but I don't think is the right way, it means a lot of checking, declaring, assigning etc.
procedure TForm1.TreeView1Click(Sender: TObject);
var
ChildObj1: TChildObject1;
ChildObj2: TChildObject2;
begin
if TreeView1.Selected <> nil then
begin
if TreeView1.Selected.Text = 'Child Object 1' then
begin
ChildObj1 := TreeView1.Selected.Data;
Edit1.Text := ChildObj1.SomeString;
end else
if TreeView1.Selected.Text = 'Child Object 2' then
begin
ChildObj2 := TreeView1.Selected.Data;
Edit1.Text := IntToStr(ChildObj2.SomeInteger);
end;
end;
end;
Tips and advice appreciated.
The main problem is which you are freeing the memory of the object that you are adding to the treeview. So the data of the nodes points to a invalid location.
To assign the objects to a node use a code like this
Obj := TChildObject1.Create(aName);
aTreeView.Items.AddObject(nil, Obj.Name, Obj);
and when you need dispose the data you can call the Free method for each node.
for i:= 0 to TreeView1.Items.Count - 1 do
begin
Obj:= TMyObject(TreeView1.Items.Item[i].Data);
if Assigned(Obj) then
Obj.Free;
end;