65.9K
CodeProject 正在变化。 阅读更多。
Home

CCCR:上下文命令、转换器和规则

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2011年4月26日

CPOL

12分钟阅读

viewsIcon

20017

downloadIcon

240

本文提供了一种统一的方法来实现用户命令、转换器和验证规则。那些非常好奇的人可以直接跳到“演示”章节和源代码(附带原始库的演示版本)。

目录 

命名约定

CCCR上下文命令、转换器和验证规则
CCR命令、转换器和验证规则(比CCCR的含义更广)
AlphaCCCR功能实体类别,在主应用程序代码中初始化。
贝塔CCCR功能实体类别,在XAML代码中初始化。

本文应用了实体命名规则,即更一般的概念放在比其不那么一般但更具体的概念之前。例如,有一个接口 `IInvoker`,它被抽象类 `InvokerBase` 部分实现;然后,从中生成类 `InvokerAlpha`;在同一层次结构级别上,有一个生成的类组 `InvokerBetaCS`、`InvokerBetaJS`、`InvokerBetaFS`、`InvokerBetaVB`、`InvokerBetaCSLambda`。这种方法使得能够以自然的方式显示实体列表,并根据通用概念的深度进行分组——根据含义。

Designations. Picture 1.

引言

本文的读者,如果曾从事WPF应用程序的构建,都会遇到实现三个重要组件的需求:命令、转换器和验证规则。通常(当然,并非总是如此),解决任务所需的组件非常特定,以至于实现的可重用性要么不可能,要么需要复杂的不自然泛化。此外,在N层应用程序模型中的任何一个级别上,通常只需要创建一级的逻辑。在后一种情况下,将此逻辑转移到应用程序模型的另一个级别上实现,会模糊源代码,并带来所有后果。这个想法的本质不是每次为新道路都要重新发明轮子,再次产生狭窄的专用类,而是,使用相同的带轮子的术语,能够根据地形和要求更换轮子。这是一种声明式编程的哲学,当多态性让位于函数式编程工具时。

CCCR的原则

正如之前提到的,上下文方法的最终目标是为开发人员提供一种一致的机制来处理CCR三联体(CCR, -)的实体,这种机制既适用于用户界面的XAML标记,也适用于源代码本身。这就是策略。在战术上,解决了以下任务:易用性、可扩展性、声明性、可移植性。这指的是功能丰富但资源受限(*“姹紫不嫣红”*,安德烈·卢布廖夫语)。这也指在无需更改基本代码的情况下进一步详细阐述的可能性。这也至少在一般意义上指保留声明性方法。最后,假设它独立于编程语言,并且能够将CCCR应用程序的代码(几乎不做更改)从XAML转移到主代码,反之亦然。

实现

本节的主题是关于在最终用户使用CCCR的上下文中对其实现。

上下文

不同类型的任务需要不同的输入和输出数据以及不同的行为脚本。如果对于命令,需要实现 `ICommand` 接口及其两个方法 `CanExecute`、`Execute` 和 `CanExecuteChanged` 事件,那么对于转换器,需要实现 `IValueConverter`、`IMultiValueConverter` 接口及其 `Convert` 和 `ConvertBack` 方法。反过来,验证规则必须实现 `ValidationRule` 抽象类的 `Validate` 方法。不言而喻,每个方法都有自己的一组参数。将它们联系起来的唯一一点是,任何组件都有一些执行上下文,具有输入和输出数据数量以及附加参数化。实现的基础已经奠定!正如在算术中,当总数被分解时,我们的情况也是如此,总数据和参数化被提取到 `IContex` 接口中,约束其后代实现它们的成员:对象数组 `In`、`Out`,以及对象属性 `Parameter` 和 `Source`。单独实体的特异性保留在括号中(见上文)。

下图显示了 `ContextCommand`、`ContextConverter`、`ContextRule` 类的图。

Contexts. Picture 2.1. Contexts. Picture 2.2.

通过以这种方式指定上下文,我们有机会将CCCR基本接口/类的虚拟方法调用(命令的 `ICommand`,转换器的 `IValueConverter`、`IMultiValueConverter`,验证规则的 `ValidationRule`)转换为任何一个类方法的调用,该类承诺执行特定类型的功能。我们称之为`Invoker`(`IInvoker`)的类,是这些类的基本接口,它描述了在理解CCCR概念的两个关键成员:类型为 `TContex`(从 `IContext` 生成)的 `Context` 属性,以及 `Invoke` 方法。这些成员的名称足以自明:`Context` - 调用抽象方法 `Invoke` 的当前上下文。由于方法调用可能伴随着异常的发出,`IInvoker` 接口是从 `IExceptor` 接口生成的,具有 `LastException` 属性(调用方法时发生的最后一个异常)和 `NoException` 布尔类型,表示在拦截异常时是否需要重传异常。除了Invoker本身的接口(`IInvoker`)之外,每个CCCR实体都应该实现类似的异常处理机制。接下来,本文将讨论这两种不同类别的实体:`Alpha` 和 `Beta`。在此,我们仅限于此,即这些类别与 `ICCCRAlpha` 和 `ICCCRBeta` 接口相关联。下图显示了这些接口的依赖图。

Contexts. Picture 3.1. Contexts. Picture 3.2.

分类

在XAML标记和主代码这两种不同的环境中都使用CCCR的愿望,是由将功能划分为两个类别的需求决定的,其中一个类别负责在主代码中创建CCCR实体,另一个类别负责在XAML中创建。文章作者的希腊血统以及实现对Lambda表达式的某些确定性,促使作者想到使用希腊字母来指代这些类别。然而,所有艺术家都倾向于将他们的特征赋予所描绘的人物。因此,`Alfa` 类别是第一种(主代码)的类别,`Beta` 类别是第二种(XAML)的功能类别。`Alpha` 类别的实体接受 `Action` 委托的输入(`ICCCRAlpha.Function` 属性)。`Beta` 类别的实体接受代码字符串的输入(`ICCCRBeta.Code` 属性)。在当前的实现中,可以用来编写代码的语言是C#、VB.Net、JScript、F#,以及一些有限的C# Lambda表达式语法。显然,第一种类别的类别将在最终用户的用法中占主导地位。这种逻辑也体现在类别的标题中:`Alpha` 字母先于 `Beta` 字母。

下图显示了两个类别实体的依赖图。

Categories. Picture 4.1. Categories. Picture 4.2. Categories. Picture 4.3.

调用者(Invokers)

层次结构很简单。大部分类指的是 `Beta` 类别,这取决于语言差异的实现。可以不用CCCR的最终(具体)实体类,因为它们只是实例化CCCR的泛型类,但首先,XAML不“理解”泛型,其次,存在缩写类型显著简化了代码。代码最重要的部分集中在CCCR的基类和`Invoker`类中。以下图表显示了CCCR实体与`Invoker`类之间的弱联系(关系由箭头表示,正如可以看到的,所有关系都通过接口进行中介)。

Invokers. Picture 5.1. Invokers. Picture 5.2.

`Invoker`将得到更详细的检查。它们的作用在于以某种方式解释外部关于某个方法的信息。顺便说一句,这不一定来自CCCR实体,但原则上,只是来自代码。用户(广义上)a) 创建一个`Invoker`,指定上下文的类型,b) 初始化上下文,c) 调用`Invoke`方法。技术上,`Invoke` 方法是著名的`设计模式`的公共*模板方法*,其背后有一个抽象的受保护方法`_Invoke`。这是抽象类`InvokerBase`的简要实现。`Alpha`和`Beta`类别提供了具体性。这样,`InvokerAlpha`类的`Invoke`方法仅调用传递给它的委托。`InvokerBetaBase`类的相同方法已经被密封,通过该方法中断了其后代类行为的多态差异。但这并没有结束`InvokerBetaBase`的多态性,而是获得了新的发展,通过新的抽象`CreateFunction`方法,该方法返回`Action`委托。这种机制允许我们只计算一次委托,从那时起只调用已经收到的(缓存的)委托。当与`IInvoker.Code`字符串属性关联的代码发生变化时,会发生委托的替换。

以下三张图显示了`IInvoker`后继者的层次结构以及上述`Invoke`方法的运行机制。

Invokers. Picture 6.1. Invokers. Picture 6.2. Invokers. Picture 6.3. Invokers. Picture 6.4.
Invokers. Picture 7.1. Invokers. Picture 7.2.

ICCCRAlpha和ICCCRBeta接口的实现显示在以下调用序列图中。

Invokers. Picture 8. Invokers. Picture 9.

CCCR

本文前面描述了在`Context`-`Invoker`绑定中实现CCCR的机制。接下来,将重点关注更简单的`Invoker`-`CCCR`绑定,以及整个关于CCCR实现的推理链。幸运的是,这里可以有一个简洁的陈述——“上下文”部分已经触及了每个CCCR实体的具体细节。因此,任何CCCR实体都包含`Invoker`属性。基本命令类`CommandBase`将其通过调用重写的`CanExecute`和`Execute`方法获得输入数据打包到其`Invoker`上下文的`IContext.In`对象数组中,并调用`Invoke`方法。转换器和验证规则也发生类似的事情。这里将提到`ConverterBase`和`RuleBase`类。对于前者,调用上下文由`Convert`和`ConvertBack`方法决定,对于后者,由`Validate`方法决定。再次,输入数据被打包到对象数组中,调用`IInvoker.Invoke`方法,然后将`Invoker.Out`对象数组作为输出传递给`Convert`、`ConvertBack`和`Validate`方法。

命令类、转换器类和验证规则类的层次结构如下图所示。

CCCR. Picture 10.1. CCCR. Picture 10.2.
CCCR. Picture 11.1. CCCR. Picture 11.2.
CCCR. Picture 12.1. CCCR. Picture 12.2.

比较分析

下表显示了`Invoker`现有实现的质量特征——CCCR类本身只是它们的“客户端”,对性能和逻辑没有影响。

调用者(Invokers) 描述
Alpha 性能:最大(5)
缓存:不需要
系统资源要求:最小(1)
用法:仅主代码

Beta JScript 性能:令人满意(3)
缓存:当前实现中不可能(如果拒绝`Eval`方法并转向使用`JScriptCodeDomProvider`,则情况类似C#和VB.Net)
系统资源要求:低(2)
用法:代码、XAML

Beta C# 性能:良好(4),调用缓存函数时
缓存:已实现,等待时间令人满意(3)
系统资源要求:高(4),一个委托 – 一个程序集
用法:代码、XAML

Beta C# Lambda 性能:良好(4),调用缓存函数时
缓存:已实现,等待时间良好(4)
系统资源要求:低(2)
用法:代码、XAML 限制:操作仅可能对预定义类型(基本类型、`Math`、`Convert`)

Beta VB.Net 与Beta C#类似

Beta F# 性能:令人满意(3),调用缓存函数时
缓存:已实现,等待时间良好(2)
系统资源要求:高(4),一个委托 – 一个程序集
用法:代码、XAML

该表提供了理论数据,因此可能与实际结果存在一些差异。

前景

为了提高Beta C#、Beta VB.Net、Beta F#调用者的生产力,可以实现XAML代码的预加载,以便缓存生成的委托并为所有委托生成一个程序集(在此实现中,创建单独的委托会导致在计算机内存中创建单独的程序集)。在这种情况下,这类程序集的生成只需要在应用程序(工作)的整个过程中进行一次,并且在开发人员端,因此性能问题可以一劳永逸地解决!

演示

也许文章中最有趣的部分是代码示例,实际上,正是为了这个相当庞大的工作才开始了。

下面是XAML代码片段。它指定了`CommandBetaFS`——一个用F#语言编写的命令,它启动一个进程,该进程的名称由`CommandParameter`定义。需要提醒的是,在`CommandParameter`的上下文中,`ContextCommand`获得了一个新名称`Context.Parameter`。考虑到`Context.Parameter`是对象类型,不要忘记将其转换为正确的类型(在本例中是`ToString`)。F#语言对缩进敏感,因此命令脚本放在`xml:space`字段定义值为“preserve”的`string`标签中。

<Button
  HorizontalContentAlignment="Left"
  Content="XAML / CommandBetaFS : Run Notepad"
  CommandParameter="notepad.exe"
  >
  <Button.Command>
    <CCCR:CommandBetaFS
      AlwaysCanExecute="True">
        <CCCR:CommandBetaFS.Code>
          <system:String xml:space="preserve">
            let s = Context.Parameter.ToString()
            System.Diagnostics.Process.Start(s)
          </system:String>
        </CCCR:CommandBetaFS.Code>
      </CCCR:CommandBetaFS>
   </Button.Command>
 </Button>
下面是C#语言编写的主应用程序代码片段。这里我们使用`CommandAlpha`类的构造函数,并将`Action`委托作为参数。如果命令参数为空,则会提供一个固定的字符串,然后将其传递给`MessageBox.Show`方法。值得注意的是,传递给`Alpha`类别CCCR构造函数的代码,可以完全不变地用作传递给`Beta`类别CCCR构造函数脚本的字符串。
new CommandAlpha (
  Context => {
    if (Context.GetCanExecute)
      Context.CanExecute = true;
    else
      MessageBox.Show(
        (Context.Parameter ?? "Alpha1-(NO PARAMs)").ToString()); } );
下面的XAML标记片段指定了`ConverterBetaCSLambda`转换器。在直接转换的情况下,输入会加上“Forward: ”前缀;在反向转换的情况下,会使用另一个前缀“Back: ”。
<DockPanel HorizontalAlignment="Stretch">
  <TextBlock Width="50">Target:</TextBlock>
  <TextBox x:Name="m_tTxt1"
    Text="{Binding
      ElementName=m_tTxt2,
      Path=Text,
      UpdateSourceTrigger=PropertyChanged,
      Converter={CCCR:ConverterBetaCSLambda Code=
        '(Context.IsBack ? &quot;Back: &quot; : &quot;Forward: &quot;) + (Context.In[0]).ToString()',
        NoExceptions=False}}"
      />
</DockPanel>
<DockPanel HorizontalAlignment="Stretch">
  <TextBlock Width="50">Source:</TextBlock>
  <TextBox x:Name="m_tTxt2"></TextBox>
</DockPanel>
以下是演示应用程序的快照,这里提供了更完整的XAML标记代码片段。

Demonstration. Picture 13.

C#

    private static CommandAlpha m_tCommandAlpha1;
    public static CommandAlpha CommandAlpha1 {
      get {
        if (m_tCommandAlpha1 == null)
          m_tCommandAlpha1 =
// Creating a command CommandAlpha, which to the request CanExecute always returns True,
// and displays a dialog box with the message given by parameter CommandParameter.
            new CommandAlpha (
              Context => {
                if (Context.GetCanExecute)
                  Context.CanExecute = true;
                else
                  MessageBox.Show(
                    (Context.Parameter ?? "Alpha1-(NO PARAMs)").ToString()); } );
        return m_tCommandAlpha1; } }

    private static CommandAlpha m_tCommandAlpha2;
    public static CommandAlpha CommandAlpha2 {
      get {
        if (m_tCommandAlpha2 == null)
// Creating a command CommandAlpha as above, but in a slightly different syntax.
          m_tCommandAlpha2 =
            new CommandAlpha (Context =>
              MessageBox.Show(!String.IsNullOrWhiteSpace(
               (Context.Parameter ?? "").ToString()) ?
                 Context.Parameter.ToString() : "Alpha2-(NO PARAMs)"))
                   { AlwaysCanExecute = true };
        return m_tCommandAlpha2; } }

    private static CommandBetaJS m_tCommandBetaJS1;
    public static CommandBetaJS CommandBetaJS1 {
      get {
        if (m_tCommandBetaJS1 == null)
// Creating of a command CommandBetaJS as before,
// but here a script is used instead of lambda expressions.
          m_tCommandBetaJS1 = 
            new CommandBetaJS (
              @"MessageBox.Show(!String.IsNullOrWhiteSpace(Context.Parameter) ?
                Context.Parameter : ""Beta1-(NO PARAMs)"")") { AlwaysCanExecute = true };
        return m_tCommandBetaJS1; } }

XAML

  <Window.Resources>
<!--
An entry into the window resource of simple command which displays a dialog box 
Request CanExecute is not served when the program starts, as
parameter AlwaysCanExecute is initialized in the "True".
-->
    <CCCR:CommandBetaJS
      x:Key="x_tCmd_MsgBox"
      Code="MessageBox.Show(Context.Parameter)"
      AlwaysCanExecute="True">
    </CCCR:CommandBetaJS>

<!--
An entry of ConverterBetaJS converter into the window resource
to convert String (in) to Integer and vice versa.
-->
    <CCCR:ConverterBetaJS x:Key="x_tCnv_JS_0_100" NoExceptions="True">
    <CCCR:ConverterBetaJS.Code>
      var c : ContextConverter = Context;
      if (!c.IsBack) {
        var n = int(c.In[0]);
        c.Out[0] = n.ToString(); }
      else {
        var n = Int32.Parse(c.In[0]);
        c.Out[0] = n; }
    </CCCR:ConverterBetaJS.Code>
    </CCCR:ConverterBetaJS>

<!--
This converter ConverterBetaCS is similar to the previous, but additionally
checks the CommandParameter and number on the membership
to the range of values [0, 100].
-->

    <CCCR:ConverterBetaCS x:Key="x_tCnv_CS_0_100" NoExceptions="False">
      <CCCR:ConverterBetaCS.Code>
        try {
          var c = Context;
          if (c.IsBack == ((c.Parameter ?? "").ToString() == "Slider")) {
            var n = Convert.ToInt32(c.In[0]);
            c.Out[0] = n.ToString(); }
          else {
            var s = (c.In[0] ?? "0").ToString().Trim();
            int n = 0;
            Int32.TryParse(s, out n);
            if (0 &gt; n || n &gt; 100)
              throw new Exception("Out of range [0; 100]!");
            c.Out[0] = n; } }
          catch (Exception e) {
            /*MessageBox.Show(e.Message);*/ }
      </CCCR:ConverterBetaCS.Code>
    </CCCR:ConverterBetaCS>

<!--
Style definition TextBox, to display Tooltips when there are validation errors
-->
   <Style x:Key="{x:Type TextBox}" TargetType="{x:Type TextBox}">
      <Style.Triggers>
        <Trigger Property="Validation.HasError" Value="True">
          <Setter Property="ToolTip"
            Value="{Binding RelativeSource={RelativeSource Self},
            Path=(Validation.Errors)[0].ErrorContent}"/>
          <Setter Property="Background" Value="Red"></Setter>
        </Trigger>
      </Style.Triggers>
    </Style>
    <Style x:Key="{x:Type Slider}" TargetType="{x:Type Slider}">
      <Style.Triggers>
        <Trigger Property="Validation.HasError" Value="True">
          <Setter Property="ToolTip"
            Value="{Binding RelativeSource={RelativeSource Self},
            Path=(Validation.Errors)[0].ErrorContent}"/>
          <Setter Property="Background" Value="Red"></Setter>
        </Trigger>
        <EventTrigger RoutedEvent="ValueChanged">
        </EventTrigger>
      </Style.Triggers>
    </Style>
  </Window.Resources>
  <Window.InputBindings>

<!--Binding commands to hotkeys.-->
    <KeyBinding Key="F1" Command="{x:Static local:WinMain.CommandAlpha1}"/>
    <KeyBinding Key="F2" Modifiers="Alt"  Command="{x:Static local:WinMain.CommandAlpha2}"/>
    <KeyBinding Key="F3" Command="{x:Static local:WinMain.CommandBetaJS1}"/>
    <KeyBinding Key="F4" Modifiers="Control"
      Command="{CCCR:CommandBetaVB 'System.Diagnostics.Process.Start((Context.Parameter))',
      AlwaysCanExecute=True, NoExceptions=True }"
      CommandParameter="calc.exe"/>
    <KeyBinding Key="F5"
      Command="{StaticResource x_tCmd_MsgBox}"
      CommandParameter="You pushed F5 key."/>
  </Window.InputBindings>

  <Grid Margin="10,10,10,10" >
    <StackPanel>
      <TextBlock TextWrapping="Wrap">
        The functionality except the one
      of the first three buttons on the left side is fully implemented in XAML.
      </TextBlock>
      <TextBlock TextWrapping="Wrap">
      That became possible by means of an anproach named
      <TextBlock Text="CCCR" FontWeight="Bold" /> (Context Commands, Converters and Rules)
      </TextBlock>

      <Grid Margin="0,6,0,0">
        <Grid.ColumnDefinitions>
          <ColumnDefinition Width="*"/>
          <ColumnDefinition Width="4"/>
          <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>

      <StackPanel Grid.Column="0">
        <TextBlock Margin="0,0,0,10"
        Background="{StaticResource {x:Static SystemColors.ControlLightBrushKey}}">
        Hotkeys: F1, Alt+F2, F3, Ctrl+F4, F5
        </TextBlock>

<!--Binding a command of Alpha category (initialized in the main code)
to the button's Click event.-->
        <Button
          x:Name="m_tBtnAlpha1"
          HorizontalContentAlignment="Left"
          Content="Code / CommandAlpha : Show Message Box"
          CommandParameter="'This string is a parameter'"
          Command="{x:Static local:WinMain.CommandAlpha1}"
        />

<!--Binding a command of Alpha category (initialized in the main code)
to the button's Click event.-->
      <Button
        x:Name="m_tBtnAlpha2"
        HorizontalContentAlignment="Left"
        Content="Code / CommandAlpha : Show -//- (Shorter syntax)"
        CommandParameter="    "
        Command="{x:Static local:WinMain.CommandAlpha2}"
        />

<!--Binding a command of Beta category (initialized in the main code)
to the button's Click event.-->
      <Button
        x:Name="m_tBtnBetaJS1"
        HorizontalContentAlignment="Left"
        Content="Code / CommandBetaJS : Show -//-//-"
        CommandParameter="    "
        Command="{x:Static local:WinMain.CommandBetaJS1}"
        />


<!--Binding a command of Beta category (initialized in the window resources)
to the button's Click event.-->
      <Button
        HorizontalContentAlignment="Left"
        Content="XAML / CommandBetaJS : Show Message Box"
        Command="{StaticResource x_tCmd_MsgBox}"
        CommandParameter="Command Parameter ABRACADABRA"
        >
          </Button>

<!--
By the button's Click event a command in F# language is bound.
Pay attention to the use of the markup <system:String xml:space="preserve">
owing to the need to preserve the indentation from the left edge.
-->
      <Button
        HorizontalContentAlignment="Left"
        Content="XAML / CommandBetaFS : Run Notepad"
        CommandParameter="notepad.exe"
        >
        <Button.Command>
          <CCCR:CommandBetaFS
            AlwaysCanExecute="True">
              <CCCR:CommandBetaFS.Code>
                <system:String xml:space="preserve">
                  let s = Context.Parameter.ToString()
                  System.Diagnostics.Process.Start(s)
                </system:String>
              </CCCR:CommandBetaFS.Code>
            </CCCR:CommandBetaFS>
        </Button.Command>
      </Button>

<!--
By the button's Click event a command in VB.Net language is bound.
The launch of Calculator program is going.
-->
     <Button
        HorizontalContentAlignment="Left"
        Content="XAML / CommandBetaVB : Run Calculator"
        Command="{CCCR:CommandBetaVB 'System.Diagnostics.Process.Start(Context.Parameter.ToString())',
          AlwaysCanExecute=True, NoExceptions=True }"
        CommandParameter="calc.exe"
      />
          
<!--
By the button's Click event a command in the language of JS is bound. 
Error handling is demonstrated. Having set NoExceptions in False,
arising exceptions (Invoker) are passed further on to the stack calls.
Information about this exeption is also available in the property LastException.
-->
      <Button
            HorizontalContentAlignment="Left"
            Content="XAML / CommandBetaJS : NoExceptions=False"
            Command="{CCCR:CommandBetaJS 'fjh@#rt(y85h%$#93;',
              AlwaysCanExecute=True, NoExceptions=False }"
            CommandParameter="notepad.exe"
          />

<!--
By the button's Click event command in the language of JS is bound.
Error handling is demonstrated. Having installed NoExeptions in True,
arising exceptions in Invoker are intercepted, a recently emerged
exception is available in the property LastException.
-->
          <Button
            HorizontalContentAlignment="Left"
            Content="XAML / CommandBetaJS : NoExceptions=True"
            Command="{CCCR:CommandBetaJS 'fjh@#rt(y85h%$#9[u43',
              AlwaysCanExecute=True, NoExceptions=True }"
            CommandParameter="notepad.exe"
          />

<!--
By the button's Click event command in the language of VB is bound. 
Binding of a reference to the window to CommandParameter is Demonstrated.
The command closes the tied window.
-->
      <Button
          HorizontalContentAlignment="Left"
          Content="XAML / CommandBetaVB : Close Window"
          Command="{CCCR:CommandBetaVB 'Context.Parameter.Close()',
            AlwaysCanExecute=True, NoExceptions=False}"
          CommandParameter=
            "{Binding RelativeSource=
              {RelativeSource FindAncestor,
              AncestorType=local:WinMain, AncestorLevel=1}}"
          />

<!--
Command in JScript language is similar to the previous.
AlwaysCanExecute parameter was not defined, so the script
contains code to initialize ContextCommand.CanExecute
-->
      <Button
          HorizontalContentAlignment="Left"
          Content="XAML / CommandBetaJS : Close Window"
          CommandParameter=
            "{Binding RelativeSource=
             {RelativeSource FindAncestor,
             AncestorType=local:WinMain, AncestorLevel=1}}"
        >
        <Button.Command>
          <CCCR:CommandBetaJS>
            <CCCR:CommandBetaJS.Code>
              var v = Context;
                if (v.GetCanExecute)
                  v.CanExecute = true;
                else
                  v.Parameter.Close();
            </CCCR:CommandBetaJS.Code>
          </CCCR:CommandBetaJS>
        </Button.Command>
      </Button>

<!--
Command is in limited syntax of lambda expressions of C#.
Demonstrates the impossibility of invocation for the types not included in mscorlib.dll
-->
      <Button
          HorizontalContentAlignment="Left"
          Content="XAML / CommandBetaCSLambda : Close... (Exception)"
          Command="{CCCR:CommandBetaCSLambda 'w =&gt; w.CloseWindow()',
            AlwaysCanExecute=True, NoExceptions=False}"
          CommandParameter=
            "{Binding RelativeSource=
              {RelativeSource FindAncestor,
              AncestorType=local:WinMain, AncestorLevel=1}}"
        />

          <TextBlock Margin="0,10,0,0"
        Background="{StaticResource {x:Static SystemColors.ControlLightBrushKey}}">
        ConverterBetaCSLambda: Source &lt;-&gt; Target</TextBlock>
      <DockPanel HorizontalAlignment="Stretch">
        <TextBlock Width="50">Target:</TextBlock>
<!--
Converter in a limited syntax of lambda expressions in C#.
Demonstrates a possibility of a bidirectional exchange.
-->
        <TextBox x:Name="m_tTxt1"
          Text="{Binding
            ElementName=m_tTxt2,
            Path=Text,
            UpdateSourceTrigger=PropertyChanged,
            Converter={CCCR:ConverterBetaCSLambda Code=
              '(Context.IsBack ? &quot;Back: &quot; : &quot;Forward: &quot;) + (Context.In[0]).ToString()',
              NoExceptions=False}}"
            />
      </DockPanel>
      <DockPanel HorizontalAlignment="Stretch">
        <TextBlock Width="50">Source:</TextBlock>
        <TextBox x:Name="m_tTxt2"></TextBox>
      </DockPanel>
      <TextBlock Margin="50,0,0,0" FontStyle="Italic">Change the contents of the textboxes</TextBlock>


        </StackPanel>

      <StackPanel Grid.Column="2">

<!--Example of usage of a combination of JS converter and C# validation rules.-->
      <TextBlock Margin="0,0,0,0"
        Background="{StaticResource {x:Static SystemColors.ControlLightBrushKey}}">
      ConverterBetaJS + RuleBetaCS: TextBox &lt;-&gt; Slider</TextBlock>
      <DockPanel HorizontalAlignment="Stretch">
        <TextBlock Width="50">Target:</TextBlock>
        <TextBox x:Name="m_tTxt3">
          <TextBox.Text>
            <Binding
              ElementName="m_tSlider1"
              Path="Value" Mode="TwoWay"
              UpdateSourceTrigger="PropertyChanged"
              Converter="{StaticResource x_tCnv_JS_0_100}">
<!--Creating of a validation rule of integer values in the range [0, 100].-->
              <Binding.ValidationRules>
                <CCCR:RuleBetaCS>
                  <CCCR:RuleBetaCS.Code>
                    var c = (ContextRule)Context;
                    var n = Int32.Parse(c.In[0].ToString());
                    if (n < 0 || n > 100)
                      throw new Exception("Out of range [0; 100]!");
                  </CCCR:RuleBetaCS.Code>
                </CCCR:RuleBetaCS>
              </Binding.ValidationRules>
            </Binding>
          </TextBox.Text>
        </TextBox>
      </DockPanel>
      <DockPanel HorizontalAlignment="Stretch">
        <TextBlock Width="50">Source:</TextBlock>
        <Slider x:Name="m_tSlider1" Minimum="0" Maximum="100" Value="17"></Slider>
      </DockPanel>
      <TextBlock Margin="50,0,0,0" FontStyle="Italic">Play with text of the textbox. Tooltips.</TextBlock>

<!--Example of usage of the combination of C# Converter and JS validation rules.-->
      <TextBlock Margin="0,10,0,0"
        Background="{StaticResource {x:Static SystemColors.ControlLightBrushKey}}">
      ConverterBetaCS + RuleBetaJS: Slider &lt;-&gt; TextBox</TextBlock>
      <DockPanel HorizontalAlignment="Stretch">
        <TextBlock Width="50">Source:</TextBlock>
        <TextBox x:Name="m_tTxt4"/>
      </DockPanel>
      <DockPanel HorizontalAlignment="Stretch">
        <TextBlock Width="50">Target:</TextBlock>
        <Slider x:Name="m_tSlider2" Minimum="0" Maximum="100">
          <Slider.Value>
            <Binding
              ElementName="m_tTxt4"
              Path="Text" Mode="TwoWay"
              ConverterParameter="Slider"
              UpdateSourceTrigger="PropertyChanged"
              Converter="{StaticResource x_tCnv_CS_0_100}"
              ValidatesOnExceptions="True">

<!--
Creating a rule validation integer values in the range [20, 80].
Pay attention to ValidatesOnExceptions="True" and UpdateSourceTrigger="PropertyChanged".
-->
              <Binding.ValidationRules>
                <CCCR:RuleBetaJS>
                  <CCCR:RuleBetaJS.Code>
                    var n = int(double(Context.In[0]));
                  if (n < 20 || n > 80)
                    throw new Exception("Slider is Out of range [20; 80]!");
                  </CCCR:RuleBetaJS.Code>
                </CCCR:RuleBetaJS>
              </Binding.ValidationRules>
            </Binding>
          </Slider.Value>
          <!---->
        </Slider>
      </DockPanel>
      <TextBlock Margin="50,0,0,0" FontStyle="Italic">
      Play with the slider (left/right edges). Tooltips.</TextBlock>

      <TextBlock Margin="0,10,0,0"
        Background="{StaticResource {x:Static SystemColors.ControlLightBrushKey}}">
      ConverterBetaVB: Target <-> Four Sources</TextBlock>
      <DockPanel HorizontalAlignment="Stretch">
        <DockPanel.Resources>
          <CCCR:CommandBetaCS x:Key="x_tCmdUndo" 
            AlwaysCanExecute="True" NoExceptions="False">
<!--
Inserting into the DockPanel’s resource a command CommandBetaCS,
which finds in the text a symbol ';' cancels input.
-->
            <CCCR:CommandBetaCS.Code>
              var t=(TextBox)Context.In[0]; 
              var s=(string)Context.In[1]; 
              if (t.Text.Contains(s)) {
                Task.Factory.StartNew(() =>; {
                  t.Dispatcher.Invoke(
                    new Action(() => {
                      var sMsg = string.Format(
                        "The text contains '{0}' : \"{1}\"\n\nThe operation 'Undo' will be applied.", s, t.Text);
                      MessageBox.Show(sMsg, "CCCR:CommandBetaCS"); t.Undo(); }), null);
                    }); }
              else
                MessageBox.Show("Try to insert semicolons here!", "CCCR:CommandBetaCS");
            </CCCR:CommandBetaCS.Code>
          </CCCR:CommandBetaCS>
        </DockPanel.Resources>
        <TextBlock Width="50">Source 1:</TextBlock>
        <TextBox x:Name="m_tTxt5a" Text="Text1">
<!--Multiple binding to pass parameters to the command "x_tCmdUndo".-->
          <i:Interaction.Triggers>
            <i:EventTrigger EventName="TextChanged" >
              <i:InvokeCommandAction Command="{StaticResource x_tCmdUndo}">
                <i:InvokeCommandAction.CommandParameter>
                  <MultiBinding
                    Converter="{CCCR:ConverterBetaCS
                      Code='Context.Out[0] = new object[]{Context.In[0], Context.In[1]}'}">
                    <Binding
                      RelativeSource=
                        "{RelativeSource FindAncestor,
                         AncestorType=TextBox, AncestorLevel=1}"/>
                    <Binding>
                      <Binding.Source><system:String>;</system:String></Binding.Source>
                    </Binding>
                  </MultiBinding>
                </i:InvokeCommandAction.CommandParameter>
              </i:InvokeCommandAction>
            </i:EventTrigger>
          </i:Interaction.Triggers>
        </TextBox>
      </DockPanel>
      <DockPanel HorizontalAlignment="Stretch">
        <TextBlock Width="50">Source 2:</TextBlock>
        <TextBox x:Name="m_tTxt5b" Text="Text2">
          <i:Interaction.Triggers>
            <i:EventTrigger EventName="TextChanged" >
<!-–Demonstrated the use of TriggerAction in VB.Net.-->
              <CCCR:ActionBetaVB 
                  Code="MessageBox.Show(&quot;Try insert semicolons into the 1st textbox.&quot;, &quot;CCCR:ActionBetaVB&quot;)"/>
              </i:EventTrigger>
          </i:Interaction.Triggers>
        </TextBox>
      </DockPanel>
      <DockPanel HorizontalAlignment="Stretch">
        <TextBlock Width="50">Source 3:</TextBlock>
        <TextBox x:Name="m_tTxt5c" Text="Text3">
          <i:Interaction.Triggers>
            <i:EventTrigger EventName="TextChanged" >
<!-–Demonstrated the use of TriggerAction in VB.Net.-->
              <CCCR:ActionBetaVB 
                  Code="MessageBox.Show(&quot;Try insert semicolons into the 1st textbox.&quot;, &quot;CCCR:ActionBetaVB&quot;)"/>
            </i:EventTrigger>
          </i:Interaction.Triggers>
        </TextBox>
      </DockPanel>
      <DockPanel HorizontalAlignment="Stretch">
        <TextBlock Width="50">Source 4:</TextBlock>
        <TextBox x:Name="m_tTxt5d" Text="Text4">
          <i:Interaction.Triggers>
            <i:EventTrigger EventName="TextChanged" >
              <CCCR:ActionBetaVB 
                  Code="MessageBox.Show(&quot;Try insert semicolons into the 1st textbox.&quot;, &quot;CCCR:ActionBetaVB&quot;)"/>
            </i:EventTrigger>
          </i:Interaction.Triggers>
        </TextBox>
      </DockPanel>
      <DockPanel HorizontalAlignment="Stretch">
        <TextBlock Width="50">Target:</TextBlock>
        <TextBox x:Name="m_tTxt7">
          <TextBox.Text>
            <MultiBinding UpdateSourceTrigger="PropertyChanged">
              <MultiBinding.Bindings>
                <Binding ElementName="m_tTxt5a" Path="Text"/>
                <Binding ElementName="m_tTxt5b" Path="Text"/>
                <Binding ElementName="m_tTxt5c" Path="Text"/>
                <Binding ElementName="m_tTxt5d" Path="Text"/>
              </MultiBinding.Bindings>
              <MultiBinding.Converter>
                <CCCR:ConverterBetaVB>
<!--Example of the use of Beta VB.Net converter in multiple two-way binding-->
                  <CCCR:ConverterBetaVB.Code>
                    <system:String xml:space="preserve">
                      Dim c = Context
                      If Not c.IsBack Then
                        Dim f = Function(i) i.ToString()
                        'c.Out(0) = String.Join(" ; ", c.In.Select(Function(i) i.ToString().Trim()))
                        c.Out(0) = String.Join(" ; ", (Enumerable.Select(Of Object, String)(c.In, Function(i) i.ToString().Trim())))
                      Else
                        Dim a = c.In(0).ToString().Split(";"C)
                        c.Out(0) = IIf((a.Length > 0), a(0).Trim(), "")
                        c.Out(1) = IIf((a.Length > 1), a(1).Trim(), "")
                        c.Out(2) = IIf((a.Length > 2), a(2).Trim(), "")
                        c.Out(3) = IIf((a.Length > 3), a(3).Trim(), "")
                      End If
                    </system:String>
                  </CCCR:ConverterBetaVB.Code>
                </CCCR:ConverterBetaVB>
              </MultiBinding.Converter>
            </MultiBinding>
          </TextBox.Text>
        </TextBox>
      </DockPanel>
      <TextBlock Margin="50,0,0,0" FontStyle="Italic">Change the contents of the textboxes</TextBlock>


    </StackPanel>
    </Grid>
    </StackPanel>
  </Grid>

注释

我故意避开了关于TriggerAction的讨论,尽管后者与CCCR一起在Bourlesque.Lib.Windows.CCCR库(ActionAlpha, ActionBetaCS, ActionBetaVB等)中实现,但即使有很大的成功,它们也可以被命令取代(有机会定义一个TriggerAction无法使用的CommandParameter)。编译代码需要以下软件产品:

结论

本文所含材料的需求程度,将由时间以及各位读者来决定。我只能希望,花费时间来创作它,并没有白费,但它至少能带来适度的精神满足感和小小的益处。

历史

2011/04/26 - 初始版本。
2011/05/05 - 次要的格式化更改。
© . All rights reserved.