DEV Community

yohan
yohan

Posted on Originally published at cnblogs.com

Analyzing Caliburn.Micro — Part 3

Following on from the previous post, which briefly introduced Caliburn.Micro (CM) Action, this article continues with the next CM feature: Convention.

What Is a Convention?

Convention is often translated as “pact” or “agreement”—a shared rule of conduct that all parties, having reached consensus, follow. In CM, Convention is mainly used for pairing and matching. That pairing primarily means pairing the View with the ViewModel. As mentioned earlier, CM is based on the MVVM pattern. In MVVM (Model-View-ViewModel), the Model holds the data and is relatively fixed; the relationship between View and ViewModel is more fluid. Let’s briefly introduce that.

View-First or ViewModel-First?

In MVVM, the ViewModel’s job is to hold the View’s state and encapsulate the Model’s data. The ViewModel evolved from the PresentationModel pattern, and it does not store every piece of View state—only state that might be modified. WPF mainly contributed two technologies that made the ViewModel practical:

  1. Data binding. WPF provides data binding: the View binds to the ViewModel and uses the ViewModel as its DataContext. When View state stored in the ViewModel changes, the View refreshes automatically.
  2. Command. WPF provides ICommand so that when the user operates the View, the operation is passed to the ViewModel through a Command.

The View’s two main jobs—displaying data and manipulating data—are abstracted out, and View state lives in the ViewModel. Because of data binding, the View associates with the ViewModel through DataContext, and the two are loosely coupled. That is the beauty of the MVVM pattern. Back to the topic of this article: how do View and ViewModel find each other? View-First or ViewModel-First?

ViewModel-First

In general, ViewModel-First is recommended. Design the ViewModel first, then generate the corresponding View with a DataTemplate. The benefit is that the View can be swapped at any time, and unit tests can mock a View. The CM framework provides ViewLocator to locate the View for a given ViewModel, with methods as follows:

public static Func<object, DependencyObject, object, UIElement> LocateForModel
public static Func<Type, DependencyObject, object, UIElement> LocateForModelType
Enter fullscreen mode Exit fullscreen mode

CM makes heavy use of Func overrides for polymorphism. LocateForModel and LocateForModelType take a ViewModel instance or type respectively and return the corresponding View. Those names are a bit weak—GetCorrespondingView would be clearer.

How does locating the View from the ViewModel work? It takes two steps. First, find the View’s Type from the ViewModel. Second, create the View with reflection via Activator.CreateInstance(Type type).

To operate on the View from the ViewModel more conveniently, CM provides IViewAware so a ViewModel can cache the corresponding View. The IViewAware interface is defined as follows:

public interface IViewAware
{
    void AttachView(object view, object context = null);
    object GetView(object context = null);
    event EventHandler<ViewAttachedEventArgs> ViewAttached;
}
Enter fullscreen mode Exit fullscreen mode

ViewAware is the default implementation of IViewAware. When the ViewModel and View are bound together, IViewAware’s AttachView is called. You can override OnViewAttached to process the View—for example, register View events (be careful of strong references). When the View is loaded into the visual tree, ViewAware.OnViewLoaded is called; you can walk the View’s visual tree there.

When When ViewLocator calls LocateForModel, it first checks whether the ViewModel implements IViewAware and has already cached a View. If so, it returns the cached View instead of creating a new one. That brings us back to step one: how to find the View’s Type from the ViewModel.

NameTransformer

CM uses NameTransformer to infer possible View types with regular expressions. The implementation works like this:

NameTransformer defines ReplacePattern (the replacement rule), ReplaceValue (the replacement value), and GlobalFilterPattern (a filter rule); the inference is done with regular expressions. For example, set ReplacePattern to ViewModel$ and ReplaceValue to View. This rule means: replace the trailing ViewModel part of the string with View. If the ViewModel type passed in is Illusion.ViewModels.ShellViewModel, the returned type after replacement is Illusion.ViewModels.ShellView.

ViewLocator ships with five default rules:

  1. Strip a trailing View.
  2. Replace a trailing ViewModel with View.
  3. Replace a trailing PageViewModel with Page.
  4. Replace Illusion.ViewModels.ShellViewModel with Illusion.Views.ShellView (replace ViewModels → Views, ShellViewModel → ShellView).
  5. Replace Illusion.ViewModels.ShellPageViewModel with Illusion.Views.ShellPage (same idea as above).

For real projects, if you follow CM’s recommended naming—Illusion.ViewModels.ShellViewModel → Illusion.Views.ShellView—CM will find the corresponding View automatically. If you separate View and ViewModel and do not follow those naming conventions, you must add corresponding rules to ViewLocator’s NameTransformer so it can find the View type.

After ViewLocator resolves the View type, what it actually has is a string. CM still needs to turn that string into a real Type via an Assembly. CM does not scan every assembly in the AppDomain; it keeps an AssemblySource that holds the set of assemblies to search. You can override BootStrapper.SelectAssemblies to return the assemblies in scope, or call AssemblySource’s Add / AddRange methods to add more assemblies.

For example, if you use DirectoryCatalog to watch plugins and the corresponding View types live in plugins, you must add the plugin assemblies to AssemblySource—otherwise ViewLocator cannot find the View. In View–ViewModel matching, CM’s design provides some implementations and room for extension, but it is not fully automatic; you still need project-specific customization.

Of course, you can skip smart matching altogether, create View and ViewModel by hand, and bind them with ViewModelBinder’s Bind method.

View-First

In the WP7 environment, View-First is recommended: locate the ViewModel from the View, then bind them together. The implementation and matching rules are the same as ViewModel-First and are omitted here.

Action Convention

In the Action covered in the previous article, we wrote markup like:

<Button cm:Message.Attach="[Event MouseEnter] = [Action Show('Enter')];
                           [Event MouseLeave] = [Action Show('Leave')]" />
Enter fullscreen mode Exit fullscreen mode

That is Action Convention: CM turns the string after Attach into the corresponding Actions. There is not much room for extension here, so I will not go into detail.

Property Convention

This is one of the powerful parts of CM, and the details—including ElementConvention—are quite interesting. I’ll save them for the next post. ^_^

Top comments (0)