0

I have a .json file with so-called commands:

"Commands":[{
   "EventName": "MouseLeftButtonUp",
   "MethodToExecute": "NextJson",
   "Args": "Next.json"
},{
   "EventName": "MouseRightButtonUp",
   "MethodToExecute": "CloseApp"
}

I deserialize this json to this class:

    public class Command
    {
        [JsonPropertyName("EventName")]
        public string EventName { get; set; }

        [JsonPropertyName("MethodToExecute")]
        public string MethodToExecute { get; set; }

        [JsonPropertyName("Args")]
        public string Args { get; set; }

        /*Methods*/
    }

EventName is a name of UIElement class events. MethodToExecute is a name of method to call, when event triggered. Args are the args, passed to the MethodToExecute.

I don't want my users to be able to call any method in the application, so I don't use reflection to get MethodInfo, instead I create Dictionary: Dictionary<string, Delegate> MethodsDictionary. The key in this dictionary is the name of method (MethodToExecute from Command class), and the value is something like this:

MethodsDictionary.Add(nameof(CloseApp), new Action(CloseApp));
MethodsDictionary.Add(nameof(NextJson), new Action<string>(NextJson));

Without using reflection, I'd added the event handler like this:

button.MouseLeftButtonUp += (sender, args) => MethodsDictionary[command.MethodToExecute].DynamicInvoke(command.Args);

But I'd like to make a dynamic binding of events. Well, of course I can make it through ugly switch-case on command.Name property, but I still would like to try the solution with reflection. The solutuion, as I see it, should looke something like:

foreach (var command in commands)
{
    command.Bind(uielement, MethodsDictionary[command.MethodToExecute]);
}

//And command.Bind method is like:

public void Bind(UIElement uielement, Delegate methodToExecute)
{
    //I know there's no such method like GetEventHandler, just an example
    var handler = uielement.GetEventHandler(EventName);
    handler += (sender, args) => methodToExecute.DynamicInvoke(Args);
}

I searched through several pretty similar questions:

Subscribe to an event with Reflection

https://social.msdn.microsoft.com/Forums/vstudio/en-US/d7f184f1-0964-412a-8659-6759a0e2db83/c-reflection-problem-subscribing-to-event?forum=netfxbcl

https://learn.microsoft.com/en-us/dotnet/framework/reflection-and-codedom/how-to-hook-up-a-delegate-using-reflection

AddEventHandler using reflection

Add Event Handler using Reflection ? / Get object of type?

But these doesn't help me to solve the problem the way I want to. I tried some of the solutions above, but they didn't work out for me, failing with different exceptions.

UPD.

I tried to implement handler binding through switch-case, as I mentioned above. It resulted in this method inside Command class:

public void Bind(UIElement element)
{
    switch (this.Name)
    {
        case "MouseRightButtonUp":
        {
            element.MouseRightButtonUp += (sender, args) => MethodsDictionary[this.MethodToExecute].DynamicInvoke(this.Args);
            break;
        }
        case "Click":
        {
            //UIElement doesn't have Click event
            var button = element as ButtonBase;
            button.Click += (sender, args) => MethodsDictionary[this.MethodToExecute].DynamicInvoke(this.Args);
            break;
        }
        /*And so on for each event*/
        default:
        {
            throw new NotSupportedException();
        }
    }
}

I don't like, how that part with adding new handlers is just a copy-paste of previous section, but I don't see another workaround in this situation. I'd like to use the reflection in this case, but I don't know if it's possible.

Gigas002
  • 13
  • 7

2 Answers2

0

If you can't get reflection to work, you can use another Dictionary to store supported event subscriber methods:

Command.cs

public class Command
{
    [JsonPropertyName("EventName")]
    public string EventName { get; set; }

    [JsonPropertyName("MethodToExecute")]
    public string MethodToExecute { get; set; }

    [JsonPropertyName("Args")]
    public string Args { get; set; }

    /*Methods*/
}

JsonEventMapper.cs

class JsonEventMapper
{
  private Dictionary<string, Action<UIElement, EventHandler>>> SupportedEventSubscriberMap { get; }
  private Dictionary<string, EventHandler> RegisteredEventHandlerMap { get; }   

  public JsonEventMapper()
  {
    this.SupportedEventSubscriberMap = new Dictionary<string, Action<UIElement, EventHandler>>() 
    { 
      { 
        nameof(UIElement.MouseRightButtonUp), (uiElement, handler) => uiElement.MouseRightButtonUp += handler.Invoke 
      },
      { 
        nameof(UIElement.LostFocus), (uiElement, eventSubscriber) => uiElement.LostFocus += handler.Invoke 
      }
    };

    this.RegisteredEventHandlerMap = new Dictionary<string, EventHandler>()
    {
      {
        nameof(UIElement.MouseLeftButtonUp), CloseApp
      },
      {
        nameof(UIElement.LostFocus), NextJson
      }
    };
  }

  public void RegisterJsonCommands(IEnumerable<Command> commands, UIElement uiElement)
  {    
    foreach (var command in commands)
    {
      BindCommandToEvent(uiElement, command);
    }
  }

  private void BindCommandToEvent(UIElement uiElement, Command command)
  {    
    if (this.SupportedEventSubscriberMap.TryGetValue(command.EventName, out Action<UIElement, EventHandler> eventSubscriber)
      && this.RegisteredEventHandlerMap.TryGetValue(command.EventName, out EventHandler eventHandler))
    {
      eventSubscriber.Invoke(uiElement, eventHandler);
    }
  }

  private void CloseApp(object sender, EventArgs args)
  {    
    // Handle event
  }

  private void NextJson(object sender, EventArgs args)
  {    
    // Handle event
  }
}

Usage

IEnumerable<Command> commands = DeserializeJsonToCommands();
var eventMapper = new JsonEventMapper();
eventMapper.RegisterJsonCommands(commands, button);

You surely want to adjust details to your specific scenario.

Don't forget to unsubscribe from the events (e.g., by defining another SupportedEventUnsubscriberMap).

BionicCode
  • 1
  • 4
  • 28
  • 44
  • Thank you for your detailed answer! I tried to implement handler binding through `switch-case`, as I mentioned in the OP. It resulted in this method `Bind` inside `Command` class (updated OP). I don't like, how that part with adding new handlers is just a copy-paste of previous section, but I don't see another workaround in this situation. I didn't try your solution yet, but from code I think it has pretty much the same problem. I'd like to use the reflection in this case, but I don't know if it's possible. – Gigas002 Mar 03 '20 at 20:33
  • Yes, of course does reflection work. You've already posted a few simple examples, but you didn't seem to be able to make it work. You also never get into details about what exactly wasn't working with your attempts, so I thought you might be happier with a reflection-free solution. If you still want to make reflection work, you should post your code and describe what exactly is not working and which exact error you get. I will then take a look at it. – BionicCode Mar 03 '20 at 21:03
  • [Here's](https://github.com/Gigas002/DynamicCommandBinding) my repo with sample app. I've also included the closest (judjing by the answers on previous questions) variation of binding through reflection and written in comments what exactly doesn't work (see Models/CommandModel.cs). – Gigas002 Mar 04 '20 at 17:10
0

I think what you're trying to do is something like that:

public class CommandModel
{
    /// properties

    public void Bind(UIElement element)
    {
        EventBinder.Bind(() => MainViewModel.RunCommand(MethodToExecute, Args), element, EventName);
    }
}

public static class EventBinder
{
    public static void Bind(Action action, object eventSource, string eventName)
    {
        var eventInfo = eventSource.GetType().GetEvent(eventName);

        EventHandler eventHandler = (s, e) => action();

        eventInfo.AddEventHandler(eventSource, ConvertDelegate(eventHandler, eventInfo.EventHandlerType));
    }

    private static Delegate ConvertDelegate(Delegate originalDelegate, Type targetDelegateType)
    {
        return Delegate.CreateDelegate(
            targetDelegateType,
            originalDelegate.Target,
            originalDelegate.Method);
    }
}
Danillo
  • 23
  • 1
  • 5