3. 关于Actions的一切
我们在第1章中简要介绍了动作,但还有很多需要了解的。为了开始我们的研究,我们将以简单的“Hello”示例为例,看看当我们显式地创建动作而不是使用约定时,它是下面Xaml样子的:
1 <UserControl x:Class="Caliburn.Micro.Hello.ShellView" 2 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 3 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 4 xmlns:i="http://schemas.microsoft.com/xaml/behaviors" 5 xmlns:cal="http://www.caliburnproject.org"> 6 <StackPanel> 7 <Label Content="Hello please write your name" /> 8 <TextBox x:Name="Name" /> 9 <Button Content="Click Me"> 10 <i:Interaction.Triggers> 11 <i:EventTrigger EventName="Click"> 12 <cal:ActionMessage MethodName="SayHello" /> 13 </i:EventTrigger> 14 </i:Interaction.Triggers> 15 </Button> 16 </StackPanel> 17 </UserControl>
如您所见,Actions特性利用Microsoft.Xaml.Behaviors作为它的触发机制。这意味着你可以使用任何从Microsoft.Xaml.Behaviors.TriggerBase继承的东西来触发ActionMessage的发送。
- 也许最常见的触发器是EventTrigger,但您可以创建几乎任何类型的触发器,或者利用社区已经创建的一些常见触发器。当然ActionMessage这个标记是 Caliburn.Micro-specific 的一部分。它表明当触发器发生时,我们应该发送“SayHello”消息。那么,为什么在描述此功能时使用“发送消息”而不是“执行方法”?这是有趣和强大的部分。ActionMessage在Visual Tree中搜索可以处理它的目标实例。如果找到目标,但没有“SayHello”方法,则框架将继续冒泡,直到找到一个,如果没有找到“Handler”则抛出异常。
-
ActionMessage的冒泡特性在许多有趣的场景中会派上用场,Master/Details是一个关键的用例。另一个需要注意的重要特性是Action守卫。当找到“SayHello”消息的处理程序时,它将检查该类是否也有一个名为“CanSayHello”的属性或方法。如果你有一个保护属性,并且你的类实现了INotifyPropertyChanged,那么框架将观察到该属性的变化,并相应地重新评估保护。我们将在下面更详细地讨论方法保护。
Action 目标
现在你可能想知道如何指定ActionMessage的目标。看看上面的标记,没有明显的迹象表明目标将是什么。那么,这是从哪里来的呢?因为我们使用了模型优先的方法,当Caliburn。Micro(以下简称CM)创建了视图,并使用ViewModelBinder将其绑定到ViewModel,它为我们设置了这些。任何经过ViewModelBinder的东西都会自动设置它的动作目标。但是,您也可以使用附带的属性Action.Target自己设置它。设置此属性将ActionMessage“处理程序”定位在Visual Tree中与声明该属性的节点相连的位置。它还将DataContext设置为相同的值,因为您经常希望这两者相同。但是,您可以更改Action。如果你喜欢,可以从DataContext中获取目标。只需使用Action。TargetWithoutContext附加属性。Action的一个优点是。目标是您可以将其设置为系统。字符串和CM将使用该字符串从IoC容器解析实例,并使用提供的值作为其键。如果您愿意,这为您提供了一种执行View-First MVVM的好方法。如果你想要行动。目标集,并且希望应用操作/绑定约定,则可以使用Bind。模型附加属性以同样的方式。
视图优先
让我们看看如何使用视图优先技术来实现MVVM。下面是我们如何改变引导器:
1 public class MefBootstrapper : BootstrapperBase 2 { 3 //same as before 4 5 protected override void OnStartup(object sender, StartupEventArgs e) 6 { 7 Application.RootVisual = new ShellView(); 8 } 9 10 //same as before 11 }
因为我们使用View-First,我们继承了非泛型Bootstrapper。MEF配置与前面看到的相同,因此为了简洁起见,我将其省略。唯一改变的是如何创建视图。在这种情况下,我们只需重写OnStartup,自己实例化视图并将其设置为RootVisual(或者在WPF的情况下调用Show)。接下来,我们将稍微改变我们如何导出我们的ShellViewModel,通过添加一个显式命名的契约:
1 [Export("Shell", typeof(IShell))] 2 public class ShellViewModel : PropertyChangedBase, IShell 3 { 4 //same as before 5 }
最后,我们将改变我们的视图来拉入VM并执行所有绑定:
1 <UserControl x:Class="Caliburn.Micro.ViewFirst.ShellView" 2 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 3 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 4 xmlns:cal="http://www.caliburnproject.org" 5 cal:Bind.Model="Shell"> 6 <StackPanel> 7 <TextBox x:Name="Name" /> 8 <Button x:Name="SayHello" 9 Content="Click Me" /> 10 </StackPanel> 11 </UserControl>
注意,这里使用了 Bind.Model 附加属性。这将从IoC容器中 Key 解析VM,设置Action.Target和DataContext并应用所有约定。我认为展示CM是如何完全支持View-First开发的是件好事,但我主要是想明确您可以为操作设置目标的各种方法以及使用每种技术的含义。以下是可用附加属性的摘要:
- Action.Target 设置Action.Target属性和DataContext属性添加到指定实例。字符串值用于从IoC容器中解析实例。
- Action.TargetWithoutContext 只将 Action.Target 属性设置为指定的实例。字符串值用于从IoC容器中解析实例。
- Bind.Model 视图优先-为指定的实例设置 Action.Target 和DataContext属性。将约定应用于视图。字符串值用于从IoC容器中解析实例。(在根节点如 Window/UserControl/Page 上使用。)
- Bind.ModelWithoutContext 视图优先-将 Action.Target 设置为指定实例。将约定应用于视图。(在DataTemplate内部使用。)
- View.Model 视图优先-定位指定VM实例的视图,并将其注入到内容站点。设置VM为 Action.Target 和DataContext。将约定应用于视图。
Action 参数
现在,让我们看一下ActionMessage: Parameters的另一个有趣的方面。为了看到它的作用,让我们切换回原来的ViewModel-First bootstrapper等,并开始改变我们的ShellViewModel,看起来像这样:
1 using System.Windows; 2 3 public class ShellViewModel : IShell 4 { 5 public bool CanSayHello(string name) 6 { 7 return !string.IsNullOrWhiteSpace(name); 8 } 9 10 public void SayHello(string name) 11 { 12 MessageBox.Show(string.Format("Hello {0}!", name)); 13 } 14 }
这里有几件事需要注意。首先,我们现在使用的是一个完全POCO类;这里没有INPC goop 。其次,我们向SayHello方法添加了一个输入参数。最后,我们将CanSayHello属性更改为具有与动作相同输入的方法,但使用bool返回类型。现在,让我们来看看Xaml:
1 <UserControl x:Class="Caliburn.Micro.HelloParameters.ShellView" 2 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 3 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 4 xmlns:i="http://schemas.microsoft.com/xaml/behaviors" 5 xmlns:cal="http://www.caliburnproject.org"> 6 <StackPanel> 7 <TextBox x:Name="Name" /> 8 <Button Content="Click Me"> 9 <i:Interaction.Triggers> 10 <i:EventTrigger EventName="Click"> 11 <cal:ActionMessage MethodName="SayHello"> 12 <cal:Parameter Value="{Binding ElementName=Name, Path=Text}" /> 13 </cal:ActionMessage> 14 </i:EventTrigger> 15 </i:Interaction.Triggers> 16 </Button> 17 </StackPanel> 18 </UserControl>
我们的标记现在有一个修改:我们使用ElementName绑定将参数声明为ActionMessage的一部分。你可以有任意数量的参数。Value是一个DependencyProperty,所以所有的标准绑定功能都适用于参数。我说过你可以在Blend里做这些吗?

这样做的好处之一是,每次参数的值发生变化时,我们都会调用与动作相关的guard方法(在本例中为CanSayHello),并使用其结果来更新ActionMessage所附加的UI。继续并运行应用程序。您将看到它的行为与前面的示例相同。
除了文字值和绑定表达式之外,还有许多有用的“特殊”值可以用于参数。这些允许您方便地访问常见的上下文信息:
- $eventArgs 将EventArgs或输入参数传递给Action。注意:对于保护方法,这将为空,因为触发器实际上没有发生。
- $dataContext 传递ActionMessage所附加的元素的DataContext。这在Master/Detail场景中非常有用,其中ActionMessage可能会冒泡到父VM,但需要携带要执行的子实例。
- $source 触发ActionMessage被发送的实际FrameworkElement。
- $view 绑定到ViewModel的视图(通常是UserControl或Window)。
- $executionContext 动作的执行上下文,其中包含上述所有信息以及更多信息。这在高级场景中很有用。
- $this 动作附加到的实际UI元素。在这种情况下,元素本身不会作为参数传递,而是作为其默认属性传递。
您必须以“$”作为变量的开头,但是CM会以不区分大小写的方式处理该名称。这些可以通过向MessageBinder.SpecialValues中添加值来扩展。
注意:使用特殊值,如$this或Named Element
当您不指定属性时,CM使用默认属性,这是由特定的控件约定指定的。对于按钮,该属性恰好是“DataContext”,而TextBox默认为Text, Selector默认为SelectedItem等。当在视图中使用对另一个命名控件的引用而不是$this时,也会发生同样的情况。使CM将名为“someTextBox”的文本框中的文本传递给MyAction。实际控制从未传递给操作的原因是vm不应该直接处理UI元素,所以惯例不鼓励这样做。但是请注意,无论如何都可以使用扩展语法(基于System.Windows.Interactivity)填充参数或自定义Parser轻松访问控件本身。
枚举值
如果你想传递Enum值作为参数,你需要传递一个(大写)字符串的值:
1 ... 2 <Fluent:Button Header="Go!" cal:Message.Attach="[Event Click] = [Action MethodWithEnum('MONKEY')]" />
1 public enum Animals 2 { 3 Unicorn, 4 Monkey, 5 Dog 6 } 7 8 public class MyViewModel 9 { 10 public void MethodWithEnum(Animals a) 11 { 12 Animals myAnimal = a; 13 } 14 }
忠告
参数是一个方便的特性。它们非常强大,可以帮助你摆脱一些棘手的问题,但它们很容易被滥用。就我个人而言,我只在最简单的场景中使用参数。一个地方,他们已经很好地为我工作是在登录表单。如前所述,另一个场景是Master/Detail操作。
现在,你想看看真正邪恶的东西吗?把你的Xaml改成这样:
<UserControl x:Class="Caliburn.Micro.HelloParameters.ShellView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <StackPanel> <TextBox x:Name="Name" /> <Button x:Name="SayHello" Content="Click Me" /> </StackPanel> </UserControl>
运行应用程序将为您确认CM的约定甚至理解ActionMessage参数。我们将在以后更多地讨论约定,但是您应该高兴地知道,这些约定是不区分大小写的,甚至可以检测到前面提到的“特殊”值。
Acton冒泡
现在,让我们看一个简单的Master/Detail场景,它演示了ActionMessage冒泡,但是让我们用一种简洁的语法来实现它,这种语法被设计成对开发人员更友好。我们首先添加一个简单的新类Model:
1 using System; 2 3 public class Model 4 { 5 public Guid Id { get; set; } 6 }
然后我们把我们的ShellViewModel改成这样:
1 using System; 2 using System.ComponentModel.Composition; 3 4 [Export(typeof(IShell))] 5 public class ShellViewModel : IShell 6 { 7 public BindableCollection<Model> Items { get; private set; } 8 9 public ShellViewModel() 10 { 11 Items = new BindableCollection<Model>{ 12 new Model { Id = Guid.NewGuid() }, 13 new Model { Id = Guid.NewGuid() }, 14 new Model { Id = Guid.NewGuid() }, 15 new Model { Id = Guid.NewGuid() } 16 }; 17 } 18 19 public void Add() 20 { 21 Items.Add(new Model { Id = Guid.NewGuid() }); 22 } 23 24 public void Remove(Model child) 25 { 26 Items.Remove(child); 27 } 28 }
现在,我们的shell有了一个Model实例集合,并且能够在集合中添加或删除这些实例。注意,Remove方法接受一个Model类型的参数。现在,让我们更新ShellView:
1 <UserControl x:Class="Caliburn.Micro.BubblingAction.ShellView" 2 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 3 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 4 xmlns:cal="http://www.caliburnproject.org"> 5 <StackPanel> 6 <ItemsControl x:Name="Items"> 7 <ItemsControl.ItemTemplate> 8 <DataTemplate> 9 <StackPanel Orientation="Horizontal"> 10 <Button Content="Remove" 11 cal:Message.Attach="Remove($dataContext)" /> 12 <TextBlock Text="{Binding Id}" /> 13 </StackPanel> 14 </DataTemplate> 15 </ItemsControl.ItemTemplate> 16 </ItemsControl> 17 <Button Content="Add" 18 cal:Message.Attach="Add" /> 19 </StackPanel> 20 </UserControl>
Message.Attach
首先要注意的是,我们正在使用一种对xml开发人员更友好的机制来声明ActionMessages。Message.Attach属性由一个简单的解析器支持,该解析器接受其文本输入并将其转换为完整的交互。Trigger/ActionMessage,你已经看到了。如果你主要使用Xaml编辑器而不是设计器,你会喜欢Message.Attach。注意,Message.Attach指定应该发送消息的事件。如果不使用该事件,解析器将使用 ConventionManager 来确定要用于触发器的默认事件。对于Button,它是Click。如果我们声明所有内容,下面是Remove消息的完整语法:
<Button Content="Remove" cal:Message.Attach="[Event Click] = [Action Remove($dataContext)]" />
假设我们要用 Message.Attach 语法重写参数化的SayHello动作。它看起来是这样的:
1 <Button Content="Click Me" cal:Message.Attach="[Event Click] = [Action SayHello(Name.Text)]" />
你也可以将字面量指定为参数,甚至可以用分号分隔多个动作:
1 <Button Content="Let's Talk" cal:Message.Attach="[Event MouseEnter] = [Action Talk('Hello', Name.Text)]; [Event MouseLeave] = [Action Talk('Goodbye', Name.Text)]" />
警告
那些要求我将此功能扩展为成熟的表达式解析器的开发人员将被带回去……处理。Message.Attach不是把代码塞进Xaml。它的目的是提供一种简化的语法,用于声明要发送到ViewModel的 when/what 消息。请不要滥用这个。
如果还没有,请运行该应用程序。当您看到消息冒泡像广告那样工作时,希望您的任何疑虑都能得到解决:)我还想指出的是CM会自动对参数执行类型转换。例如,你可以抽取TextBox。将文本输入系统。双参数而不用担心类型转换问题。
所以,我们已经讨论了使用交互。带有ActionMessage的触发器,包括使用带有文字的参数、元素绑定和特殊值。我们已经讨论了根据您的需求/架构风格设置操作目标的各种方法:Action.Target, Action.TargetWithoutContext, Bind.Model 或者 View.Model。我们还看到了ActionMessage冒泡特性的一个例子,并使用简化的 Message.Attach 语法演示了它。一路走来,我们也看到了各种惯例的例子。现在,我们还没有讨论ActionMessage的最后一个杀手级特性——Coroutines(协程)。但是,那得等到下次了。

浙公网安备 33010602011771号