Function Request: ExposeEvents and Handles


(Theo) #1

Already spoken of with @mh but now an official function request.
This is needed to easily convert from VB.Net to Oxygene.

In contrast to C# and current Oxygene, VB does _not _ hard wire the event handlers in code (although it is possible to do this), but instead the wiring is compiler generated.

An example of the current event handler syntax:

type
      MainForm = partial class(ExactUI.ExactMainForm)
      private
          method btnSluiten_Click(sender: System.Object; e: System.EventArgs);
      assembly
        btnSluiten:= System.Windows.Forms.Button;
      public
        constructor;
      end;
implementation

constructor MainForm;
begin
  InitializeComponent();
  //Wire the event handler
  self.btnSluiten.Click += new System.EventHandler(self.btnSluiten_Click);
end;

method MainForm.btnSluiten_Click(sender: System.Object; e: System.EventArgs);
begin
    //click code here
end;

In VB style the would become:

type
      MainForm = partial class(ExactUI.ExactMainForm)
      private
          //based on the handles modifier (that can have multiple events to implement them all on one method)
         // the compiler will generate the wiring
          method btnSluiten_Click(sender: System.Object; e: System.EventArgs); handles btnSluiten.Click;
      assembly
         //The ExposeEvents modifier exposes the events of this component and makes the use of Handles on it available
         btnSluiten:= System.Windows.Forms.Button; ExposeEvents;
      public
        constructor;
      end;
implementation

constructor MainForm;
begin
  InitializeComponent();
 //No event wiring needed; this is done on compiletime based on the used Handles modifiers
end;

method MainForm.btnSluiten_Click(sender: System.Object; e: System.EventArgs);
begin
    //click code here
end;

The internal implementation of an Expose Event field is a property that removes all current event handlers and sets the new ones on the set method, the get method just returns the field.
The content of the set method is generated on compile time.

In this case the code after compile would be:

type
      MainForm = partial class(ExactUI.ExactMainForm)
      private
          btnSluitenField:= System.Windows.Forms.Button;
          method SetbntSluiten(value: System.Windows.Forms.Button);
          method btnSluiten_Click(sender: System.Object; e: System.EventArgs)
      assembly
         property btnSluiten: System.Windows.Forms.Button; read btnSluitenField write SetbtnSluiten;
      public
        constructor;
      end;
implementation

constructor MainForm;
begin
  InitializeComponent();
 //No event wiring needed; this is done on compiletime based on the used Handles modifiers
end;

method MainForm.SetbtnSluiten((value: System.Windows.Forms.Button));
begin
   if btnSluitenField <> nil then
   begin
     btnSluitenField.Click.RemoveAll;
   end;
   btnSluitenField = value;
   btnSluitenField.Click += new System.EventHandler(self.btnSluiten_Click);
end;

method MainForm.btnSluiten_Click(sender: System.Object; e: System.EventArgs);
begin
    //click code here
end;

(Carlo Kok) #2

I don’t entirely understand what it does. But you could do this with a simple aspect?

[Handles(nameOf(btnSluiten), "Click")]
  method btnSluiten_Click(sender: System.Object; e: System.EventArgs); 

where Handles is an AOP aspect that injects the btnSluiten.Click += btnSluiten_Click; in the initializecomponents method?


(Theo) #3

I just added the generated code to the initial post.
The power of this is that you can replace the instance, and the new instance is also wired immediately (and the old unwired).
You can also create an uninitialized variable with event handlers; they will be wired as soon as it is assigned.


(Theo) #4

From the Visual Basic Language Definition:

9.6.2 WithEvents Variables
A type can declare that it handles some set of events raised by one of its instance or shared variables by declaring the instance or shared variable that raises the events with the WithEvents modifier. For example:

Class Raiser
  Public Event E1()
  Public Sub Raise()
    RaiseEvent E1
  End Sub
End Class

Module Test
  Private WithEvents x As Raiser
  Private Sub E1Handler() Handles x.E1
    Console.WriteLine("Raised")
  End Sub
  Public Sub Main()
    x = New Raiser()
  End Sub
End Module

In this example, the method E1Handler handles the event E1 that is raised by the instance of the type Raiser stored in the instance variable x.
The WithEvents modifier causes the variable to be renamed with a leading underscore and replaced with a property of the same name that does the event hookup. For example, if the variable’s name is F, it is renamed to _F and a property F is implicitly declared. If there is a collision between the variable’s new name and another declaration, a compile-time error will be reported. Any attributes applied to the variable are carried over to the renamed variable.
The implicit property created by a WithEvents declaration takes care of hooking and unhooking the relevant event handlers. When a value is assigned to the variable, the property first calls the remove method for the event on the instance currently in the variable (unhooking the existing event handler, if any). Next the assignment is made, and the property calls the add method for the event on the new instance in the variable (hooking up the new event handler). The following code is equivalent to the code above for the standard module Test:

Module Test
  Private _x As Raiser
  Public Property x() As Raiser
    Get
      Return _x
    End Get
    Set (Value As Raiser)
      ' Unhook any existing handlers.
      If _x IsNot Nothing Then
        RemoveHandler _x.E1, AddressOf E1Handler
      End If
      ' Change value.
      _x = Value
     ' Hook-up new handlers.
     If _x IsNot Nothing Then
       AddHandler _x.E1, AddressOf E1Handler
     End If
    End Set
End Property

Sub E1Handler()
  Console.WriteLine("Raised")
End Sub

Sub Main()
  x = New Raiser()
End Sub

End Module

It is not valid to declare an instance or shared variable as WithEvents if the variable is typed as a structure.
In addition, WithEvents may not be specified in a structure, and WithEvents and ReadOnly cannot be combined.


(Carlo Kok) #5

I’m wondering if this wouldn’t be a better candidate for an aspect. Our AOP infrastructure should let you do this exactly.


(Theo) #6

If the result is the same, I have no problems with it; the aspect should generate the property to get the same behavior.


(Carlo Kok) #7

It would use an attribute instead of keywords.

The power of AOP is that anyone can define them, instead of having them hardcoded in the compiler.


(Theo) #8

I am not really familiar with AOP … (Reading Wikipedia now)


(Theo) #9

If I understand it well, OAP can add code. In this case, code has to be changed; a field has to be converted to a property (the ExposeEvents modifier in my example), and the property must be implemented based on the handles modifier (in my example).

Can you do this with AOP?


(Carlo Kok) #10

Yes.

1 example is a simple AOP aspect I wrote:

[ClassLibrary1.Handles(nameOf(button1), "Click")]

The aspect is here (this is in a separate dll, the compiler will remove the reference to that):

namespace ClassLibrary1;
uses 
  System.Linq,
  RemObjects.Elements.Cirrus.*;

type
  [AttributeUsage(AttributeTargets.Method)]
  HandlesAspect = public class(Attribute, IMethodInterfaceDecorator)
  private
    fField, fEvent: String;
  protected
  public
    constructor(aField, aEvent: String);
    begin 
      fField := aField;
      fEvent := aEvent;
    end;

    method HandleInterface(Services: IServices; aMethod: IMethodDefinition);
    begin 
      var lMethod := aMethod.Owner.GetMethod('InitializeComponent', new IType[0], []) as IMethodDefinition;
      if lMethod = nil then begin 
        Services.EmitError('InitializeComponent not found!');
        exit;
      end;

      var lField := aMethod.Owner.GetField(fField) ;
      if lField = nil then begin 
        Services.EmitError('Field '+fField+ ' not found!');
        exit;
      end;

      var lType := lField.Type;
      var lEvent: IEvent;
      while lType <> nil do begin
        lEvent := lType.GetEvents(fEvent).FirstOrDefault;
        if lEvent <> nil then break;
        lType := lType.ParentType;
      end;

      if lEvent = nil then begin 
        Services.EmitError('Event '+fEvent+ ' not found!');
        exit;
      end;

      var lBeg := new BeginStatement(
        new PlaceHolderStatement(),
        new StandaloneStatement(new ProcValue(new IdentifierValue(fField), 'add_'+fEvent, new NewValue(lEvent.Type, new IdentifierValue(aMethod.Name)))));
      lMethod.ReplaceMethodBody(lBeg);
    end;
  end;

end.

this just hooks up the event inside InitializeComponents, by calling add_click(new self.method1);


(Theo) #11

@ck, I do not really see that AOP can do this (because I see only code adding, no code changing), but I completely trust your insight.


(Theo) #12

By this you mean I have to solve this myself?
If so, can you lead me to the place where I can learn about this AOP in Oxygene?
BTW: This is the only point left that VB can do and Oxygene not (except for XML literals, but I never used them).


(Carlo Kok) #13

Above should already implement the “Handles”, so it’s just a question of doing the “WithEvents” part.

I can try & take a look later today or tomorrow.


(Carlo Kok) #14

So something like:

  MainForm = partial class(System.Windows.Forms.Form)
  private
  protected
    method Dispose(disposing: Boolean); override;
  public
    constructor;

    [ClassLibrary1.ExposesEvents]
    btn: Button;

    [ClassLibrary1.Handles(nameOf(btn), "Click")]
    method btnSluiten_Click(sender: System.Object; e: System.EventArgs);
    begin 
      MessageBox.Show('Hello');
    end;
  end;

where the generated code looks like:

// WindowsApplication8.MainForm
using System;
using System.ComponentModel;
using System.Drawing;
using System.Windows.Forms;
using WindowsApplication8;

internal class MainForm : Form
{
	private Container components = null;

	private Button button1;

	public Button _btn;

	public Button btn
	{
		get
		{
			return this.btn;
		}
		set
		{
			if (this._btn != value)
			{
				if (this._btn != null)
				{
					this.btn.Click -= this.btnSluiten_Click;
				}
				this._btn = value;
				if (this._btn != null)
				{
					this.btn.Click += this.btnSluiten_Click;
				}
			}
		}
	}

	private void InitializeComponent()
	{
		ComponentResourceManager resources = new ComponentResourceManager(typeof(MainForm));
		this.button1 = new Button();
		this.SuspendLayout();
		this.button1.Location = new Point(0, 0);
		this.button1.Name = "button1";
		this.button1.Size = new Size(75, 23);
		this.button1.TabIndex = 0;
		this.button1.Text = "button1";
		this.button1.UseVisualStyleBackColor = true;
		this.AutoScaleDimensions = new SizeF(6f, 13f);
		this.AutoScaleMode = AutoScaleMode.Font;
		this.ClientSize = new Size(292, 273);
		this.Controls.Add(this.button1);
		this.Icon = (resources.GetObject("$this.Icon") as Icon);
		this.Name = "MainForm";
		this.Text = "MainForm";
		this.ResumeLayout(false);
	}

	protected override void Dispose(bool disposing)
	{
		if (disposing && this.components != null)
		{
			this.components.Dispose();
		}
		base.Dispose(disposing);
	}

	public MainForm()
	{
		this.InitializeComponent();
	}

	public void btnSluiten_Click(object sender, EventArgs e)
	{
		MessageBox.Show("Hello");
	}
}

?


(marc hoffman) #15

https://docs.elementscompiler.com/Concepts/Aspects/ is the docs topic for this, btw.


(Carlo Kok) #16

WindowsApplication8.zip (71.1 KB)

@Theo69 here’s a project that shows this.


(Theo) #17

Hi Carlo,
Looks ok to me.


(Theo) #18

Going to experiment with it.


(Theo) #19

Hi Carlo,

Is this already available?

Regards,
Theo


(Carlo Kok) #20

What do you mean? This worked with the build from last week and the sample I pasted above.