VContainer icon indicating copy to clipboard operation
VContainer copied to clipboard

If I want to create a prefab with its own LifetimeScope using RegisterComponentInNewPrefab(), how do I set the parent of that LifetimeScope?

Open ganaware opened this issue 3 years ago • 4 comments

What I want to do

When I create a prefab (filename: MyBehaviour.prefab) with its own LifetimeScope like this:

▼ GameObjectA (with components: MyBehaviour, MyPrefabLifetimeScope)
   ▼ GameObjectB
        GameObjectC
public class MyBehaviour : MonoBehaviour { ... }
public class MyPrefabLifetimeScope : LifetimeScope { ... }

I want to set the parent scope of MyPrefabLifetimeScope to the scope in which I instantiate the prefab.

If only one parent scope exists

public class GameLifetimeScope : LifetimeScope
{
    [SerializeField] MyBehaviour prefab; // this referes MyBehaviour.prefab

    public override void Configure(IContainerBuilder builder)
    {
        builder.RegisterComponentInNewPrefab(prefab, Lifetime.Singleton);
    }
}

In this case, it is no problem to set GameLifetimeScope as the parent scope of MyPrefabLifetimeScope. I can do so in the inspector of MyPrefabLifetimeScope.

When multiple parent scopes exist

public class GameLifetimeScope : LifetimeScope { /* same as above */ }

// same as GameLifetimeScope
public class TestLifetimeScope : LifetimeScope
{
    [SerializeField] MyBehaviour prefab; // this also referes MyBehaviour.prefab

    public override void Configure(IContainerBuilder builder)
    {
        builder.RegisterComponentInNewPrefab(prefab, Lifetime.Singleton);
    }
}

I am having trouble finding a way to successfully set the parent scope if I want to use the same MyBehaviour.prefab for TestLifetimeScope in addition to GameLifetimeScope.

Dirty Solution

Currently, I am writing the following code to get by:

  • Set MyPrefabLifetimeScope.parentReference to point to the runtime parent scope
    • In that case, ParentReference.Type { set; } is private, so I use reflection to force it to be written
public class MyBehaviour : MonoBehaviour
{
    [SerializeField] MyPrefabLifetimeScope myPrefabLifetimeScope;
    
    [Inject]
    public void Construct(LifetimeScope lifetimeScope)
    {
        myPrefabLifetimeScope.InitParentReference(lifetimeScope);
    }
}

public class MyPrefabLifetimeScope : LifetimeScope
{
    public void InitParentReference(LifetimeScope parent)
    {
        System.Type prentType = parent.GetType();
        object pr = new ParentReference { TypeName = parentType.FullName };
        SetPrivatePropertyValue(pr, nameof(ParentReference.Type), parentType);
        this.parentReference = (ParentReference)pr;
    }

    static void SetPrivatePropertyValue<T>(object obj, string propName, T val)
    {
        Type t = obj.GetType();
        if (t.GetProperty(propName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) == null)
            throw new ArgumentOutOfRangeException();
        t.InvokeMember(propName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.SetProperty | BindingFlags.Instance, null, obj, new object[] { val });
    }
}

What I really want to do

Actually, what I really want to do is calling InjectGameObject() to GameObjectB. However, I want to do the injection before the Awake() of MyBehaviour and GameObjectB.

If there is another way to meet this requirement, that is fine.

In the above example, MyPrefabLifetimeScope.autoInjectGameObjects references to GameObjectB.

ganaware avatar May 07 '22 06:05 ganaware

Instead of using Container.Instantiate() you should be calling LifetimeScope.CreatChildFromPrefab(yourPrefab)

Here's how you can instantiate a gameobject with a scope on it.

public MyBehaviour prefab;

Protected override void Configure(IContainerBuilder builder)
{
    builder.RegisterFactory<MyBehaviour>(c => () => 
    {
        var childScope = prefab.GetComponent<LifetimeScope>();
        var instanceChildScope = CreateChildFromPrefab(childScope);
        var instanceMyBehaviour = instanceChildScope.GetComponent<MyBehaviour>();
        return instanceMyBehaviour;
    }, Lifetime.Scoped);

SimonNordon4 avatar Apr 15 '23 14:04 SimonNordon4

Thank you SimonNordon4. Your method certainly satisfies "What I really want to do".

Sorry, I forgot to mention my assumption that I want to create a singleton from a prefab. And I want to write "MyBehaviour" as an argument to the constructor of other classes.

I don't think this can be expressed well using a factory.

nayuta-cr avatar Apr 17 '23 10:04 nayuta-cr

Does the singleton object have to be a prefab? It would be easier if it existed in the scene:

builder.RegisterComponentInHeirachy<MyBehaviour>();

otherwise I think you would have to use a factory.

builder.RegisterFactory<MyBehaviour>(c => () =>
{
    if (MyBehaviour.Singleton = null)
        // There should be logic in MyBehaviour to set itself as the Singleton Instance when created.
        CreateChildFromPrefab(myBehaviour.GetComponent<LifetimeScope>();
    return MyBehaviour.Singleton;        
}, Lifetime.Scoped);

Although this is starting to get quite messy.

SimonNordon4 avatar Apr 18 '23 00:04 SimonNordon4

I prefer to use a prefab because it often changes unexpectedly when placed in a scene.

I think the latter code would need to run a factory method in order to use it, as follows:

class A {
    MyBehaviour _MyBehaviour;
    [Inject]
    public A(Func<MyBehaviour> factory) {
        _MyBehaviour = factory();
    }
}

But what I want to do is to pass it to the constructor normally as follows. My "Dirty Solution" allows me to do that.

class A {
    MyBehaviour _MyBehaviour;
    [Inject]
    public A(MyBehaviour mb) {
        _MyBehaviour = mb;
    }
}

Thanks anyway!

nayuta-cr avatar Apr 18 '23 02:04 nayuta-cr