Friday, October 31, 2008

Persistent View Data in MVC

So, here's a question that's been puzzling me for a long time. I don't have an answer, I'm just throwing this out to see what folks think.

Imagine that you have an application with a Model-View-Controller architecture. You have multiple views on the model, and at least one of the views has some state that ought to get saved with the document. For example, imagine an application for developing an interactive presentation (yes, I did actually write this one once) - your model contains presentation screens, and links between them. Some screens are menus, which might branch out to multiple other screens depending on the user's selection. One of your views is a graphical layout, representing the screens as nodes and the links as arcs between them.

Obviously, the screens and the links between them belong in the model, but what about the way that they're laid out in the view? That's not technically part of the data - for example, it has no effect on the final presentation. It's relevant only to the single view that uses it - if you had two graphical layout views, you could reasonably expect that they might have different layouts on the same model. What if you had three or four views, each with its own persistent state of this type? For example, imagine that one of your other views is a list of slides, and the list ordering is customizable.

As I see it, there are two choices:
1. Store the layout in the model. This has the advantage of simplicity - the view has no state, there's no synchronization to take care of, everything gets serialized together. Unfortunately, it means that any time you add another view, you have to add a whole bunch of view-specific crap to your model, even though you technically haven't changed the underlying data. It also means that all views have access to all other views' data, which could cause problems and violates the Principle of Least Knowledge.
2. Store the layout in the view. This has the advantage of keeping the data where it's needed, and hiding it from everyone who doesn't need it. However, it seriously complicates serialization of the data to a file (because now all the views have to get involved), and it means that the view's data needs to be kept in sync with the model's data (if someone deletes a slide from one view, all the other views need to discard their related data).

My personal experience is that option 1 is less damaging in the long run than option 2, even though option 2 is a technically more elegant design. There are hacks you can put into the model to make this less bad - for example, you can hand each view a unique cookie that it can use to store arbitrary data in a bucket that the model treats as a black box, but that it serializes out with its data. That way, the views don't have access to each others' data, but it's still a kludge.

Any thoughts? Am I missing some elegant solution for this one? This comes up surprisingly often, though usually not as dramatically as in my example.

4 comments:

Aaron Sher said...

Wow, dead silence. Funny, that's the same reaction I got the last time I asked this question (on an internal mailing list at Vanteon, my company). Is the question not clear? Are folks just not interested?

Sean McGranaghan said...

I actually read this yesterday but didn't have time to comment. I prefer option two. In the canonical MVC the model can't be aware of the view.

If I understand it correctly the problem is that all views need to be synchronized with each other. Is there a way to use an observer pattern so that all views are updated appropriately before serialization to a file?

Aaron Sher said...

Neither option actually requires the model to have knowledge of the view, exactly - in option 1, the view state becomes part of the model, so technically it's no longer tied to the view. It's "coincidence" that this particular data gets used by only a single view.

That said, of course, it's not really true - you do have stuff in the model that's encoding the state of a view, so there's at least implicit knowledge there. If the view changes, the model might need to change, so there's a dependency in the Single Responsibility Principle sense of the word.

The problem with option 2 isn't so much that the views need to be synchronized to each other, it's that the views need to be synchronized to the model. If the user makes a change to the model (which command ought to go via the Controller in classical MVC, so the view's out of the loop), the model needs to inform all the views that something's changed, and the views need to update their state data. Because the views have to keep a "mirror" of the model data (for example, in my presentation layout app, the layout view would need to have an object corresponding to each slide that stores the slide's position on the layout canvas), the synchronization becomes nontrivial. A view with no state of its own can just say "update", and it'll pull all the data it needs from the model during its refresh.

The Observer pattern is the traditional solution to this problem, but it doesn't really address the issue. What Observer lets you do is to remove knowledge of the views from the model (i.e., the model doesn't need to know about the views specifically, only that there are abstract listeners who are interested in certain types of changes to the data). That's great, as far as it goes, but it doesn't address the synchronization problem; each view's event handler method will need to update the view's internal state to correspond with the new state of the model. In the worst cases (and I've seen this happen), updating the view's state might trigger other model changes, which cause recursive notifications to the view.

None of this addresses the serialization problem. Imagine implementing the Save menu command - traditionally, your Controller would accept the event, and ask the model to serialize its data. The Controller would make sure the data got written to a file, and you're done. Unfortunately, if views have data that needs to get saved, somebody needs to ask each view to serialize its data, which imposes a direct dependency on the views - you can't do that through an Observer pattern, you need to use Visitor or some such.

I'm really hoping that somebody will come up with a third solution, or a variation on one of the first two that avoids the inherent problems with them.

Chris said...

Sorry for my silence on everything. Been busy ramping up on stuff atthe new Gig the past 2 weeks so have not had much time for extra curricular geeking around :->! I will resume soon though!