DEV Community

yohan
yohan

Posted on Originally published at cnblogs.com

Analyzing Caliburn.Micro — Part 4

Picking up from earlier posts, we continue introducing Convention in Caliburn.Micro (CM).

Preface

This Caliburn.Micro series has already covered several posts. For a third-party application framework, an exhaustive walk-through of every detail is not the top priority—grasping the overall design quickly and accurately matters more. Caliburn.Micro includes many features: Action and Convention, which we have already introduced, plus Coroutine, EventAggregator (weak events), and more. Many of those are icing on the cake; EventAggregator, for example, is also covered in frameworks like Prism. CM’s core ideas are Action and Convention. This article follows that main line in detail.

The CM Main Line

Caliburn.Micro positions itself as a lightweight MVVM framework. In MVVM, the View and ViewModel are associated through DataContext; the View passes operations to the ViewModel via Binding and Command, and refreshes automatically when bound properties in the ViewModel change. This table compares Caliburn.Micro with a typical MVVM framework:

Feature Typical MVVM framework Caliburn.Micro
Automatically associate View and ViewModel (via the View’s DataContext) Not supported Supported, via NameTransformer
Command Supported; Command is recommended Not supported; Action is the alternative
Binding Supported; Binding is recommended Supported; Convention can be used as an alternative
Validation, StringFormat, Converter, UpdateSourceTrigger Supported through Binding’s corresponding mechanisms Supported; Convention can be used as an alternative

A simple side-by-side example:

Typical MVVM framework:

<Button Command="{Binding Path=ChangeNameCommand}"/>
Enter fullscreen mode Exit fullscreen mode
<TextBox Text="{Binding Path=UserName}"/>
Enter fullscreen mode Exit fullscreen mode

Using Caliburn.Micro:

<Button x:Name="ChangeName"/>
Enter fullscreen mode Exit fullscreen mode
<TextBox x:Name="UserName"/>
Enter fullscreen mode Exit fullscreen mode

The corresponding ViewModel:

Caliburn.Micro ViewModel with matching property and method names

With Caliburn.Micro you only set the Name of the Button and TextBox so that it matches the corresponding property and method names on the ViewModel; CM sets up the binding and the Button method invocation automatically.

That is intentional in CM’s design: make View–ViewModel interaction simpler. Set the right names and let CM do the rest.

Command vs Action

WPF defines a Command mechanism. The interface is ICommand, with Execute and CanExecute methods, and CommandParameter is available as the call argument. The Command pattern decouples the object that initiates an operation from the object that handles it—a sound idea in WPF, though the concrete implementation still has some shortcomings.

Some WPF controls expose a Command property that lets you plug in an ICommand for a specific operation. Command triggering is based on Routed Events; for example, a Button triggers Command execution on OnClick. WPF implements Command on many controls such as Button, but coverage is incomplete—controls like ComboBox have no Command.

A Command can respond to only one Routed Event, and for built-in controls that response is hard-wired (Click). WPF does provide CommandBindings on controls so you can attach multiple Commands, but the syntax is verbose and support for the MVVM pattern is weak. To support MVVM better, Blend introduced TriggerAction as a simpler way to attach Commands:

<Button>
      <i:Interaction.Triggers>
          <i:EventTrigger EventName="Click">
              <i:ExecuteCommandAction TargetCommand="LoginCommand"/>
          </i:EventTrigger>
      </i:Interaction.Triggers>
</Button>
Enter fullscreen mode Exit fullscreen mode

Here the Command’s trigger is the Click event; when it fires, the corresponding ViewModel’s LoginCommand runs.

CM’s Action is based on Blend’s TriggerAction. Its ActionMessage inherits from TriggerAction<FrameworkElement>. The difference is that it does not call a Command on the ViewModel; instead it uses reflection to look up the corresponding method on the ViewModel and invoke it.

Action and Convention

For convenience, a CM Action need not name a method explicitly; it matches properties and methods on the ViewModel using the control’s Name. For how Action is used in practice, see the first article. We are discussing framework design, and designs do not appear out of thin air. ActionMessage was introduced to replace Command—let’s look at the problems that follow:

  1. CM’s Action design is deliberately foolproof: you only design the control’s Name and smart matching takes care of the rest. Because this is a “smart” mode, that intelligence is a control point. Which control properties should be smart-bound, how to keep that intelligence smart enough, and ensuring that using it does not break existing design patterns—all matter.
  2. Command and Binding are two of WPF’s highlights. After adopting CM the code is leaner, but Command and Binding seem to disappear. How do you preserve features such as Command’s CanExecute, and Binding’s Validation, StringFormat, Converter, and so on?
  3. Complexity versus flexibility. A feature-rich framework with oceans of code is intimidating; a lean framework that lacks extensibility is unsatisfying. Striking that balance is also a challenge.

To address these issues, CM introduces the concept of Convention.

ElementConvention

For how View and ViewModel are matched automatically, see the previous post. Here we focus on ElementConvention. In short, it was introduced to handle the “intelligence” problem: it is the description of how a Convention is processed for a concrete type. We saw earlier that with

<TextBox x:Name="UserName"/>
Enter fullscreen mode Exit fullscreen mode

CM automatically binds the TextBox’s Text to the ViewModel’s UserName property. CM does two things here: first, based on the control type (TextBox), it decides which property of the control to bind; second, it uses the control’s name for smart matching. We will discuss the second point later; for now, see how CM handles different control types.

CM uses ElementConvention as the type description for Convention processing. CM provides the static ConventionManager class that lets you manipulate ElementConventions. ElementConvention looks like this:

public class ElementConvention
{
    public Type ElementType;
    public string ParameterProperty;
    public Func<DependencyObject, DependencyProperty> GetBindableProperty;
    public Func<System.Windows.Interactivity.TriggerBase> CreateTrigger;
}
Enter fullscreen mode Exit fullscreen mode

For a TextBox, ElementType is its type (TextBox), ParameterProperty is the property it targets (Text), GetBindableProperty is a Func that returns TextProperty for a TextBox, and CreateTrigger returns the trigger event (TextChanged).

ConventionManager provides default handling descriptions for several types in its static functions, for example:

AddElementConvention<ButtonBase>(ButtonBase.ContentProperty, "DataContext", "Click");
AddElementConvention<TextBox>(TextBox.TextProperty, "Text", "TextChanged");
AddElementConvention<TextBlock>(TextBlock.TextProperty, "Text", "DataContextChanged");
Enter fullscreen mode Exit fullscreen mode

You can call ConventionManager’s AddElementConvention method to register a new type or change an existing ElementConvention.

Convention

Having covered the Convention handling description (ElementConvention), let’s talk about the Convention process itself.

The whole Convention process starts with binding View and ViewModel:

  1. Manually or automatically, CM calls ViewModelBinder’s Bind method (passing the corresponding View and ViewModel). CM uses View.GetApplyConventions(View) to decide whether to apply Convention on that View. If you do not want CM’s Convention features, set the attached property View.SetApplyConventions to False on the View, or set the global flag ViewModelBinder.ApplyConventionsByDefault to False to disable Convention.
  2. Convention is enabled by default. CM walks the View’s VisualTree and collects every named control.
  3. Convention is applied to those named controls. There are two kinds: ActionConvention and PropertyConvention. Action means method invocation; Property means property binding.
  4. If, when calling AddElementConvention, the last argument supplied a trigger event name (Click, TextChanged, and so on), that control type can have an ActionConvention. CM matches the control’s name against all public method names on the ViewModel; if there is a match, ActionConvention is applied. For the earlier Button → ChangeName example, the method matches the corresponding ViewModel method, and the trigger is the Click event registered by AddElementConvention: <Button x:Name="ChangeName" />
  5. If, when calling AddElementConvention, the second argument supplied a property name, PropertyConvention applies to that control type. The method is similar: match the control’s name against all public property names on the ViewModel and, if there is a match, set up the binding.

Validation, StringFormat, Converter…

If a control gets PropertyConvention, CM has set up the property binding for us automatically. How do Validation, StringFormat, Converter, and the rest of Binding’s features fit in?

ConventionManager provides SetBinding to perform PropertyConvention. Here is its code:

public static Func<Type, string, PropertyInfo, FrameworkElement, ElementConvention, bool> SetBinding =
    (viewModelType, path, property, element, convention) => {
        var bindableProperty = convention.GetBindableProperty(element);
        if(bindableProperty == null || HasBinding(element, bindableProperty))
            return false;

        var binding = new Binding(path);

        ApplyBindingMode(binding, property);
        ApplyValueConverter(binding, bindableProperty, property);
        ApplyStringFormat(binding, convention, property);
        ApplyValidation(binding, viewModelType, property);
        ApplyUpdateSourceTrigger(bindableProperty, element, binding, property);

        BindingOperations.SetBinding(element, bindableProperty, binding);

        return true;
    };
Enter fullscreen mode Exit fullscreen mode

ApplyBindingMode, ApplyValueConverter, and the rest are all Funcs you can replace with your own processing; the defaults are quite simple. In other words, the property bindings CM sets up for us are the most basic ones. CM’s PropertyConvention handling is deliberately simple. If you have already set Text="{Binding Path=ChangeName}" on the property by hand, CM ignores that property. PropertyConvention only produces the most basic property binding; if you want smarter bindings, replace these Funcs with your own logic. This is also an overall CM design idea: do smart things to simplify the program, but make those operations optional, do only what is necessary, and leave room for customization.

Conclusion

The greatest success of CM’s design is its simplicity and flexibility. It works well alongside other frameworks and can be integrated into existing code with relative ease. If you already have your own framework, try integrating CM. I will not go into CM’s remaining features here. Questions and comments are welcome—I also hope to exchange ideas with more friends. Thank you.

Top comments (0)