0

I'm very new to events/delegates so sorry if I use the incorrect terminology.

I'm using an inventory script for Unity, that uses C# events/delegates to subscribe to a right click event on an item slot.

The problem is when I dynamically add new item slots, I need to add the event handlers to the new slots. If I just run UpdateEvents(), the ones that were there in the first place, now has duplicate triggers.

The current code is using a lambda syntax, and I've studied these threads on how to create a delegate instance:

Here's the original lambda subscription:

// This is the lambda expression that I want to unsubscribe to
ItemSlots[i].OnRightClickEvent += slot => EventHelper(slot, OnRightClickEvent);

Here's what I tried, and I marked with ** on the parts that my IDE highlights as wrong:

// Try 1
EventHandler lambda = slot => EventHelper(slot, OnRightClickEvent);
ItemSlots[i].OnRightClickEvent += lambda;

// Try 2
EventHandler handler = (sender, e) => EventHelper(sender, OnRightClickEvent);
ItemSlots[i].OnRightClickEvent += handler;

// Try 3    
var myDelegate = delegate(sender, e) { EventHelper(**e**, OnRightClickEvent); };
ItemSlots[i].OnRightClickEvent += myDelegate;

I also tried converting it without using lambda, but it doesn't work like it should. I'm not sure what "slot" refers to in the lambda. Is it the instance triggering the event? Here's what didn't work, but didn't give any errors:

// Try without lambda
ItemSlots[i].OnRightClickEvent      += OnRightClickEvent;

Here's a shortened version of the complete code. I don't fully understand how the EventHelper()-method works, but it seems to be some shortcut to check for null.

using System;
using System.Collections.Generic;
using UnityEngine;

public abstract class ItemContainer : MonoBehaviour, IItemContainer {
    public List<ItemSlot> ItemSlots;

    // There are really 8 event here, but I simplified it
    public event Action<BaseItemSlot> OnRightClickEvent;

    protected virtual void Awake() {
        UpdateEvents();
        SetStartingItems();
    }

    public virtual void UpdateEvents() {
        for (int i = 0; i < ItemSlots.Count; i++) {
            // This is the lambda expression that I want to unsubscribe to
            ItemSlots[i].OnRightClickEvent += slot => EventHelper(slot, OnRightClickEvent);
        }
    }

    private void EventHelper(BaseItemSlot itemSlot, Action<BaseItemSlot> action) {
        if (action != null)
            action(itemSlot);
    }
}
Niclas
  • 1,362
  • 1
  • 11
  • 24
  • 2
    Possible duplicate of [Unsubscribe anonymous method in C#](https://stackoverflow.com/questions/183367/unsubscribe-anonymous-method-in-c-sharp) – Eliasar Mar 07 '19 at 17:25

2 Answers2

1

Let's do step by step:

// This is the lambda expression that I want to unsubscribe to
ItemSlots[i].OnRightClickEvent += slot => EventHelper(slot, OnRightClickEvent);

on the left side you have an event (OnRightClickEvent). The problem with the right side is that C# and .NET developers have gone way too far in simplifying the code syntax that it becomes harder to understand. Basically you could extend to:

ItemSlots[i].OnRightClickEvent += (slot) =>  { EventHelper(slot, OnRightClickEvent); };
    ItemSlots[i].OnRightClickEvent += delegate(slot){ EventHelper(slot, OnRightClickEvent);};

Those are the same. And if that is still a stretch:

private void ItemSlot_OnRightClickEvent(BaseItemSlot slot)
{
     EventHelper(slot, OnRightClickEvent);
}    

then you assign:

ItemSlots[i].OnRightClickEvent += ItemSlot_OnRightClickEvent;

slot is the parameter that will come from the ItemSlots object that is calling the event. This one is best to also remove the listener:

ItemSlots[i].OnRightClickEvent -= ItemSlot_OnRightClickEvent;

With an anonymous method, you can't remove the method since you don't have any pointer to it. And since it is event, you can only wipe the whole event from the owning class (ItemSlot in this case).

I think it is best you keep it simple and use the old syntax. For instance, Visual Studio will automatically offer the right method signature using tab to autocomplete when you start writing:

 ObjectName.EventName += [tab]

Visual studio will generate a method called ObjectName_EventName with right return value and parameter.

Everts
  • 10,408
  • 2
  • 34
  • 45
  • I ended up using the old syntax, and then unsubscribe and subscribe all slots, since it seems to work to unsubscribe even if it isn't subscribed. Thanks! – Niclas Mar 09 '19 at 07:47
0

The usual pattern, with C# events, is this:

    public event Action SomethingHappened;
    void OnSomethingHappened()
    {
        if (SomethingHappened!= null) SomethingHappened();
    }

    public void DoSomething()
    {
        // Actual logic here, then notify listeners
        OnSomethingHappened();
    }

You first declare the event, usually named using past tense. Then a private method used to fire the event, usually named with the On prefix (this could also be protected if derived classes need to fire it instead). Finally, a public method for external callers to make something happen and, in it, you notify the listeners firing the event.

You can give this event to both the ItemSlot and the ItemContainer classes:

    public event Action<BaseItemSlot> RightButtonClicked;
    void OnRightButtonClicked(BaseItemSlot item)
    {
        if (RightButtonClicked != null) RightButtonClicked(item);
    }

Then, you need to call ItemContainer.OnRightButtonClicked everytime an ItemSlot.RightButtonClicked fires.

So you pipe the events:

    void OnEnable()
    {
        for (int i = 0; i < ItemSlots.Count; i++)
        {
            ItemSlots[i].RightButtonClicked += OnRightButtonClicked;
        }
    }
    void OnDisable()
    {
        for (int i = 0; i < ItemSlots.Count; i++)
        {
            ItemSlots[i].RightButtonClicked -= OnRightButtonClicked;
        }
    }
dogiordano
  • 691
  • 6
  • 13
  • Actually, the OnSomething method should be protected so it can be called from subclass. – Everts Mar 07 '19 at 20:45
  • @Everts I think that depends on the event logic being totally contained in the base class or not. Anyway, I added a note with your suggestion. Thank you for pointing that out. – dogiordano Mar 07 '19 at 20:57
  • The C# pattern is usually done with protected and virtual. If your class is sealed, then protected and virtual will show as an error as it. If the class is not sealed, it is meant to be inherited and then, you most likely want to be able to call the method from subclass since it also contains the event (being public). – Everts Mar 07 '19 at 21:04
  • Thanks a lot, I like your explanation of events. But I have a problem fitting this solution into what I currently have. I have an event in the BaseItemSlot, which is triggered on right click, in ItemContainer I add its OnRightClickEvent, which I then subscribe to from the Character class. I'm drawing it all down, to try and understand it.. I'm very new to C# and events, so sorry for having trouble grasping this. – Niclas Mar 07 '19 at 21:05
  • Wouldn't your last code trigger the event on all slots at the same time, when each slot is clicked? – Niclas Mar 07 '19 at 21:09
  • Yes, but I think that's what's happening in your code too. So I simplified the code but kept that... But I guess that's why you want to unsubscribe some of them. I'll edit my answer. – dogiordano Mar 07 '19 at 21:13