3

I have a Unity + Zenject setup with a ProjectInstaller with some global dependencies that adhere to a "modal" interface, e.g.,

public class ProjectInstaller : MonoInstaller {
  public override void InstallBindings() {
    Container.Bind<ModalManager>().AsSingle();

    Container.Bind<Modal>().To<DialogManager>().AsSingle();
  }
}

Some modals are only relevant to certain scenes, so I bind those in the SceneInstaller:

public class SceneInstaller : MonoInstaller {
  public override void InstallBindings() {
    Container.BindInterfacesAndSelfTo<InventoryManager>()
      .FromComponentInNewPrefab(InventoryPrefab)
      .AsSingle()
  }
}

I want to manage all modals from the single ModalManager, defined at the project scope. So it has a List<Modal> binding:

public class ModalManager : MonoBehaviour {
    [Inject]
    protected List<Modal> _modals;
}

When I run this, the ModalManager only gets a single modal: the one defined in the project scope. In my understanding the SceneContext is a subcontainer of the ProjectContext. So I should be able to use FromSubContainerResolve in the ProjectInstaller to bind items in the child scene, perhaps by adding a line like:

// ProjectInstaller.cs
public override void InstallBindings() {
  // ...
  Container.Bind<Modal>().To<InventoryManager>().FromSubContainerResolve();
}

But I'm not sure which of the eleventy FromSubContainerResolve methods make sense for this case. They all seem pertinent to prefabs with a game object context, not for use from within the ProjectContext.

Does this use case make sense? Is there an easier or better way?

Charlie
  • 15,069
  • 3
  • 64
  • 70

1 Answers1

3

The problem is that that ModalManager can only be injected with dependencies that are added directly to ProjectContext. For these kinds of problems I recommend using the following pattern:

public interface IModal
{
}

public class ModalManager
{
    private readonly List<IModal> _modals = new List<IModal>();

    public IReadOnlyList<IModal> Modals
    {
        get { return _modals; }
    }

    public void AddModal(IModal modal)
    {
        _modals.Add(modal);
    }

    public bool RemoveModal(IModal modal)
    {
        return _modals.Remove(modal);
    }
}

public class ModalRegisterHandler : IInitializable, IDisposable
{
    private readonly List<IModal> _modals;
    private readonly ModalManager _modalManager;

    public ModalRegisterHandler(
        // We need to use InjectSources.Local here, otherwise we will
        // add any project context modals again in each scene
        [Inject(Source = InjectSources.Local)]
        List<IModal> modals, ModalManager modalManager)
    {
        _modals = modals;
        _modalManager = modalManager;
    }

    public void Initialize()
    {
        foreach (var modal in _modals)
        {
            _modalManager.AddModal(modal);
        }
    }

    public void Dispose()
    {
        // We don't want ModalManager to retain references to Modals defined in unloaded scenes
        // (dispose is executed on scene unload)
        foreach (var modal in _modals)
        {
            _modalManager.RemoveModal(modal);
        }
    }
}

public class SceneInstaller : MonoInstaller
{
    public override void InstallBindings()
    {
        Container.Bind<IModal>().To<FooModal>();
        Container.Bind<IModal>().To<BarModal>();
    }
}

public class ProjectInstaller : MonoInstaller
{
    public override void InstallBindings()
    {
        // We use CopyIntoDirectSubContainers so that ModalRegisterHandler gets automatically added to every 
        // scene context
        Container.BindInterfacesTo<ModalRegisterHandler>().AsSingle().CopyIntoDirectSubContainers();

        Container.Bind<ModalManager>().AsSingle();

        Container.Bind<IModal>().To<QuxModal>();
        Container.Bind<IModal>().To<FizzModal>();
    }
}
Steve Vermeulen
  • 1,406
  • 1
  • 19
  • 25
  • Ahh, that's a clever workaround. Appreciate the quick response Steve! – Charlie Feb 04 '22 at 17:40
  • 1
    `// We use CopyIntoDirectSubContainers so that ModalRegisterHandler gets automatically added to every // scene context` -> I found this confusing because I thought that bindings were visible by the container itself and its children so this should not be needed. Aren't the bindings inherited directly in subcontainers? – rustyBucketBay Feb 04 '22 at 17:49
  • 2
    @rustyBucketBay the binding is inherited, but since he's using `BindInterfacesTo` my hunch is he's adding its `IInitializable` / `IDisposable` lifecycle into each child scene context. So those `Initialize` and `Dispose` methods will be invoked on each scene transition automatically! – Charlie Feb 04 '22 at 18:35