📜 ⬆️ ⬇️

WPF: Binding without trivial converters

Good day!

Every time when I started writing a new project on WPF, I was tormented by the idea: why in order to attach to the negation of a Boolean variable or convert a Boolean variable to the Visibility type, do I need to write my own converter, which then later must be specified in each Binding? And if we need to display the sum of two numbers, or just divide the number by 2, we need to write so much code that we don’t want to add and divide anything.

To solve this problem, once and for all, I wrote an analogue of standard binding, allowing you to bind to any expression from one or more sources of binding. About how it works and how to use it, I want to tell you more.

Compare, the same binding performed using standard Binding and new Binding (c: Binding):
')
Before:

<Label> <Label.Content> <MultiBinding Conveter={x:StaticResource MyCustomConverter2}> <Binding A/> <Binding B/> <Binding C/> </MultiBinding> </Label.Content> <Label> 

After:

 <Label Content="{c:Binding A+B+C }" /> 

Before:

 <Button Visibility="{Binding IsChecked Converter={x:StaticResource NegativeBoolToVisibilityConveter}}" /> 

After:

 <Button Visibility="{c:Binding !IsChecked}" /> 

As you can see from these examples, the new Binding can take in the Path property any expression from one or more Source Property. All basic arithmetic and logical operations are supported, as well as string addition operations and the ternary operator.

Other examples:


Mathematical operations:
 <TextBox Text="{c:Binding A+B+C}"/> <TextBox Text="{c:Binding ABC}"/> <TextBox Text="{c:Binding A*(B+C)}"/> <TextBox Text="{c:Binding 2*AB*0.5}"/> <TextBox Text="{c:Binding A/B, StringFormat={}{0:n2} --StringFormat is used}"/> <TextBox Text="{c:Binding A%B}"/> <TextBox Text="{c:Binding '(A == 1) ? 10 : 20'}"/> 

Logical operations:
 <CheckBox Content="!IsChecked" IsChecked="{c:Binding !IsChecked}"/> <TextBox Text="{c:Binding 'IsChecked and IsFull'}"/> {'and'  '&&' . } <TextBox Text="{c:Binding '!IsChecked or (A > B)'}"/> {'or'  '||',    '||'} <TextBox Text="{c:Binding '(A == 1) and (B less= 5)'}"/> {'less='  '<=' . } <TextBox Text="{c:Binding (IsChecked || !IsFull)}"/> 

Working with bool and visibility:
 <Button Content="TargetButton" Visibility="{c:Binding HasPrivileges, FalseToVisibility=Collapsed}"/> <Button Content="TargetButton" Visibility="{c:Binding !HasPrivileges}"/> <Button Content="TargetButton" Visibility="{c:Binding !HasPrivileges, FalseToVisibility=Hidden}"/> 

Work with strings:
 <TextBox Text="{c:Binding (Name + \' \' + Surname)}" /> <TextBox Text="{c:Binding (IsMan?\'Mr\':\'Ms\') + \' \' + Surname + \' \' + Name}"/> 

Work with the Math class
 <TextBox Text="{c:Binding Math.Sin(A*Math.PI/180), StringFormat={}{0:n5}}"/> <TextBox Text="{c:Binding A*Math.PI}" /> <TextBox Text="{c:Binding 'Math.Sin(Math.Cos(A))'}"/> 

Opportunities


Listed below are all the features of the new Binding that are currently available:

OpportunityRead moreExample
Arithmetic operations+ - / *%<Label Content = "{c: Binding A * 0.5 + (B / C - B% C)}" />
Logical operations! && || ==! =, <,>, <=,> =<TextBox Text = "{c: Binding (IsChecked ||! IsFull)}" />
Working with strings+<TextBox Text = "{c: Binding (Name + \ '\' + Surname)}" />
Ternary operator?<TextBox Text = "{c: Binding '(A == 1)? 10: 20'}" />
Automatic translation from bool to VisibilityFalseToVisibility determines how to display false<Button Visibility = "{c: Binding! IsChecked}" />
Math class supportAll methods and constants<TextBox Text = "{c: Binding Math.Sin (A * Math.PI / 180)}" />

Automatic calculation of inverse function and reverse linkingIf the expression can build the opposite, then it will be done automatically and the binding will become bidirectional
0% loss in work speedThe original Path is compiled into an anonymous function only 1 time, which is then constantly substituted for the modified property values.

Bool to visibility


The new Binding automatically converts the bool type to the Visibility type if properties are associated with such types. By default, it is assumed that false is translated to Visibility.Collapsed, but this behavior can be changed by setting the optional property FalseToVisibility (Collapsed or Hidden):

 <Button Visibility="{c:Binding IsChecked, FalseToVisibility=Hidden}" /> 

How is CalcBinding inside


For those who first learned about the possibility of expanding xaml, I will make a small digression. Everything we refer to from xaml using curly brackets is quite ordinary classes written in C #, but necessarily inherited from the MarkupExtension class. Such classes are called - Markup Extension (markup extension). WPF allows you to add a custom set of standard Markup Extension. A number of articles are devoted to writing such additions, for example [1] and it is really easy to write them.

When I began to study the Binding class (which of course is the Markup Extension), my first thought was to inherit from it, and in certain places to change the binding behavior. But, unfortunately, it turned out that the ProvideValue method, whose behavior was to be changed, is marked as sealed (declared with the sealed keyword) still in the parent class BindingBase. This arrangement of affairs forced me to create a new empty Markup Extension and copy into it all the properties used in standard Binding and MultiBinding. Of course, such a solution requires modifying the class with any change in Binding and MultiBinding, but if we recall that WPF has not been developing for a long time, I think it does not threaten me.

How does CalcBinding work? At the beginning of the ProvideValue method, the Path property is analyzed and if the Path contains one variable, a Binding is created, otherwise a MultiBinding is created that contains one Binding for each variable. In the created Binding or MultiBinding, the values ​​of all CalcBinding properties are skipped, and my converter, which implements the IConverter and IMultiConverter interfaces and performs the work of compiling the Path and launching the resulting anonymous function, is passed as a converter. The resulting Binding or MultiBinding method is called ProvideValue and the result of the work is returned as the result of the work of the external ProvideValue. In this way, WPF works with its standard binding classes, and my class acts as a kind of factory.

As mentioned above, in order for the new solution not to lose in speed to the old, the original expression specified in the Path property is compiled only once, when you first call the Convert or ConvertBack converter methods. As a result of the compilation, an anonymous function is obtained that takes as parameters the source property, to which the target property is attached. If any of the source property changes, the resulting function is called with the new parameters and accordingly returns the new value.

Parsing a string expression occurs in two stages: on the first of the string, an expression tree is built (System.Linq.Expressions.Expression), on the second, the resulting expression is compiled using standard methods. To create an Expression from a string, there is a parser from Microsoft, which is located in the DynamicExpression library [2] . Unfortunately, it showed problems that did not allow using this solution in an unchanged form, for example, a bug [3] . Fortunately, the parser was laid out in opensource, and I used its fork called DynamicExpresso [4] , which solved this and several other problems, and also expanded the list of supported types.

Omitting the details, you can imagine the logic of the Convert method of the converter as follows:

 private Lambda compiledExpression; public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) { if (compiledExpression == null) { var expressionTemplate = (string)parameter; compiledExpression = new Interpreter().Parse(expressionTemplate, values.Select(v => getParameter(v)).ToList()); } var result = compiledExpression.Invoke(values); return result; } 


Reverse binding


After the linking away from the sources to the target property was successfully tested, I wanted to be able to automatically search for the inverse function and linking in the opposite direction.

It is clear that in order to be able to construct an inverse function, it is required that it have only one argument and this argument has been encountered exactly 1 time. These are necessary but not sufficient conditions. From the school mathematics course, we remember that if Y = F (X) is a complex function, represented as Y = F 1 (F 2 (F 3 ... (F N (X)))), then X = F -1 ( Y) = F N -1 (F N-1 -1 (F N-2 -1 (... (F 1 -1 (Y)))))

Thus, in order to construct a complex inverse function, we need to find the inverse functions of all the functions constituting this function and apply them in the reverse order.

Let's go back to programming. From a programmatic point of view, we need to build an expression tree that implements the inverse function using a well-known expression tree that implements a direct function. The restrictions imposed on Expression are such that it is impossible to modify the generated expression tree, so you have to build new ones.

Let's look at an example of what we need to do for this. For example, the original function looks like this:
 Path = 10 + (6+5)*(3+X) - Math.Sin(6*5) 


According to this expression will build the following tree:



Imagine it in such a way that the path from the sheet containing X to the top of the Path was straight, and the remaining nodes are located above and below:



In this picture, it becomes clear that in order to build an inverse expression tree, it is necessary to replace all functions standing in the nodes on the path from the sheet with variable X to the root with the Path result to inverse, and then change the order of their application to the reverse:



Branches that calculate fixed values ​​do not need to be inverted. As a result, we obtain the inverse expression tree, from which the inverse function is obtained:
 X = ((Path - 10) + Math.Sin(6*5)) / (6 + 5) - 3 

Variable search, validation and construction of the inverse tree is performed with just one recursive function.

Currently the following list of functions is supported, for which the inverse are automatically defined:

The resulting tree of expressions is calculated and compiled into an anonymous function also only 1 time, when the first binding operation is triggered.

Disadvantages of the solution


Like any other, this solution has a number of limitations and disadvantages. The deficiencies identified are listed below.

1.) If one of the sourceProperty is null, then it becomes impossible for it to determine the type at the stage of the creation of the Expression, since typeof (null) returns nothing. This makes it impossible to correctly handle for example such an expression:
 <Label Content="{c:Binding Path = (A == null) ? 3 : 4}" /> 

Unfortunately, it is not directly possible to find the source property type from the Binding class, so the only possible solution is to recognize the source property type through reflection, but in this case you will have to implement half of the Binding functionality for Source (RelativeSource, ElementName, etc.) search. Perhaps there are alternative solutions to this problem, so I will welcome your suggestions on this topic.

2.) Since the xaml markup is xml markup, it prohibits a number of characters, such as an opening tag or an ampersand icon, meaning also more and more logical AND signs. In order to escape from forbidden characters, Path uses a series of substitutions for these operators, presented below:

OperatorReplacing in PathNote
&&and
||orPermitted for symmetry, optional
<less
<=less =


3.) You cannot set your own converter in CalcBinding. I did not come up with a script that would require such an opportunity, so if you have any suggestions, I will be glad to read them.

Links to the project:


The library is available on github . The project contains the source code of the library, a full-fledged example of the use of all features and tests. The library has created a nuget package available at:
www.nuget.org/packages/CalcBinding

References to the sources used:


[1] 10rem.net/blog/2011/03/09/creating-a-custom-markup-extension-in-wpf-and-soon-silverlight
[2] weblogs.asp.net/scottgu/dynamic-linq-part-1-using-the-linq-dynamic-query-library Article
msdn.microsoft.com/en-us/vstudio/bb894665.aspx Download Link
[3] connect.microsoft.com/VisualStudio/feedback/details/677766/system-linq-dynamic-culture-related-floating-point-parsing-error
[4] github.com/davideicardi/DynamicExpresso

Source: https://habr.com/ru/post/248197/


All Articles