C1Binding Expressions in Depth
Introduction
WPF and Silverlight use Binding objects to connect data values to UI properties. For example, to display the name of a customer in a TextBlock element, you could use this XAML:
<TextBlock
Text="{Binding FirstName}" />
In this example, the Text property of the TextBlock will be automatically synchronized with the content of the FirstName property of the data object currently assigned to the DataContext property of the TextBox element or any of its ancestors. The Binding class is convenient but not very flexible. For example, if you wanted to bind the TextBlock above to the customer's full name, or to make the text red when the customer account is negative, you would need a Converter. The Converter parameter of the Binding object allows you to add arbitrary logic to the binding, converting values when they are transferred between the source and the target. To illustrate, and still using the example above, imagine we wanted the TextBlock to show the customer's full name, and to display the text in red when the customer's account is negative. To accomplish this, we would need two converters, implemented as follows:
/// <summary>
/// Converter for Foreground property: returns a red brush if the
/// customer amount is negative, black otherwise.
/// </summary>
public class ForegroundConverter : System.Windows.Data.IValueConverter
{
public object Convert(object value, Type targetType, object parameter,
CultureInfo culture)
{
var c = value as Customer;
return c.Amount < 0
? new SolidColorBrush(Colors.Red)
: new SolidColorBrush(Colors.Black);
}
public object ConvertBack(object value, Type targetType, object parameter,
CultureInfo culture)
{
throw new NotImplementedException();
}
}
/// <summary>
/// Converter for customer's full name: returns a string containing
/// the customer's full name.
/// </summary>
public class FullNameConverter : System.Windows.Data.IValueConverter
{
public object Convert(object value, Type targetType, object parameter,
CultureInfo culture)
{
var c = value as Customer;
return string.Format("{0} {1}", c.FirstName, c.LastName);
}
public object ConvertBack(object value, Type targetType, object parameter,
CultureInfo culture)
{
throw new NotImplementedException();
}
}
Once the converters have been defined, they can be used in XAML as follows:
<Grid>
<Grid.Resources>
<local:FullNameConverter x:Key="_cvtFullName" />
<local:ForegroundConverter x:Key="_cvtForeground" />
</Grid.Resources>
<TextBlock
Text="{Binding Converter={StaticResource _cvtFullName}}"
Foreground="{Binding Converter={StaticResource _cvtForeground}}" />
</Grid>
This solution works fine, but there are a few shortcomings:
- You can't tell what the output will look like by looking at the XAML. The color converter for example could return any color, using any logic. The only way to figure this out would be to look at the converter implementation.
- If you wanted to set the Opacity, or Visibility, or FontWeight properties of the TextBlock element, you would need to create more converter classes, and they would be tightly coupled with your XAML.
- The semantics of the converter class only stipulate that it should convert an object into another object. This provides a lot of flexibility, but it also makes converters relatively fragile.
The C1Binding Class
The C1Binding class addresses these issues by allowing you to use rich expressions in your bindings. Using C1Binding, the example above could be written as:
<TextBlock
Text="{c1:C1Binding Expression='concatenate(FirstName, | |, LastName)' }"
Foreground="{c1:C1Binding Expression='if(Amount < 0, |red|, |black|)' }" />
Notice how much simpler and more direct this version is. There are no resources and no converters. The meaning of the bindings is clear from the XAML. If the XAML author decided that the full name should be displayed as "Last, First" instead, they could make the change directly in the XAML. If they wanted negative amounts to be shown in bold, that would be easy too:
<TextBlock
Text="{c1:C1Binding Expression='concatenate(FirstName, | |, LastName)' }"
Foreground="{c1:C1Binding Expression='if(Amount < 0, |red|, |black|)' }" />
FontWeight="{c1:C1Binding Expression='if(Amount < 0, |bold|, |normal|)' }" />
In the expressions above, the vertical bars represent quotes. They could also be written as ", but the vertical bars are easier to type and read. The "Expression=" term in the expressions above is optional in WPF. It is required in Silverlight 5.
C1Binding Expression Syntax
C1Binding objects use the C1CalcEngine class to parse and evaluate expressions. The syntax supported is similar to the one used in Excel. You can use all the usual logical operators (=, >, <,.<>, >=, <=), arithmetic (+, -, , /, ^), and you can group expressions using parentheses. For example: "1+2\3 < (1+2)*3" returns TRUE (7 < 9). Any public properties in the binding source object can be used in expressions as well. For example: "Price * 8.5%" returns the value of the Price property times 0.085. Properties that return collections or dictionaries are also supported. For example, assuming the DataContext object has a Children property: "Children(Children.Count-1).Name" returns the name of the last child. Finally, the C1CalcEngine supports a subset of the functions available in Excel. These are documented in Appendix I.
C1Binding Usage Scenarios
Simple Arithmetic: Suppose you want to display taxes due on a certain amount. Using traditional bindings, you would need a converter to calculate the tax amount. Using C1Binding, you can calculate the amount using a simple expression:
<TextBlock
Text="{c1:C1Binding Expression='Amount * 8.5%' }" />
Combining Values: Suppose you want to extend the scenario above and display the total amount in addition to the tax amount. You could do that using several TextBlock elements, but it would be more efficient to use a single TextBlock and a C1Binding based on the CONCATENATE function:
<TextBlock
Text="{c1:C1Binding Expression=
'CONCATENATE(Amount, | Tax: |, Amount * 8.5%' }" />
Formatting Values: C1Binding and regular Binding objects have a StringFormat property that you can use to control the format of bound values. If your binding expression requires formatting several parts of the output, you can use the TEXT function as follows:
<TextBlock
Text="{c1:C1Binding Expression=
'CONCATENATE(TEXT(Amount, |c|), | Tax: |, TEXT(Amount * 8.5%, |c|))' }" />
Conditional Formatting: Suppose you want to create a user interface where amounts greater than a certain value are displayed in bold and red. You can accomplish this using the IF function, which performs conditional logic:
<TextBlock
Text="{c1:C1Binding Expression='Amount', StringFormat='c'}"
Foreground="{c1:C1Binding Expression='if(Amount > 1000, |red|, |black|)' }" />
FontWeight="{c1:C1Binding Expression='if(Amount > 1000, |bold|, |normal|)' }" />
For a complete listing of supported syntax elements, see the online documentation.
C1Binding Limitations
C1Binding objects can only be used in one-way binding scenarios. The reason for this is simple: regular bindings are based on simple properties, which can be evaluated or assigned values. C1Binding, however, is based on expressions, which can be evaluated but not assigned to. For example, you can assign a new value to the "Amount" property, but not to the "Credit - Debit" expression. In addition to this, C1Binding objects have the same limitations as regular Binding objects:
- They can only be assigned to dependency properties, which only exist in dependency objects (such as UIElement or FrameworkElement objects).
- If the binding is to be automatically updated when the property changes, then the binding source object must implement the INotifyPropertyChanged interface.
- If the types of the source and target properties are not compatible, then the target property must have the appropriate converters (e.g. most types can be converted from string values, including Color, FontWeight, etc).
The C1CalcEngine Class
The C1CalcEngine class is responsible for parsing and evaluating the expressions used in C1Binding objects, but it is a public class and can also be used independently of C1Binding. The C1CalcEngine class has two main methods: Parse and Evaluate: 1) Parse parses strings into Expression objects. 2) Evaluate parses strings and evaluates the resulting expression.
C1CalcEngine Usage Scenarios
Calculated TextBox: Suppose you want to add a calculated TextBox to your application. Users should be able to type expressions into the TextBox. The expressions should be evaluated when the control loses focus or when the user presses Enter. You could do this with the following code:
public MainWindow()
{
InitializeComponent();
var tb = new TextBox();
tb.LostFocus += (s,e) => { Evaluate(s); };
tb.KeyDown += (s, e) => { Evaluate(s); };
}
void Evaluate(object sender)
{
var tb = sender as TextBox;
var ce = new C1.WPF.Binding.C1CalcEngine();
try
{
var value = ce.Evaluate(tb.Text);
tb.Text = value.ToString();
}
catch (Exception x)
{
MessageBox.Show("Error in Expression: " + x.Message);
}
}
Turning Grids into Spreadsheets: Another typical use for the C1CalcEngine class is the implementation of calculated cells in grid controls and data objects. We have used it to implement several samples that add spreadsheet functionality to grid controls. The code is too long to include here, but you can see one of the samples in action here: http://demo.componentone.com/Silverlight/ExcelBook/.
Conclusion
You can download C1Binding and C1CalcEngine as part of ComponentOne Studio for WPF and Studio for Silverlight (Silverlight 5 only).