Click here to Skip to main content
15,867,704 members
Articles / Desktop Programming / WPF

Expression Bindings in XAML

Rate me:
Please Sign up or sign in to vote.
4.94/5 (14 votes)
20 Jul 2018MIT5 min read 26.8K   189   12   8
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


Written By
Poland Poland
I graduated from Adam Mickiewicz University in Poznań where I completed a MA degree in computer science (MA thesis: Analysis of Sound of Viola da Gamba and Human Voice and an Attempt of Comparison of Their Timbres Using Various Techniques of Digital Signal Analysis) and a bachelor degree in musicology (BA thesis: Continuity and Transitions in European Music Theory Illustrated by the Example of 3rd part of Zarlino's Institutioni Harmoniche and Bernhard's Tractatus Compositionis Augmentatus). I also graduated from a solo singing class in Fryderyk Chopin Musical School in Poznań. I'm a self-taught composer and a member of informal international group Vox Saeculorum, gathering composers, which common goal is to revive the old (mainly baroque) styles and composing traditions in contemporary written music. I'm the annual participant of International Summer School of Early Music in Lidzbark Warmiński.

Comments and Discussions

 
Question'is' operation Pin
Keyvan Kabirnia29-Nov-20 4:55
Keyvan Kabirnia29-Nov-20 4:55 
GeneralMy vote of 5 Pin
Hyland Computer Systems23-Jul-18 9:33
Hyland Computer Systems23-Jul-18 9:33 
GeneralRe: My vote of 5 Pin
Ajcek8423-Jul-18 10:30
Ajcek8423-Jul-18 10:30 
QuestionGood idea! Pin
Chris Ross 223-Jul-18 3:52
Chris Ross 223-Jul-18 3:52 
I like the idea, very much!
I also appreciate the table of capabilities and limitations, that'll be very helpful.

I have two (both very minor) pieces of feedback:

1. Do you support parentheses (a.k.a. round brackets) in your formulae - to control operator precedence?

2. In your introduction you indicate that converters have to be added to application resources in order to use them. This will be news to you, but ... that depends! Value converters are, as you know, implementations of the IValueConverter interface. That means, they can also implement a concrete class ... such as MarkupExtension. I was overjoyed when I discovered this approach! Here's an example: https://stackoverflow.com/questions/28501091/ivalueconverter-with-markupextension. This also allows any custom parameters you define for your converter to be expressed as markup extension properties, instead of having to use the binding's ConverterParameter property.

Anyway, please don't take this feedback as criticism of your idea, which I think is brilliant.
AnswerRe: Good idea! Pin
Ajcek8423-Jul-18 10:14
Ajcek8423-Jul-18 10:14 
QuestionNot bad Pin
Sacha Barber21-Jul-18 19:35
Sacha Barber21-Jul-18 19:35 
AnswerRe: Not bad Pin
Ajcek8423-Jul-18 10:30
Ajcek8423-Jul-18 10:30 
GeneralRe: Not bad Pin
Sacha Barber24-Jul-18 20:53
Sacha Barber24-Jul-18 20:53 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.