My current clunky “put all the code in the view” approach to V-Drum Explorer is creaking at the seams. I’m really not a WPF developer, and my understanding of MVVM is more theoretical than practical. I’ve read a reasonable amount, but quite a lot of aspects of V-Drum Explorer don’t really fit with the patterns described.
Eventually I’d like to present blog posts with a nice clean solution, and details of how it avoids all the current problems – but I’m not there yet, and pretending that good designs just drop fully formed onto the keyboard doesn’t feel healthy. So this blog post is a pretty raw copy/paste of the notes I’ve made around what I think I’ll do.
There’s no conclusion here, because life isn’t that neatly defined. I do hope I find time to tackle this reasonably soon – it’s going to be fun, but I’m not sure it’s going to be feasible to refactor in small steps.
The current design has the following issues:
- A lot of the UI controls are created within the code-behind for the view, so styling is hard
- The logic handling “how the UI reacts to changes” is really tricky, due to chain reactions:
- Changing a tempo sync value changes which field is displayed next to it
- Selecting a MultiFX option changes all the fields in the overlay
- Selecting an instrument group changes which instruments are selected, and the vedit parameters available
- Selecting an instrument can change the vedit parameters (e.g. cymbal size)
- The UI currently “knows” about data segments, addresses etc – it shouldn’t need to care
- The way that the schema is constructed makes the address logic bleed out; we can populate all the “fixed” containers and only generate fields for overlay containers when necessary
- Using inheritance for the commonality between ModuleExplorer and KitExplorer is a bit weird
Ideas and questions that might end up being part of the answer
- My schema has a “physical layout” and a “logical layout” – for example (in the TD-27), each kit has 24x KitPadCommon, 24x KitPadMain, 24x KitPadSub, 24x KitPadMainVEdit, 24xKitPadSubVEdit. It makes more sense to show “24x instrument info”. Is that information part of the model or the view-model?
- I suspect it’s part of the model, effectively.
- Perhaps the ViewModel shouldn’t even see the physical layout at all?
- The logical layout is worryingly “tree view + details pane”-centric though
- We have “snapshotting” in two ways when editing:
- Overall commit/cancel
- Remembering values for each multi-fx / vedit overlay while editing, so you can tweak, change to a different overlay accidentally, then change back to the original one and still have the tweaked values.
- Should this logic be in the ViewModel or the Model?
- Should the model implement INotifyPropertyChanged (et al)?
- Looks like there’s genuine disagreement between practitioners on this
- Feels like a pretty stark choice: either ViewModel controls everything, including how overlays work (i.e. when changing instrument, change the overlay too) or the model needs to tell the ViewModel when things are changing.
- Where do we keep the actual data? Still in a ModuleData?
- We could generate a model (e.g. with Kit, KitPadInst etc being full types)
- We’d then either need to generate all the corresponding ViewModels, or access everything via reflection, which defeats half the point
- Would definitely be easier to debug using a model
- Generators are a lot of work…
- Possibly reify “schema + data” in the model, to avoid having to pass the data in all over the place (hard to do binding on properties without this…)
- Reify overlays in just-in-time fashion, to avoid creating vast numbers of fields.
- Broadly the same UI we have now. At least the same features.
- No code in the View – eventually. Not clear what creates a new window when we load a file etc, but we can try to remove as much as possible to start with and chip away at the rest.
- ViewModel shouldn’t need to touch ModuleAddress. Access is view ModuleSchema, containers, ModuleData.
- 7-bit addressing should be isolated even within the model, ideally to just ModuleAddress.
- ViewModel interaction with fields should be simple.
- The command line app should be able to work with the model easily – and may require lower-level access than the UI.
7 thoughts on “V-Drum Explorer: Planning notes for MVVM”
I mean this only with empathy, but it’s gratifying to see you struggling with some of the same practical issues I ran into myself when trying to apply MVC/MVVM patterns in various APIs (Mac OS and WPF being two of the most prominent).
MVVM doesn’t have an obvious place to put “controller” type functionality. A big open question is creating new windows, whether for new instances of models or for dialogs or whatever. As near as I can tell, the “official” strategy is to provide some sort of static “service” class that handles this. To me, that looks a lot like a controller. :)
How to mediate between model and view-model? MVVM has always annoyed me with its idea that there is a data structure that’s a view-model (view logic) and a different one that’s the model (business logic). Much of the time, I wind up merging the two. Where they remain separate, I find I prefer using INotifyPropertyChanged in the model. The alternative is to explicitly copy values known to have changed in the model back to the view model, which couples the code more closely than I’d like. One of these days, I intend to put together some mechanism in my helper library to automatically map model properties to view-model properties (e.g. via custom attributes). For now, it’s a pain, as view-model needs to subscribe and delegate property-change events from model. Sigh…
Continue your focus on the idea that “view shouldn’t know business logic, and business logic shouldn’t know view”. You seem to already be prioritizing this, and it’s been my experience that sticking to your guns on that point yields good rewards in the end.
Templating is your best friend. Data triggers are your second-best friend. The latter can be used to allow for “reactive” UI elements (not in the mobile platform sense), but it’s better if your data structures (view-models) are set up so that the view simply automatically presents exactly the correct view according to the view-model in use. This means using different types, rather than flags on a single type, when trying to present similar-but-fundamentally-different objects. Triggers will still come into play when dealing with business logic that needs to react to visibility changes, for example.
I’m also a bit jealous that you’re finding time to play around with MIDI. Decades ago (1980’s) I dabbled a bit, writing a patch editor/librarian for my Korg K1-II synth, and other fiddly things. In the mid-90’s I wrote the MIDI sequencer for certain Windows CE variants. I really miss that kind of work sometimes, and especially now with MIDI 2 coming out (which looks like it will address many of the oft-resented limitations that exists in the original MIDI spec). I hope that the work done on the MIDI protocol will inspire platform owners to give some love to their currently-languishing MIDI support (pie-in-the-sky: bring back DirectMusic, and support it on UWP!).
LikeLiked by 2 people
Thanks for all of that – aside from anything, the confirmation that this isn’t all simple and clear-cut. And the “gratifying to see you struggling” part was the reason for blogging all of this at all. I think it’s something we should all do more :)
Your experience in the later paragraphs is all very helpful, and I’ll try to bear it in mind as I go with the rewrite.
LikeLiked by 1 person
I strongly suggest using RecativeUI for WPF (and even WinForms) – It deals with data binding and cascading changes in a clear declarative way that anyone comfortable with LINQ should find familiar; It gives a clear binding between view and viewmodel that your tools such as VS or resharper can clearly understand and it also provides a way to unit test asynchronous code which is another big problem for many apps.
Logically there are several different models and DTOs but everyone hits the “problem” that they are all pretty similar and most take short cuts. I would try to do things in stages – The most important is to pull business logic out of the view into the VM which should be then be unit testable and have any external data access injected as interfaces. From there you can see whether or not it’s actually worth having a business model – For simple apps it often isn’t or maybe just in a places. IMHO the only real red-lines are keeping the view and data access layers uncorrupted (i.e. the top and bottom layers of the sandwich).
One thing that I find most people under use is value converters – .e.g. If you use a converter in the view to go from say a boolean to Visibility (there’s actually one built in for this) then your VM can be much closer to your model. Another is command parameters – If you bind the command parameter you often don’t need a VM property for the selected item.
Second vote for Rx (and reactive UI to make Rx easy in WPF). All the MV* approaches leave people scratching their heads in the implementation details. This is because they are leaky abstractions. Rx separates the logic of how the UI glues the data together for presenting and how the data is sourced. It took me a while to grok that it isn’t just “LINQ for events”, I view it as a monad over temporal coupling.
As alternative to ReactiveUI, I always use Prism for both WPF and Xamarin projects. Does much of the same work as Reactive, so it’s more just personal preference. I find the code is really clean, dependency injection supported and very nice navigation API for whole screens or just sub-views. Also I hardly ever end up writing any code in the code behind, the bindings and view model code satisfy the vast majority of what I need with a very clean separation of the data from how it should be displayed.
I find it the easiest to apply MVVM patterns in WPF if i stick to a few rules that i have setup for myself over time:
Try to handle as much interaction logic exclusively in XAML. Data binding, templates, converters and storyboards can do almost all of the heavy lifting in your ui. Try to stay away from triggers if you can use storyboards instead and make use of proxy elements if you can’t bind directly.
Go with a ViewModel-first approach. This isn’t very objective, it really boils down to what you prefer. So far i have had the best experiences with this one which is why i suggest it.
Always use INotifyPropertyChanged to propagate changes upwards and Commands (actually any method will do) to trigger manipulation downwards.
Split implementation and interfaces as well as all three layers into separate assemblies. While this will clutter your solution with 6 projects, but it will enforce a clean separation of concerns. It’s all too easy to accidentally have the Model know about it’s ViewModel all of a sudden if everything is accessible in one project.
Usually if i stick to those rules, things go rather smooth.