Update (2008/09/25)
Added a ScriptExtension
and mentioned the LambdaExtension.
Introduction
Back when I started WPF, I thought Data Binding was great but, sometimes, I just couldn't get the information I needed from the value easily enough.
Here, I would describe a simple fictitious problem. Let's say, I want to display a list of integers, with the background color depending on whether the number is even or odd.
The XAML for this might looks like this:
<ListBox ItemsSource="{Binding List, ElementName=root}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}"
Background="{?? what shall I put here ??}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Upon Initial Investigation
Initially, I was thinking of writing my own MarkupExtension which would have an expression property and run some sort of script.
It turns out:
- You can't do that. The
Binding
extension does some things you can't do. For example, you can't always know when a property changes. You can't find an object by name (as with the ElementName
property of the Binding
extension), etc.
- There is a better way. The
Binding
extension already does all the work, all you need is a smart IValueConverter
to set to the Binding.Converter
property.
What’s an IValueConverter
When you do data binding in WPF, the framework automatically does some conversion for you, so that the provided value can be used to set the target property. But, what if you want to modify the behavior a little bit, and decide yourself how to do the conversion? You can do so by setting the Converter
property of the Binding
to an IValueConverter
!
public class MyConverter : IValueConverter
{
#region IValueConverter Members
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
public object ConvertBack(object value, Type targetType,
object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
#endregion
}
Now, the code can look like this:
Window1.xaml
<Window.Resources>
<local:ColorConverter x:Key="int2color"/>
</Window.Resources>
<Grid>
<ListBox ItemsSource="{Binding List, ElementName=root}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}"
Background="{Binding Converter={StaticResource int2color}}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
ColorConverter.cs
public class ColorConverter : IValueConverter
{
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
if (!(value is int))
return null;
int i = (int)value;
return i % 2 == 0 ? Brushes.Red : Brushes.Green;
}
}
Now, every time I encounter such a problem, I can write a new IValueConverter
. Wait, Nooooooooo!!!!…….
Seriously, can't I just write some built-in (in the XAML) quick code?
Script Investigation
Initially, I found the Ken Boggart converters (Sorry, no link, my Google skills have apparently decreased with old age). They even have a script based converter. I didn't like them enough because they were using a quick and dirty script engine, tightly coupled with the value converter.
I also thought of using IronPython as the script engine. And, while this is a great and mature project, there is one thing which kills me with Python. Code blocks are defined with the indentation! Seriously… And, it’s just not an aesthetic problem. Imagine a moment: write some embedded Python script into your XAML, “accidentally” reformat the document (CTRL+E,D), and voila, you have just broken your Python script! Tsss.. too bad.
I thought of writing my own simple interpreter. My initial plan was to use ANTLR. And, while it’s a great project, with a great IDE, it’s a bit tedious to test / develop a grammar in C#, as all the tools are geared towards Java. Then, I discovered Irony. I haven't used it much (compared to ANTLR), but my first impression is:
- Writing the grammar is easier, better looking, and definitely more C# friendly than ANTLR.
- The next stage, the interpreter or AST visiting is much more tedious.
- Plus, ANTLR makes it very super easy to embed actions directly into the parser, enabling an all in one parsing / interpreting phase, for example.
But then, it turns out, I didn't need to write my own Irony powered interpreter / script engine, there is already one: Script.NET.
The ScriptConverter
Script.NET is a quick and easy script engine that can, as all self-respecting .NET script engines, call upon all .NET methods and classes. It can easily be embedded into your application; and the syntax is so simple, you can teach it to yourself in 21 minutes! Great, just what I was looking for. And, of course, it uses curly braces to define code blocks!
The current version of Script.NET, at this time (Sept. 2008), doesn't support the latest Irony. It ships as three DLLs on its own (plus the outdated Irony), and it has a dependency on Windows Forms. I decided to “fix” all of that, and the version shipped in the zipped source code is not the original Script.NET source code, but a custom version which comes as only one DLL, with no WinForms dependency.
In my converter, I had to, obviously, add an expression property (for the script). Less obvious, but as critical, I had to add a known types property, so that the script could call on a static
property or on some class constructor.
How Does It Work
Well, to start with, you create a script interpreter with just one line:
Script script = Script.Compile("my script string");
Then, you can define some global variables, or add known types, as follows:
script.Context.SetItem("value", ContextItem.Variable, value);
script.Context.SetItem("Brushes", ContextItem.Type, typeof(Brushes));
And then, you can evaluate it in one line as well:
return script.Execute();
A little problem which stopped me for a few minutes was, how do I return something from my script?
Well, a simple value is an expression, and the last computed expression is the returned value. For example, “1
” is a valid script which returns 1
.
Script.NET doesn't support the (bool_expr ? true_expr : false_expr
) operator, but you can express it as:
if (bool_expr)
true_expr;
else
false_expr;
First Solution
ScriptConverter.cs
[ContentPropertyAttribute("Expression")]
public class ScriptConverter : IValueConverter
{
Script script;
TypeDictionary knownTypes = new TypeDictionary();
string expression;
public string Expression
{
get { return expression; }
set
{
expression = value;
script = Script.Compile(expression);
SetTypes(script, knownTypes);
}
}
public class TypeDictionary : Dictionary<string, Type> { }
public TypeDictionary KnownTypes
{
get { return knownTypes; }
set
{
knownTypes = value;
SetTypes(script, knownTypes);
}
}
static void SetTypes(Script ascript, TypeDictionary types)
{
foreach (var item in types)
ascript.Context.SetItem(item.Key, ContextItem.Type, item.Value);
}
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
script.Context.SetItem("value", ContextItem.Variable, value);
script.Context.SetItem("param", ContextItem.Variable, parameter);
return script.Execute();
}
}
Now, armed with this ultra cool converter, my XAML is now ready to rock!
You will notice that I initialized the Script with a list of types it needs to know to evaluate the expression.
Window1.xaml
<Window.Resources>
<local:ScriptConverter x:Key="int2color">
<local:ScriptConverter.KnownTypes>
<x:Type x:Key="Brushes" TypeName="Brushes"/>
</local:ScriptConverter.KnownTypes>
if( value % 2 == 0 )
Brushes.Red;
else
Brushes.Green;
</local:ScriptConverter>
</Window.Resources>
<Grid>
<ListBox ItemsSource="{Binding List, ElementName=root}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}"
Background="{Binding Converter={StaticResource int2color}}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
Further Simplification
Okay now I have done it all in XAML, and it works well. But I exchanged a useless C# file for a bloated XAML!
What I would like to do is just put the expression in the converter, like a lambda expression.
It’s where MarkupExtension(s) will come to the rescue. Basically extensions are a custom way to create an object with code running in the XAML. {x:Type …}
, {x:Static …}
, {Binding …}
are all MarkupExtensions
.
Creating your own extension is easy. Okay let’s write the simplified XAML I really would like to see, and then write the extension:
<ListBox ItemsSource="{Binding List, ElementName=root}">
<ListBox.ItemTemplate>
<DataTemplate>
<WrapPanel Orientation="Horizontal">
<TextBlock Text="{Binding}"
Background="{Binding
Converter={local:Script
'if(value%2==0) Brushes.Red; else Brushes.Green;',
{x:Type Brushes}}}"/>
</WrapPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Here we go, it’s a much simpler XAML code! In the Converter
parameter of the binding, I passed an extension which directly created my converter. This extension will take the following parameter: a script string
and a list of types necessary for the script to run.
Initially I was planning to use an extension constructor like that:
public ScriptExtension(string code, params Type[] types)
{
}
But it won't work because XAML won't recognize the “params Type[]
” construct, and it will expect 2 and only 2 parameter, the second being array of types. There are also a few other problems with markup extension parameter (XAML choose them sorely on the number of parameter, it doesn't do any type matching). Finally I used this code
public class ScriptExtension : MarkupExtension
{
public ScriptExtension(string code) { Init(code); }
public ScriptExtension(string code, Type type) { Init(code, type); }
public ScriptExtension(string code, Type type, Type t2) { Init(code, type, t2); }
protected ScriptExtension(string code, params Type[] types) { Init(code, types); }
void Init(string code, params Type[] types)
{
Code = code;
TypeDictionary dict = new TypeDictionary();
foreach (var aType in types)
dict[aType.Name] = aType;
KnownTypes = dict;
}
public string Code { get; private set; }
public TypeDictionary KnownTypes { get; private set; }
public override object ProvideValue(IServiceProvider isp)
{
return new ScriptConverter(Code) { KnownTypes = KnownTypes };
}
}
Designer Issue
It worked quite nicely at runtime. But there was a problem with the designer. It just could not work with those multiple constructors. In the end, I created multiple ScriptExtension
subclasses called Script0Extension
, Script1Extension
, Script2Extension
, etc… depending on the number of type parameters.
Hence, the final XAML looked like that:
<ListBox.ItemTemplate>
<DataTemplate>
<WrapPanel Orientation="Horizontal">
<TextBlock Text="{Binding}"
Background="{Binding
Converter={local:Script1
'if(value%2==0) Brushes.Red; else Brushes.Green;',
{x:Type Brushes}}}"/>
</WrapPanel>
</DataTemplate>
</ListBox.ItemTemplate>
Alternate Solution
At this stage, we got a pretty good solution for many problems. But then I investigated this LambdaExtension. It’s really cool and I added it to my personal toolbox and to this ExpressionExplorer
project as well.
It lets you define converters with Lambda expression, for example:
<TextBlock Text='{Binding Source={x:Static s:DateTime.Now},
Converter={fix:Lambda "dt=>dt.ToShortTimeString()"}}'>
It’s pretty good and I imagine Lambda expression would be way faster that my script expression. However it suffers from a few problems:
- You cannot use the other parameters of the converter method (parameter and culture).
- You cannot reference any type, you cannot return a
Brush
, i.e. it cannot solve my simple problem!
But I guess that, even with these shortcomings, it can still make an excellent converter in DataTriggers.
Additional Information
Here are some people or pages that have inspired me while working on this code:
Later on, I had additional feedback which proved to be very valuable.
First, Orcun Topdagi has improved his LambdaExtension further, as you can check out on his blog about WPFix2 and WPFix3. Wow, those are real hot goodies. I might even ditch my extension in his favor! But I have to spend some more time playing with it first (kind of super busy right now).
Also, Daniel Paull has some very interesting blog posts (here, here and here) on how to do something similar with the DLR. What I really like about these articles is that they are an easy introduction to the DLR.
History
- 30th September, 2008: Section on "Additional Information" updated
- 25th September, 2008: Article updated
- Already a first fix!
- I updated the code so there is better designer support. Also, there is my (currently experimental) work on the
LambdaExtension
as well in this assembly (from fikrimvar).
- 24th September, 2008: First release!