【译】Caliburn——Action

      最近在学习Caliburn,主要的学习资源还是官网上的文档,蛮详细的,所以翻译一下与园友们共同学习。

      官网中的文档主要是基于其Sample的,有需要的朋友可以去这里下载(我用的VS2010不能打开项目,惨淡。。。Disappointed smile),由于公司用的是1.1 RTW版本的,所以我这里用的也是1.1的。如果您下载的是其他版本的,我不能保证能正确编译运行。Smile 本文源码下载:Caliburn.Action.7z

所需要的程序集

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

最小配置

Note:下面的代码在WPF中应放置于App.xaml.cs中的构造函数中,而在Sliverlight中应放置于App.xaml.cs中的Application_Startup中。

请确保已添加上述程序集,并引入命名空间。

using Caliburn.Core;
using Caliburn.PresentationFramework;

 

CaliburnFramework
    .ConfigureCore()
    .WithPresentationFramework()
    .Start();

Note: 另一可选的手动配置是继承CaliburnApplication.

使用Actions

    Actions是Caliburn能够支持诸如MVC,MVP和MVVM等UI模式的核心特性。下面让我们了解一下Actions的基本使用。

    在您的项目中,添加一个名为Calculator的类。它将作为您的第一个控制器,放置actions。代码如下:

public class Calculator
{
    public int Divide(int left, int right)
    {
        return left / right;
    }
}
 

接下来,使用下面的标记填充您的Main Page(Silverlight)或Window(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:值得一提的是,在WPF与Sliverlight两个版本中有些许微妙的差别。首先,WPF版本中只使用一个命名空间就包含了所有Caliburn的特性。WPF可以通过XmlnsDefinition特性支持,而在SL中却是不起作用的。其次,声明变量的语法也有些差别。SL V2中不支持ElementName绑定和Freezables,在V3中也只是部分支持,因此不能像WPF那样直接绑定参数值。然而可以使用ElementName和Path去实现达到相同的效果。你可以从这里获得更多关于parameters的细节。(我没学过SL,所以上面的区别对我来说不是区别,不过还是请懂SL的朋友注意一下Open-mouthed smile)

Note:该示例中内联了Calculator的实例化。不过本人不太喜欢View Control(视图控件?没看懂啥意思,也不会翻译,还请园友们指教)。这里仅作掩饰用。

    那么,这些标记又是干什么用的呢?(不要担心标记的数量,我待会将向您演示如何大量精简)。先看看<Button/>元素。注意Message.Triggers附加属性的使用。有了这个属性,我们可以添加传递消息的触发器到任意元素。这里,我们可以使用一个EventMessageTrigger(详情请看Message Triggers)。EventMessageTriggers允许我们使用触发的事件传递信息给控制器。因此,当Button的Click事件被触发时,我们将发送附加的信息到控制器。而我们正在发送的信息是一个ActionMessage,指明将要调用的方法是"Divide”。并且ActionMessage也指明了”left”和”right”元素的Text属性应将作为参数传递给”Divide”方法。最终,“Divide”方法的返回值绑定到了“DivideResult”元素的Text属性。

    现在我们已经有了可以发送具体消息的触发器了,但是问题来了——谁将处理这个消息呢?仔细观察后发现:根元素UserControl/Window拥有一个具有值得附加属性。通过使用Action.Target属性我们可以指明一个具体的实例去处理ActionMessages。对于这个简单的例子,我们声明了一个内联实例,然而在大多数应用程序中更有可能会通过数据绑定或Resolve Extension实现。

Note:ResolveExtension仅在WPF中可用,因为在SL中目前不支持自定义标价扩展。为避免这个问题,可以通过为Action.Target指明一个字符串值。在这种情况下,该字符串被用来在实现了IContainer的实例中检索资源。如果你使用SimpleContainer(Caliburn的默认Container),且所有的服务都通过其Type注册过,那么也可以实现上面的方法——只要将其Type作为Key就好了。

    现在我们已经有了必须的代码和标记,我们也理解了基本使用,好了,是时候运行了。在两个textbox中输入一些值然后单击按钮。(如果输入正确)你会看到正确的除法运算的结果。在”Divide”方法中设置一个断点并重新运行。接下来,在右边的textbox中输入“0”,单击按钮后,抛出异常。糟糕!这可不行!我们必须修正它。

 

改变Calculator类定义,如下:

using 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: 在非视图类中调用MessageBox.Show并不是一个好的实践,考虑实现MessageBoxService。

    再次运行,在右侧的textbox中输入0。这次,会弹出一个消息框,程序也没有崩溃。一个resuce就是一个具体类型的filter,可以捕获异常。filter所需的参数只有一个,就是可以将异常信息传递过去的方法的名称。如果resuce放置一个类上,那么在该类中所有有Caliburn调用action而产生的异常都会被指定的方法所处理。此外,resuces可以被放置在具体的方法上。在这种情况下,类级别的resuce会被方法级别的重写。resuce虽然阻止了程序的崩溃,但是并未提供给我们想要的用户体验。理想情况下,我们希望如果文本框输入值不合法,按钮就应该被禁用。接下来,看另一个类型的filter可以帮助我们。

再次更新Calculator,如下:

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

    现在我们增加了Preview过滤器。该方法会在其装饰的方法执行前而执行。如果返回false,其装饰的方法不会允许执行。注意该方法和其所要装饰的方法的参数是一样的。并且,如果AffectsTriggers设置为true,Preview过滤器会影响UI的状态。这是默认值,不需要具体指明除非你想关闭这个行为。再次运行,输入值。你会发现如果输入不合法的值,按钮会自动被禁用。

Note: 按照惯例,如果有一个名为Can{ActionName}的方法,Caliburn会自动为相应的Action加上Preview,而你不需要自己加上该特性。

Note: 你也可以将Can{ActionName}作为属性,它也会被自动触发(因此,你仅仅需要为非惯例名称手动加上Preview特性)。当你这样做时,也可以引发属性改变通知强制相关联的触发器重新评估。

    现在你已经理解了一些主要概念,接下来我们将研究一些可选的标记语法去帮助我们使Caliburn更加方便。在接下来的例子中,我们会使用与前面相同的xaml,除了<Button/>。所有其他的标记对于WPF和SL都是一样。

使用下面的标记替换<Button/>:

<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>

    这个标记和上面最初的trigger/message声明非常相似。惟一的区别是没有参数。如果没有指定参数,Caliburn将尝试通过基于(方法)参数的名称检索UI元素和资源而决定其值。如果Action有返回值,方法的名称+“Result”会被用作key去检索UI而实现绑定。运行,你会发现几乎依旧正常工作。然而如果你在右侧的文本框内输入0,按钮仍然可以使用,但是action却不会触发(你可以设置断点调试确认)。这是因为Caliburn在消息被发送之前都不知道该消息包含哪些UI元素,此外它也不能提前更新UI,但仍能过滤action。(请注意)仅当不需要通过输入值去改变UI的情况下才使用推断参数。

现在,再次替换<Button/>,如下:

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

这是应优先使用的声明triggers/messages的方式。我们使用了另一个附加属性:Message.Attach。通过这个属性,我们可以提供一个字符串,然后它将被解析传入trigger。你看到的声明在运行时其实是和原来的例子是一模一样的。再次运行,确认一下行为。在等号左侧,我们指出了trigger的类型和参数。在这里,我们为Click事件声明了一个ventMessageTrigger。你可以在这里获得更多缩减trigger语法的详细信息。在等号的右侧,我们声明了消息的类型和内容。这样,我们有了一个ActionMessage,其参数从left和right文本框中获得,返回值绑定回了”Divideresult”的text属性。其中,方括号”[“是可选的,但是它显得更清晰更可读。

Note for WPF: 如果需要,你也可以为某些参数指明具体的绑定模式。如下:

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

接下来,再次替换<Button/>,如下:

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

     这个在功能上和第二个比较长的标记示例是一样的。在这种情况下,参数被Caliburn自动推断出。

再次替换<Button/>,如下:

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

    这个示例很有趣。在这里,Caliburn会基于被绑定元素的类型选择一个默认的触发器。Caliburn也会用默认的消息解析器解析(消息)。这里,Click事件的默认的触发器是EventMessageTrigger,默认的消息类型是ActionMessage,所以该标记产生了与先前的第一个例子一样的运行时行为。即使这是非常简洁的。但我个人仍更喜欢显示声明trigger和message类型,这取决于你的愿望。

再次替换<Button/>,如下:

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

    在上面的标记中,所有东西都会被Caliburn通过推断得出:触发器类型,消息类型,参数类型和返回值。它仍然可以工作,但请谨慎使用。

    经过上面的例子,你可能会困惑——哪有人会使用那么长的语法?的确,在绝大多数情况下,你不需要这么做,但是它提供了最大程度的灵活性。最后一点,如果你希望为一个元素附加多个消息,你可以用分号隔开。就像下面这样:

<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]" />
posted @ 2012-10-30 16:07  Lyon Gu  阅读(1706)  评论(0编辑  收藏  举报