MathConverter is available on Nuget. There are three packages:
| Nuget Package | UI Framework | Target Frameworks |
|---|---|---|
| MathConverter | WPF |
|
| MathConverter.XamarinForms | Xamarin.Forms |
|
| MathConverter.Maui | .NET MAUI |
|
To install MathConverter, run the one of the following commands in the Package Manager Console:
PM> Install-Package MathConverter
PM> Install-Package MathConverter.XamarinForms
PM> Install-Package MathConverter.Maui
MathConverter allows you to do Math in XAML.
MathConverter is a powerful Binding converter that allows you to specify how to perform conversions directly in XAML, without needing to define a new IValueConverter in C# for every single conversion.
It's as easy as 1-2-3.
1) Install the Nuget package.
2) Add a MathConverter resource.
<Application.Resources>
<math:MathConverter x:Key="Math" />
</Application.Resources>The math namespace is defined as follows*:
xmlns:math="http://hexinnovation.com/math"3) Do Math. Now, you can use MathConverter on any Binding. Specify a ConverterParameter to specify the rules of the conversion.
*Note: In some targets (e.g. .NET Standard 1.0), you might have to define the
mathnamespace asxmlns:math="clr-namespace:HexInnovation;assembly=MathConverter.XamarinForms"
Suppose we want to make a rounded rectangle. If we create a Border and bind bind its CornerRadius to its own ActualHeight, we end up with a flattened oval (Full XAML file):
<Border CornerRadius={Binding ActualHeight}" … />We can use MathConverter to instead bind to ActualHeight / 2 (Full XAML file):
<Border CornerRadius="{Binding ActualHeight, ConverterParameter=x/2, Converter={StaticResource Math}}" … />The simple conversion of ActualHeight / 2 works well, as long as the rectangle is wider than it is tall. If we need to make a rounded rectangle of an arbitrary size, we need to use a MultiBinding to set the CornerRadius to the smaller of the ActualWidth and the ActualHeight divided by two (Full XAML file):
<Border.CornerRadius>
<MultiBinding ConverterParameter="Min(x,y)/2" Converter="{StaticResource Math}">
<Binding Path="ActualHeight" />
<Binding Path="ActualWidth" />
</MultiBinding>
</Border.CornerRadius>Alternatively, we can use the math:Convert MarkupExtension, which is more elegant syntax for creating a MultiBinding. Under the covers, this works by actually creating a MultiBinding for us:
CornerRadius="{math:Convert 'Min(x,y)/2', x={Binding ActualHeight}, y={Binding ActualWidth}}"Note: Instead of using the
Minfunction, we could also use the ternary operator:ConverterParameter = "(x > y ? y : x) / 2", but that's a little cumbersome to add to XAML.Note: MathConverter can take any number of Bindings. The first binding's value can be accessed by
xor[0], the second can be accessed byyor[1], and the third can be accessed byzor[2]. Any value beyond the third can be accessed only by its index:[3],[4], etc.The
math:ConvertMarkupExtension is limited to ten variables:x,y,z, andVar3throughVar9. If you need more than ten variables, you're probably doing something wrong. But if you insist on usingMathConverterwith an obsene number of parameters, you'll have to use aMultiBinding.
math:Convertis a wrapper aroundMultiBinding, notBinding. If you're binding to only one variable, there's considerably less overhead if you simply use aBindingwith aMathConverter.
You can specify multiple values for types like CornerRadius, Thickness, Size, Point, and Rect, just like in normal XAML. For example, we can specify different values for vertical/horizontal margins (Full XAML file):
<Rectangle Fill="Green" … Margin="{Binding Source={StaticResource Margin}, ConverterParameter=0;x, Converter={StaticResource Math}}" />Note: To facilitate entering multiple values into XAML, MathConverter, commas and semicolons are equivalent. We can use either one as separators between the values. So the following margins are equivalent:
ConverterParameter=0;xConverterParameter='0,x'ConverterParameter=0;x;0;xConverterParameter='0,x;0,x'ConverterParameter=' $`0,{x},0,{x}` '(See the interpolated strings documentation)
The ConverterParameter is optional. When it is omitted, MathConverter will attempt to convert all of the binding values string-joined with a comma.
In this example, we create two different GridLength (margin) values: one by specifying Margin for all four sides, and the other by specifying Margin for horizontal margins, and SmallMargin for vertical margins (Full XAML file).
The first essentially converts as Margin="20". The second converts as Margin="20,10".
<Border BorderThickness="1" BorderBrush="Black" Grid.Row="2" Margin="{Binding Source={StaticResource Margin}, Converter={StaticResource Math}}">
<Border BorderThickness="1" BorderBrush="Red">
<Border.Margin>
<MultiBinding Converter="{StaticResource Math}">
<Binding Source="{StaticResource Margin}" />
<Binding Source="{StaticResource SmallMargin}" />
</MultiBinding>
</Border.Margin>
</Border>
</Border>Alternatively, we could use the Convert MarkupExtension, which creates a MultiBinding for us:
<Border … Margin="{math:Convert x={Binding Margin}, y={Binding SmallMargin}}" />Suppose you want to show a Control based on a Boolean condition. You can simply bind the Visibility parameter and use the ternary operator to convert the boolean to a Visibility (Full XAML file):
<TextBox Visibility="{Binding IsChecked, ElementName=CheckBox, ConverterParameter='x ? `Visible` : `Collapsed`', Converter={StaticResource Math}}" />There's a lot going on in this conversion, so let's take this one slowly.
The conversion parameter is x ? `Visible` : `Collapsed`. MathConverter allows us to input strings very similarly to C#. To more easily facilitate adding strings to XAML, we can use " (double quote), ' (single quote), or ` (grave) characters to start and end a string.
Suppose that x evaluates to true (CheckBox.IsChecked is true). Then, x ? `Visible` : `Collapsed` would evaluate to a System.String of "Visible". Since we're binding to a property of Visibility, MathConverter later converts this value to Visibility.Visible for us.
Note: You can backslash-escape characters such as
\tand\nin strings, just like C#. Additionally, you can backslash-escape double quotes, single quotes, and grave characters.Note: All strings must start and end with the same character. So a ConverterParameter of
'Hello, world"would throw an exception because'and"do not match, whereas ConverterParameters of`Hello, world`,"Hello, world", and'Hello, world'are equivalent.
Not only can we include arbitrary strings in the ConverterParameter, we can also use interpolated strings to format arbitrary strings (Full XAML file):
<TextBlock Text="{Binding NumClicks, ConverterParameter='$`You have clicked the button {x} time{(x == 1 ? `` : `s`)}.`', Converter={StaticResource Math}}" />Interpolated strings work the same way as they do in C#. The same rules above apply: a string must start and end with the same character. For example, the following are all valid interpolated strings: $'Coordinates: ({x:N2},{y:N2}).', $"The weather outside is {x}.", $`Progress: {x:P} complete`, whereas the string $'Invalid" would throw an exception since ' and " do not match.
Note: Just like in C#, an interpolated string is just a wrapper around a call to the
Formatfunction (which usesstring.Format), so the converter parameter$`Hello, {x}`is equivalent toFormat('Hello, {0}', x).
We've already alluded to Min and Format. There are many more functions built into MathConverter, and you can always add your own functions (see the "Custom Functions" section). For now, we're just going to cover some of the functions built into MathConverter.
Functions are case-sensitive (They were not case sensitive in version 1.x).
Functions include:
Now()returnsSystem.DateTime.NowUnsetValue()returnsDependencyProperty.UnsetValueorBindableProperty.UnsetValueCos(x),Sin(x),Tan(x),Abs(x),Acos(x)/ArcCos(x),Asin(x)/ArcSin(x),Atan(x)/ArcTan(x),Ceil(x)/Ceiling(x),Floor(x),Sqrt(x),Log(x, y),Atan2(x, y)/ArcTan2(x, y),Round(x)/Round(x, y)all behave like their counterparts inSystem.Math. They returnnullif at least one argument isnull.Deg(x)/Degrees(x)returnsx / pi * 180Rad(x)/Radians(x)returnsx / 180 * piToLower(x)/LCase(x)returns$"{x}".ToLower()ToUpper(x)/UCase(x)returns$"{x}".ToUpper()TryParseDouble(x)will attempt to cast/convertxtodoubleor cast/convertxtostringand parse it to double. The function returnsnullif it fails to convert the input.StartsWith(x, y)will returntrueorfalseif it can cast/convertxto string, based on ifxstarts withyor$"{y}". Ifxis not a string or$"{y}".Lengthis0, the function returnsnullinstead.EndsWith(x, y)behaves the same way asStartsWithexcept it detects ifxends withy.Contains(x, y)is a bit different.xcan be anIEnumerable, in which case we check to see if it containsy, or ifxis a string, the function checks ifxcontains$"{y}". If$"{y}".Lengthis zero (but notably, not ify == ""), then the function returnsnullinstead.IsNull(x, y)/IfNull(x, y)are equivalent tox ?? yAnd(),Or(), andNor()each accept an arbitrary number of functions. They use reflection to call the logical operators UnaryNot (Nor(x, y)evaluates as!Or(x, y)), BitwiseAnd, and BitwiseOr. This means that we can accept and return non-boolean values, provided that their types would compile with&&,||, and!in C#. We only evaluate as many parameters as we need to. For example,And(…)will evaulate parameters only until it encounters a false value, in which case it will return the false value.Max(),Min(), andAvg()/Average()ignore values that can't be converted to double, and returnnullif no they do not encounter any numeric values.Format()simply returnsstring.FormatConcat()simply returnsstring.Concat.Join()simply returnsstring.Join.GetType(x)simply returnsx?.GetType()ConvertType(x, y)will do whatever it can to convertxto typey. Ifyis not aTypeorxcannot be converted, the function returnsxinstead. Because TypeConverters are inconsistent, we always use InvariantCulture when converting.EnumEquals(x, y)will see if two enum values are equal. Example use cases:EnumEquals(x, `Visible`),EnumEquals(`Visible`, x). If two enum values are different types are compared,EnumEqualswill returnfalse, even ifx.Equals(y)is true for the same inputs in C#.Throw()will throw an exception when evaluated. The exception contains helpful information for debugging issues with a conversion.TryCatch()takes two or more arguments, and returns immediately as soon as it finds an argument that does not throw an exception. If every argument throws an exception,TryCatch()will not catch the last exception.
Using these operators, you can do very powerful things. One such example (Full XAML file):
<TextBlock Text="{Binding Source={x:Type sys:TimeSpan}, ConverterParameter='$`Six hours from now, the time will be {Now() + ConvertType(`6:00:00`, x):h\':\'mm\':\'ss tt}`', Converter={StaticResource Math}}" />We use ConvertType to convert "6:00:00" from string to TimeSpan, then add that TimeSpan to the current time, and format it with the format string "h:mm:ss tt".
MathConverter's built-in functions are implemented in CustomFunctions.cs. Those classes can be used as examples to follow to create your own custom functions. This allows you to effectively extend MathConverter to do whatever you want.
The main window of our demo app is a perfect example (Full XAML file).
We have a ListBox with Types added.
<ListBox>
<ListBox.Items>
<x:Type TypeName="demos:FlattenedOval" />
<x:Type TypeName="demos:WideRoundedRectangle" />
<x:Type TypeName="demos:TrueRoundedRectangle" />
<!-- More Types -->
</ListBox.Items>
<!-- More stuff -->
</ListBox>The ListBox uses a DataTemplate to show a TextBox for each item. We use the custom function GetWindowTitle() to convert the Types to a display value.
<TextBlock Text="{Binding ConverterParameter='GetWindowTitle(x)', Converter={StaticResource Math}}" />The GetWindowTitle function is added to MathConverter as follows:
<Window.Resources>
<math:MathConverter x:Key="Math">
<!-- "GetWindowTitle" in the parameter will invoke the `GetWindowTitleFunction` function. -->
<math:CustomFunctionDefinition Name="GetWindowTitle" Function="functions:GetWindowTitleFunction" />
</math:MathConverter>
</Window.Resources>GetWindowTitleFunction is defined as follows (Full C# file):
public class GetWindowTitleFunction : OneArgFunction
{
public override object Evaluate(CultureInfo cultureInfo, object argument)
{
return argument is Type t && t.IsAssignableTo(typeof(Window)) ? ((Window)Activator.CreateInstance(t)).Title : null;
}
}So, our GetWindowTitleFunction instantiates the Type and get the Title property of the resulting Window.
In this example, GetWindowTitleFunction extends OneArgFunction, but all that matters is that we extend CustomFunction. It is recommended that you implement one of its predefined subclasses:
- ZeroArgFunction
- OneArgFunction
- OneDoubleFunction
- TwoArgFunction
- ArbitraryArgFunction
Again, there are plenty of examples in CustomFunctions.cs.
Suppose you don't like how a function is implemented. You can always override the function with your own custom function.
As a concrete example, we can implement a CustomAverageFunction function. This is similar to MathConverter's built-in AverageFunction, except that it rounds each input, instead of simply taking the average.
<TextBlock x:Name="TextBlock" Text="{Binding ConverterParameter='`Average(1, 1.5) returns ` + Average(1, 1.5)', Converter={StaticResource Math}}" />With a little bit of code-behind, we can remove and replace the Average function with our CustomAverageFunction:
private void RadioButton_Changed(object sender, RoutedEventArgs e)
{
if (!(FindResource("Math") is HexInnovation.MathConverter math))
return;
if (UseStockFunction.IsChecked == true)
{
// Go back to the stock function.
math.CustomFunctions.Clear();
math.CustomFunctions.RegisterDefaultFunctions();
}
else
{
// Remove the default Average function and define our own.
math.CustomFunctions.Remove("Average");
math.CustomFunctions.Add(CustomFunctionDefinition.Create<MyCustomAverageFunction>("Average"));
}
// Tell the TextBlock to refresh its binding again.
TextBlock?.GetBindingExpression(TextBlock.TextProperty).UpdateTarget();
}Note: In this example, the built-in
Averagefunction is still available with the nameAvg. Most functions are not defined with multiple names (see the "Functions" section).
MathConverter's ConverterParameter syntax is very similar to C#, so you can generally expect it to behave just like C#. We follow the standard C# rules regarding operator ordering, except as noted below:
- Since
MathConverteris specifically designed to perform math calculations, the caret (^) operator does not perform theXORoperation. Rather, it is an exponent symbol. It usesSystem.Math.Powto evaluate expressions, and its precedence is just above multiplicative operations (*,/, and%). - The multiplication operator can often be safely ommitted. A
ConverterParametervalue ofxyzwill evaluate tox*y*z. The parameterx2ywill evaluate tox^2*y(or equivalently,xxyorx*x*y). Similarly,2x3is equivalent to2*x^3or2*x*x*x. Note thatx(2)is equivalent tox*(2), in the same way thatx(y+z)is equivalent tox*(y+z). Note that1/xywill evaluate to1/x*y, not to1/(x*y), as you might expect. MathConverterdoesn't support all of the operations that C# does. The following operators are examples of those not supported:- Assignment operators (
=,+=,&&=, etc) - Logical operators (
|,&, and^asXOR)- Note that
||and&&are supported operators.
- Note that
switchandwithexpressions are not supported.isandas(since Types are not supported)- Bitwise operations (
<<,>>,~) are not supported. - The unary operators
++and--are not supported, since they change the values of the inputs. - Primary operators (
x.y,f(x),a[i],new,typeof,checked,unchecked,default,nameof,sizeof) are not supported.
- Assignment operators (
MathConverter uses reflection to evaluate operator calls, so you can use custom types with custom operator implementations and MathConverter will use those operators while converting.
Generally, MathConverter will favor using double values over other numeric types. When evaluating which operator to call, MathConverter will convert any operands to double, if possible, before calling the operator. If an input is of type char, it will convert to int then convert to double. Where a path to implicitly convert an operand to double exists, MathConverter will convert for you in order to apply an operator that takes numeric inputs.
Hence, supposing x = 1 (an integer), C# would evaluate that 1 + x/2 = 1, since (int)1 / 2 = 0. MathConverter will implicitly converter all variables to doubles. So, the expression 1 + x/2 is evaluated as 1.0 + (double)x/2.0, so MathConverter will return 1.5.
Each time a conversion must be made, MathConverter must parse and evaluate an expression. When it parses an expression, it reads through the string one character at a time, and returns a syntax tree. The parsing is done in the Parser class. The Parser returns an AbstractSyntaxTree for each comma-separated (or semicolon-separated) value. In an effort to improve efficiency, MathConverter uses a cache to save the AbstractSyntaxTrees for each string it evaluates. Therefore, if you have a lot of conversion strings, it is discouraged to use the same MathConverter instance across your entire application. It is a better idea to use a different MathConverter object for each UserControl, Page, or Window. You can turn off caching on a per-instance basis:
<math:MathConverter x:Key="nocache" UseCache="False" />There are a few breaking changes from version 1.
- Function names are now case-sensitive.
e,pi,null,true, andfalsekeywords are now required to be lower-case.VisibleOrCollapsedandVisibleOrHiddenfunctions were deprecated, and will be removed in a future release. You should change your conversions fromVisibleOrCollapsed(x)tox ? `Visible` : `Collapsed`- There are several small differences in how/when types are converted. For example, we no longer convert from int to double unless it needs to be used as an operand in an operator such as
+,*, etc.









