Wpf – Caliburn.Micro: Binding Button in a Screen to a command in ScreenConductor

caliburn.micromvvmwpf

I'm following this tutorial on Screen and ScreenConductors in Caliburn.Micro Framework.

I'm using WPF, not Silverlight, and I have made the corresponding changes in App.xaml (using MergedDictionary for the Bootstrapper).

The original Simple Navigation example has a shell with two buttons, and a content area where two possible screens are displayed, conducted by the ShellViewModel.

Then I tried to move each button to its counterpart View, so that PageOne would have a button to take to PageTwo, and vice-versa. I did it because I don't want the home shell continuously "showing its entrails" across the application.

The fact is, if I just move a button to a Screen View, it no longer binds to the command, which is in the ShellViewModel, not in the Screen ViewModel itself. I know these bindings happen by convention, but I don't know if the convention covers this case, or I'd need to configure.

The symptom I am facing is: When I run the app, PageOneView shows, with the "Go to Page Two" button in it, but when I click the button nothing happens.

The question I ask is: "How is the proper way to "bubble" a button in a ScreenView.xaml to an action in the ScreenConductorViewModel.cs?

My current code is below:


PageOneView.xaml

<UserControl x:Class="ScreenConductor.PageOneView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <Grid Background="LightGreen">
        <Button x:Name="ShowPageTwo" Content="Show Page Two" HorizontalAlignment="Center" VerticalAlignment="Top" />
    </Grid>
</UserControl>

PageOneViewModel

using Caliburn.Micro;

namespace ScreenConductor {
    public class PageOneViewModel : Screen {
        protected override void OnActivate() {
            base.OnActivate();
        }
    }
}

ShellView.xaml

<Window x:Class="ScreenConductor.ShellView"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">

    <ContentControl x:Name="ActiveItem" />
</Window>

ShellViewModel.cs

using Caliburn.Micro;

namespace ScreenConductor 
{
    public class ShellViewModel : Conductor<object> 
    {
        public ShellViewModel() 
        {
            ShowPageOne();
        }

        public void ShowPageOne() 
        {
            ActivateItem(new PageOneViewModel());
        }

        public void ShowPageTwo() 
        {
            ActivateItem(new PageTwoViewModel());
        }
    }
}

Best Solution

As long as you bind the command using the explicit action binding syntax the bubbling should work fine. This is because to bind by name CM examines controls on the view bound to the current vm. If there is no matching named control a binding does not get created.

You can use this:

<SomeControl cal:Message.Attach="SomeMethodOnParentVM" />

This will attempt to bubble the message up the control hierarchy until a suitable handler is found. Note that this will throw an exception if no handler could be found. I recall there being an option to turn this off on the binding but you might want to check the docs (I'm contributing from my mobile at the moment :-)

Edit:

Ok if you want more info for behind the scenes, the best place to check really is the source. The author Rob Eisenberg mentions that the source is quite small (something like 2700 lines of code I think) so it's easy to inspect and easy to hold in your head

It's worth reading through all the documentation on the CodePlex site (there's a fair few pages but they explain everything in enough detail for you to piece together the rest).

Not sure how much you know about the Message class that contains the Attach attached property but this is what kicks off the action binding when the standard control Name conventions aren't used (I assume you know about attached properties in WPF/SL). You can see the standard named conventions in the ViewModelBinder class

The Message class looks like:

http://caliburnmicro.codeplex.com/SourceControl/changeset/view/35582bb2a8dfdd3fcd71a07fa82581ddb93a786f#src/Caliburn.Micro.Silverlight/Message.cs

(yes it's the Silverlight source but this is the base for all other versions and it's just a few compiler directives and some additional classes that appear in other version such as WPF/WinRT)

If you look at the source you can see that when the attached property is set, the Parser class kicks off to parse the string. The parser actually parses several different formats so you can attach to different events and methods, and also pass properties e.g.

<Button cal:Message.Attach="[Event Click] = [Action SomeButtonWasClicked()]" /> 

or

<Button cal:Message.Attach="[Event MouseEnter] = [Action MouseEnteredAButton($eventargs)" />

Above you can see that the $eventargs special value was used. There are several out-of-box special values, and you can also write your own (check out this SO question Using MessageBinder.SpecialValues in Windows 8 app not working? where a user was using SpecialValues to pass the horizontal mouse position from controls for use in a synthesizer app)

You can also pass the CM default property of other controls e.g.

<TextBox x:Name="TextBox1" />
<Button cal:Message.Attach="MouseClicked(TextBox1)" />

Where the Text value of TextBox1 will be passed to the MouseClicked method on the VM. This is specified in the default convention bindings for TextBox (look at ConventionManager.AddElementConvention and the docs on that)

The bubbling works by inspecting the visual tree and attempting to bind to each level (happens in SetMethodBinding in ActionMessage class)

It's pretty simple but effective (it just uses VisualTreeHelper to walk up the visual tree until a suitable handler is found)

Not sure what else you might need info on :P