Introduction
Data binding in WPF and UWP is a mechanism that gives great possibilities, but writing bindings in XAML can be a non-trivial and time-consuming task. In JavaScript frameworks, bindings can contain whole functions, expressions and conditions like in this case:
ng-if="!documentsVisible && categoryId != null && categoryId != 0"
or this:
data-bind="click: function(data, event) { myFunction('param1', 'param2', data, event) }"
XAML unfortunately does not offer such simplicity out of the box. First of all, you have to provide value converters for most of the bindings. The biggest absurdity is that in order to hide controls depending on the condition (most commonly used binding in most applications), you have to write a converter to convert boolean values to Visibility. The number of converters increases during the development of the project, so after some time you have such classes as BooleanToCollapsedConverter
, EmptyStringToCollapsedConverter
, MultiplicationConverter
, etc. Implementations of those classes are often inelegant because you have to manually cast values and parameters to proper types.
The second disadvantage of XAML bindings is that you can provide parameters for converters but the parameters are not DependencyProperties
and can’t be bound to another property. The solution for this is to create a multi binding and then implement IMultiValueConverter
to create logic for the binding.
The third disadvantage of XAML binding is that you have to add all converters to application resources in order to use them. In most cases, you just copy and paste the name of the converter to ApplicationResources
and give it a key equal to its name. Some automatic mechanism for that would be helpful.
Contents of this Article
I have created a mechanism that will simplify bindings in XAML and bring them closer to the simplicity of JavaScript bindings. The mechanism is a part of Manufaktura.Controls
project (https://www.codeproject.com/Articles/1252423/Music-Notation-in-NET) which is known especially from its music notation components but also contains controls and tools useful in other areas.
The code attached to this article contains only a subset of libraries from Manufaktura.Controls
project which contain the implementation of FormulaBindings
and a simple test application. You can also browse the full GIT repository at https://bitbucket.org/Ajcek/manufakturalibraries.
Formula Bindings
Let’s consider a rotating box. To make it rotate, we have to create a RotateTransform
and create a Storyboard
that will apply DoubleAnimation
for the Angle
property of the transform.
<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>
The default center of rotation is in the upper left corner of the box. We want it to rotate around its center so we have to set CenterX
and CenterY
properties of the transform. In the above example, we could set it to 50 because the width has a fixed value of 100 but if we ever want to change the size of the box or automatically scale the box, we will have to create data bindings for CenterX
and CenterY
.
Normally, we would have to write a converter to divide the ActualWidth
and ActualHeight
property of the rotating box by 2. I created a new kind of binding called FormulaBinding
to simplify that:
<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
is basically a MultiBinding
with Formula
property in which you can write almost any expression. The parameters in the expression are marked with @p#
where #
is an index of the parameter. The body of FormulaBinding
tags contains bindings for the parameters in the formula.
In the example attached to this article, I created some sliders that allow the user to shift the center of rotation:
<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" />
As you can see, the formula now contains two parameters which are provided by data bindings.
FormulaBinding
s also support functions. Consider the following example:
<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>
The expression computes arcus sinus of the slider value. Note that FormulaBinding
s support only static
functions which have to be fully qualified with the full namespace.
Screen from actual test application:
Capabilities and Limitations
FormulaBinding
currently supports the following operators:
Operator | Name | Comments |
+ | Addition | Assumes that parameters are double values |
- | Subtraction | Assumes that parameters are double values |
* | Multiplication | Assumes that parameters are double values |
/ | Division | Assumes that parameters are double values |
and | Logical and | I couldn’t use && because XAML misinterprets & for escaped character like " |
or | Logical or | I use “or” instead of “II” for consistency with “and” |
(condition) ? (exp1) : (exp2) | Three-argument conditional operator | Assumes that condition evaluates to bool |
== | Equal | If both left and right side evaluates to bool the == operator is used. Otherwise, it uses object.Equal method |
!= | Not equal | As above |
> | Greater than | As above |
< | Less than | As above |
>= | Greater than or equal | As above |
<= | Less than or equal | As above |
% | Modulo | |
^ | Power | Uses System.Math.Pow function |
! | Negation | |
| Functions | Only static functions with full namespace (example: System.Math.Sin ) |
There are also some rules concerning the returned values:
- If you want to bind to
Visibility
property and your expression evaluates to bool
, you have to set IsVisibiityBinding
on FormulaBinding
to true
- If your expression evaluates to
double
and you want to bind the value to a text control like TextBox
or TextBlock
, you have to set StringFormat
property for the binding. Otherwise, a typecast exception will be thrown.
How Does It Work
Manufaktura.Core
library contains a String2ExpressionParser
class that parses the formula string
into the expression tree. Most of the work is done by classes called Cutters
that cut the expression string
into individual Expressions:
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
property defines the string
that will be recognized as specific operator by the parser. CreateExpression
method returns the Expression
that will be created for this part of the formula. Priority defines the order of arithmetic operations. It’s highly advised to use parentheses in formulas because I’m not sure if I defined the correct priorities for all operators. J
FormulaBinding
is just a MultiBinding
that uses FormulaConverter
to convert formulas:
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
uses extension methods for String2Expression
parser to do the conversion:
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();
}
}
Some Examples from Real Production Application
FormulaBinding
s are used in Windows desktop application for English pronunciation course Say It Right: https://sayitrightonline.pl/en-US/.
Responsiveness (Hiding Elements for Specific Window Size)
<b:FormulaBinding Formula="@p0 > 640" IsVisibilityBinding="True">
<Binding Path="DataContext.AppViewModel.WindowWidth" ElementName="root" />
</b:FormulaBinding>
Responsiveness (Providing Different Width for Different Window Sizes)
<controls:PhonemControl.Width>
<b:FormulaBinding Formula="@p0 >= 1024 ? 140 : 112">
<Binding Path="DataContext.AppViewModel.WindowWidth" ElementName="root" />
</b:FormulaBinding>
</controls:PhonemControl.Width>
Hiding Elements on Conditions
<b:FormulaBinding Formula="@p0 and !@p1" IsVisibilityBinding="True">
<Binding Path="IsCurrentPageExercisePage" />
<Binding Path="IsLoading" />
</b:FormulaBinding>
Enabling or Disabling Controls on Condition
<controls:IconButton.IsEnabled>
<bindings:FormulaBinding Formula="@p0 and @p1">
<Binding Path="IsPlaying" />
<Binding Path="IsPaused" />
</bindings:FormulaBinding>
</controls:IconButton.IsEnabled>
Checking Radio Buttons on Condition
<RadioButton.IsChecked>
<b:FormulaBinding Formula="@p0 == @p1">
<Binding Mode="OneWay" Path="DataContext.CurrentLanguageVariant" ElementName="variantList" />
<Binding Mode="OneWay" />
</b:FormulaBinding>
</RadioButton.IsChecked>