OK, this isn’t going to be a line-by-line code quality review. Anyone who is interested can take a look (and will find the quality is excellent). This is about my experiences so far using Caliburn Micro for relatively simple UIs.
First off, let me say that I’m unaware of any competitors to CM and, at the time of writing, I can’t imagine ever not using it unless I decide to build my own. If I built my own, it would probably look quite like CM.
Convention Based Binding
This is the raison d’etre of CM, and it just works straight out of the box. It’s a joy to use. I did, however, find it quite difficult to figure out how to set up new conventions for things like SyncFusion graphs. I imagine this is a problem quite a lot of people run into, and a fair number probably do what I did: just drop back down to normal binding declarations. There’s nothing particularly wrong with this approach if you’ve only got a couple of instances, like I have, but I would have appreciated more guidance on it in the documentation. I’d have loved to have seen more convention-based thinking in the event handlers. For instance, is it really necessary for the developer to specify the parameters to the target function like $view, when you could simply introspect the target function to work out which parameters it needs. (This is probably the first of many remarks that will elicit an extremely sensible “here’s why it doesn’t do that” response.)
The Event Aggregator
It’s interesting to look at the event aggregator from the point of view of a Retlang user. Retlang is explicitly built to handle both straight concurrency and the synchronization with the UI. Here’s the main differences:
- Caliburn Micro keeps Weak References to subscriber objects. Retlang requires an explicit unsubscribe.
- Caliburn Micro requires subscriber objects to implement explicit interfaces, Retlang does not.
- The handling of events in Caliburn Micro is performed on the UI thread. You can override this on a per-publish basis. Retlang offers a wide variety of modes, and you can have different modes for the one published event.
- Caliburn Micro is oriented towards you creating one bus with all events sent on the same event aggregator. Retlang Channels are strongly typed and you typically have a lot of channels.
This makes the two approaches so different it feels like they’re addressing different problems. To a certain extent they are: CM’s use cases are simpler, and it’s easier to use CM for these cases. However, if you need the power, there’s nothing stopping you using Retlang within a CM project and they play well together.
The main problem with the event aggregator is that it’s just slightly too simple for its own use cases. Basically, this objection boils down to “actions aren’t events”
- You can’t kick off co-routines from events. (You can kick off co-routines from anywhere using SequentialResult, but the effect isn’t pretty.)
- You can’t localize or bubble events.
Basically, although a nice piece of code, I don’t think publish and subscribe can be divorced from broader synchronization questions. Which brings me to co-routines.
I’ve talked before about how impressed I was with this particular trick. However, it’s worth examining the limitations of the approach: yield return is nice, but it’s limited to a single function. If you want to achieve something more complex (and by more complex, I mean about 5 or 6 things need doing), you’re going to have to solve the composition problem yourself. However, this is easier said than done. As I’ve already mentioned, you can’t kick off co-routines from events. However, even if your task is simpler than that, there are still problems:
Let’s take the following example of something you might want to do in response to a button press
- show busy indicator
- perform 4 DB accesses
- Hit a web service
- Hide the busy indicator
- Compute some data from the data we’ve obtained (I’m assuming the computation is fast)
- Set that to a local property
Truth is, the database accesses and the web service aren’t probably going to want to be executed in sequence. The co-routine trick is useful for waiting until they’re all complete, but you’d like to be able to kick them off in parallel. Really, you want functionality similar to async.js. The co-routine trick basically replaces waterfall, but it doesn’t really address the other cases. Sadly, implementing functionality such as “auto” in C# is probably impossible due to the nature of the type system.*
Then comes the question of what to do with the results. The Build Your Own MVVM framework code suggests callbacks to process the “return values” of the queries, but that’s pretty ugly. In practice you want something like futures.
Finally, there’s no batteries included in the IResult model. There’s no IResult for running something on a threadpool, no IResult for wrapping an asychronous function pair. It’s a nice trick, but it’s not a framework.
The weird thing is, there is a system that actually addresses these concerns: Reactive Extensions. Now, I spent some time investigating it last year and found it over-complex and underdocumented, but it’s got parallel execution, it’s got execution in different sychronization contexts, it’s got built in support for asynchronous results. It’s possible that Rx could bring genuine benefits to Caliburn Micro.
*Java has the same problem.
There are a number of things that, while they’re not exactly wrong, I don’t much like. For one, who decided to call a class “Action”? That’s just plain annoying for anyone who writes a lot of functions that take delegates.
More seriously, the problem with any convention-based system is “what happens when the conventions go wrong”? Krzysztof has spent a long time trying to improve the diagnostics for Castle Windsor, and the same process is needed here. In fairness to CM, it’s not all CM’s fault, the development error reporting of WPF isn’t exactly world class to begin with. However, the ability to see clearly what was bound and why, and being able to dig into why a certain property or action wasn’t bound, would be invaluable. I suspect a lot of CM users just link the source code directly into their projects. (One of the blessings of open source; try doing the same with Rx).
A special mention should be given to the approved way of working with a busy indicator, the “show busy” and “hide busy” IResult implementations. I’m sure plenty of people are happy with this approach but, for my money, it’s a huge breaking of encapsulation. I don’t want every action that performs a database query to have to flip the busy indicator back and forth. Ideally, I’d like the execution of a query against your back end to automatically show or hide the appropriate busy indicator. I’m not quite sure how elegantly you could add this to the architecture, though.
Finally, I loathe seeing globally available IoC containers. It’s just not necessary, all of that stuff could be set up in the bootstrapper, and would probably make the code marginally more elegant.
I can’t imagine developing another WPF project without using Caliburn Micro or something very like it. The convention-based property binding and the action are phenomenal. I would really like to see more documentation on how to build your own conventions, because I think this part is phenomenal.
The event handling/synchronization features are deliberately simple. There’s a great deal to be said for explicit simplicity, but I do feel that sometimes this edges into “didn’t address the issue”. Still, I’d much rather have a clean, simple, incomplete framework than an incomprehensible monster. As useful as Rx is, it is definitely that kind of a monster. CM is small and enabling, and that’s how I like my frameworks.