Action Basics

Prerequisites

  1. Obtain and Build the Code
  2. Setting Up a Project

Referenced Assemblies

  • Caliburn.Core
  • Caliburn.PresentationFramework
  • Microsoft.Practices.ServiceLocation

Minimum Configuration

Note: Configuration should be placed in the App.xaml.cs constructor for WPF or the App.xaml.cs Application_Startup for Silverlight.

CaliburnFramework
    .ConfigureCore()
    .WithPresentationFramework()
    .Start();
Note: As an alternative to manual configuration, you can inherit your application from CaliburnApplication.

Using Actions (Based on Samples - Actions)

Actions are a core feature of Caliburn which can enable a number of UI patterns such as MVC, MVP and Presentation Model (MVVM). Let's look at a basic usage of Actions.
  • In your project, create a new class named Calculator. This will serve as your first controller, and will be the type where we place actions. Use the code below:
public class Calculator
{
    public int Divide(int left, int right)
    {
        return left / right;
    }
}
  • Next, use the following markup to implement your main Page (for Silverlight) or your Window (for WPF):

Silverlight
<UserControl x:Class="Actions.Page"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:ca="clr-namespace:Caliburn.Actions;assembly=Caliburn.Actions"
             xmlns:local="clr-namespace:Actions"
             xmlns:cm="clr-namespace:Caliburn.RoutedUIMessaging;assembly=Caliburn.RoutedUIMessaging"
             xmlns:ct="clr-namespace:Caliburn.RoutedUIMessaging.Triggers;assembly=Caliburn.RoutedUIMessaging"
             Width="400"
             Height="300">
    <ca:Action.Target>
        <local:Calculator />
    </ca:Action.Target>
 
    <StackPanel x:Name="LayoutRoot">
        <Grid Margin="10">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="*" />
            </Grid.ColumnDefinitions>
 
            <TextBox x:Name="left"
                     Grid.Column="0" />
            <TextBlock Text="/"
                       Margin="10 0"
                       Grid.Column="1" />
            <TextBox x:Name="right"
                     Grid.Column="2" />
            <Border BorderBrush="Black"
                    BorderThickness="0 0 0 1"
                    Margin="10 0 0 0"
                    Grid.Column="3">
                <TextBlock x:Name="DivideResult" />
            </Border>
        </Grid>
 
        <Button Content="Divide (Trigger Collection w/ Explicit Parameters)">
            <cm:Message.Triggers>
                <ct:RoutedMessageTriggerCollection>
                    <ct:EventMessageTrigger EventName="Click">
                        <ct:EventMessageTrigger.Message>
                            <ca:ActionMessage MethodName="Divide"
                                              OutcomePath="DivideResult.Text">
                                <cm:Parameter ElementName="left"
                                              Path="Text" />
                                <cm:Parameter ElementName="right"
                                              Path="Text" />
                            </ca:ActionMessage>
                        </ct:EventMessageTrigger.Message>
                    </ct:EventMessageTrigger>
                </ct:RoutedMessageTriggerCollection>
            </cm:Message.Triggers>
        </Button>
    </StackPanel>
</UserControl>

WPF
<Window x:Class="Actions.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:Actions"
        xmlns:cal="http://www.caliburnproject.org"
        Title="Window1"
        SizeToContent="WidthAndHeight">
    <cal:Action.Target>
        <local:Calculator />    
    </cal:Action.Target>
     
    <StackPanel>
        <Grid Margin="10">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="*" />
            </Grid.ColumnDefinitions>
            
            <TextBox x:Name="left" 
                     Grid.Column="0"/>
            <TextBlock Text="/"
                       Margin="10 0"
                       Grid.Column="1"/>
            <TextBox x:Name="right" 
                     Grid.Column="2"/>
            <Border BorderBrush="Black"
                    BorderThickness="0 0 0 1"
                    Margin="10 0 0 0"
                    Grid.Column="3">
                <TextBlock x:Name="DivideResult" />
            </Border>
            
        </Grid>
         
        <Button Content="Divide (Trigger Collection w/ Explicit Parameters)">
            <cal:Message.Triggers>
                <cal:RoutedMessageTriggerCollection>
                    <cal:EventMessageTrigger EventName="Click">
                        <cal:EventMessageTrigger.Message>
                            <cal:ActionMessage MethodName="Divide"
                                           OutcomePath="DivideResult.Text">
                                <cal:Parameter Value="{Binding ElementName=left, Path=Text}"/>
                                <cal:Parameter Value="{Binding ElementName=right, Path=Text}"/>
                            </cal:ActionMessage>
                        </cal:EventMessageTrigger.Message>
                    </cal:EventMessageTrigger>
                </cal:RoutedMessageTriggerCollection>
            </cal:Message.Triggers>
        </Button>
    </StackPanel>
</Window>

Note: There are a couple of subtle differences between the WPF and Silverlight versions that should be pointed out. First, the WPF version can use a single namespace for all of Caliburn's features. WPF supports this through the use of the XmlnsDefinition attribute which does not function properly in Silverlight. Second, the syntax for declaring Parameters is slightly different. Silverlight does not support ElementName binding and Freezables in v2 and only partially in v3, therefore it is unable to use straight databinding for parameters values, like WPF. Instead, it must use ElementName and Path properties to achieve this effect. You can learn more about the details of parameters here.

Note: This sample instantiates the Calculator instance inline. I generally prefer not to have the view control instantiation. This is for demo purposes only.

So, what does all this markup do? (Don't worry about the quantity of markup, I'll show you how to greatly reduce that later.) Let's begin by looking at the <Button /> element. Notice the use of the Message.Triggers attached property. With this property we can add a set of message sending triggers to any element. In this case, we have specified the use of a single EventMessageTrigger (see Message Triggers). EventMessageTriggers allow us to use the firing of an event to send a message to our controller. So, when the Button's Click event is fired, we will send the enclosed message to the controller. The message we are sending is an ActionMessage, specifying that the method to call is "Divide." It also indicates that the text properties of the "left" and "right" elements should be passed as the parameters to this method. Finally, the return value of the "Divide" method should be bound back to the "DivideResult" element's text property.

Now that we have a trigger that can send a specific message, who will handle this message? Notice that the root element UserControl/Window also has an attached property value specified. By using the Action.Target property we can specify an instance of a class to handle ActionMessages. For this simple example, we have declared the instance inline, however in most non-trivial applications it's likely that the Action.Target will be set via databinding or by using the Resolve Extension.

Note: The ResolveExtension is only available in WPF becuase Silverlight does not currently support custom markup extensions. To get around this problem in Silverlight, you can specify a string value for Action.Target. In this case the string will be used to look up an instance from the IContainer. If you are using SimpleContainer (Caliburn's default) all services registered by type can also be accessed this way, using the type's FullName as key.

Now that we have all of our code and markup in place and we understand the basic usage, run the application. Fill in the text boxes with various numbers and click the button. You should see the results of a successful divide operation appear in the UI. Try placing a break point in the "Divide" method and repeat the process. Next, try entering zero for the "right" value. Click the button and an exception is thrown. Youch! That's not good. Let's fix that.
  • Change the Calculator class to match the following definition (you will need to add a using/import statement for Caliburn.PresentationFramework.Filters):
[Rescue("GeneralRescue")]
public class Calculator
{
    public int Divide(int left, int right)
    {
        return left / right;
    }
 
    public void GeneralRescue(Exception ex)
    {
        MessageBox.Show(ex.Message);
    }
}

Note: It is not a good practice to call MessageBox.Show from a non-View class. Consider implementing a MessageBoxService.

Now, run the code, type in zero for the "right" value and click the button. This time a message box is displayed and your application is prevented from crashing. A rescue is a special type of filter that can catch exceptions. The only parameter it requires is the name of the method to pass the exception to. If a Rescue is placed on the class (as it is here), any action invoked by Caliburn on that class is protected from unhandled exceptions by that method. Additionally, rescues can be placed on specific methods. In this case, the class rescue will be overridden by the method level rescue. This fixes our application crash, but it doesn't provide us with our desired user experience. Ideally, we would like the button to be disabled if the text boxes have invalid values. Let's see how another type of filter can help us out.
  • Once again update the Calculator class to match the following definition:
[Rescue("GeneralRescue")]
public class Calculator
{
    [Preview("CanDivide", AffectsTriggers = true)]
    public int Divide(int left, int right)
    {
        return left / right;
    }
 
    public bool CanDivide(int left, int right)
    {
        return right != 0;
    }
  
    public void GeneralRescue(Exception ex)
    {
        MessageBox.Show(ex.Message);
    }
}

Now we've added the Preview filter. This method occurs before the execution of the decorated Action. If it returns false the action will not be allowed to execute. Notice that it takes the same parameters as the action it decorates. Also, if the AffectsTriggers property is set to true, the preview filter will affect the state of the UI. This is the default value, so you really don't need to specify it unless you want to turn this behavior off. Run the application now and type various values in the text boxes. Notice that the button is now automatically disabled if you provide invalid values.

Note: By convention, if you name a method Can{ActionName}, Caliburn will automatically add the Preview filter to the action without you needing to add the attribute.
Note: You can also use a Can{ActionName} as a property. It too will be wired automatically. (So, you only have to use PreviewAttribute for nonconventional names.) When you do this, you can also raise property change notifications to force the associated triggers to reevaluate.

Now that you understand several of the main concepts, we're going to investigate some alternative markup syntaxes that should acutally make using the framework quite convenient. For the following examples, we'll use all the same Xaml as before, with the exception of the <Button /> element. We'll change this up each time so that you can learn a few different ways of declaring triggers and actions. All the remaining markup is identical for both WPF and Silverlight.
  • Replace the <Button /> element with the following markup:

<Button Content="Divide (Trigger Collection w/ Inferred Parameters)">
    <cm:Message.Triggers>
        <ct:RoutedMessageTriggerCollection>
            <ct:EventMessageTrigger EventName="Click">
                <ct:EventMessageTrigger.Message>
                    <ca:ActionMessage MethodName="Divide" />
                </ct:EventMessageTrigger.Message>
            </ct:EventMessageTrigger>
        </ct:RoutedMessageTriggerCollection>
    </cm:Message.Triggers>
</Button>

This markup is very similar to our initial trigger/message declaration. The only difference is that we have left off the paramaters. If we leave out the parameters, Caliburn will try to automatically determine the values by looking up UI elements based on the parameter names (it will also search resources). If the action has a return value, the name of the method + "Result" will be looked up in the UI for binding. Run the application and you'll notice that everything still works, almost. Notice that if you put a zero into the "right" box the button is still enabled, but the action never fires (you can confirm this by putting a break point in the divide method.) This is because Caliburn does not know what UI elements are involved in the message until it is sent, and thus cannot update the UI ahead of time, but it can still filter the action. Use inferred parameters only when you don't need the UI to update according to changes in input values.
  • Now, replace the <Button /> element with the following markup:

<Button Content="Divide (Attachment w/ Explicit Parameters)"
        cm:Message.Attach="[Event Click] = [Action Divide(left.Text, right.Text) : DivideResult.Text]" />

This is the preferred way of declaring triggers/messages. Notice that we are using a different attached property: Message.Attach. Using this property, we can provide a string that will be parsed out into a trigger. The declaration you see here is identical at runtime to our original example. Try running the application to confirm its behavior. On the left side of the equals sign, we indicate the trigger type and parameters. Here we are are declaring an EventMessageTrigger for the Click event. You can get more details on the shortened trigger syntax here. On the right side of the equals sign, we are declaring the type of message and it's contents. In this case, we have an ActionMessage with its parameter values coming from the text properties of the "left" and "right" text boxes and its return value being bound back to the text property of the "DivideResult" element. The brackets "[]" are optional in this syntax, but I prefer them because they add some visual clarity.

Note for WPF: You can also specify binding modes for the parameters if something other than the default is required. See below:

<Button Content="Divide (Attachment w/ Explicit Parameters and Modes)"
        cm:Message.Attach="[Event Click] = [Action Divide(left.Text:TwoWay, right.Text:OneWay) : DivideResult.Text]" />
  • Next, replace the <Button /> element with the following markup:

<Button Content="Divide (Attachment w/ Inferred Parameters)"
        cm:Message.Attach="[Event Click] = [Action Divide]" />

This is functionally equivalent to our second long markup example. In this case, our parameters will be inferred by Caliburn.
  • Replace the <Button /> element with the following markup:

<Button Content="Divide (Attachment w/ Default Trigger/Message and Explicit Parameters)"
        cm:Message.Attach="Divide(left.Text, right.Text) : DivideResult.Text" />

This is an interesting case. Here, Caliburn will choose a default trigger based on the type of element being attached to. Caliburn is also parsing with its default message parser. In this case the default trigger is an EventMessageTrigger for the Click event and the default message type is ActionMessage, so this markup produces the same runtime behavior as our first example. Even though this is extremely succinct, I still prefer to declare the trigger and message type explicitly, but you have this option available if you desire.
  • Replace the <Button /> element with the following markup:

<Button Content="Divide (Attachment w/ Defaults)"
        cm:Message.Attach="Divide" />

With this markup, everything will be inferred by Caliburn: trigger type, message type, parameters and return value. It works, but please use with caution.

After all of that, you may be wondering why anyone would use the long syntax. In 99% of cases you shouldn't need it, but it is there for maximum flexibility. One final thing to note. If you wish to attach multiple messages to the same element, you can do this by separating them with a semicolon. Here's an example:

<Button Content="Divide or Multiply"
        cm:Message.Attach="[Event Click] = [Action Divide(left.Text:TwoWay, right.Text:OneWay) : DivideResult.Text];
                           [Event MouseRightButtonUp] = [Action Multiply(left.Text:TwoWay, right.Text:OneWay) : MultiplyResult.Text]" />

Last edited May 5, 2010 at 8:31 PM by EisenbergEffect, version 35

Comments

marcoamendola Jun 26, 2010 at 9:33 AM 
@gtewksbury: see http://caliburn.codeplex.com/wikipage?title=Parameters

gtewksbury Jun 25, 2010 at 7:30 PM 
Let's say I have a custom event with a number of values in the event args. Is there any way to pass values from the event args into the message?

alamb Nov 19, 2009 at 9:16 PM 
The button markup for Trigger Collection w/ Inferred Parameters, Attachment w/ Inferred Parameters, and Attachment w/ Defaults doesn't work in Silverlight either. The button stays disabled even when valid parameters are entered for left and right.

alamb Nov 19, 2009 at 8:58 PM 
The Silverlight code for this example seems to be out of date. I had to change the namespaces to:
xmlns:ca="clr-namespace:Caliburn.PresentationFramework.Actions;assembly=Caliburn.PresentationFramework"
xmlns:cm="clr-namespace:Caliburn.PresentationFramework;assembly=Caliburn.PresentationFramework"
xmlns:ct="clr-namespace:Caliburn.PresentationFramework.Triggers;assembly=Caliburn.PresentationFramework"

Also the prefix for RoutedMessageTriggerCollection changed to cm instead of ct.

EisenbergEffect Nov 4, 2009 at 2:00 AM 
Thanks for the fix.

JMichelson Nov 3, 2009 at 6:25 PM 
The OneTime parameter in this example causes the framework to disable the button. Removing the OneTime parameter or changing it to OneWay fixes the problem.

EisenbergEffect Aug 20, 2009 at 2:44 PM 
thlorenz, Thanks for catching that. The docs are updated now.

thlorenz Jul 28, 2009 at 2:00 PM 
The latest version uses OutcomePath instead of ReturnPath, so in order to not confuse people you should update this section of code in your doc:
<cal:ActionMessage MethodName="Divide"
------->>> ReturnPath="DivideResult.Text">
<cal:Parameter Value="{Binding ElementName=left, Path=Text}"/>
<cal:Parameter Value="{Binding ElementName=right, Path=Text}"/>
</cal:ActionMessage>
Still great documentation so far, really gets you up and running quickly.

SteveGentile Mar 4, 2009 at 5:07 PM 
I was able to use the $dataContext instead - much better- thanks Rob :)

SteveGentile Mar 4, 2009 at 4:30 PM 
Another question - is it possible to pass along the tag of the button itself in the message?
ie. the Button tag would be the object bound, whereas the text is the 'display' (basically I have an editable list and I need to pass the id as well as the new value to a call on the presenter) I'm trying to figure out where to bind the 'id' to - as I don't want to show it, but I need it passed in the message.

SteveGentile Mar 4, 2009 at 4:10 PM 
Great work Rob.

'What if' I want to bind to two elements on the same TextBox - ie. the Text and the Tag :
<TextBox x:Name="selectedServiceDescription" Style="{StaticResource InPlaceEditTextBox}" Tag="{Binding ServiceID, Mode=TwoWay}" Text="{Binding ServiceDescription, Mode=TwoWay}" />

<Button Content="Save">
<cm:Message.Triggers>
<Triggers:RoutedMessageTriggerCollection>
<Triggers:EventMessageTrigger EventName="Click">
<Triggers:EventMessageTrigger.Message>
<Actions:ActionMessage MethodName="SaveService">
<cm:Parameter ElementName="selectedServiceDescription"
Path="Text" />
<cm:Parameter ElementName="selectedServiceDescription"
Path="Tag" />
</Actions:ActionMessage>
</Triggers:EventMessageTrigger.Message>
</Triggers:EventMessageTrigger>
</Triggers:RoutedMessageTriggerCollection>
</cm:Message.Triggers>
</Button>

I get errors when attempting to do this?

EisenbergEffect Feb 2, 2009 at 12:10 PM 
I have fixed the designer problem. The update is in the trunk and will appear in the forthcoming Beta 1.

etaupier Jan 6, 2009 at 2:25 PM 
I am getting an error in the designer window regarding the EventMessagTrigger - could not create an instance of type EventMessageTrigger. I can run the app fine, but apparently there's some problem working with the designer. Any ideas on how to fix this? Thanks.

EisenbergEffect Oct 25, 2008 at 6:48 PM 
You are right. The initialization would need to be earlier in that case. As long as it occurs before any Xaml gets parsed that uses Caliburn features, you should be fine. In my samples, it happened that I was manually setting RootVisual in the Application_Startup, thus the initialization could actually happen there, as long as it was before the RootVisual property was set.

endquote Oct 24, 2008 at 11:53 PM 
For Silverlight, I found that I needed to have the initialization code in the App() constructor rather than the Application_Startup method. The Page.xaml.cs constructor runs before Application_Startup, and the constructor calls InitializeComponent() which tries to parse the trigger XAML, but Caliburn is not initialized at that point and throws an error.