LaKraven / LKSL

LaKraven Studios Standard Library
http://otapi.com
Other
51 stars 15 forks source link

Using the Event Engine to call Methods across multiple Forms of the same Form Type #29

Closed carmas123 closed 9 years ago

carmas123 commented 9 years ago

I've this situation: I've created an event structure like this:

type
  TArchiveItemSelectedEvent = class(TLKEvent)
  private
    FArchiveClass: TArchiveClass;
  public
    constructor Create(AArchiveClass: TArchiveClass); reintroduce;
    property ArchiveClass: TArchiveClass read FArchiveClass;
  end;

  TArchiveItemSelectedEventListener = class(TLKEventListener<TArchiveItemSelectedEvent>)
  end;

var
  EvT_ArchiveItemSelectedEvent: TLKEventThread = nil;

implementation

constructor TArchiveItemSelectedEvent.Create(AArchiveClass: TArchiveClass);
begin
  inherited Create;
  FArchiveClass := AArchiveClass;
end;

initialization
EvT_ArchiveItemSelectedEvent := TLKEventThread.Create();

finalization
EvT_ArchiveItemSelectedEvent.Kill;
end.

My project contain some form and I create a TArchiveItemSelectedEventListener on every form:

procedure TfrmeBase.InitializeListeners;
begin
  // CREATING THE LISTENER
  FItemSelectedListener := TArchiveItemSelectedEventListener.Create(EvT_ArchiveItemSelectedEvent, ItemSelectedEvent);
end;

procedure TfrmeBase.ItemSelectedEvent(const AEvent: TArchiveItemSelectedEvent);
begin
  // Virtual Method (EVENT FIRED)
end;

procedure TfrmeBase.FinalizeListeners;
begin
  // FREEING THE LISTENER
  FreeAndNil(FItemSelectedListener);
end;

So, all work fine when I instantiate a single form, but when I try to create a second form I got an access violation on this method:

function TLKEventListener.GetTypeRestriction: TLKEventTypeRestriction;
begin
  Lock; <--- HERE

Can you help me to solve this problem? I tried to remove the "FinalizeListener" and this exception will not raised but a critical section does not destroyed and I've a critical section for every form created.

LaKraven commented 9 years ago

First of all, you cannot directly instantiate TLKEventThread. It is now defined as a Abstract (and should always have been, sorry for that).

I suggest you take a look at the section on Defining an Event Thread in the LKSL Wiki to see how you need to set up your TLKEventThread properly.

Without seeing the declaration of TfrmeBase, I cannot tell what type you're inheriting it from.

I can say that your line EvT_ArchiveItemSelectedEvent := TLKEventThread.Create(); is a significant problem (as I mentioned earlier, you cannot create an instance of TLKEventThread itself... you can only inherit from it in a descendant class as shown in the Wiki entry linked above).

Please reply back to let me know once you've redone the code to conform to that shown in the Wiki :)

LaKraven commented 9 years ago

I'm having a little difficulty trying to understand what it is you're attempting to do here, by the way.

procedure TfrmeBase.ItemSelectedEvent(const AEvent: TArchiveItemSelectedEvent);
begin
  // Virtual Method (EVENT FIRED)
end;

^ This doesn't show me what you're looking to do with the Event once it gets triggered for processing on the Event Thread.

If you want to create an instance of your Event Thread for each TForm instance, you'll need to make the Event Thread a member of the Form.

If, however, you want a TForm descendant instance to be created each time the Event occurs, you'll need to Synchronize the ItemSelectedEvent method. This is because ItemSelectedEvent is being executed in the context of the Event Thread, not the GUI Thread.

Example:

procedure TfrmeBase.ItemSelectedEvent(const AEvent: TArchiveItemSelectedEvent);
begin
  Synchronize(procedure begin
                         Application.CreateForm(TMyForm, MyForm);
                       end);
end;

^ Also, don't forget that this code would cause issues because there's only one MyForm container, and you'd be disassociating the previous TMyForm instance each time ItemSelectedEvent is fired. However, since each TMyForm instance would be owned by the Application object, there would be no memory leaks.

carmas123 commented 9 years ago

Now this is my unit

type
  TArchiveItemSelectedEvent = class(TLKEvent)
  private
    FArchiveClass: TArchiveClass;
  public
    constructor Create(AArchiveClass: TArchiveClass); reintroduce;
    property ArchiveClass: TArchiveClass read FArchiveClass;
  end;

  TArchiveItemSelectedEventListener = class(TLKEventListener<TArchiveItemSelectedEvent>)
  end;

  TArchiveItemSelectedThread = class(TLKEventThread)
  end;

var
  EvT_ArchiveItemSelectedEvent: TArchiveItemSelectedThread = nil;

implementation

constructor TArchiveItemSelectedEvent.Create(AArchiveClass: TArchiveClass);
begin
  inherited Create;
  FArchiveClass := AArchiveClass;
end;

initialization

EvT_ArchiveItemSelectedEvent := TArchiveItemSelectedThread.Create();

finalization

EvT_ArchiveItemSelectedEvent.Kill;

end.

and this is my form:

type
  TMyForm = class(TForm)
    private
      FItemSelectedListener: TArchiveItemSelectedEventListener;
      procedure InitializeListeners;
      procedure FinalizeListeners;
    public
      procedure ItemSelectedEvent(const AEvent: TArchiveItemSelectedEvent);
      constructor Create(AOwner: TComponent); override;
      destructor Destroy; override;
  end;

implementation

procedure TMyForm.ItemSelectedEvent(const AEvent: TArchiveItemSelectedEvent);
begin
  // This is the event "fired"
end;

constructor TMyForm.Create(AOwner: TComponent);
begin
  inherited;
  InitializeListeners;
end;

destructor TMyForm.Destroy;
begin
  FinalizeListeners;

  inherited;
end;

procedure TMyForm.InitializeListeners;
begin
  FItemSelectedListener := TArchiveItemSelectedEventListener.Create(EvT_ArchiveItemSelectedEvent, ItemSelectedEvent);
end;

procedure TMyForm.FinalizeListeners;
begin
  FreeAndNil(FItemSelectedListener);
end;

end.

On first form created all work, but when create another form I got the exception

LaKraven commented 9 years ago

You should not be creating Listeners from the UI Thread! They should only be created within the context of a TLKEventThread descendant, and should really belong to that TLKEventThread descendant:

type
  TArchiveItemSelectedEvent = class(TLKEvent)
  private
    FArchiveClass: TArchiveClass;
  public
    constructor Create(AArchiveClass: TArchiveClass); reintroduce;
    property ArchiveClass: TArchiveClass read FArchiveClass;
  end;

  TArchiveItemSelectedEventListener = class(TLKEventListener<TArchiveItemSelectedEvent>);

  TArchiveItemSelectedThread = class(TLKEventThread)
  private
    FItemSelectedListener: TArchiveItemSelectedEventListener;
    procedure ItemSelectedEvent(const AEvent: TArchiveItemSelectedEvent);
  protected
    procedure InitializeListeners; override;
    procedure FinalizeListeners; override;
  end;

var
  EvT_ArchiveItemSelectedEvent: TArchiveItemSelectedThread = nil;

implementation

{ TArchiveItemSelectedEvent }

constructor TArchiveItemSelectedEvent.Create(AArchiveClass: TArchiveClass);
begin
  inherited Create;
  FArchiveClass := AArchiveClass;
end;

{ TArchiveItemSelectedThread }

procedure TArchiveItemSelectedThread.InitializeListeners; 
begin
  FItemSelectedListener := TArchiveItemSelectedEventListener.Create(Self, ItemSelectedEvent)
end;

procedure TArchiveItemSelectedThread.FinalizeListeners;
begin
  FItemSelectedListener.Free;
end;

procedure TArchiveItemSelectedThread.ItemSelectedEvent(const AEvent: TArchiveItemSelectedEvent);
begin
  // Thing to do when TArchiveItemSelectedEvent occurs
end;

initialization
  EvT_ArchiveItemSelectedEvent := TArchiveItemSelectedThread.Create();
finalization
  EvT_ArchiveItemSelectedEvent.Kill;

end.
LaKraven commented 9 years ago

Following on from the reply above, if you want an Event to perform a certain action across multiple Forms, then the TLKEventThread descendant instance should be created as a member of the Form:

type
  TForm1 = class(TForm)
  private
    FEventThread: TArchiveItemSelectedThread;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
  end;

implementation

{ TForm1 }

constructor TForm1.Create(AOwner: TComponent);
begin
   FEventThread := TArchiveItemSelectedThread.Create;
end;

destructor TForm1.Destroy;
begin
  FEventThread.Kill;
end;

You would then remove the following lines from the code example in my previous response:

var
  EvT_ArchiveItemSelectedEvent: TArchiveItemSelectedThread = nil;

and

initialization
  EvT_ArchiveItemSelectedEvent := TArchiveItemSelectedThread.Create();
finalization
  EvT_ArchiveItemSelectedEvent.Kill;
carmas123 commented 9 years ago

Oh yes :) this can work fine, but the problem is that I don't want a global reference to my form. I create my form in a local var and not with a global reference. How can I interact from the Thread to my form? I need to create a custom thread that reference to this?

LaKraven commented 9 years ago

Oh, and to hook up a procedure on your TForm descendant to be called when its respective TArchiveItemSelectedThread's ItemSelectedEvent is called, we'd do something like this...

In your Event unit:

type
  TItemSelectedEvent = procedure(const AEvent: TArchiveItemSelectedEvent) of object;

  TArchiveItemSelectedThread = class(TLKEventThread)
  private
    FOnArchiveItemSelected: TItemSelectedEvent;
    FItemSelectedListener: TArchiveItemSelectedEventListener;

    function GetOnArchiveItemSelected: TItemSelectedEvent;
    procedure SetOnArchiveItemSelected(const AOnArchiveItemSelected: TItemSelectedEvent);

    procedure ItemSelectedEvent(const AEvent: TArchiveItemSelectedEvent);
  protected
    procedure InitializeListeners; override;
    procedure FinalizeListeners; override;
  public
    property OnArchiveItemSelected: TItemSelectedEvent read GetOnArchiveItemSelected write SetOnArchiveItemSelected;
  end;

We'd now implement the Getter and Setter like this:

function TArchiveItemSelectedThread.GetOnArchiveItemSelected: TItemSelectedEvent;
begin
  Lock;
  try
    Result := FOnArchiveItemSelected
  finally
    Unlock;
  end;
end;

procedure TArchiveItemSelectedThread.SetOnArchiveItemSelected(const AOnArchiveItemSelected: TItemSelectedEvent);
begin
  Lock;
  try
    FOnArchiveItemSelected := AOnArchiveItemSelected;
  finally
    Unlock;
  end;
end;

We'd now implement ItemSelectedEvent like this:

procedure TArchiveItemSelectedThread.ItemSelectedEvent(const AEvent: TArchiveItemSelectedEvent);
var
  LOnArchiveItemSelected: TItemSelectedEvent;
begin
  LOnArchiveItemSelected := GetOnArchiveItemSelected;
  if Assigned(LOnArchiveItemSelected) then
  begin
    Synchronize(procedure begin
      LOnArchiveItemSelected(AEvent);
    end);
  end;
end;

To hook this up, we modify the Form's declaration like this:

  TForm1 = class(TForm)
  private
    FEventThread: TArchiveItemSelectedThread;
    procedure ItemSelectedEvent(const AEvent: TArchiveItemSelectedEvent);
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
  end;

Now we complete the implementation like this:

{ TForm1 }

constructor TForm1.Create(AOwner: TComponent);
begin
   FEventThread := TArchiveItemSelectedThread.Create;
   FEventThread.OnArchiveItemSelected := ItemSelectedEvent;
end;

procedure TForm1.ItemSelectedEvent(const AEvent: TArchiveItemSelectedEvent);
begin
  // Do whatever we need to do on this particular Form Instance.
end;

So there we go! Each time you dispatch a TArchiveItemSelectedEvent, the ItemSelectedEvent on each Form will be executed within the scope of that Form.

carmas123 commented 9 years ago

This is a very good solution!! But I've another question to you. If I have 5 events (5 distinct event's type) to manage and I have 6 or more form I need 30 threads?

LaKraven commented 9 years ago

but the problem is that I don't want a global reference to my form. I create my form in a local var and not with a global reference.

This is fine, the code I've provided does not require a global reference to your Form... it uses a callback method (like OnClick etc. in the VCL) to pass the ItemSelectedEvent call over to the Form on which that Thread has been created. This way, the Form will need to know about your Event Thread type, but not the other way around :)

LaKraven commented 9 years ago

If I have 5 events (5 distinct event's type) to manage and I have 6 or more form I need 30 threads?

No!

If all of the Event Types are in some way related (such as, each one performs an action on the Form like the one we've discussed above), then you simply create the additional listeners as members of that one Event Thread type:

  TArchiveItemSelectedThread = class(TLKEventThread)
  private
    FListener1: TListener1;
    FListener2: TListener2;
    FListener3: TListener3;

    procedure Event1Call(const AEvent: TEvent1);
    procedure Event2Call(const AEvent: TEvent2);
    procedure Event3CAll(const AEvent: TEvent3);
    // Blah Blah Blah
  end;

You would then hook up on FOnEvent members, getters, setters and the Properties for those just as we did before. You'd also create each Listener in InitializeListeners, and destroy them in FinalizeListeners just like you do for the one you already have.

There is no limit to the number of Listeners you can have within an Event Thread type!

LaKraven commented 9 years ago

Also, don't forget that TLKEventThread descendants are "good citizen" Threads!

When they aren't doing anything, they properly wait until there's work for them to perform, yielding their computation time to other Threads and consuming 0% of your CPU. This means that you can have as many Threads created (while idle) as the OS will physically allow (of course remembering to consider that each Thread will consume memory, so in 32bit you wouldn't want to create 2GB worth of Thread instances).

In my game engine, for example, I currently create just 6 threads, and only 2 of those are "always active". The rest wake up and go to sleep as and when they are required. The engine (internally) has over 200 distinct Event Types, and an actual Game Implementation on top of the Engine could have many thousands of Event Types!

LaKraven commented 9 years ago

Please let me know if I can consider this issue closed now :)

carmas123 commented 9 years ago

Only this: If I need to handle 3 types of event, I can create a 3 types of listener in a single thread?

LaKraven commented 9 years ago

If I need to handle 3 types of event, I can create a 3 types of listener in a single thread?

Yes! You can create as many types of Listener in a single Thread as you need :+1:

carmas123 commented 9 years ago

Ok, so I only need a thread for each for and all gone?

LaKraven commented 9 years ago

Ok, so I only need a thread for each for and all gone?

Yes, if you're simply wanting to have different Event Types execute a method on multiple instances of your Form type, then you need only create a single Event Thread as a member of that Form Type to do it. On that Event Thread type, you instantiate as many Event Listeners as you require :+1:

carmas123 commented 9 years ago

Ok thank you so much

LaKraven commented 9 years ago

You're very welcome! Glad I could help.