XAML 中的表达式绑定
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。随着项目的开发,转换器的数量会增加,因此过一段时间后,您就会看到像 BooleanToCollapsedConverter
、EmptyStringToCollapsedConverter
、MultiplicationConverter
等类。这些类的实现通常不优雅,因为您必须手动将值和参数转换为正确的类型。
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>
默认的旋转中心位于盒子左上角。我们希望它围绕中心旋转,因此必须设置变换的 CenterX
和 CenterY
属性。在上面的示例中,我们可以将其设置为 50,因为宽度固定为 100,但如果我们想更改盒子的大小或自动缩放盒子,我们就必须为 CenterX
和 CenterY
创建数据绑定。
通常,我们需要编写一个转换器来将旋转框的 ActualWidth
和 ActualHeight
属性除以 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 会将 & 误解为转义字符,例如 "。 |
或 | 逻辑或 | 为了与“and”保持一致,我使用了“or”而不是“II”。 |
(condition) ? (exp1) : (exp2) | 三元条件运算符。 | 假设条件评估为布尔值。 |
== | 等于 | 如果左右两侧都评估为布尔值,则使用 == 运算符。否则,它使用 object.Equal 方法。 |
!= | 不等于 | 同上。 |
> | 大于 | 同上。 |
< | 小于 | 同上。 |
>= | 大于等于 | 同上。 |
<= | 小于等于 | 同上。 |
% | 模 | |
^ | 幂 | 使用 System.Math.Pow 函数。 |
! | 取反 | |
函数 | 仅支持带完整命名空间的 static 函数(例如:System.Math.Sin )。 |
关于返回值也有一些规则:
- 如果您想绑定到
Visibility
属性,并且您的表达式评估为bool
,则必须将IsVisibiityBinding
设置为true
onFormulaBinding
。 - 如果您的表达式评估为
double
,并且您想将值绑定到文本控件(如TextBox
或TextBlock
),则必须为绑定设置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
属性定义了解析器将识别为特定运算符的 string
。CreateExpression
方法返回将为该公式部分创建的 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>