-
Notifications
You must be signed in to change notification settings - Fork 1
Events
Events in Telefrag are exposed via the Telefrag.Event class, which provides additional functionality beyond native delegate-based events.
Subscribing to events is easy. Here are some examples:
With a separate function
Bot.Events.PollingStarted.Subscribe(pollStartedHandler);
void pollStartedHandler(Context c, EventArgs e) { ... }With a lambda
Bot.Events.ChatMessage.Subscribe((c, e) => {
Log($"{c.From} says {c.Message.TextOrCaption}");
});Keeping a reference to the subscription
The Subscribe() method returns an EventSubscription object which can be held onto to manage the event subscription if needed.
var sub = UpdateReceived.Subscribe(UpdateHandler);
/* later on ... */
sub.Unsubscribe(); Telefrag supports two types of event handler hookup:
-
Native handlers are standard methods with a fixed signature (
Context c, EventArgs e) and are the most performant event dispatch method, and can be hooked up using standard C# delegate syntax, inline lambdas, or any other standard method dispatch. -
Advanced Handlers are methods with any arbitrary signature that has been hooked up to an event. When the event is raised, DI will be used to automatically populate the parameters you've written with meaningful references. Advanced handlers can be hooked up by referencing the
MethodInfoobject that represents the exact method overload/signature you'd like to subscribe, but the Bot Toolkit automatically hooks up handlers for you based on attributes such as[Command]and[Callback].
Native event handlers are of the delegate type TelefragEventHandler<TArgs> where TArgs is the subtype of EventArgs used for the event in question. This delegate has the following signature:
public delegate EventHandler<TArgs>(Context c, TArgs e);Native handlers can be subscribed using:
Event.Subscribe(handler)Event.Subscribe(handler, scope)
The two arguments passed directly to every native event handler are:
-
The Context object associated with the Event. This contains references to the sender (Bot or Client), chat, update, message, security parameters, the Activity currently in scope (if any), etc.
-
The
eparameter (which will beEventArgsor a subclass of it) feeds you any detailed event-specific information or references to event-specific objects that may be helpful.
Telefrag supports event dispatch to methods of arbitrary signatures using DI registrations to automatically populate the parameters you've requested.
public class Foo {
public void Bar(Bot b, Message m, string _text) {
// ...
}
}
var instance = new Foo();
var method = instance.GetType().GetMethod(nameof(Foo.Bar));
Bot.Events.ChannelPost.Subscribe(method, instance);
/* OR */
Bot.Events.ChannelPost.Subscribe(instance,
f => f.Bar(default, default, default));
// The argumens provided in the lambda
// don't matter and are only used to
// select the desired method overload
Event handlers have access to set the Handled property of the Context object to true in order to indicate to the object raising the event that you have in some way processed the event.
Normally when an Event is raised, all subscribers to the event are notified (in the order they subscribed), however some types of events (known canonically as Hooks) are raised such that if one event handler "handles" an event (by setting Handled to true), event processing stops and no further handlers are invoked, regardless of active subscriptions.
For this reason it is possible to provide a parameter (subscribePreference) in the call to Subscribe() to indicate whether you would like to be inserted at the front or end of the list of subscriptions depending on whether you want to handle an event or hook before any other existing subscribers or afterwards.
Unsubscribing from events can be accomplished a variety of ways:
- Call
Unsubscribe()on theEventSubscriptionprovided when you subscribed. - Call
Unsubscribe()on theEventand pass in theEventSubscription - Call
Event.UnsubscribeAll()to clear all subscriptions. - If you have control over the scope object (
IEventScope) used in your subscriptions, you can have it returnfalsewhenIsActiveis queried (though there is no guarantee how soon Telefrag will check this value).
When you subscribe to an Event, the Subscribe method takes an optional parameter called scope. Every EventSubscription has a specific ILifetimeScope object that determines the lifetime and validity of the event subscription.
By default, the value of Context.Sender is used if available, which is typically the Bot, Client, etc. associated with the event, however neither the Context.Sender nor the object owning the Event implement IEventScope (or are not available), then a dummy object is used and the subscription will always be valid and active unless explicitly unsubscribed.
The benefit to scoping event subscriptions to scope owners is that when a Bot or Client is removed, or a module or plugin is unloaded -- corresponding event handlers can be automatically unhooked and resources can be released.
Any object can implement ILifetimeScope and be the scope for a subscription; all it needs is a bool IsActive property to tell Telefrag whether or not events hooked up under the auspices of that object are still valid or not. Telefrag periodically goes through and removes all event subscriptions scoped to objects no longer active.
Event objects also provide a fair number of diagnostics and statistics that standard .NET delegate events do not:
- The FireCount property tracks how many times the
Eventwas raised - The SubscriberCount and Subscriptions properties track subscriptions
- The ToString() method ensures an
Eventis represented by a developer-friendly string in the format:{[<event owner type>::<event name>] on <instance ToString() output> (<subscriber count>)}
- The Quiet property on
Eventobjects suppresses log messages involving their invocation if set totrue. - The Subscribed boolean quickly exposes whether the
Eventhas any subscribers or not (so you can avoid allocating anEventArgsobject if no one is there to receive it).
Events can be added to any class that implements IEventOwner, which simply requires a single Guid identifier.
- Create one or more public properties of type
Eventon your class - In your classes constructor, call
Event.Setup(this);
Example:
public class Aldi
{
public Event StoreOpened { get; private set; }
public Event StoreClosed { get; private set; }
public Aldi() {
Event.Setup(this);
}
}Event.Setup will look for any Event property on your class and automatically instantiate it so that it's immediately available and will never be uninitialized.
If you want to use a specific EventArgs subclass, simply provide that as the type argument to the Event<TArgs> type:
public class Walmart
{
public Event<LayawayEventArgs> LayawayItemLost { get; private set; }
}To raise your event, simply call Raise(), passing in an instance of EventArgs.
StoreOpen.Raise(new());The Event will automatically capture the current Context and add a context frame describing the event being handled. If you want to check the value of Handled, you can look for a return value:
var handled = StopOpen.Raise(new());
// this will return true if any of the EventHandlers set
// Context.Handled = trueIf you want to stop invoking event handlers after the first one sets Handled to true, then use the stopOnHandled parameter of Raise():
var handled = StopOpen.Raise(new(), stopOnHandled: true);
// If an EventHandler sets Handled to true, no further event
// handling will occur and the function will return true,
// otherwise other subscribed handlers will be invoked By default, the Raise() function will throw an UnauthorizedAccessException if it is called from a type other than the one it is defined on.
You can configure the scope of what types are allowed to raise an Event by using one or both of these attributes:
- [AllowedCaller] can be used to whitelist a specific type so that it is allowed to call Raise.
- [EventSecurity] defines the general code security scope of who may call Raise. See the table below for more details. Each access level permits all access levels below it.
CodeRelationship value |
Description |
|---|---|
| Private (Default) | The event can only be raised by the Type it is defined on |
| Protected | The event can only be raised by the Type it is defined on and any derived types |
| InternalChild | The event can also be raised by types that are nested children of the type the Event is defined on |
| InternalParent | The event can also be raised by the containing class that |
| Public | Anyone can raise this event from any type in any assembly |
| Example: |
[AllowedCaller(typeof(Salesman))]
[EventSecurity(CodeRelationship.Protected)]
public Event<UsedCarSalesEventArgs> UsedCarSales { get; private set; }
// This Event can be called by
// * Methods defined on this type
// * Methods defined on subtypes (due to the EventSecurity attribute)
// * Methods defined on type 'Salesman' (due to the AllowedCaller attribute)