WPF: Binding without trivial converters

Good afternoon!

Whenever I started writing a new project in WPF, I was tormented by the thought: why in order to get attached to the negation of a Boolean variable or convert a Boolean variable to the Visibility type, you need to write your own converter, which is then specified in each Binding? And if we need to print 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.

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

Compare, the same binding done using the standard Binding and the new Binding (c: Binding):

Before:


After:


Before:


After:


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

Other examples:


Mathematical operations:

Logical operations:
 {'and' эквивалент '&&' см. ниже}
 {'or' эквивалент '||', но можно оставить '||'}
 {'less=' эквивалент '<=' см. ниже}

Work with bool and visibility:

Work with strings:

Working with the Math Class

Opportunities


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

OpportunityMore detailsExample
Arithmetic operations + - / *%
Logical operations! && || ==! =, <,>, <=,> =
Work with strings +
Ternary operator?
Automatic translation from bool to VisibilityFalseToVisibility defines how to display false
Math class support All methods and constants

Automatic inverse function calculation and inverse bindingIf the expression can be used to build the opposite, then this will be done automatically and the binding will become bidirectional
0% loss in speedThe original Path is compiled into the anonymous function only 1 time, into which the changed property values ​​are then constantly substituted

Bool to visibility


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


How CalcBinding is structured inside


For those who first learned about the possibility of expanding haml, I will make a small digression. Everything that we access from xaml using curly braces is quite an ordinary class, written in C #, but always inherited from the MarkupExtension class. Such classes are called Markup Extension. WPF allows you to complement the set of standard Markup Extension with your own custom ones. 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 is naturally the Markup Extension), the first thought was to inherit from it, and to change the binding behavior in certain places. But, unfortunately, it turned out that the ProvideValue method, whose behavior needed to be changed, was marked as sealed (declared with the sealed keyword) in the BindingBase parent class. 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 for any changes to Binding and MultiBinding, but if you remember that WPF has not been developed for a long time, I think it does not threaten me.

How is CalcBinding structured? At the beginning of the ProvideValue method, the Path property is analyzed and if Path contains one variable, then Binding is created, otherwise a MultiBinding is created containing one Binding for each variable. The values ​​of all CalcBinding properties are thrown into the created Binding or MultiBinding, and my converter is transferred as a converter, which implements the IConverter and IMultiConverter interfaces and does the job of compiling Path and running the resulting anonymous function. The resulting Binding or MultiBinding method calls the ProvideValue method and the result is returned as the result of the external ProvideValue. Thus, WPF works with its standard classes for binding, and my class acts as a kind of factory.

As mentioned above, in order for the new solution not to lose the speed of the old one, the original expression specified in the Path property is compiled only once, the first time the Convert or ConvertBack methods are called. The compilation results in an anonymous function that takes source property as parameters, to which target property is bound. When changing any of the source property, the resulting function is called with new parameters and, accordingly, returns a new value.

Parsing a string expression occurs in two stages: on the first of the lines, the tree of expressions (System.Linq.Expressions.Expression) is built, and on the second, the resulting expression is compiled using standard methods. To create Expression from a string, there is a parser from Microsoft, which is located in the DynamicExpression library [2] . Unfortunately, it revealed problems that did not allow using this solution unchanged, for example, a bug [3] . Fortunately, the parser was uploaded to 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, we can present the logic of the Convert converter method 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;
}	


Back binding


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

It is clear that in order to be able to construct the inverse function, it is required that it has only one argument and this argument occurs exactly 1 time. These are necessary but insufficient conditions. From the course of school mathematics, we remember that if Y = F (X) is a complex function that can be 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 that make up this function and apply them in 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 from the well-known expression tree that implements the direct function. The restrictions introduced on Expression are such that it is impossible to modify the created expression tree, so you will have to build new ones.

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


Based on this expression, the following tree will be constructed:



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



In this figure, it becomes clear that in order to construct the inverse tree of expressions , you need to replace all the functions that are in the nodes on the path from a sheet with variable X to the root with the Path result to the inverse ones, and then change the order of their application to the opposite:



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

Searching for a variable, validating and constructing the inverse tree is performed by just one recursive function.

Currently, the following list of functions is supported, for which the inverse is automatically determined:
  • +, -, *, /
  • !
  • Math.Sin, Math.ASin
  • Math.Cos, Math.ACos
  • Math.Tan, Math.ATan
  • Math.pow
  • Math.log

The resulting expression tree is calculated and compiled into an anonymous function also only 1 time, the first time the feedback is triggered.

Disadvantages of the solution


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

1.) If one of sourceProperty is null, then it becomes impossible to determine the type at the stage of Expression creation, since typeof (null) does not return anything. This makes it impossible to correctly process for example such an expression:

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

2.) Since the xaml markup is xml markup, a number of characters are forbidden in it, for example, the opening tag or ampersand icon, which also means the signs “more” and “logical AND”. In order to get away from forbidden characters, Path uses a number of replacements for these operators, presented below:

OperatorSubstitution in PathNote
&&and
|| or Introduced for symmetry, optional
< less
<=less =


3.) You cannot set your own converter in CalcBinding. I did not come up with a scenario 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 library source codes, a full-fledged example of using all the features, and tests. A nuget package was created for the library, available at:
www.nuget.org/packages/CalcBinding

References to 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

Also popular now: