Replacing Events in MV-C
This article shows you how to remove events from the MV-C design pattern to make it simpler to understand, safer to use, and faster to execute. It’s a step-by-step lesson in refactoring. Start by reading the MV-C article, then read on!
I originally described the MV-C design pattern like this:
Last week’s article presented many reasons why you might not want to use events for the “Input” and “Data Change” signaling from the model-view to the controller. You might use interfaces or delegates instead, but most tasks that MV-C is used for can do even better.
How many controllers do you think you’ll use at a time with a model-view? The answer is almost always one. This means you can use an interface instead of an event since you don’t need to call multiple event handler functions when dispatching the event.
How many controller classes do you think will implement that interface? The answer is usually just one. Even while unit testing you probably won’t ever mock the controller since it’s the logic you want to test. You’re much more likely to mock the model-view. This means you can use a class instead of an interface since the interface is always an instance of that one class.
This changes the design from using events to using direct function calls on a class instance. The design now looks like this:
Here’s what we need to do to make this change:
- Remove events from the model-view interface
- Remove events from the model-view class
- Add a public property to the model-view interface and class with a setter for the controller class reference
- Make all of the controller’s event handlers public
- Replace the event handlers’ parameters with the contents of the
EventArgs
types - Replace the model-view’s event dispatches with instance method calls on the controller class
Let’s start with the model-view’s interface:
using UnityEngine; // Interface for the model-view public interface IEnemyModelView { // Enemy logic EnemyController Controller { get; set; } // Position of the enemy Vector3 Position { get; set; } // Health of the enemy float Health { get; set; } // Destroy the enemy void Destroy(); }
Notice that the events have been replaced by a property for the controller class. You can delete the EventArgs
classes outright!
Now the model-view:
using System; using UnityEngine; // The combined model (data representation) and view (input and output) for an enemy. Does no logic. public class EnemyModelView : MonoBehaviour, IEnemyModelView { // Enemy logic public EnemyController Controller { get; set; } // Data stored by the model-view // Outputs the data by mapping it to the material color private float health; public float Health { get { return health; } set { if (value != health) { health = value; GetComponent<Renderer>().material.color = Color.Lerp(Color.red, Color.green, value); Controller.HandleHealthChanged(value); } } } // Wrapper for data already stored by Unity // Outputs the data by direct pass-through: no logic public Vector3 Position { get { return transform.position; } set { if (value != transform.position) { transform.position = value; Controller.HandlePositionChanged(value); } } } // Default values can be set where appropriate void Awake() { health = 1; } // Handle input by calling HandleClicked, not performing logic void Update() { if (Input.GetMouseButtonDown(0)) { var ray = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(ray, out hit) && hit.transform == transform) { Controller.HandleClicked(); } } } // Destroy the enemy, but we don't know why public void Destroy() { Destroy(gameObject); } }
We replaced the events with a property for the controller class and all event dispatches with calls to public instance functions of the controller class.
Let’s continue by looking at the controller.
// The logic for an enemy. Relies on a model-view to store the data representation, gather input, // and output to the user. public class EnemyController { private readonly IEnemyModelView modelView; private readonly float clickDamage; public EnemyController(IEnemyModelView modelView, float clickDamage) { this.modelView = modelView; this.clickDamage = clickDamage; } public void HandleClicked() { // Perform logic as a result of this input // Here it's just a simple calculation to damage the enemy by reducing its health // Update the model-view's data representation accordingly if this is the case modelView.Health -= clickDamage; } public void HandleHealthChanged(float health) { // Perform logic as a result of this data change // Here it's just a simple rule that an enemy with no health is dead and gets destroyed // Output to the model-view accordingly if this is the case if (health <= 0) { modelView.Destroy(); } } public void HandlePositionChanged(Vector3 pos) { } }
The controller no longer subscribes to the model-view’s events in its constructor. The event handler functions are now public and mandatory, so HandlePositionChanged
is now present.
Finally for the runtime code is the script that uses the model-view and controller:
using System; using UnityEngine; public class TestScript : MonoBehaviour { void Awake() { var prefab = Resources.Load<GameObject>("Enemy"); var instance = Instantiate(prefab); var modelView = instance.GetComponent<IEnemyModelView>(); modelView.Controller = new EnemyController(modelView, 0.25f); } }
The last line used to look very strange because the controller wasn’t kept around as a local variable or a field. It looked like it should have been garbage-collected, but it actually wasn’t. The reason is that the controller’s constructor subscribed to the model-view’s events with its instance functions as the listeners. Since instance functions require an instance in order to be called, the model-view’s events were implicitly holding onto a reference to the controller. It works, but it’s quite hard to understand and not at all clear to many programmers without some deeper thinking about how C# implements events and delegates. Now the controller is simply a property of the model-view. Easy.
Let’s finish up the code by looking at the unit tests:
using NSubstitute; using NUnit.Framework; #pragma warning disable 0168, 0219 // unused variables // Class with tests [TestFixture] public class TestEnemyController { // A single test [Test] public void ReducesHealthByClickDamageWhenClicked() { // Make a fake model-view and give it to a real controller var modelView = Substitute.For<IEnemyModelView>(); modelView.Health = 1; var controller = new EnemyController(modelView, 0.25f); // Fake the HandleClicked call controller.HandleClicked(); // Make sure the controller damaged the enemy Assert.That(modelView.Health, Is.EqualTo(0.75f).Within(0.001f)); } // Three tests in one. Specify parameters for each run of this function. [TestCase(0.1f, 0)] [TestCase(0, 1)] [TestCase(-0.1f, 1)] public void OnlyDestroysWhenHealthChangesToLessThanOrEqualToZero( float health, int numDestroyCalls ) { // Make a fake model-view and give it to a real controller var modelView = Substitute.For<IEnemyModelView>(); modelView.Health = 1; var controller = new EnemyController(modelView, 0.25f); // Fake the HandleHealthChanged call controller.HandleHealthChanged(health); // Make sure the controller called Destroy() the right number of times modelView.Received(numDestroyCalls).Destroy(); } }
The only change here is to replace the fake model-view events with direct calls to the controller. It’s even more direct since we’re simply calling an instance function—just like in the runtime code—rather than resorting to NSubstitute’s magical ability to fake events from outside the class.
At this point we’ve gained a big win over the original event-based design. Any reader of the model-view has a much better idea what’s going to happen compared to dispatching events. It’s extremely clear that exactly one function is called. In most IDEs you can even use a shortcut to jump straight to it.
Gone are the mysteries of events. Is anyone subscribed? How many functions are subscribed? In what order are they currently subscribed? How can I find them? In place of these mysteries is simply a function call. You don’t need to break your train of thought to figure out which functions the event will call and can instead focus on what the handler function does.
You can apply this technique to many situations, not just MV-C. Remember that you can always add layers of abstraction later if you end up needing them. It’s really easy to make the model-view’s reference to the controller into an interface so you can have multiple controllers. You can even reintroduce events if you really need all of that flexibility. If you don’t need multiple controllers or the flexibility of events, consider simplifying your code by simply using a reference to a class instance.
Which way do you prefer- events or class instance references? Let me know in the comments!
#1 by Valerie on November 13th, 2016 ·
Simple solution that will work more than 95% of the time. For the 5% of the time this solution doesn’t work, we just add some levels of abstraction as you say. I’m using this design pattern on my current client demo. I have to say it’s super fast and easy to implement without all of the factories and interfaces.
Keep up the great work Jackson!
#2 by Valerie on December 26th, 2016 ·
Hey Jackson,
I think there’s a problem, or some typos…
In the enemy modelView you call:
Controller.OnHealthChanged(value);
Controller.OnPositionChanged(value);
Controller.OnClicked;
I don’t see the corresponding public instance functions in the controller.
#3 by jackson on December 26th, 2016 ·
Thanks for letting me know! I corrected these by changing “On” to “Handle”.
#4 by WeslomPo on February 14th, 2017 ·
It does problem, when you want to change a controller for that view. Make it stupid.
I research some mvc model, that more decoupled that I see here, maybe it similar to that I saw in previous your article.
I make a games, that have a lot of UI, and it pretty straightforward, that they responds from UI clicks and other things directly to my controllers with simple Action callbacks.
View does not know anything about controller and model. Model does not know anything about view and controller. Controller does know about model and view, but only their public interfaces. It simply connects models to views – and works perfect. Controllers now very clear to understand, views focused on UX, models stay tuned. Here are example from real game:
Controller for Name menu
View for “Name” menu.
But… it works perfectly only if you using a IoC, and it will be pain in ass without it (Transitions class it very cool thing, I think, it show exactly all transitions from that class, but it use IoC in full power to resolve it).
You made a great article, I very like it.
#5 by jackson on February 14th, 2017 ·
I’m glad you liked the article. It sounds like your MVC system is very similar to the MVC system from my last article. It also sounds like you’ve ditched the events, just like this article describes. In a way you’ve filled in the gap: MVC without events.
Thanks for sharing!
#6 by Phu Ha on January 7th, 2020 ·
The code style is very nice and really hit my mind, this is the way i was looking for long
Reading your code, I can understand the concept in general, and I eager want to know more in the way you code the other classes, aka: a high level class design, please can you share us a bit more detail?
Thank you so much!
#7 by Phu Ha on January 7th, 2020 ·
Especially the UxManager and the WindowManager, this is what i’m in researching
and which IoC tool do you prefer, I personally use Zenject, but in my opinion, it’s too big for just the Injection, do you have any other recommended lightweight alternative?
thanks again!
#8 by Danta1st on April 4th, 2017 ·
Thank you for the article.
Any specific reason for not creating an ‘IEnemyController’ interface, and injecting it into the ‘IEnemyModelView’ instead of the concrete implementation?
#9 by jackson on April 4th, 2017 ·
You’re welcome!
You can certainly use interface references rather than concrete class references if you need more abstraction. The point I made in the article is that you might not need this much abstraction in many cases:
It’s my personal preference to start as simple as possible and then add abstraction as it becomes necessary. In this case that means starting with concrete class references and then adding interfaces later when you need to have multiple classes implementing that interface. This is essentially the “you ain’t gonna need it” (YAGNI) principle. Of course you’re free to start with higher levels of complexity and abstraction if you so choose and that’s totally valid in many cases, such as if you know for sure that there will be multiple classes implementing the interface.
#10 by Valerie on September 18th, 2017 ·
In your TestScript Awake method you call the modelview.Position before constructing the Controller. Wouldn’t the call Controller.HandlePositionChanged(value) in the Position setter cause an exception?
I’m also thinking that TestEnemyController.ReducesHealthByClickDamageWhenClicked() method will throw an exception when the setter for Health is called. Does this seem correct or am I just missing something?
Thanks Jackson.
#11 by jackson on September 18th, 2017 ·
You’re correct about
modelview.Position
because the controller hadn’t been set yet. I’ve updated the article to correct the issue.In the case of
ReducesHealthByClickDamageWhenClicked
, it’s actually fine to setHealth
on a substitute. Remember that NSubstitute is returning a class of its own making that implementsIEnemyModelView
rather than a real instance ofEnemyModelView
. The class it returns is pretty much just no-ops and call recording, so it won’t actually try to use the controller.Thanks for pointing this out!
#12 by Valerie on September 24th, 2017 ·
I’m using this pattern and I am confused about how to tackle “listening” to an event. That is, when to start listening. I have a 3rd party Map asset which I use for LocationService events. I assign the listener handler in the Start() of the model-view component. But, isn’t is possible to miss the OnLocationChange event during that time when the model-view is created, but not the controller; which handles the event?
Should I create the model-view, then the controller, then call some StartListening() method? I assume this would be a model-view method…
For some reason this feels contrived or not correct because the model-view is input/output and data changes. With this implementation there really isn’t any data changes, though I could set some kind of property like _listen = true;
Maybe I’m over thinking this, but what would you do?
Here’s what it currently looks like…
…
As always, thanks for your help Jackson!
#13 by jackson on September 24th, 2017 ·
It seems like there are two issues here. First:
Unless you’re using multiple threads, then there’s no way an event can happen between your other lines of code. For example, let me mark up your
MainState.BeginEnter
:Second, the question on this line:
This is an inherent problem with the design of
MonoBehaviour
. You can’t really use constructors, so there’s no way to construct in a state where you have a non-nullController
field and preserve that invariant throughout the lifetime of the object by never assigning null to the field. What everybody seems to do is to set a public field, public property, or call a public function to set theController
field immediately after theMonoBehaviour
is created. Really, that’s about all you can do.#14 by Valerie on October 5th, 2017 ·
I’m trying to run the unit test for this example using Unity 5.5.1. With this version of Unity, which supposedly comes with new suite of internal test runner, I simply downloaded NSubstitute from GitHub and imported the NSubstitute 3.5 dll to my project. I can run simple tests fine, however when using the single test you’ve provided above, I get an error on the first line
var modelView = Substitute.For();
Method not found: ‘System.Reflection.Emit.TypeBuilder.DefineProperty’.
I’m sure there’s something I’ve not setup correctly, but have no experience with unit testing (yet). Thought I’d see if you’re using the newer Unity version (which doesn’t require download of Unity Test Tools asset) and if you needed any further dlls/plugins to get the TestEnemyController working?
Thanks again Jackson.
#15 by Valerie on October 5th, 2017 ·
Sorry, the error line is:
var modelView = Substitute.For();
#16 by jackson on October 5th, 2017 ·
The problem may be due to using NSubstitute from GitHub. Unity Test Tools comes with NSubstitute already, so you shouldn’t need to install anything more than that package.
#17 by Valerie on October 6th, 2017 ·
Thanks, that was it.
#18 by Philip Walker on July 24th, 2018 ·
I just can’t get this to work…
I keep getting NullReferenceException errors. Does anyone have a working project folder/package of this?
It would be really helpful.
Thanks
#19 by Philip Walker on July 24th, 2018 ·
With further investigating the EnemyModelView.cs controller is being called too early causing a null reference?
#20 by Philip Walker on July 24th, 2018 ·
….ahh because Health = 1 is being called in it’s Awake
#21 by jackson on July 24th, 2018 ·
Thanks for pointing out this issue. I’ve updated the article with the simple fix: set the field instead of calling the property’s
set
function. Here’s the change:This prevents the property from making the unwanted calls and dereferencing null.
#22 by Philip Walker on July 24th, 2018 ·
Awesome piece of framework btw – thats why I’m so persistent in getting it nailed. :D