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