Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WPF

Expression Bindings in XAML

4.94/5 (14 votes)
20 Jul 2018MIT5 min read 29K   199  
Complex bindings in XAML made almost as simple as in JavaScript frameworks

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:

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

or this:

HTML
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.

XML
<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:

XML
<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:

XML
<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.

FormulaBindings also support functions. Consider the following example:

XML
<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 FormulaBindings support only static functions which have to be fully qualified with the full namespace.

Screen from actual test application:

Image 1

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 &quot;
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:

C#
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:

C#
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:

C#
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

FormulaBindings 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)

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

Responsiveness (Providing Different Width for Different Window Sizes)

XML
<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

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

Enabling or Disabling Controls on Condition

XML
<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

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

License

This article, along with any associated source code and files, is licensed under The MIT License