Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to Register Singletons that reference other Singletons? #702

Open
fckaye opened this issue Aug 28, 2024 · 1 comment
Open

How to Register Singletons that reference other Singletons? #702

fckaye opened this issue Aug 28, 2024 · 1 comment

Comments

@fckaye
Copy link

fckaye commented Aug 28, 2024

I am quite new to this Dependency Injection thing in general, so I'm sorry if the question is silly or if there is a workaround that I am not aware of.

I was trying to follow the HelloWorld example on the documentation and start using VContainer from there.

Then I had a problem, when I was registering 2 classes which implement IStartable and one has a reference to another. I would get a message like the following:

VContainerException: Failed to resolve FooFeature.FooController : No such registration of type: BarFeature.BarController

I tried to reduce my app to a minimal setup so my trouble would be easier to understand/reproduce.

Imagine we have the following 5 scripts:

BarController

It basically takes BarView Button and OnClick, it will change the text contents of the Label of FooView through FooController

public class BarController : IStartable
    {
        private readonly BarView barView;
        private readonly FooController fooController;

        public BarController(BarView barView, FooController fooController)
        {
            this.barView = barView;
            this.fooController = fooController;
        }

        void IStartable.Start()
        {
            barView.BarButton.onClick.AddListener(() => fooController.UpdateLabelText("Bullshit coming from BarController"));
        }

        public void UpdateLabelText(string contents)
        {
            barView.BarLabel.text = $"BarLabel: \n{contents}";
        }
    }

BarView

    public class BarView : MonoBehaviour
    {
        public Button BarButton;
        public TextMeshProUGUI BarLabel;
    }

FooController

It basically takes FooView Button and OnClick, it will change the text contents of the Label of BarView through BarController

    public class FooController : IStartable
    {
        private readonly FooView fooView;
        private readonly BarController barController;

        public FooController(FooView fooView, BarController barController)
        {
            this.fooView = fooView;
            this.barController = barController;
        }

        void IStartable.Start()
        {
            fooView.FooButton.onClick.AddListener(() => barController.UpdateLabelText("Bullshit coming from FooController"));
        }
        
        public void UpdateLabelText(string contents)
        {
            fooView.FooLabel.text = $"FooLabel: \n{contents}";
        }
    }

FooView

    public class FooView : MonoBehaviour
    {
        public Button FooButton;
        public TextMeshProUGUI FooLabel;
    }

GameLifetimeScope

LifetimeScope to register components

public class GameLifetimeScope : LifetimeScope
{
    [SerializeField] private FooView fooView;
    [SerializeField] private BarView barView; 
        
    protected override void Configure(IContainerBuilder builder)
    {
        builder.RegisterComponent(fooView);
        builder.RegisterComponent(barView);

        // If you have multiple Entry Points, you can also use the 
        // following declaration as grouping.
        builder.UseEntryPoints(Lifetime.Singleton, entryPoints =>
        {
            entryPoints.Add<FooController>();
            entryPoints.Add<BarController>();
        });
    }
}

Wiring up all the references on UnityEditor and pressing play will result in the following VContainerException:

VContainerException: Failed to resolve FooFeature.FooController : No such registration of type: BarFeature.BarController
VContainer.Internal.ReflectionInjector.CreateInstance (VContainer.IObjectResolver resolver, System.Collections.Generic.IReadOnlyList`1[T] parameters) (at ./Library/PackageCache/jp.hadashikick.vcontainer@afaeff8204/Runtime/Internal/ReflectionInjector.cs:51)
VContainer.Internal.InstanceProvider.SpawnInstance (VContainer.IObjectResolver resolver) (at ./Library/PackageCache/jp.hadashikick.vcontainer@afaeff8204/Runtime/Internal/InstanceProviders/InstanceProvider.cs:21)

I would really appreciate any help!

@ZumiKua
Copy link

ZumiKua commented Sep 12, 2024

The problem here is that you do not register BarController in your GameLifetimeScope, then VContainer does not know how to find a BarController for you. entryPoints.Add<BarController>(); only does two things:

  1. Make VContainer knows BarController is a IStartable (and other interfaces it implements), you can also do that by builder.Register<BarController>(Lifetime.Singleton).AsImplementedInterfaces(), now, everytime you require a IStartable, VContainer will throw you an instance of BarController.
  2. Make sure EntryPointDispatcher is registered, a class who is responsible for calling IStartable.Start()

Please note here, entryPoints.Add<BarController>(); does not tell VContainer what to do when someone needs a BarController, to tell VContainer that, you need the following code:

    entryPoints.Add<BarController>().AsSelf();

But here is another problem, your BarController requires FooController too, this creates a circular reference.

When VContainer create your FooController, it must create a BarController first, otherwise it has no argument for FooController's constructor. But when creating BarController, VContainer must find a FooController too. which creates a dead loop.

To solve this problem, you can remove the dependency of FooController from BarController's constructor, instead setting BarController.fooController at the end of FooController's constructor.

Please note, this is not the best practice, you should avoid circular dependency.

Here is all the code that changed:

public class GameLifetimeScope : LifetimeScope
{
    [SerializeField] private FooView fooView;
    [SerializeField] private BarView barView; 
        
    protected override void Configure(IContainerBuilder builder)
    {
        builder.RegisterComponent(fooView);
        builder.RegisterComponent(barView);
 
        builder.UseEntryPoints(Lifetime.Singleton, entryPoints =>
        {
            entryPoints.Add<FooController>().AsSelf();
            entryPoints.Add<BarController>().AsSelf();
        });
    }
}
public class FooController : IStartable
{
    private readonly FooView fooView;
    private readonly BarController barController;

    public FooController(FooView fooView, BarController barController)
    {
        this.fooView = fooView;
        this.barController = barController;
        this.barController.fooController = this;
    }

    void IStartable.Start()
    {
        fooView.FooButton.onClick.AddListener(() => barController.UpdateLabelText("Bullshit coming from FooController"));
    }
        
    public void UpdateLabelText(string contents)
    {
        fooView.FooLabel.text = $"FooLabel: \n{contents}";
    }
}
public class BarController : IStartable
{
    private readonly BarView barView;
    public FooController fooController;

    public BarController(BarView barView)
    {
        this.barView = barView;
    }

    void IStartable.Start()
    {
        barView.BarButton.onClick.AddListener(() => fooController.UpdateLabelText("Bullshit coming from BarController"));
    }

    public void UpdateLabelText(string contents)
    {
        barView.BarLabel.text = $"BarLabel: \n{contents}";
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants
@ZumiKua @fckaye and others