简体   繁体   中英

How do I add event handlers in a loop?

I am trying to create a tray icon in C#/.NET and so far I have this code that works:

        ....

        Icon i = new Icon("favicon.ico");
        ContextMenu cm = new ContextMenu();
        ni.Icon = i;            

        MenuItem delMi = new MenuItem("Delete stuff");
        MenuItem closeMi = new MenuItem("Close");
        MenuItem testMi = new MenuItem("Test");

        cm.MenuItems.Add(testMi);
        cm.MenuItems.Add(delMi);
        cm.MenuItems.Add(closeMi);

        testMi.Click += TestMi_Click;
        delMi.Click += DelMi_Click;
        closeMi.Click += CloseMi_Click;

        ni.ContextMenu = cm;
    }

    private void TestMi_Click(object sender, EventArgs e)
    {
        // Test event here
    }

    private void CloseMi_Click(object sender, EventArgs e)
    {
        // Close event here
    }

    private void DelMi_Click(object sender, EventArgs e)
    {
        // Delete event here
    }

But I am trying to separate the code by having a function that returns an array of MenuItem instances, and having a loop that adds them to the ContextMenu , but I'm not sure how to add the click event handlers to the MenuItem instances in the loop:

        ....
        Icon i = new Icon("favicon.ico");
        ContextMenu cm = new ContextMenu();
        ni.Icon = i;            

        MenuItem[] miArray = getArrayMI();

        foreach(MenuItem mi in miArray)
        {
            cm.MenuItems.Add(mi);

            //Not sure what to do here
            mi.Click += mi
        }

        // How do I put this section into the loop instead 
        // of adding the event handlers one by one?  
        testMi.Click += TestMi_Click;
        delMi.Click += DelMi_Click;
        closeMi.Click += CloseMi_Click;

        ni.ContextMenu = cm;
    }

    private MenuItem[] getArrayMI( )
    {
        MenuItem[] miArray = { new MenuItem("Delete stuff"), new MenuItem("Close"), new MenuItem("Test") };
        return miArray;
    }

    private void TestMi_Click(object sender, EventArgs e)
    {
        // Test event here
    }

    private void CloseMi_Click(object sender, EventArgs e)
    {
        // Close event here
    }

    private void DelMi_Click(object sender, EventArgs e)
    {
        // Delete event here
    }

The only thing I could think of would be to do something like this:

    foreach(MenuItem mi in miArray)
    {
        cm.MenuItems.Add(mi);

        mi.Click += mi.ToString() + "_Click";
    }

I don't think it's a bad idea to abstract your original code, but I'd suggest looking at the abstraction in a different way. I'd recommend implementing some kind of separation of the view from the model - MVC, MVP, MVVM, etc. In this way, the code that actually happens when the click occurs is abstracted away from the view, into another layer of code.

For example, consider something like this (writing without an IDE so please forgive typos):

public interface IContextAction
{
    string DisplayName { get; }
    Action Invoke { get; }
}


public class WindowViewModel
{
    public IEnumerable<IContextAction> ContextActions { get; private set; }
    /* ... */
}


    /* ... */
    ContextMenu cm = new ContextMenu();
    foreach (IContextAction action in viewModel.ContextActions)
    { 
        MenuItem item = new MenuItem(action.DisplayName);
        cm.MenuItems.Add(item);
        item.Click += (sender,args) => action.Invoke();
    }

I agree with the comment that suggests that, at least for the code example you posted, there is no need to "improve" the code. It's already a reasonable way to implement that particular logic. Furthermore, it is my preference to avoid relying on naming conventions to tie specific code to specific run-time objects. Doing so results in a fragile (ie easily broken) implementation, and restricts your ability to change the name of the code (eg to address some unrelated aspect of naming which would otherwise provide for more readable code).

That said, if you really want to do this, you can. Here is a Minimal, Complete, and Verifiable code example that illustrates how to create a delegate instance for an event handler based on the name of the object, and subscribe to the object's event:

class Program
{
    static void Main(string[] args)
    {
        Class[] classInstances =
        {
            new Class("A"),
            new Class("B"),
            new Class("C"),
        };

        foreach (Class c in classInstances)
        {
            string methodName = c.Name + "_Event";
            MethodInfo mi = typeof(Program).GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Static);
            EventHandler handler = (EventHandler)Delegate.CreateDelegate(typeof(EventHandler), mi);

            c.Event += handler;
        }

        foreach (Class c in classInstances)
        {
            c.RaiseEvent();
        }
    }

    static void A_Event(object sender, EventArgs e) { Console.WriteLine("A_Event handler"); }
    static void B_Event(object sender, EventArgs e) { Console.WriteLine("B_Event handler"); }
    static void C_Event(object sender, EventArgs e) { Console.WriteLine("C_Event handler"); }
}

class Class
{
    public string Name { get; }

    public Class(string name)
    {
        Name = name;
    }

    public event EventHandler Event;

    public void RaiseEvent()
    {
        Event?.Invoke(this, EventArgs.Empty);
    }
}

Personally, I prefer a more explicit approach. That is, if there's really a need to encapsulate the assignment of handler to object in an abstracted way, to put this in explicit code. For example, provide a single event handler method to subscribe to all controls, and then have that method dispatch to the appropriate method by name:

static void Main(string[] args)
{
    Class[] classInstances =
    {
        new Class("A"),
        new Class("B"),
        new Class("C"),
    };

    foreach (Class c in classInstances)
    {
        c.Event += All_Event;
    }

    foreach (Class c in classInstances)
    {
        c.RaiseEvent();
    }
}

static void All_Event(object sender, EventArgs e)
{
    switch (((Class)sender).Name)
    {
        case "A":
            A_Event(sender, e);
            break;
        case "B":
            B_Event(sender, e);
            break;
        case "C":
            C_Event(sender, e);
            break;
    }
}

Alternatively, you can use a dictionary to represent the mapping from name to method:

static void Main(string[] args)
{
    Class[] classInstances =
    {
        new Class("A"),
        new Class("B"),
        new Class("C"),
    };

    Dictionary<string, EventHandler> nameToHandler = new Dictionary<string, EventHandler>()
    {
        { "A", A_Event },
        { "B", B_Event },
        { "C", C_Event },
    };

    foreach (Class c in classInstances)
    {
        c.Event += nameToHandler[c.Name];
    }

    foreach (Class c in classInstances)
    {
        c.RaiseEvent();
    }
}

In both of those examples, you don't save any typing (the switch -based approach is particularly verbose), but it does move the object-to-handler relationship into its own area of the code, allowing it to be maintained more easily without having to deal with the event subscription itself.

If you really want a fully dynamic, reflection-based approach, I would opt for something more explicit and less fragile that relying on the method name. For example, you can create a custom attribute for the event handler methods, used to define what method goes with what object. This provides for a reasonably minimal amount of typing, but disconnects the method name from the mapping, so that you can go ahead and refactor the code to your heart's content without worrying about the event-handling aspect.

That would look something like this:

class Program
{
    static void Main(string[] args)
    {
        Class[] classInstances =
        {
            new Class("A"),
            new Class("B"),
            new Class("C"),
        };

        Dictionary<string, EventHandler> nameToHandler =
                (from mi in typeof(Program).GetMethods(BindingFlags.NonPublic | BindingFlags.Static)
                 let attribute = (Handler)mi.GetCustomAttribute(typeof(Handler))
                 where attribute != null
                 select new { attribute.Target, mi })
             .ToDictionary(x => x.Target, x => (EventHandler)Delegate.CreateDelegate(typeof(EventHandler), x.mi));

        foreach (Class c in classInstances)
        {
            c.Event += nameToHandler[c.Name];
        }

        foreach (Class c in classInstances)
        {
            c.RaiseEvent();
        }
    }

    [Handler("A")]
    static void A_Event(object sender, EventArgs e) { Console.WriteLine("A_Event handler"); }
    [Handler("B")]
    static void B_Event(object sender, EventArgs e) { Console.WriteLine("B_Event handler"); }
    [Handler("C")]
    static void C_Event(object sender, EventArgs e) { Console.WriteLine("C_Event handler"); }
}

class Handler : Attribute
{
    public string Target { get; }

    public Handler(string target)
    {
        Target = target;
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM