Click here to Skip to main content
15,867,453 members
Articles / Desktop Programming / WPF
Article

The Last IValueConverter

Rate me:
Please Sign up or sign in to vote.
4.92/5 (21 votes)
2 Oct 2008CPOL7 min read 100.3K   487   38   32
Data-bind anything to anything with little work with this script powered IValueConverter

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.

ExpressionConverter

The XAML for this might looks like this:

XML
<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:

  1. 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.
  2. 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!

C#
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

XML
<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

C#
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:

C#
Script script = Script.Compile("my script string");

Then, you can define some global variables, or add known types, as follows:

C#
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:

C#
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:

C#
if (bool_expr)
    true_expr;
else
    false_expr;

First Solution

ScriptConverter.cs

C#
[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

XML
 <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!smile_embaressed

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:

XML
<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:

C#
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

C#
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; }

    // return the converter here
    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:

C#
<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:

XML
<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!

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) http://www.ansibleww.com.au
Australia Australia
The Australia born French man who went back to Australia later in life...
Finally got over life long (and mostly hopeless usually, yay!) chronic sicknesses.
Worked in Sydney, Brisbane, Darwin, Billinudgel, Darwin and Melbourne.

Comments and Discussions

 
AnswerAnother option that is a more complete implementation of same concept Pin
WPF Programmer11-Aug-17 4:53
WPF Programmer11-Aug-17 4:53 
SuggestionUsing TypeConverter Pin
Clifford Nelson9-Feb-12 11:23
Clifford Nelson9-Feb-12 11:23 
GeneralRe: Using TypeConverter Pin
Super Lloyd9-Feb-12 11:57
Super Lloyd9-Feb-12 11:57 
GeneralI have little different Issue then regular IValueconverter Pin
Chetan Sheladiya14-Jul-10 1:31
professionalChetan Sheladiya14-Jul-10 1:31 
GeneralFails if the bound type is decimal Pin
88Keys22-Sep-09 13:44
88Keys22-Sep-09 13:44 
GeneralRe: Fails if the bound type is decimal Pin
Super Lloyd23-Sep-09 1:25
Super Lloyd23-Sep-09 1:25 
GeneralThanks! Also, someones alternative Pin
AdrianoF31-Jul-09 5:17
AdrianoF31-Jul-09 5:17 
GeneralRe: Thanks! Also, someones alternative Pin
Super Lloyd31-Jul-09 13:28
Super Lloyd31-Jul-09 13:28 
GeneralGeneral solution using the DLR Pin
Daniel Paull29-Sep-08 19:16
Daniel Paull29-Sep-08 19:16 
AnswerRe: General solution using the DLR Pin
Super Lloyd30-Sep-08 3:10
Super Lloyd30-Sep-08 3:10 
GeneralRe: General solution using the DLR Pin
Daniel Paull30-Sep-08 4:45
Daniel Paull30-Sep-08 4:45 
GeneralRe: General solution using the DLR Pin
Super Lloyd30-Sep-08 12:21
Super Lloyd30-Sep-08 12:21 
Now there is WPFix2[^] and WPFix3[^], Wayyyyyyy better!

I do plan to have a deeper look at your work very soon as well! Wink | ;-)
Although... in a little while, I'm kind of super busy right now!

A train station is where the train stops. A bus station is where the bus stops. On my desk, I have a work station....
_________________________________________________________
My programs never have bugs, they just develop random features.

GeneralRe: General solution using the DLR Pin
Super Lloyd1-Oct-08 1:16
Super Lloyd1-Oct-08 1:16 
GeneralRe: General solution using the DLR Pin
Daniel Paull1-Oct-08 1:40
Daniel Paull1-Oct-08 1:40 
GeneralRe: General solution using the DLR Pin
Super Lloyd1-Oct-08 1:56
Super Lloyd1-Oct-08 1:56 
GeneralRe: General solution using the DLR Pin
Daniel Paull8-Oct-08 2:14
Daniel Paull8-Oct-08 2:14 
GeneralRe: General solution using the DLR Pin
Super Lloyd9-Oct-08 0:32
Super Lloyd9-Oct-08 0:32 
GeneralRe: General solution using the DLR Pin
Super Lloyd9-Oct-08 8:55
Super Lloyd9-Oct-08 8:55 
GeneralThanks Pin
M. Orcun Topdagi29-Sep-08 4:16
M. Orcun Topdagi29-Sep-08 4:16 
AnswerRe: Thanks Pin
Super Lloyd30-Sep-08 3:03
Super Lloyd30-Sep-08 3:03 
GeneralPerformance issues Pin
WillemM27-Sep-08 1:45
WillemM27-Sep-08 1:45 
AnswerRe: Performance issues Pin
Super Lloyd27-Sep-08 2:57
Super Lloyd27-Sep-08 2:57 
GeneralUber good [modified] Pin
Sacha Barber26-Sep-08 21:18
Sacha Barber26-Sep-08 21:18 
GeneralRe: Uber good Pin
Super Lloyd27-Sep-08 1:31
Super Lloyd27-Sep-08 1:31 
GeneralRe: Uber good Pin
Sacha Barber27-Sep-08 2:03
Sacha Barber27-Sep-08 2:03 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.