[Из песочницы] WPF: Binding без тривиальных конвертеров
Добрый день! Всякий раз, когда я начинал писать новый проект на WPF, меня мучала мысль: почему для того, чтобы привязаться к отрицанию булевой переменной или перевести булеву переменную в тип Visibility, необходимо писать свой конвертер, который потом еще указывать в каждом Binding? А уж если нам необходимо вывести сумму двух чисел, или просто поделить число на 2, требуется написать столько кода, что уже складывать и делить ничего не хочется.
Для решения этой проблемы раз и навсегда я написал аналог стандартного биндинга, позволяющий привязываться к любому выражению от одного или нескольких источников привязки. О том, как это работает и как этим пользоваться, я хочу рассказать подробнее.Сравните, одна и та же привязка, выполненная с использованием стандартного Binding и нового Binding (c: Binding):
До:
Возможность
Подробнее
Пример
Арифметические операции
+ — / * %
Как устроен CalcBinding? В начале метода ProvideValue анализируется свойство Path и если Path содержит одну переменную, то создаётся Binding, иначе создаётся MultiBinding, содержащий по одному Binding на каждую переменную. В созданный Binding или MultiBinding прокидываются значения всех свойств CalcBinding, а в качестве конвертера передаётся мой конвертер, реализующий интерфейсы IConverter и IMultiConverter и выполняющий работу по компиляции Path и запуску полученной анонимной функции. У полученного Binding или MultiBinding вызывается метод ProvideValue и результат работы возвращается в качестве результата работы внешного ProvideValue. Таким образом WPF работает со своими стандартными классами для связывания, а мой класс выступает в роли своеобразной фабрики.
Как уже было сказано выше, для того чтобы новое решение не проигрывало по скорости работы старому, исходное выражение, заданное в свойстве Path, компилируется всего один раз, при первом вызове методов конвертера Convert или ConvertBack. В результате компиляции получается анонимная функция, принимающая в качестве параметров source property, к которым и привязывается target property. При изменении любого из source property полученная функция вызывается с новыми параметрами и соответственно возвращает новое значение.
Парсинг строкового выражения происходит в две стадии: на первой из строки строится дерево выражений (System.Linq.Expressions.Expression), на второй полученное выражение компилируется стандартными методами. Для создания Expression из строки существует парсер от Microsoft, который расположен в библиотеке DynamicExpression[2]. К сожалению, в нём обнаружились проблемы, которые не позволили использовать это решение в неизмененном виде, например баг [3]. К счастью парсер был выложен в opensource, и я использовал его fork под названием DynamicExpresso [4], в котором решена эта и несколько других проблем, а также расширен список поддерживаемых типов.
Опуская детали, можно представить логику метода Convert конвертера следующим образом:
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; } Обратное связывание После того как было успешно протестировано связывание в сторону от источников к целевому свойству, мне захотелось иметь возможность автоматического поиска обратной функции и связывания в обратную сторону.Понятно, что для того, чтобы можно было построить обратную функцию, требуется чтобы у неё был всего один аргумент и этот аргумент встречался ровно 1 раз. Это необходимые, но недостаточные условия. Из курса школьной математики мы помним, что если Y = F (X) сложная функция, представимая как Y = F1(F2(F3…(FN (X)))), то X = F-1(Y) = FN-1(FN-1–1(FN-2–1(…(F1–1(Y)))))
Таким образом для того чтобы построить сложную обратную функцию, нам нужно найти обратные функции от всех составляющих эту функцию функций и применить их в обратном порядке.
Вернёмся в программирование. С программной точки зрения нам требуется по известному дереву выражений, реализующему прямую функцию, построить дерево выражений, реализующее обратную функцию. Ограничения, вводимые на Expression таковы, что модифицировать созданное дерево выражений нельзя, поэтому придется строить новые.
Посмотрим на примере, что нам необходимо сделать для этого. Например, исходная функция выглядит следующим образом:
Path = 10 + (6+5)*(3+X) — Math.Sin (6×5) По данному выражению построится следующее дерево:
Представим его в таком виде, что путь от листа, содержащего Х, до вершины Path, был прямым, а остальные узлы расположим сверху и снизу:
На таком рисунке становится видно, что для того чтобы построить обратное дерево выражений, требуется заменить все функции, стоящие в узлах на пути от листа с переменной X до корня с результатом Path на обратные, а затем поменять порядок их применения на обратный:
Ответвления, вычисляющие констатные значения, инвертировать не нужно. В результате мы получим обратное дерево выражений, из которого получается обратная функция:
X = ((Path — 10) + Math.Sin (6×5)) / (6 + 5) — 3 Поиск переменной, валидацию и построение обратного дерева выполняется всего одной рекурсивной функцией.В настоящее время поддерживается следующий список функций, для которых автоматически определяются обратные:
+, -, *, / ! Math.Sin, Math.ASin Math.Cos, Math.ACos Math.Tan, Math.ATan Math.Pow Math.Log Полученное дерево выражений вычисляется и компилируется в анонимную функцию также всего 1 раз, при первом срабатывании обратного связывания.Недостатки решения Как и любое другое, данное решение обладает рядом ограничений и недостатков. Выявленные недостатки перечислены ниже.1.) Если одно из sourceProperty имеет значение null, то у него становится невозможным определить тип на стадии создания Expression, поскольку typeof (null) не возвращает ничего. Это делает невозможным корректную обработку например такого выражения:
Оператор Замена в Path Примечание && and || or Введено для симметрии, необязательно < less <= less= 3.) В CalcBinding нельзя задать свой собственный конвертер. Я не придумал сценария, в котором требовалась бы такая возможность, поэтому если у вас есть предложения, буду рад их прочитать.
Ссылки на проект: Библиотека доступна на github. В проекте находятся исходные коды библиотеки, полноценный пример использования всех возможностей и тесты. Для библиотеки создан nuget пакет, доступный по адресу: www.nuget.org/packages/CalcBinding/Ссылки на использованные источники: [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 Статьяmsdn.microsoft.com/en-us/vstudio/bb894665.aspx Ссылка для скачивания[3] connect.microsoft.com/VisualStudio/feedback/details/677766/system-linq-dynamic-culture-related-floating-point-parsing-error[4] github.com/davideicardi/DynamicExpresso