pio pio
pio pio

Reputation: 796

Communication between TOwnedCollection and Owner Class in Delphi

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

Answers (1)

Deltics
Deltics

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

Related Questions