Views and Mediators

Introduction

This tutorial teaches how to make and instantiate a view and how to bind a mediator to it. Views are, next to the ContextRoot, the only MonoBehaviours in IoC+ and therefore the only real connection to the Unity Engine. They visualize an object, process user input or maybe even check for collisions. Mediators link the views to the IoC framework. A mediator listens to its view via signals and make its view act according to signals dispatched by commands.

In this tutorial we continue with the result of the previous tutorial. If this is the first time using IoC+, we suggest to read the introduction first.

The View

Views are what we as players of our game can see and interact with. In IoC+ they are the only real connection to the Unity Engine and the only part that we cannot fully unit test. Therefore, we need to make the views as simple and straightforward as possible. Let’s make a view that handles the player’s input to make a character jump. Create a new script file called “PlayerInputView.cs” and add the following code.

using IoCPlus;
using UnityEngine;

public class PlayerInputView : View {

    public readonly Signal JumpInputSignal = new Signal();

    private void Awake() {
        Debug.Log("PlayerInputView is instantiated!");
    }

    private void Update() {
        if (Input.GetKeyDown(KeyCode.UpArrow)) {
            JumpInputSignal.Dispatch();
        }
    }

}

All views in IoC+ must subclass the View class. This PlayerInputView declares a public signal called JumpInputSignal. In its Update() method, it checks for the up arrow key to be pressed and if so, dispatches the JumpInputSignal. In its Awake() method it logs a short message so we can test whether the view has been instantiated.

The Mediator

Views will need a mediator to control it. Mediators link the views to our IoC framework of contexts and commands. Create a new script file called “PlayerInputMediator.cs” and add the following code.

using IoCPlus;

public class PlayerInputMediator : Mediator<PlayerInputView> {

    public override void Initialize() {
        view.JumpInputSignal.AddListener(OnViewJumpInputSignal);
    }

    public override void Dispose() {
        view.JumpInputSignal.RemoveListener(OnViewJumpInputSignal);
    }

    private void OnViewJumpInputSignal() {
        // Dispatch a signal to execute a command
    }

}

All mediators in IoC+ must subclass the Mediator<T> class, where we define the type of view that we want to mediate. The instance of the view that a mediator controls is assigned automatically. In the override Initialize() method we add a listener to the view’s JumpInputSignal and make it call the OnViewJumpInputSignal() method once it is dispatched. In the Dispose() method we make sure we stop listening to the signal once this mediator is removed, which is considered a good practice even though we’re not yet removing any mediators from views.

Binding the View to the Mediator

Now that we have both the view and the mediator we’ll have to tell our context we want the PlayerInputMediator to control our PlayerInputView. We do this by adding the following code to the GameContext.

using IoCPlus;

public class GameContext : Context {

    protected override void SetBindings() {
        base.SetBindings();

        Bind<string>("Welcome! Look at me, I'm injecting a value!");

        BindMediator<PlayerInputMediator, PlayerInputView>();

        On<EnterContextSignal>().Do<WelcomeCommand>();
    }

}

The BindMediator<T,U>() allows us to set what mediator we want to bind to a view type. All PlayerInputView instances that would fall under this context will now be controlled by a PlayerInputMediator instance.

Instantiating a View

That’s all fine and dandy, but we’re still not seeing any view when we hit play! We still need to instantiate the view. Let’s make a command to do that for us. Add a new script file called “InstantiatePlayerInputCommand.cs” and add the following code.

using IoCPlus;

public class InstantiatePlayerInputCommand : Command {

    [Inject] IContext context;

    protected override void Execute() {
        context.InstantiateView<PlayerInputView>();
    }

}

This command has a dependency on the context from which it is executed. This injection binding is set by default via the base.SetBindings() in the Context. Using the context instance we can instantiate a view in that context using the InstantiateView<T>() method. This will create a new gameobject, add the PlayerInputView component to it and assign a mediator based on the context’s mediator bindings. In our case this will be the PlayerInputMediator.

Let’s execute this command when the context is entered. Add the following code to the GameContext.

using IoCPlus;

public class GameContext : Context {

    protected override void SetBindings() {
        base.SetBindings();

        Bind<string>("Welcome! Look at me, I'm injecting a value!");

        BindMediator<PlayerInputMediator, PlayerInputView>();

        On<EnterContextSignal>().Do<WelcomeCommand>()
                                .Do<InstantiatePlayerInputCommand>();
    }

}

As you can see, we can bind multiple commands as a response to one signal. These commands will be executed in the same order that we bind them. Go ahead and hit the play button. The view should now be instantiated and visible in Unity’s scene hierarchy. We should also get the log message that the InputPlayerView outputs in its Awake() method.

Injecting a Custom Signal

But we’re not done yet. In the PlayerInputMediator’s OnViewJumpInputSignal() method we would like the context to execute a command, so we can respond to the input of the player. The mediator depends on a signal that needs to be injected. Let’s first declare a new signal type. Create a new script called “JumpInputSignal.cs” and add the following code.

using IoCPlus;
    
public class JumpInputSignal : Signal { }

Now that we have a custom signal type, we can use this type to bind an injection via the context. Open up the GameContext and add the following code.

using IoCPlus;

public class GameContext : Context {

    protected override void SetBindings() {
        base.SetBindings();

        Bind<string>("Welcome! Look at me, I'm injecting a value!");
        Bind<JumpInputSignal>();

        BindMediator<PlayerInputMediator, PlayerInputView>();

        On<EnterContextSignal>().Do<WelcomeCommand>()
                                .Do<InstantiatePlayerInputCommand>();
    }

}

Notice that in the Bind<T>() method for the JumpInputSignal we are not setting any value. This is a shorthand method; the IoC+ framework will automatically set the value to a new instance of the given type. Keep in mind that the same instance is injected in all commands and mediators that depend on it.

Dispatching an Injected Signal

The signal will now be injected in all commands and mediators that depend on it, so let’s extend our PlayerInputMediator with the following.

using IoCPlus;

public class PlayerInputMediator : Mediator<PlayerInputView> {

    [Inject] JumpInputSignal jumpInputSignal;

    public override void Initialize() {
        view.JumpInputSignal.AddListener(OnViewJumpInputSignal);
    }

    public override void Dispose() {
        view.JumpInputSignal.RemoveListener(OnViewJumpInputSignal);
    }

    private void OnViewJumpInputSignal() {
        jumpInputSignal.Dispatch();
    }

}

The PlayerInputMediator uses the injected JumpInputSignal from the context to dispatch it once its view says it is time to. Keep in mind that there is a big difference between the signal of the view and the signal that is injected by the context. Signals from a view are only listened to by its mediator. The injected signal in the mediator will be listened to by the context to execute a command.

Let’s make a simple command to, for now, test our signal. Create a new script file named “JumpCommand.cs” and add the following code.

using IoCPlus;
using UnityEngine;

public class JumpCommand : Command {

    protected override void Execute() {
        Debug.Log("Jump!");
    }

}

Now that we have the JumpCommand we can make the GameContext bind it as a response to the JumpInputSignal. Open up the GameContext and add the following code.

using IoCPlus;

public class GameContext : Context {

    protected override void SetBindings() {
        base.SetBindings();

        Bind<string>("Welcome! Look at me, I'm injecting a value!");
        Bind<JumpInputSignal>();

        BindMediator<PlayerInputMediator, PlayerInputView>();

        On<EnterContextSignal>().Do<WelcomeCommand>()
                                .Do<InstantiatePlayerInputCommand>();

        On<JumpInputSignal>().Do<JumpCommand>();
    }

}

There! Now, when the PlayerInputView notices the arrow up key being pressed, it dispatches its JumpInputSignal, making the PlayerInputMediator dispatch its injected JumpInputSignal, making the GameContext perform the JumpCommand. Go ahead, hit the play button and press the arrow up key a couple of times. You should get the jump message that is logged by the JumpCommand.

Sure seems like a lot of classes to produce a single log on a key press!

At first, it does seem like a lot of unnecessary steps to output a simple debug log. Without the use of IoC+, you could get the same result with only one script file! This is true, but as your project gets bigger and requires more and more design changes, it will become a real benefit to have your classes decoupled like this. In IoC+, you can even switch a context with another context to dynamically ignore the player’s input and trigger the JumpCommand based on something else entirely!

The IoC+ Monitor

Let’s review our creations in the IoC+ Monitor!

Woah, a bunch of new stuff is added! We can see that the JumpInputSignal is added to the list of injections. The InstantiatePlayerInputCommand is added as a second response to the EnterContextSignal and the JumpCommand is set as a response to the JumpInputSignal. In the list of mediators we can see that the PlayerInputView is set to be controlled by the PlayerInputMediator.

The PlayerInputMediator instance that is created to control our instantiated PlayerInputView is displayed under Mediator Instances so we can see in which context the mediator currently resides. Notice how the instance shows up only after the InstantiatePlayerInputCommand is executed.

Last but not least, when we press the arrow up key the JumpInputSignal flashes in both the commands and the injections list. This will come of great use once we start nesting contexts.

Conclusion

In this tutorial we’ve learned how to make a view and a mediator. We use the BindMediator<T,U>() method in a context’s SetBindings() method to set the mediator type that will control a view type. We’ve learned that views are the only real connection to the Unity Engine and that mediators link the view to the IoC framework. Views use signals to communicate to their mediator and mediators dispatch injected signals, executing commands that are set as a response to those signals in the context.