动作
我们在 配置 章节中只是简要的介绍了动作,其实还有很多地方是没有提及的。为了开始我们的研究,我们将以一个简单的 Hello
示例为例,看看当我们显式地创建动作而不是使用约定时是什么样子的:
<UserControl x:Class="Caliburn.Micro.Hello.ShellView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
xmlns:cal="http://www.caliburnproject.org">
<StackPanel>
<TextBox x:Name="Name" />
<Button Content="Click Me">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<cal:ActionMessage MethodName="SayHello" />
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
</StackPanel>
</UserControl>
如你所见,动作利用 System.Windows.Interactivity
为它的触发机制。这意味着你可以使用从 System.Windows.Interactivity.TriggerBase
继承的任何内容来触发一个 动作消息 (ActionMessage) 的发送。
也许最常见的触发器是 EventTrigger
。但是您几乎可以创建任何可以想到的触发器,或者利用社区已经创建的一些常见触发器。当然,ActionMessage
此标记是 Caliburn.Micro
特定部分。它表示当触发器发生时,我们应该发送 SayHello
消息。那么,为什么在描述这个功能时我使用的语言是 “发送消息” 而不是“执行方法”?这是有趣而有力的部分。ActionMessage
通过可视化树搜索能够处理它的目标实例。如果找到目标,但没有 SayHello
方法,框架将继续冒泡,直到找到为止,如果没有找到处理程序则抛出异常。
ActionMessage
的这种冒泡特性在许多有趣的场景中派上用场,主/细节是一个关键的用例。需要注意的另一个重要特性是 动作守卫。当找到 SayHello
消息的处理程序时,它将检查该类是否还有一个名为 CanSayHello
的属性或方法。如果你有一个守卫属性并且你的类实现了 INotifyPropertyChanged
,那么框架将会观察该属性的变化并相应地重新评估守卫。我们将在下面进一步详细讨论方法守卫。
动作目标
现在你可能想知道如何指定 ActionMessage
的目标。看看上面的标记,没有明显的迹象表明目标是什么。那么,它来自哪里?因为我们使用了 模型优先(Model-First) 的方法,当 Caliburn.Micro (以下简称 CM) 创建视图并使用 ViewModelBinder
将其绑定到 ViewModel
时,CM 为我们设置了它。通过 ViewModelBinder
的任何内容都将自动设置其操作目标。但是,你也可以使用附加属性 Action.Target
自己设置它。设置此属性会在附加到节点的可视化树中定位 ActionMessage
的处理程序,并声明该属性。它还将 DataContext
设置为相同的值,因为你通常希望这两者是相同的。但是,如果你愿意,可以从 DataContext
更改 Action.Target
。或仅使用 Action.TargetWithoutContext
附加属性即可。 Action.Target
的一个好处是你可以将它设置为 System.String
,CM 将使用该字符串来解析来自 IoC 容器的实例,使用提供的值作为其键。如果你愿意,这为你提供了一种很好的 视图优先 (View-First) MVVM 方法。 如果你想要设置 Action.Target
并且您也希望应用 动作/绑定约定,则可以以相同的方式使用 Bind.Model
附加属性。
视图优先 (View First)
让我们看看如何使用视图优先技术来实现 MVVM。下面是我们如何改变引导程序:
public class MefBootstrapper : BootstrapperBase
{
//same as before
protected override void OnStartup(object sender, StartupEventArgs e)
{
Application.RootVisual = new ShellView();
}
//same as before
}
因为我们使用的是视图优先,所以我们继承了非通用的 Bootstrapper。 MEF 配置与之前看到的相同,所以为了简洁起见,我把它留了下来。 唯一改变的是视图的创建方式。 在这种情况下,我们只是重写 OnStartup
,自己实例化视图并将其设置为 RootVisual
(在WPF的情况下调用Show)。 接下来,我们将通过添加一个明确命名的契约来稍微改变我们导出 ShellViewModel
的方式:
[Export("Shell", typeof(IShell))]
public class ShellViewModel : PropertyChangedBase, IShell
{
//same as before
}
最后,我们将改变我们的视图以引入 VM 并执行所有绑定:
<UserControl x:Class="Caliburn.Micro.ViewFirst.ShellView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cal="http://www.caliburnproject.org"
cal:Bind.Model="Shell">
<StackPanel>
<TextBox x:Name="Name" />
<Button x:Name="SayHello"
Content="Click Me" />
</StackPanel>
</UserControl>
注意使用 Bind.Model
附加属性。 这将通过 IoC 容器中的密钥解析我们的 VM,设置 Action.Target
和 DataContext
并应用所有约定。我认为展示 CM 如何完全支持视图优先开发是很好的,但是我主要想要阐明你可以为动作设置目标的各种方法以及使用每种技术的含义。 以下是可用附加属性的摘要:
Action.Target
指处理动作消息的目标实例,通常是视图模型实例
Action.Target:将 Action.Target
属性和 DataContext
属性都设置为指定的实例。字符串值用于解析 IoC 容器中的实例。
Action.TargetWithoutContext:仅将 Action.Target
属性设置为指定的实例。字符串值用于解析 IoC 容器中的实例。
Bind.Model:视图优先 - 将 Action.Target
和 DataContext
属性设置为指定的实例。将约定应用于视图。 字符串值用于解析 IoC 容器中的实例。(用于根节点,如 Window/UserControl/Page)。字符串值在 3.2及以上版本被删除
Bind.ModelWithoutContext:视图优先 - 将 Action.Target
设置为指定的实例。将约定应用于视图。(在DataTemplate中使用)
View.Model:视图模型优先 - 找到指定 VM 实例的视图,并将其注入内容。 将 VM 设置为 Action.Target
和 DataContext
。将约定应用于视图。
动作参数
现在,我们来看看 ActionMessage
的另一个有趣的方面:参数。 要看到这一点,让我们切换回我们原来的 视图模型优先(ViewModel-First) 引导程序等等,然后将 ShellViewModel
更改为如下所示:
using System.ComponentModel.Composition;
using System.Windows;
[Export(typeof(IShell))]
public class ShellViewModel : IShell
{
public bool CanSayHello(string name)
{
return !string.IsNullOrWhiteSpace(name);
}
public void SayHello(string name)
{
MessageBox.Show(string.Format("Hello {0}!", name));
}
}
这里有一些需要注意的事情。首先,我们现在使用的是完全 POCO 类,这里没有 INPC。其次,我们在 SayHello
方法中添加了一个输入参数。最后,我们将 CanSayHello
属性更改为具有与操作相同输入的方法,但具有 bool
返回类型。现在,让我们来看看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"
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
xmlns:cal="http://www.caliburnproject.org">
<StackPanel>
<TextBox x:Name="Name" />
<Button Content="Click Me">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<cal:ActionMessage MethodName="SayHello">
<cal:Parameter Value="{Binding ElementName=Name, Path=Text}" />
</cal:ActionMessage>
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
</StackPanel>
</UserControl>
我们的标记现在有了一个修改:我们使用 ElementName
绑定将参数声明为 ActionMessage
的一部分。 值是依赖属性,因此所有标准绑定功能都适用于参数。我提到你可以在 Blend 中完成所有这些吗?
这样做的一个好处是,每次参数值发生变化时,我们都会调用与操作关联的守卫方法 (在本例中为CanSayHello),并使用其结果更新 ActionMessage
所附加的 UI。 继续运行应用程序。 你将看到它的行为与之前的示例相同。
除了文字值和绑定表达式之外,还有许多有用的“特殊”值可用于参数。这些允许您方便地访问常见的上下文信息:
$eventArgs:将 EventArgs
或输入参数传递给动作。 注意:对于守卫方法,这将为null,因为触发器实际上没有发生。
$dataContext:传递 ActionMessage
附加到元素的 DataContext
。 这在主/细节场景中非常有用,其中 ActionMessage
可能会冒泡到父视图模型,但需要携带要执行操作的子实例。
$source:触发 ActionMessage
发送的实际元素。
$view:绑定到 ViewModel
的视图 (通常是UserControl或Window)。
$executionContext:动作的执行上下文,其中包含上述所有信息和更多信息。这在高级场景中非常有用。
你必须以 $ 开头,但 CM 会以不区分大小写的方式处理该名称。可以通过向 MessageBinder.SpecialValues
添加值来扩展它们。
注意: 使用特殊值,如 $this 或命名元素
当不指定属性时,CM 使用默认属性,这是由特定的控件约定指定的。对于按钮,该属性恰好是DataContext
,而 TextBox
默认为 Text
,Selector
为 SelectedItem
等。在视图中使用对另一个命名控件的引用而不是 $this
时也会发生同样的情况。
以下内容:使 CM 将名为 someTextBox
的 TextBox
中包含的 Text
传递给 MyAction
。实际的控件从未传递给操作的原因是 VM 不应该直接处理 UI 元素,因此约定不允许这样做。但请注意,无论如何都可以使用扩展语法 (基于 System.Windows.Interactivity
) 轻松访问控件本身以填充参数或自定义解析器。
枚举值
如果要将枚举值作为参数传递,则需要将值作为 (大写) 字符串传递:
...
<Fluent:Button Header="Go!" cal:Message.Attach="[Event Click] = [Action MethodWithEnum('MONKEY')]" />
public enum Animals
{
Unicorn,
Monkey,
Dog
}
public class MyViewModel
{
pubilc void MethodWithEnum(Animals a)
{
Animals myAnimal = a;
}
}
智者说
参数是一种便利功能。 它们非常强大,可以帮助你摆脱一些棘手的问题,但它们很容易被滥用。 就个人而言,我只在最简单的场景中使用参数。 其中一个对我来说很好的地方是登录表单。如前所述,另一种场景是主/细节操作。
现在,你想看到真正淘气的东西吗? 将您的 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
参数。我们将在以后更多地讨论约定。但你应该很高兴知道这些约定不区分大小写,甚至可以检测前面提到的 特殊 值。
动作冒泡
现在,让我们看一个演示 ActionMessage
冒泡的简单主/细节场景,但是让我们用一种简化的语法来做,这种语法设计更适合开发人员。我们首先添加一个名为 Model
的新类:
using System;
public class Model
{
public Guid Id { get; set; }
}
然后我们将 ShellViewModel
更改为:
using System;
using System.ComponentModel.Composition;
[Export(typeof(IShell))]
public class ShellViewModel : IShell
{
public BindableCollection<Model> Items { get; private set; }
public ShellViewModel()
{
Items = new BindableCollection<Model>{
new Model { Id = Guid.NewGuid() },
new Model { Id = Guid.NewGuid() },
new Model { Id = Guid.NewGuid() },
new Model { Id = Guid.NewGuid() }
};
}
public void Add()
{
Items.Add(new Model { Id = Guid.NewGuid() });
}
public void Remove(Model child)
{
Items.Remove(child);
}
}
现在,我们的 shell 拥有一组模型实例,以及从集合中添加或删除的功能。注意:Remove
方法只接受一个类型为 Model
的参数。现在,让我们更新 ShellView
:
<UserControl x:Class="Caliburn.Micro.BubblingAction.ShellView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cal="http://www.caliburnproject.org">
<StackPanel>
<ItemsControl x:Name="Items">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Button Content="Remove"
cal:Message.Attach="Remove($dataContext)" />
<TextBlock Text="{Binding Id}" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Button Content="Add"
cal:Message.Attach="Add" />
</StackPanel>
</UserControl>
消息附加 (Message.Attach)
首先要注意的是,我们使用了一种对 XAML 开发人员更友好的机制来声明 ActionMessage
。 Message.Attach
属性由一个简单的解析器支持,该解析器接受文本输入并将其转换为完整的交互。你以前见过的 Interaction.Trigger/ActionMessage
。 如果你主要在 XAML 编辑器中而不是在设计器中工作,那么你将会喜欢 Message.Attach
。请注意,Message.Attach
声明都没有指定应该发送消息的事件,如果你省略了事件,解析器将使 ConventionManager 确定用于触发器的默认事件。在 Button
的情况下,它是 Click
。 你总是可以明确省略。如果我们要声明所有内容,那么删除消息的完整语法如下:
<Button Content="Remove" cal:Message.Attach="[Event Click] = [Action Remove($dataContext)]" />
假设我们使用 Message.Attach
语法重新编写参数化的 SayHello
动作。 它看起来像这样:
<Button Content="Click Me" cal:Message.Attach="[Event Click] = [Action SayHello(Name.Text)]" />
但是我们也可以利用解析器的一些智能默认值,然后这样做:
<Button Content="Click Me" cal:Message.Attach="SayHello(Name)" />
你也可以将文字指定为参数,甚至通过分号分隔它们来声明多个动作:
<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
。 请不要滥用这个。
如果你还没有运行应用程序,请运行该应用程序。当你看到消息冒泡如通知那样工作时,希望你的任何疑问都能得到解决 ?
我还想指出的是 CM 会自动对参数执行类型转换。例如,你可以将 TextBox.Text
里输入 System.Double
参数,而不必担心出现转换问题。
因此,我们讨论了将 Interaction.Triggers
与 ActionMessage
一起使用,包括使用带文字的参数、元素绑定和特殊值。我们已经讨论了根据您的需求/架构风格设置动作目标的各种方法:Action.Target
、Action.TargetWithoutContext
、Bind.Model
和 View.Model
。
我们还看到了 ActionMessage
的冒泡性质的示例,并使用简化的 Message.Attach
语法对其进行了演示。在此过程中,我们还观察了各种实践中的约定示例。
现在,ActionMessage
还有一个最后的杀手级功能我们还没有讨论过… 协同程序 (Coroutines) 。 但是,这将不得不等到下一次。