50Ply Blog

Building Things

User Interfaces Are Hard

| Comments

I’m struggling with what seems like a simple problem: keeping my views and my models synchronized with each other.

Objectives:

  1. Make sure that data committed by a user using a view is correctly reflected into the model.
  2. Ensure that updates to the model from a variety of sources are correctly reflected back into any views that would be impacted by those changes.
  3. Make sure that models know nothing about the views presenting their data.
  4. Minimize or eliminate any knowledge that views have of the models they are mirroring.

Make sure that data committed by a user using a view is correctly reflected into the model.

Rationale: Accurately capturing the user’s intent is the whole purpose of any application.

Complications: Bare data captured by the view must be validated and transformed into the richer data-space of the model. Suppose we’re editing an email address in our contact list. When the captured data (the new email address) is returned from the view we’ll need additional knowledge (perhaps the ID of the user we were editing) before we can update the model with the new data. Where should we remember what user ID we’re editing? What if we allow the user to edit multiple email addresses simultaneously?

Solutions: The need for a place to store this binding information (user X is being edited by view Y) is one of the reasons we have “C” in “MVC.” We need some third entity to mediate the relationship between the view and the model so that neither become overly specialized. In my MOVE library I’m calling this mediating entity the “interactor” instead of a controller because I’m trying to take a slightly different approach. I am trying to embody application use-cases (like “user adds contact” or “user edits contact”) in my interactor entities. An nteractor can trigger other interactors and each is responsible for presenting an appropriate sequence of views. Some of my previous posts were written because I’ve been trying to figure out how to make flow of steps used to accomplish an interactor’s task as explicit in the code as possible. In contrast, a controller typically mediates the relationship between one kind of view and one kind of data in the model (the “contact” controller.) I stole the word “interactor” and the definition I’m using from Uncle Bob.

Ensure that updates to the model from a variety of sources are correctly reflected back into any views that would be impacted by those changes.

Rationale: The data the user is seeing in the view should be correct. The model is the canonical source of truth. Again, this is really the whole point.

Complications: We need to make good decisions about what kind changes we communicate to the view. Data-binding frameworks like SproutCore really shine in 90% of the scenarios we face here but can make the final 10% a real challenge. Aggregate views like the count of the number of items in a list are a typical corner case where data binding struggles.

Solutions: The aggregate view corner case is another opportunity to think about the coupling between view and model. Where should model derived state (like the count) be kept? It would be tempting and fairly reasonable to keep track of the count in the view. It would also be tempting but ill-advised to have the view query the model for the count directly. In a task list example I’m putting together in MOVE, I receive the list-add and list-remove events in the interactor and then query the model for the new count. Then the interactor pushes the count into the view directly.

Make sure that models know nothing about the views presenting their data.

Rationale: If the application is actually doing something interesting then there should be quite a lot of code that can be developed and tested independently of the views. Strong coupling between the model and the view could limit the usefulness of the model code in circumstances that don’t require a view (like testing, report generation, experimenting at the REPL, etc.)

Complications: Since the model is the canonical state of the world it often feels natural to push changes in the model directly out to the places we know that its needed.

Solutions: This is actually pretty easy to solve. We embrace the idea that the model should tell the world when it changes but we reject the idea that the model has any knowledge of what specifically is in the world. To accomplish this we use buzzwords like “events” or “publish-subscribe.” In my experience this works pretty well. I typically use events to communicate the novelty that the changes added (like what was added, edited, or subtracted.) I don’t use events for communicating the total state of the world (the new list of all of contacts, etc.) Since events only communicate novelty, we must avoid the mistake of having entities only observe the model through events. In my experience, such a decision forces those entities into maintaining their own internal “little-models.”

Minimize or eliminate any knowledge that views have of the models they are mirroring.

Rationale: If the view is unaware of the application’s model then it is more reusable, more testable, and less sensitive to the implementation of the model.

Complications: We don’t want the view to query the model. But, we also want to avoid creating a re-implementation of the model within the view (see “little-model” commentary in the previous objective.)

Solutions: Again we need to lean on a mediating object to help reduce the coupling between the view and the model. The strategy that is working well for me in MOVE is having the interactor register for any view-relevant change events when it creates the view. When those events fire, it is the interactor that pushes updated information to the view (perhaps after consulting the model) when it is needed. Obviously this helps make the views and models easier to test. Less intuitively, it also simplified the testing of the interactors. One approach I take is mocking the view and then firing data change events at the interactor to make sure the view is getting appropriate updates in response.

Comments