Reputation: 796
I have my own component (TNiftyRVFrameWithPopups) with a TOwnedCollection as a property (TagList).
Every time I add items to TagList the same item should be added to another object (FMenu). This is performed by the procedure RefreshMenu
called from TNiftyRVFrameWithPopups.Loaded
on design time.
My issue is I cannot add items on runtime, because TNiftyRVFrameWithPopups.Loaded
is not called.
I thought one solution would be Postmessage
but I didn't manage to make it work.
The following is the source:
TNiftyListTag = class(TCollectionItem)
private
FTagValue: string;
FDisplayTextTag: string;
public
procedure Assign(Source: TPersistent); override;
published
property DisplayTag: string read FDisplayTextTag write FDisplayTextTag;
property Value: string read FTagValue write FTagValue;
end;
TNiftyListTags = class(TOwnedCollection)
protected
function GetItem(Index: Integer): TNiftyListTag;
procedure SetItem(Index: Integer; Value: TNiftyListTag);
public
constructor Create(AOwner: TPersistent; ItemClass: TCollectionItemClass);
function Add: TNiftyListTag;
end;
TNiftyRVFrameWithPopups = class(TRVEditFrame)
private
FMenu: TAdvSmoothListBox;
FMenuList: TStringList;
FCollectionTags: TNiftyListTags;
procedure SetCollectionTags(const Value: TNiftyListTags);
procedure RefreshMenu;
public
{ Public declarations }
constructor Create(AOwner: TComponent); override;
procedure Loaded; override;
published
property TagList: TNiftyListTags read FCollectionTags write SetCollectionTags;
end;
implementation
constructor TNiftyRVFrameWithPopups.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FMenuList := TStringList.Create;
FCollectionTags := TNiftyListTags.Create(Self, TNiftyListTag);
end;
procedure TNiftyRVFrameWithPopups.SetCollectionTags(const Value: TNiftyListTags);
begin
FCollectionTags.Assign(Value);
end;
procedure TNiftyRVFrameWithPopups.RefreshMenu;
var
i: Integer;
begin
FMenu.Items.Clear;
for i := 0 to FCollectionTags.Count - 1 do
begin
FMenu.Items.Add;
FMenu.Items.Items[i].Caption := FCollectionTags.Items[i].FDisplayTextTag;
end;
end;
procedure TNiftyRVFrameWithPopups.Loaded;
begin
inherited Loaded;
if Assigned(FRVEditor) then
begin
RefreshMenu;
end;
end;
{ TNiftyListTag }
procedure TNiftyListTag.Assign(Source: TPersistent);
begin
if Source is TNiftyListTag then
begin
FTagValue := TNiftyListTag(Source).FTagValue;
FDisplayTextTag := TNiftyListTag(Source).FDisplayTextTag;
end
else
inherited;
end;
{ TNiftyListTags }
function TNiftyListTags.Add: TNiftyListTag;
begin
Result := TNiftyListTag(inherited Add);
end;
constructor TNiftyListTags.Create(AOwner: TPersistent; ItemClass: TCollectionItemClass);
begin
inherited Create(AOwner, ItemClass);
end;
procedure TNiftyListTags.SetItem(Index: Integer; Value: TNiftyListTag);
begin
inherited SetItem(index, Value);
end;
function TNiftyListTags.GetItem(Index: Integer): TNiftyListTag;
begin
Result := TNiftyListTag(inherited GetItem(Index));
end;
EDIT
After Deltics' advice I have amended my code:
TNiftyListTag = class(TCollectionItem)
private
FTagValue: string;
FDisplayTextTag: string;
public
procedure Assign(Source: TPersistent); override;
published
property DisplayTag: string read FDisplayTextTag write FDisplayTextTag;
property Value: string read FTagValue write FTagValue;
end;
TNiftyListTags = class(TOwnedCollection)
private
fOnChanged: TNotifyEvent;
procedure DoOnChanged;
protected
function GetItem(Index: Integer): TNiftyListTag;
procedure SetItem(Index: Integer; Value: TNiftyListTag);
public
constructor Create(AOwner: TPersistent; ItemClass: TCollectionItemClass);
function Add: TNiftyListTag;
procedure AppendItem(const aDisplayText, aTag: string);
end;
TNiftyRVFrameWithPopups = class(TRVEditFrame)
private
FMenu: TAdvSmoothListBox;
FMenuList: TStringList;
FCollectionTags: TNiftyListTags;
procedure RefreshMenu;
procedure SetCollectionTags(const Value: TNiftyListTags);
public
{ Public declarations }
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
procedure Loaded; override;
published
property TagList: TNiftyListTags read FCollectionTags write SetCollectionTags;
end;
implementation
constructor TNiftyRVFrameWithPopups.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FMenuList := TStringList.Create;
FCollectionTags := TNiftyListTags.Create(Self, TNiftyListTag);
FCollectionTags.fOnChanged := RefreshMenu;
end;
destructor TNiftyRVFrameWithPopups.Destroy;
begin
FreeAndNil(FMenuList);
FCollectionTags.Free;
inherited;
end;
procedure TNiftyRVFrameWithPopups.RefreshMenu;
var
i: Integer;
begin
FMenu.Items.Clear;
for i := 0 to FCollectionTags.Count - 1 do
begin
FMenu.Items.Add;
FMenu.Items.Items[i].Caption := FCollectionTags.Items[i].FDisplayTextTag;
end;
end;
procedure TNiftyRVFrameWithPopups.Loaded;
begin
inherited Loaded;
RefreshMenu(Self);
end;
procedure TNiftyRVFrameWithPopups.RefreshMenu;
var
i: Integer;
begin
if Assigned(FRVEditor) then
begin
(FRVEditor as TCustomRichViewEdit).OnRVMouseUp := OnMouseUp;
FMenu.Parent := FRVEditor;
fmenu.Items.Clear;
for i := 0 to FCollectionTags.Count - 1 do
begin
FMenu.Items.Add;
FMenu.Items.Items[i].Caption := FCollectionTags.Items[i].FDisplayTextTag;
end;
end;
end;
procedure TNiftyRVFrameWithPopups.SetCollectionTags(const Value: TNiftyListTags);
begin
FCollectionTags.Assign(Value);
end;
{ TNiftyListTag }
procedure TNiftyListTag.Assign(Source: TPersistent);
begin
if Source is TNiftyListTag then
begin
FTagValue := TNiftyListTag(Source).FTagValue;
FDisplayTextTag := TNiftyListTag(Source).FDisplayTextTag;
end
else
inherited;
end;
{ TNiftyListTags }
function TNiftyListTags.Add: TNiftyListTag;
begin
Result := TNiftyListTag(inherited Add);
end;
procedure TNiftyListTags.AppendItem(const aDisplayText, aTag: string);
var
a: TNiftyListTag;
begin
a := TNiftyListTag(inherited Add);
a.FTagValue := aTag;
a.FDisplayTextTag := aDisplayText;
DoOnChanged;
end;
constructor TNiftyListTags.Create(AOwner: TPersistent; ItemClass: TCollectionItemClass);
begin
inherited Create(AOwner, ItemClass);
end;
procedure TNiftyListTags.DoOnChanged;
begin
if Assigned(fOnChanged) then
fOnChanged(self);
end;
procedure TNiftyListTags.SetItem(Index: Integer; Value: TNiftyListTag);
begin
inherited SetItem(index, Value);
end;
function TNiftyListTags.GetItem(Index: Integer): TNiftyListTag;
begin
Result := TNiftyListTag(inherited GetItem(Index));
end;
procedure TNiftyListTags.SetItem(Index: Integer; Value: TNiftyListTag);
begin
inherited SetItem(index, Value);
DoOnChanged;
end;
end.
Items can be added at run time in the following way:
var
a:TNiftyRVFrameWithPopups;
begin
a:=TNiftyRVFrameWithPopups.Create(self);
.....
a.TagList.AppendItem('a','b');
a.TagList.AppendItem('c','d');
end
Upvotes: 0
Views: 694
Reputation: 23046
Your TNiftyListTags is owned by the TNiftyRVFrameWithPopups.
Your only 'problem' is that the TOwnedCollection class does not provide a typed reference to the owner, by which to invoke the necessary method(s) to refresh the owner when the collection changes.
There are a number of ways to achieve what you want. However, before presenting options, whatever you do I suggest you do not call Loaded to achieve your update/refresh since this method has specific meaning. Whilst your code in the overridden method may be safe in this context, the inherited implementation may not be.
I would suggest moving the if Assigned(fRVEditor)
pre-condition check to RefreshMenu itself. Loaded then simply calls RefreshMenu as may any other code that may need to also call RefreshMenu, with the necessary pre-condition checked by the method itself.
Now, as for how and when to call the RefreshMenu method, one simple mechanism is to directly invoke the refresh method whenever the content of the collection changes. e.g. in the Add method of the collection. Since you are using a TOwnedCollection as the base class, you could simply type-cast the Owner:
function TNiftyListTags.Add: TNiftyListTag;
begin
Result := TNiftyListTag(inherited Add);
TNiftyRVFrameWithPopups(Owner).RefreshMenu;
end;
However, this couples your collection class directly to the specific component acting as the owner. If your collection is specialised to this class specifically then this may be valid, but it is still undesirable.
To de-couple the collection from the component you could alternatively introduce an OnChange event on the collection. A simple TNotifyEvent will usually suffice.
Whichever component then owns the collection may then install a handler for this event. Whenever the collection changes, invoke the OnChange handler. In this case the TNiftyRVFrameWithPopups component will respond to those changes by calling its own RefreshMenu method.
procedure TNiftyListTags.DoOnChanged;
begin
if Assigned(fOnChanged) then
fOnChanged(self);
end;
function TNiftyListTags.Add: TNiftyListTag;
begin
Result := TNiftyListTag(inherited Add);
DoOnChanged;
end;
procedure TNiftyRVFrameWithPopups.OnTagsChanged(Sender: TObject);
begin
RefreshMenu;
end;
This is typically the approach I adopt and I make the OnChange event a private implementation detail, with the handler specified in the constructor by the component instantiating the collection. This prevents anyone from inadvertently replacing the event handler via any public property etc.
constructor TNiftyRVFrameWithPopups.Create(Owner: TComponent);
begin
inherited Create(self);
fTags := TNiftyListTags.Create(self, OnTagsChanged);
..
end;
To facilitate this you obviously need a custom constructor to accept the event handler:
TNiftyListTags = class(TOwnedCollection)
..
private
fOnChanged: TNotifyEvent;
public
constructor Create(aOwner: TPersistent; aOnChange: TNotifyEvent); reintroduce;
..
end;
constructor TNiftyListTags.Create(aOwner: TPersistent;
aOnChange: TNotifyEvent);
begin
inherited Create(aOwner, TNiftyListTag);
fOnChange := aOnChange;
end;
Note that the inherited constructor also accepts two parameters, the second being the class of the collection items. Sine you are introducing a custom constructor you can remove this from the parameters of your own constructor and simply specify the item class in the inherited Create call.
NOTE: This does not increase the coupling between the collection and the item class - they are already tightly coupled, by definition (and design).
Upvotes: 3