Quite often I have wanted to adjust a bound value in my XAML by a mathematical formula.
For example, I might want to size a control to 50% of it’s parent’s size. Or I’ll want to position it 30% down and 20% left of a parent panel. Or I might even want to make a control’s width something like 50% of (WindowHeight – 200). Of course, I could always write converters for this, but I got tired of doing that repeatedly so sat down one day to write one big MathConverter.
Using the Math Converter
Using the MathConverter is simple. All you have to do is pass the Converter a math equation as the ConverterParameter, substituting @VALUE for your bound value. The equation can consist of numbers, basic operators (+, -, *, /, %), parentheses, and @VALUE.
For example, if you wanted a control whose height was 50% of the current Window Size, you would create a binding that looked like this:
Height="{Binding ElementName=RootWindow, Path=ActualHeight, Converter={StaticResource MathConverter}, ConverterParameter=@VALUE/2}"
Or if you wanted to make the control’s width equal to 30% of (WindowWidth – 200) your binding might look like this:
Width="{Binding ElementName=RootWindow, Path=ActualWidth, Converter={StaticResource MathConverter}, ConverterParameter=((@VALUE-200)*.3)}"
The Math Converter
Here’s the actual converter code.
It takes the ConverterParameter, substitutes the bound value for @VALUE, then starts reading the equation from left-to-right. If it encounters an operator, it performs the specified operation on the previous number and the next number in the list. If it encounters a parentheses, it handles the equation inside the group first, then replaces the grouped equation in the string with the result of the grouped equation. Any character it finds that is not a number, operator, or parentheses throws an exception.
// Does a math equation on the bound value. // Use @VALUE in your mathEquation as a substitute for bound value // Operator order is parenthesis first, then Left-To-Right (no operator precedence) public class MathConverter : IValueConverter { private static readonly char[] _allOperators = new[] { '+', '-', '*', '/', '%', '(', ')' }; private static readonly List<string> _grouping = new List<string> { "(", ")" }; private static readonly List<string> _operators = new List<string> { "+", "-", "*", "/", "%" }; #region IValueConverter Members public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { // Parse value into equation and remove spaces var mathEquation = parameter as string; mathEquation = mathEquation.Replace(" ", ""); mathEquation = mathEquation.Replace("@VALUE", value.ToString()); // Validate values and get list of numbers in equation var numbers = new List<double>(); double tmp; foreach (string s in mathEquation.Split(_allOperators)) { if (s != string.Empty) { if (double.TryParse(s, out tmp)) { numbers.Add(tmp); } else { // Handle Error - Some non-numeric, operator, or grouping character found in string throw new InvalidCastException(); } } } // Begin parsing method EvaluateMathString(ref mathEquation, ref numbers, 0); // After parsing the numbers list should only have one value - the total return numbers[0]; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); } #endregion // Evaluates a mathematical string and keeps track of the results in a List<double> of numbers private void EvaluateMathString(ref string mathEquation, ref List<double> numbers, int index) { // Loop through each mathemtaical token in the equation string token = GetNextToken(mathEquation); while (token != string.Empty) { // Remove token from mathEquation mathEquation = mathEquation.Remove(0, token.Length); // If token is a grouping character, it affects program flow if (_grouping.Contains(token)) { switch (token) { case "(": EvaluateMathString(ref mathEquation, ref numbers, index); break; case ")": return; } } // If token is an operator, do requested operation if (_operators.Contains(token)) { // If next token after operator is a parenthesis, call method recursively string nextToken = GetNextToken(mathEquation); if (nextToken == "(") { EvaluateMathString(ref mathEquation, ref numbers, index + 1); } // Verify that enough numbers exist in the List<double> to complete the operation // and that the next token is either the number expected, or it was a ( meaning // that this was called recursively and that the number changed if (numbers.Count > (index + 1) && (double.Parse(nextToken) == numbers[index + 1] || nextToken == "(")) { switch (token) { case "+": numbers[index] = numbers[index] + numbers[index + 1]; break; case "-": numbers[index] = numbers[index] - numbers[index + 1]; break; case "*": numbers[index] = numbers[index] * numbers[index + 1]; break; case "/": numbers[index] = numbers[index] / numbers[index + 1]; break; case "%": numbers[index] = numbers[index] % numbers[index + 1]; break; } numbers.RemoveAt(index + 1); } else { // Handle Error - Next token is not the expected number throw new FormatException("Next token is not the expected number"); } } token = GetNextToken(mathEquation); } } // Gets the next mathematical token in the equation private string GetNextToken(string mathEquation) { // If we're at the end of the equation, return string.empty if (mathEquation == string.Empty) { return string.Empty; } // Get next operator or numeric value in equation and return it string tmp = ""; foreach (char c in mathEquation) { if (_allOperators.Contains(c)) { return (tmp == "" ? c.ToString() : tmp); } else { tmp += c; } } return tmp; } }
One day I plan on expanding this further to use an IMultiValueConverter that accepts multiple bindings, but I haven’t had a need for that yet so it hasn’t gotten done.
Thanks so much, Rachel!
For others who would like to use this with Xamarin Forms, the changes required are only imports and adding a ConvertBack method which can throw a NotImplementedException.
Hi Rachel, This converter is Great. Nice Job. I noticed a small defect in it. when I pass in have an equation like “(@VALUE*(4/10))”. To fix it simply check the nextToken first in the following bit of code:
// Verify that enough numbers exist in the List to complete the operation
// and that the next token is either the number expected, or it was a ( meaning
// that this was called recursively and that the number changed
if (numbers.Count > (index + 1) &&
nextToken == “(” || double.Parse(nextToken) == numbers[index + 1])
{
Thanks Again.
Tim
Hi Rachel, thanks for your great code! What’s the code’s license, may we use it in a commercial project? š
Hi Simon, I have no license for my code. Its nice if there’s a link pointing to where you got it from, but all the code I post on here is relatively simple and could easily be reproduced by another developer looking to accomplish the same thing, so I don’t feel the need to license any of it. Thanks for asking though!
Excellent job! By the way, I was able to trigger the InvalidCastExpection because the number passed was “9.124678E”. This value is from a bound double property and the fomula was only “@VALUE*50”. I believe the solution is to use NumberStyles.Float in the double.TryParse. Hope that helps!
Great stuff. š
One more question: can this be extended to handle multiple binding? For example, to be able to use @VALUE, @VALUE2, @VALUE3 in the expression? Thanks.
Yes, its easy to change this to an IMultiValueConverter and do a .Replace on each @VALUE#. The only thing to remember is loop backwards through the @VALUES so you don’t accidently replace something like @VALUE10 with @VALUE1.
I actually have the code for my MultiValueConverter on Stack Overflow here if you want.
Cool. I will check on that. Thanks.
Thanks so much for providing this; it worked great!
Can the following equation be included? (@VALUE*(-1))
Hrrmm it would probably throw an exception on that one because there is no number before the operator, but that could be easy to fix. Would probably need to modify the if statement before the switch(token) in EvaluateMathString and have it use a 0 by default if no number is found on the left side of the operator.
I added the following operators :
case “>”:
numbers[index] = Math.Max(numbers[index] , numbers[index + 1]);
break;
case “0<500 will keep the value between 0 and 500
Good code and thank you.
Nice one!
Really useful, I can now forget entirely about converters! Thank you
Thank you Rachel, very helpful!
This code doesn’t compile. _allOperators.Contains(c) is not valid.
Hi Paul, add a reference to System.Linq