Caliburn.Micro: Binding Button in a Screen to a command in ScreenConductor
Asked Answered
N

2

2

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());
        }
    }
}

Nephrolith answered 2/5, 2013 at 18:28 Comment(0)
M
3

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

Meissner answered 2/5, 2013 at 20:1 Comment(8)
Oh the cal namespace is the caliburn consolidated namespace, iirc it's http://caliburnproject.orgMeissner
I am using WPF, not Silverlight. Which namespace should I add to XAML? I tried xmlns:cal="Caliburn.Micro[;assembly:Caliburn.Micro] and it didn't went into Intellisense (brackets to show that I tried either with and without assembly part).Nephrolith
oops... it's =, not :. Got it Intellisensing with xmlns:cal="clr-namespace:Caliburn.Micro;assembly=Caliburn.Micro". And by the way now it works. Can you elaborate just a bit more on how this works behind the scenes? I know about the "messaging" stuff in Caliburn.Micro, which concepts should I read about?Nephrolith
Not sure since I'm lazy and use ReSharper :) what version of cm? I'm sure you can add http://www.caliburnproject.orgMeissner
I'll edit with more info shortly, got to put the kids to bed first and ones on a naked rampage!Meissner
let us continue this discussion in chatMeissner
@Charleh: using NuGet makes proper installation of Caliburn.Micro easy and the namespace declaration xmlns:cal="http://www.caliburnproject.org" should work fine then.Hako
Yeah I use NuGet, I have a NuGet repository for all our internal libs, it's great because you add a single lib and it adds NHibernate, CSLA, Automapper etc etc in one command :)Meissner
H
0

If you have not yet referenced System.Windows.Interactivity (goes along with the Blend SDK) I'd really recommend it. I suppose a Caliburn.Micro project runs without it though I have never tried it. It's referenced per default as you install Caliburn.Micro via NuGet.

Your problem can be solved properly with using System.Windows.Interactivity.Interaction along with Caliburn.Micro.ActionMessage like this:

<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"
             xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
             xmlns:cal="http://www.caliburnproject.org"
             mc:Ignorable="d"
             d:DesignHeight="300" d:DesignWidth="300">
    <Grid>
        <Button Content="ShowPageTwo">
            <i:Interaction.Triggers>
                <i:EventTrigger EventName="Click">
                    <cal:ActionMessage MethodName="ShowPageTwo" />
                </i:EventTrigger>
            </i:Interaction.Triggers>
        </Button>
    </Grid>
</UserControl>

You can find more about actions here, the official documentation at CodePlex.

Action messages are going to bubble to the conductor and if no suitable method can be found an Exception is thrown.

Hako answered 2/5, 2013 at 22:30 Comment(6)
Do you not prefer the CM action parser? I find the syntax is much more terse and essentially it just creates this under the hoodMeissner
Mine is definately not the one solution though I prefer it over the action parser approach as I just like the syntax more and personally find it more readable.Hako
I always found cal:Message.Attach="ShowPageTwo" to be much more readable. Or if you want it to be more explicit: cal:Message.Attach="[Event Click] = [Action ShowPageTwo()]" - I think the explicit XAML is too wordyMeissner
If you do not specify the event like cal:Message.Attach="ShowPageTwo", on what event is the control going to react? Is this control specific? I never tried that.Hako
It reacts to the default event based on CM conventions. For Buttons it's Click - if you look at ConventionManager.AddElementConvention the signature is something like ConventionManager.AddElementConvention(<Bindable Dependency Prop>, '<Default Event>', '<Default Property>'` so for Button I think the default is ConventionManager.AddElementConvention(Button.ContentProperty, "Click", "Text")Meissner
Quick check on the source and it's actually: AddElementConvention<ButtonBase>(ButtonBase.ContentProperty, "DataContext", "Click");. Where datacontext is the convention based action parameter and click is the convention based default event. (So <Button x:Name="Button1" DataContext="hello" /><Button x:Name="Button2" cal:Message.Attach="DoSomething(Button1)" /> would call DoSomething on the VM when the button was clicked and pass the datacontext of Button1 to the methodMeissner

© 2022 - 2024 — McMap. All rights reserved.