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

XAML 中的表达式绑定

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (14投票s)

2018年7月20日

MIT

5分钟阅读

viewsIcon

30073

downloadIcon

234

XAML 中的复杂绑定几乎和 JavaScript 框架一样简单。

引言

WPF 和 UWP 中的数据绑定是一种具有巨大可能性的机制,但在 XAML 中编写绑定可能是一项非平凡且耗时的任务。在 JavaScript 框架中,绑定可以包含整个函数、表达式和条件,就像本例一样。

ng-if="!documentsVisible && categoryId != null && categoryId != 0"

或者这个

data-bind="click: function(data, event) { myFunction('param1', 'param2', data, event) }"

XAML 遗憾的是,默认情况下并不提供这种简洁性。首先,您必须为大多数绑定提供值转换器。最大的荒谬之处在于,为了根据条件隐藏控件(在大多数应用程序中最常用的绑定),您必须编写一个转换器来将布尔值转换为 Visibility。随着项目的开发,转换器的数量会增加,因此过一段时间后,您就会看到像 BooleanToCollapsedConverterEmptyStringToCollapsedConverterMultiplicationConverter 等类。这些类的实现通常不优雅,因为您必须手动将值和参数转换为正确的类型。

XAML 绑定的第二个缺点是,您可以为转换器提供参数,但这些参数不是 DependencyProperties,也不能绑定到另一个属性。解决方案是创建一个多重绑定,然后实现 IMultiValueConverter 来创建绑定的逻辑。

XAML 绑定的第三个缺点是,您必须将所有转换器添加到应用程序资源中才能使用它们。在大多数情况下,您只需将转换器的名称复制并粘贴到 ApplicationResources 中,并为其分配一个与其名称相同的键。自动机制对此会有所帮助。

本文内容

我创建了一个机制,可以简化 XAML 中的绑定,并使其更接近 JavaScript 绑定的简洁性。该机制是 Manufaktura.Controls 项目的一部分(https://codeproject.org.cn/Articles/1252423/Music-Notation-in-NET),该项目尤其以其音乐符号组件而闻名,但也包含在其他领域中很有用的控件和工具。

本文附带的代码仅包含 Manufaktura.Controls 项目中的一部分库,其中包含 FormulaBindings 的实现和一个简单的测试应用程序。您也可以在 https://bitbucket.org/Ajcek/manufakturalibraries 浏览完整的 GIT 存储库。

公式绑定

让我们考虑一个旋转的盒子。为了使其旋转,我们必须创建一个 RotateTransform,并创建一个 Storyboard 来为变换的 Angle 属性应用 DoubleAnimation

        <Border Margin="50" Width="100" Height="100" 
        Background="Turquoise" x:Name="someBorder">
            <Border.Triggers>
                <EventTrigger RoutedEvent="FrameworkElement.Loaded">
                    <BeginStoryboard>
                        <Storyboard>
                            <DoubleAnimation Storyboard.TargetName="rotateTransform" 
                             Storyboard.TargetProperty="Angle" From="0" To="360" 
                             RepeatBehavior="Forever" Duration="0:0:2" />
                        </Storyboard>
                    </BeginStoryboard>
                </EventTrigger>
            </Border.Triggers>
            <Border.RenderTransform>
                <RotateTransform x:Name="rotateTransform" />            
            </Border.RenderTransform>
        </Border>

默认的旋转中心位于盒子左上角。我们希望它围绕中心旋转,因此必须设置变换的 CenterXCenterY 属性。在上面的示例中,我们可以将其设置为 50,因为宽度固定为 100,但如果我们想更改盒子的大小或自动缩放盒子,我们就必须为 CenterXCenterY 创建数据绑定。

通常,我们需要编写一个转换器来将旋转框的 ActualWidthActualHeight 属性除以 2。我创建了一种新的绑定类型,称为 FormulaBinding,以简化这一点。

<RotateTransform.CenterX>
    <bindings:FormulaBinding Formula="@p0 * 0.5">
        <Binding Path="ActualWidth" ElementName="someBorder" />
    </bindings:FormulaBinding>
</RotateTransform.CenterX>
<RotateTransform.CenterY>
    <bindings:FormulaBinding Formula="@p0 * 0.5">
        <Binding Path="ActualHeight" ElementName="someBorder" />
    </bindings:FormulaBinding>
</RotateTransform.CenterY>

FormulaBinding 本质上是一个 MultiBinding,带有一个 Formula 属性,您可以在其中编写几乎任何表达式。表达式中的参数用 @p# 标记,其中 # 是参数的索引。FormulaBinding 标签的主体包含公式参数的绑定。

在本文附带的示例中,我创建了一些滑块,允许用户移动旋转中心。

<Border Margin="50" Width="100" Height="100" 
Background="Turquoise" x:Name="someBorder">
    <Border.Triggers>
        <EventTrigger RoutedEvent="FrameworkElement.Loaded">
            <BeginStoryboard>
                <Storyboard>
                     <DoubleAnimation Storyboard.TargetName="rotateTransform" 
                     Storyboard.TargetProperty="Angle" From="0" To="360" 
                     RepeatBehavior="Forever" Duration="0:0:2" />
                </Storyboard>
            </BeginStoryboard>
        </EventTrigger>
    </Border.Triggers>
    <Border.RenderTransform>
        <RotateTransform x:Name="rotateTransform">
            <RotateTransform.CenterX>
                <bindings:FormulaBinding Formula="@p0 * @p1">
                    <Binding Path="ActualWidth" ElementName="someBorder" />
                    <Binding Path="Value" ElementName="sliderX" />
                </bindings:FormulaBinding>
            </RotateTransform.CenterX>
            <RotateTransform.CenterY>
                <bindings:FormulaBinding Formula="@p0 * @p1">
                    <Binding Path="ActualHeight" ElementName="someBorder" />
                    <Binding Path="Value" ElementName="sliderY" />
                </bindings:FormulaBinding>
            </RotateTransform.CenterY>
        </RotateTransform>
    </Border.RenderTransform>
</Border>
<TextBlock Margin="20,20,20,0" 
Text="Change center of rotation X (0-1):" FontSize="24" />
<Slider Margin="20" x:Name="sliderX" 
Minimum="0" Maximum="1" Value="0.5" 
LargeChange="0.1" SmallChange="0.01" />
<TextBlock Margin="20,20,20,0" 
Text="Change center of rotation Y (0-1):" FontSize="24" />
<Slider Margin="20" x:Name="sliderY" 
Minimum="0" Maximum="1" Value="0.5" 
LargeChange="0.1" SmallChange="0.01" />

可以看到,公式现在包含两个参数,它们由数据绑定提供。

FormulaBinding 也支持函数。考虑以下示例:

<TextBlock Margin="20,20,20,0" Text="Example of function 
(Asin of center Y):" FontSize="24" />
<TextBox Margin="20" FontSize="24">
    <TextBox.Text>
        <bindings:FormulaBinding Formula="System.Math.Asin(@p0)" StringFormat="0.00">
            <Binding Path="Value" ElementName="sliderY" />
        </bindings:FormulaBinding>
    </TextBox.Text>
</TextBox>

该表达式计算滑块值的反正弦。请注意,FormulaBinding 仅支持 static 函数,这些函数必须使用完整命名空间进行限定。

实际测试应用程序的屏幕截图

功能和限制

FormulaBinding 当前支持以下运算符:

运算符 名称 注释
+ 加法 假设参数是双精度值。
- 减法 假设参数是双精度值。
* 乘法 假设参数是双精度值。
/ 除法 假设参数是双精度值。
逻辑与 我无法使用 &&,因为 XAML 会将 & 误解为转义字符,例如 &quot。
逻辑或 为了与“and”保持一致,我使用了“or”而不是“II”。
(condition) ? (exp1) : (exp2) 三元条件运算符。 假设条件评估为布尔值。
== 等于 如果左右两侧都评估为布尔值,则使用 == 运算符。否则,它使用 object.Equal 方法。
!= 不等于 同上。
> 大于 同上。
< 小于 同上。
>= 大于等于 同上。
<= 小于等于 同上。
%  
^ 使用 System.Math.Pow 函数。
! 取反  
  函数 仅支持带完整命名空间的 static 函数(例如:System.Math.Sin)。

关于返回值也有一些规则:

  • 如果您想绑定到 Visibility 属性,并且您的表达式评估为 bool,则必须将 IsVisibiityBinding 设置为 true on FormulaBinding
  • 如果您的表达式评估为 double,并且您想将值绑定到文本控件(如 TextBoxTextBlock),则必须为绑定设置 StringFormat 属性。否则,将抛出类型转换异常。

它是如何工作的

Manufaktura.Core 库包含一个 String2ExpressionParser 类,该类解析公式 string 成表达式树。大部分工作由称为 Cutters 的类完成,这些类将表达式 string 切割成单个表达式。

public class AndExpressionCutter : ExpressionCutter
{
    public override string Operator => "and";
    public override int Priority => 0;

    public override Expression CreateExpression(Expression left, Expression right)
    {
        return Expression.AndAlso(Expression.Convert(left, typeof(bool)), 
                                  Expression.Convert(right, typeof(bool)));
    }
}

Operator 属性定义了解析器将识别为特定运算符的 stringCreateExpression 方法返回将为该公式部分创建的 Expression。Priority 定义了算术运算的顺序。强烈建议在公式中使用括号,因为我不确定是否为所有运算符定义了正确的优先级。J

FormulaBinding 只是一个使用 FormulaConverter 转换公式的 MultiBinding

public class FormulaBinding : MultiBinding
    {
        public FormulaBinding()
        {
            Converter = new FormulaConverter();
            Mode = BindingMode.OneWay;
        }

        public string Formula
        {
            get
            {
                return ConverterParameter as string;
            }
            set
            {
                ConverterParameter = value;
            }
        }

        public bool IsVisibilityBinding
        {
            get
            {
               return Converter is FormulaVisibilityConverter;
            }
            set
            {
                Converter = value ? (IMultiValueConverter)new FormulaVisibilityConverter() : 
                            new FormulaConverter();
            }
        }
    }

FormulaConverter 使用 String2Expression 解析器的扩展方法来执行转换。

public class FormulaConverter : IMultiValueConverter
    {
        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            var unparsedParameter = parameter as string;
            var lambda = unparsedParameter.ToLambdaExpression();
            var result = lambda.Compile().DynamicInvoke(values);

            return result;
        }

        public object[] ConvertBack(object value, Type[] targetTypes, 
                        object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }

来自实际生产应用程序的一些示例

FormulaBinding 在 Windows 桌面应用程序 Say It Right(一个英语发音课程)中使用:https://sayitrightonline.pl/en-US/

响应性(针对特定窗口大小隐藏元素)

<b:FormulaBinding Formula="@p0 > 640" IsVisibilityBinding="True">
    <Binding Path="DataContext.AppViewModel.WindowWidth" ElementName="root" />
</b:FormulaBinding>

响应性(为不同窗口大小提供不同宽度)

<controls:PhonemControl.Width>
    <b:FormulaBinding Formula="@p0 >= 1024 ? 140 : 112">
        <Binding Path="DataContext.AppViewModel.WindowWidth" ElementName="root" />
    </b:FormulaBinding>
</controls:PhonemControl.Width>

根据条件隐藏元素

<b:FormulaBinding Formula="@p0 and !@p1" IsVisibilityBinding="True">
     <Binding Path="IsCurrentPageExercisePage" />
     <Binding Path="IsLoading" />
</b:FormulaBinding>

根据条件启用或禁用控件

<controls:IconButton.IsEnabled>
    <bindings:FormulaBinding Formula="@p0 and @p1">
        <Binding Path="IsPlaying" />
        <Binding Path="IsPaused" />
    </bindings:FormulaBinding>
</controls:IconButton.IsEnabled>

根据条件选中单选按钮

<RadioButton.IsChecked>
    <b:FormulaBinding Formula="@p0 == @p1">
        <Binding Mode="OneWay" Path="DataContext.CurrentLanguageVariant" ElementName="variantList" />
        <Binding Mode="OneWay" />
    </b:FormulaBinding>
</RadioButton.IsChecked>
© . All rights reserved.