Unsubscribe from IObservableElementEnumerable.EnumerableChanged doesn't work?

StackOverflow https://stackoverflow.com/questions/10922416

  •  13-06-2021
  •  | 
  •  

문제

Parts of our UI uses IObservableElementEnumerable.EnumerableChanged in order to update if the user e.g. deletes a domain object from a folder.

When the UI is disposed, we unsubscribe from the event... or so we thought. It turns out that the unsubscribe doesn't have any effect, and our event handler is still called. This caused a number of odd bugs, but also leads to memory leaks.

The only time unsubscription works, is if we store the IObservableElementEnumerable reference instead of calling IObservableElementEnumerableFactory.GetEnumerable(obj) again. But this, in turn, is likely to keep a live reference to the folder object, which will break if the folder itself is deleted by the user.

This is particularly puzzling as the GetEnumerable() documentation clearly states: "It is expected that subsequent calls with the same domain object will yield the same instance of IObservableElementEnumerable." Is this not to be interpreted as a guarantee?

Should there be any reason for unsubscription not working?

The following code replicates the issue on Petrel 2011 (add to a simple plugin with a menu extension, or get the full solution here (DropBox)):

using System;
using System.Linq;
using System.Windows.Forms;
using Slb.Ocean.Core;
using Slb.Ocean.Petrel;
using Slb.Ocean.Petrel.Basics;
using Slb.Ocean.Petrel.UI;

namespace ObservableElementEnumerable
{
  public class OEEForm : Form
  {
    private Droid _droid;
    private bool _disposed;

    public OEEForm()
    {
      IInput input = PetrelProject.Inputs;
      IIdentifiable selected = input.GetSelected<object>().FirstOrDefault() as IIdentifiable;
      if (selected == null)
      {
        PetrelLogger.InfoOutputWindow("Select a folder first");
        return;
      }
      _droid = selected.Droid;

      GetEnumerable().EnumerableChanged += enumerable_EnumerableChanged;
      PetrelLogger.InfoOutputWindow("Enumerable subscribed");
    }

    protected override void Dispose(bool disposing)
    {
      base.Dispose(disposing);
      if (disposing && !_disposed)
      {
        GetEnumerable().EnumerableChanged -= enumerable_EnumerableChanged;
        PetrelLogger.InfoOutputWindow("Enumerable unsubscribed (?)");
        _droid = null;
        _disposed = true;
      }
    }

    IObservableElementEnumerable GetEnumerable()
    {
      if (_disposed)
        throw new ObjectDisposedException("OEEForm");
      object obj = DataManager.Resolve(_droid);
      IObservableElementEnumerableFactory factory = CoreSystem.GetService<IObservableElementEnumerableFactory>(obj);
      IObservableElementEnumerable enumerable = factory.GetEnumerable(obj);
      return enumerable;
    }

    void enumerable_EnumerableChanged(object sender, ElementEnumerableChangeEventArgs e)
    {
      PetrelLogger.InfoOutputWindow("Enumerable changed");
      if (_disposed)
        PetrelLogger.InfoOutputWindow("... but I am disposed and unsubscribed!");
    }
  }

  public static class Menu1
  {
    public static void OEEBegin1_ToolClick(object sender, System.EventArgs e)
    {
      OEEForm f = new OEEForm();
      f.Show();
    }
  }
}

To replicate:

  1. Run Petrel with the plugin
  2. Load a project with a folder with objects
  3. Select the folder
  4. Activate the plugin menu item
  5. With the popup open, delete an object in the folder
  6. Close the Form popping up
  7. Delete an object in the folder

The message log should clearly show that the event handler is still called after the form is disposed.

도움이 되었습니까?

해결책

You already keep a reference to the underlying enumerable by connecting the event. Events are references as well. Just keep a reference to the enumerable and unsubscribe from the same instance as the one you subscribe to.

To deal with the issue of objects that are deleted by the user you need to listen to the delete event.

라이센스 : CC-BY-SA ~와 함께 속성
제휴하지 않습니다 StackOverflow
scroll top