Create forwards via C#

I’ve been digging the Avalonia event system, and I found that it involves forwarding numerous RoutedEventArgs and RoutedEvent to user. I would like to ask for a suggestion, how this maybe done programmatically. I’ve checked the NodeFactory, i got some untested GPT suggestion for handling this:

Summary
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Reflection;
using Avalonia.Interactivity;
using VL.Core;
using VL.Core.Diagnostics;

namespace VL.Avalonia.Factories
{
    public class AvaloniaRoutedEventFactory : IVLNodeDescriptionFactory
    {
        private readonly IVLNodeDescriptionFactory _factory;
        private readonly ImmutableArray<IVLNodeDescription> _nodes;

        public AvaloniaRoutedEventFactory(IVLNodeDescriptionFactory factory)
        {
            _factory = factory;
            _nodes = ScanForRoutedEvents(factory);
        }

        /// <summary>
        /// Registers this factory with the VL AppHost. 
        /// Call this from your main Package Entry point or library initialization.
        /// </summary>
        public static void Register(IVLFactory services)
        {
            services.RegisterNodeFactory("Avalonia.RoutedEvents", factory => 
            {
                var impl = new AvaloniaRoutedEventFactory(factory);
                return NodeBuilding.NewFactoryImpl(impl.GetNodeDescriptions());
            });
        }

        public ImmutableArray<IVLNodeDescription> GetNodeDescriptions() => _nodes;

        private static ImmutableArray<IVLNodeDescription> ScanForRoutedEvents(IVLNodeDescriptionFactory factory)
        {
            var nodes = new List<IVLNodeDescription>();
            
            // 1. Get all loaded assemblies that might contain Avalonia controls
            var assemblies = AppDomain.CurrentDomain.GetAssemblies()
                .Where(a => a.GetName().Name!.StartsWith("Avalonia"));

            foreach (var assembly in assemblies)
            {
                foreach (var type in GetTypesSafe(assembly))
                {
                    // 2. Find static readonly fields of type RoutedEvent
                    var eventFields = type.GetFields(BindingFlags.Public | BindingFlags.Static)
                        .Where(f => typeof(RoutedEvent).IsAssignableFrom(f.FieldType));

                    foreach (var field in eventFields)
                    {
                        if (field.GetValue(null) is RoutedEvent routedEvent)
                        {
                            // 3. Create a VL Node Description for this event
                            var node = CreateNodeDescription(factory, type, field.Name, routedEvent);
                            nodes.Add(node);
                        }
                    }
                }
            }

            return nodes.ToImmutableArray();
        }

        private static IVLNodeDescription CreateNodeDescription(
            IVLNodeDescriptionFactory factory, 
            Type ownerType, 
            string fieldName, 
            RoutedEvent routedEvent)
        {
            // Naming convention:
            // Name: "Click"
            // Category: "Avalonia.Events.Button"
            
            // Clean up field name (remove "Event" suffix convention)
            var name = fieldName.EndsWith("Event") 
                ? fieldName.Substring(0, fieldName.Length - 5) 
                : fieldName;

            var category = $"Avalonia.Events.{ownerType.Name}";

            return factory.NewNodeDescription(
                name: name,
                category: category,
                fragmented: true,
                init: ctx =>
                {
                    // This defines the inputs/outputs of the node
                    // We have 0 inputs and 1 output (the RoutedEvent object)
                    
                    var outputPin = ctx.NewOutputPin<RoutedEvent>("Event");

                    return new NodeImplementation(
                        create: nodeContext =>
                        {
                            // On Create, we simply push the static RoutedEvent instance to the output
                            // Since it's constant, we can set it once.
                            var solution = new Pin<RoutedEvent>(outputPin.ID)
                            {
                                Value = routedEvent
                            };
                            
                            return solution;
                        },
                        update: (state, inputVals) =>
                        {
                            // No updates needed, it's a constant
                            return state;
                        }
                    );
                }
            );
        }

        private static IEnumerable<Type> GetTypesSafe(Assembly assembly)
        {
            try { return assembly.GetTypes(); }
            catch (ReflectionTypeLoadException e) { return e.Types.Where(t => t != null)!; }
            catch { return Enumerable.Empty<Type>(); }
        }
    }
}

I’m not sure if I am on right track to use NodeFactory for this… Honestly i would prefeare to use some static code generation for that but I see no other way…

Also it’s not obvious where should I host this. In AvaloniaLayer, or it’s somehow statically/magically imported on it’s own?

Prolly I would answer this myslef, when I get back to it tommorow, but would be still nice to know how it was planned.

Reference to myslef Node Factories | vvvv gamma documentation

@Elias sorry for ping, still can’t figure it out:

That’s my initialization:

The question is this breakpoint is never actually hit:

I don’t see any additional nodes in node browser, makes me think it’s not evaluated.
Console.WrtieLine in there shows nothing, Just My Code is off.

This should easily be doable with the ImportTypeAttribute.

For example
[assembly:ImportType(typeof(RoutedEvent))]

Or to place it in different category in VL
[assembly:ImportType(typeof(RoutedEvent), Category="FooBar")]

The problem is that I have:

namespace Avalonia.Controls
{
    public class Button : ContentControl, ICommandSource, IClickableControl
    {
        // ...

        /// <summary>
        /// Defines the <see cref="Click"/> event.
        /// </summary>
        public static readonly RoutedEvent<RoutedEventArgs> ClickEvent =
            RoutedEvent.Register<Button, RoutedEventArgs>(nameof(Click), RoutingStrategies.Bubble);
       
        // ...
    }
}

So I need to append ClickEvent to Avalonia.Controls.Button

For the event args it just a bunch of inherited classes e.g.:

namespace Avalonia.Input
{
    public class HoldingRoutedEventArgs : RoutedEventArgs ...
}

Ok so the event classes you can forward like I showed you. The static fields I am not quite sure what you want to have at the end. From your gist I see your generated node would have returned a type - is that what is needed? A node called „Click“ returning a „Type“? Really?

I guess this would illustrate the issue:

Right now if i depend on .net avalonia, i see:

Honestly I would like just to make a filter and forward everything that is under it, since forwarding with VL would be pain in the … , and this would be even better approach to handle all the other random types out there…

So what about

[assembly:ImportType(typeof(RoutedEvent<>))]
[assembly:ImportType(typeof(RoutedEventArgs))]
...
[assembly:ImportType(typeof(AvaloniaEvents))]
public static class AvaloniaEvents
{
    public static RoutedEvent<RoutedEventArgs> Click => Button.ClickEvent;
    public static RoutedEvent<RoutedEventArgs> GotFocus => InputElement.GotFocus;
    ...
}

Ok first part seems to work good.

@Elias there seems to be an issue with:

[assembly: ImportType()]

That’s my generated code:

Summary
using System;
using VL.Core.Import;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Input.TextInput;
using Avalonia.Interactivity;

[assembly: ImportType(typeof(Avalonia.Input.DragEventArgs), Category = "Avalonia.Input")]
[assembly: ImportType(typeof(Avalonia.Input.GotFocusEventArgs), Category = "Avalonia.Input")]
[assembly: ImportType(typeof(Avalonia.Input.HoldingRoutedEventArgs), Category = "Avalonia.Input")]
[assembly: ImportType(typeof(Avalonia.Input.KeyEventArgs), Category = "Avalonia.Input")]
[assembly: ImportType(typeof(Avalonia.Input.PinchEventArgs), Category = "Avalonia.Input")]
[assembly: ImportType(typeof(Avalonia.Input.PinchEndedEventArgs), Category = "Avalonia.Input")]
[assembly: ImportType(typeof(Avalonia.Input.PointerDeltaEventArgs), Category = "Avalonia.Input")]
[assembly: ImportType(typeof(Avalonia.Input.PointerEventArgs), Category = "Avalonia.Input")]
[assembly: ImportType(typeof(Avalonia.Input.PointerPressedEventArgs), Category = "Avalonia.Input")]
[assembly: ImportType(typeof(Avalonia.Input.PointerReleasedEventArgs), Category = "Avalonia.Input")]
[assembly: ImportType(typeof(Avalonia.Input.PointerCaptureLostEventArgs), Category = "Avalonia.Input")]
[assembly: ImportType(typeof(Avalonia.Input.PointerWheelEventArgs), Category = "Avalonia.Input")]
[assembly: ImportType(typeof(Avalonia.Input.PullGestureEventArgs), Category = "Avalonia.Input")]
[assembly: ImportType(typeof(Avalonia.Input.PullGestureEndedEventArgs), Category = "Avalonia.Input")]
[assembly: ImportType(typeof(Avalonia.Input.ScrollGestureEventArgs), Category = "Avalonia.Input")]
[assembly: ImportType(typeof(Avalonia.Input.ScrollGestureEndedEventArgs), Category = "Avalonia.Input")]
[assembly: ImportType(typeof(Avalonia.Input.ScrollGestureInertiaStartingEventArgs), Category = "Avalonia.Input")]
[assembly: ImportType(typeof(Avalonia.Input.TappedEventArgs), Category = "Avalonia.Input")]
[assembly: ImportType(typeof(Avalonia.Input.TextInputEventArgs), Category = "Avalonia.Input")]
[assembly: ImportType(typeof(Avalonia.Input.VectorEventArgs), Category = "Avalonia.Input")]
[assembly: ImportType(typeof(Avalonia.Input.TextInput.TextInputMethodClientRequeryRequestedEventArgs), Category = "Avalonia.Input.TextInput")]
[assembly: ImportType(typeof(Avalonia.Input.TextInput.TextInputMethodClientRequestedEventArgs), Category = "Avalonia.Input.TextInput")]
[assembly: ImportType(typeof(Avalonia.Interactivity.CancelRoutedEventArgs), Category = "Avalonia.Interactivity")]
[assembly: ImportType(typeof(Avalonia.Controls.CalendarDateChangedEventArgs), Category = "Avalonia.Controls")]
[assembly: ImportType(typeof(Avalonia.Controls.CalendarModeChangedEventArgs), Category = "Avalonia.Controls")]
[assembly: ImportType(typeof(Avalonia.Controls.ContextRequestedEventArgs), Category = "Avalonia.Controls")]
[assembly: ImportType(typeof(Avalonia.Controls.NumericUpDownValueChangedEventArgs), Category = "Avalonia.Controls")]
[assembly: ImportType(typeof(Avalonia.Controls.RefreshRequestedEventArgs), Category = "Avalonia.Controls")]
[assembly: ImportType(typeof(Avalonia.Controls.RequestBringIntoViewEventArgs), Category = "Avalonia.Controls")]
[assembly: ImportType(typeof(Avalonia.Controls.ScrollChangedEventArgs), Category = "Avalonia.Controls")]
[assembly: ImportType(typeof(Avalonia.Controls.SelectionChangedEventArgs), Category = "Avalonia.Controls")]
[assembly: ImportType(typeof(Avalonia.Controls.SizeChangedEventArgs), Category = "Avalonia.Controls")]
[assembly: ImportType(typeof(Avalonia.Controls.SpinEventArgs), Category = "Avalonia.Controls")]
[assembly: ImportType(typeof(Avalonia.Controls.TextChangedEventArgs), Category = "Avalonia.Controls")]
[assembly: ImportType(typeof(Avalonia.Controls.TextChangingEventArgs), Category = "Avalonia.Controls")]
[assembly: ImportType(typeof(Avalonia.Controls.TransitionCompletedEventArgs), Category = "Avalonia.Controls")]
[assembly: ImportType(typeof(Avalonia.Controls.Primitives.RangeBaseValueChangedEventArgs), Category = "Avalonia.Controls.Primitives")]
[assembly: ImportType(typeof(Avalonia.Controls.Primitives.TemplateAppliedEventArgs), Category = "Avalonia.Controls.Primitives")]
[assembly: ImportType(typeof(Avalonia.Interactivity.RoutedEventArgs), Category = "Avalonia.Interactivity")]

And that’s what I get when I try to add node:

That’s a commit if more info needed added routed event listners generator · bj-rn/VL.Avalonia@cc52cc5 · GitHub

changing to [assembly: ImportType(typeof(DragEventArgs), Category = "Avalonia.Input")] does not help

Checked out your branch, opened routed event help patch, getting this

But not the crash you talk about - can you be more specific with detailed steps what to do?

try Create one of this RoutedEventArgs (Avalonia.Input) for instance or RoutedEventArgs

yes the patch is in error state, i got this error and messed it up a bit. But the problem is not with RoutedEvenntListners but with RoutedEventArgs

That’s prolly the real error:

Basically the Listener is a generic delegate, and generated ones provide you with event args, the issue happens as soon as you try type event args or access them.

Thanks, can reproduce it now.

@antokhio Sorry for not telling earlier, but the type forwarding issue should’ve been fixed in latest preview. Can you confirm?

Seems to look good now.