Share via


A Comparable DataTrigger

Property triggers today only check for equality. We’d like to add support for other comparison operators, but that hasn’t happened yet. But I needed them for a project, and wrote a workaround for it. It’s a bit hacky in a couple of places, but if you can get past that, it’s a handy way to simplify some coding.

 

Here’s a sample of what I ended up with:

 

<DataTrigger Binding="{l:ComparisonBinding Age, LT, 65}" Value="{x:Null}" >

 

The basics:

· You have to set the DataTrigger.Value to null. That’s the main hack.

· The supported comparison operators are GT, GTE, LT, LTE, and EQ.

· The comparand (“65” in the above example) is converted from string to the type of the target value (presumably Age is an int in the above example), using Compare.ChangeType or the target’s TypeConverter.

 

That’s all there is to use it. You have to remember to set DataTrigger.Value to null, otherwise it’s relatively straightforward.

 

And here’s the implementation:

 

//

// ComparisonBinding is a Binding that should be used in a DataTrigger.Binding.

// It supports a comparison operator and a comparand, so that you can use it as a

// conditional DataTrigger. The trick is to set {x:Null} as the DataTrigger.Value.

// E.g.:

//

// <DataTrigger Value={x:Null}

// Binding={h:ComparisonBinding Width, EQ, 100}"

//

// The operator can be EQ, LT, LTE, GT, GTE.

//

public class ComparisonBinding : Binding

{

    // Default constructor

    public ComparisonBinding()

        : this(null, ComparisonOperators.EQ, null)

    {

    }

    // Construction with an operator & comparand

    public ComparisonBinding(string path, ComparisonOperators op, object comparand)

        : base(path)

    {

        RelativeSource = RelativeSource.Self;

        Comparand = comparand;

        Operator = op;

        Converter = new ComparisonConverter( this );

    }

    // Operator and comparand

    public ComparisonOperators Operator { get; set; }

    public object Comparand { get; set; }

}

// Supported types of comparisons

public enum ComparisonOperators

{

    EQ = 0,

    GT,

    GTE,

    LT,

    LTE

}

//

// Thie IValueConverter is used by the StyleBinding to

// implement the logical comparisson. ConvertBack isn't supported.

// Convert returns null if the condition is met, non-null otherwise.

//

internal class ComparisonConverter : IValueConverter

{

    // Keep a back reference to the StyleBinding

    ComparisonBinding _styleBinding;

    // Return this if the condition isn't met

    static object _notNull = new Object();

    // In construction, get a reference to the StyleBinding

    public ComparisonConverter(ComparisonBinding styleBinding)

    {

        _styleBinding = styleBinding;

    }

    //

    // IValueConverter.Convert

    //

    // Return null of the condition is met, non-null if not.

    //

    public object Convert(

        object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)

    {

        // Simple check for null

        if (value == null || _styleBinding.Comparand == null)

        {

            return ReturnHelper( value == _styleBinding.Comparand );

        }

        // Convert the comparand so that it matches the value

        object convertedComparand = _styleBinding.Comparand;

        try

        {

            // Only support simple conversions in here.

            convertedComparand = System.Convert.ChangeType(_styleBinding.Comparand, value.GetType());

        }

        catch (InvalidCastException)

        {

            // If Convert.ChangeType didn't work, try a type converter

            TypeConverter typeConverter = TypeDescriptor.GetConverter(value);

            if (typeConverter != null)

            {

                if (typeConverter.CanConvertFrom(_styleBinding.Comparand.GetType()))

                {

                    convertedComparand = typeConverter.ConvertFrom(_styleBinding.Comparand);

                }

            }

        }

        // Simple check for the equality case

        if (_styleBinding.Operator == ComparisonOperators.EQ)

        {

            // Actually, equality is a little more interesting, so put it in

            // a helper routine

            return ReturnHelper(

                        CheckEquals(value.GetType(), value, convertedComparand) );

        }

        // For anything other than Equals, we need IComparable

        if (!(value is IComparable) || !(convertedComparand is IComparable))

        {

            Trace(value, "One of the values was not an IComparable");

            return ReturnHelper(false);

        }

  // Compare the values

        int comparison = (value as IComparable).CompareTo(convertedComparand);

        // And return the comparisson result

        switch (_styleBinding.Operator)

        {

            case ComparisonOperators.GT:

                return ReturnHelper( comparison > 0 );

            case ComparisonOperators.GTE:

                return ReturnHelper( comparison >= 0 );

            case ComparisonOperators.LT:

                return ReturnHelper( comparison < 0 );

            case ComparisonOperators.LTE:

                return ReturnHelper( comparison <= 0 );

        }

        return _notNull;

    }

    //

    // This helper produces the return value; null if the values

    // match, non-null otherwise.

    //

  object ReturnHelper(bool result)

    {

        return result ? null : _notNull;

    }

    //

    // Trace output to the debugger

    //

    void Trace(object value, string message)

    {

        if (Debugger.IsAttached)

        {

            Debug.WriteLine("StyleBinding couldn't convert '"

                             + value.GetType()

                             + "' to '"

                             + _styleBinding.Comparand.GetType()

                             + "'");

            Debug.WriteLine("(" + message + ")");

        }

    }

    //

    // Check for equality of two values

    //

    private bool CheckEquals(Type type, object value1, object value2)

    {

        if (type.IsValueType || type == typeof(string))

        {

            return Object.Equals(value1, value2);

        }

        else

        {

            return Object.ReferenceEquals(value1, value2);

        }

    }

    //

    // IValueConverter.ConvertBack isn't supported.

    //

    public object ConvertBack(

        object value,

        Type targetType,

        object parameter,

        System.Globalization.CultureInfo culture)

    {

        throw new NotImplementedException();

    }

}

Comments

  • Anonymous
    September 29, 2008
    PingBack from http://www.easycoded.com/a-comparable-datatrigger/

  • Anonymous
    October 06, 2008
    Do you work with databindings in WPF and find that you have ever wanted to do this?? &lt;DataTrigger Binding="{l:ComparisonBinding Age, LT, 65}" Value="{x:Null}" &gt; One of the most requested WPF features is the ability to do comparisons in a databinding.

  • Anonymous
    October 06, 2008
    Do you work with databindings in WPF and find that you have ever wanted to do this?? &lt;DataTrigger Binding="{l:ComparisonBinding Age, LT, 65}" Value="{x:Null}" &gt; One of the most requested WPF features is the ability to do comparisons in a databinding.

  • Anonymous
    October 07, 2008
    I do something similar in my code, but my implementation is superior (sorry, you are "Doing It Wrong", Mike).  1) Your ComparisonBinding has an arity of two. 2) You don't explain how you deal with three-valued logic (a major problem with WPF's current Binding story, you guys pretend like the problem doesn't even exist) 3) You can't compare sets (where is the Strategy pattern for introducing my own Comparator?  That hard-coded enumeration is silly, and brittle and will result in client bugs) I saw Josh Smith and Brennon Williams complaining about this on the WPF Disciples mailing list, and they are just plain wrong.  I agree people should be complaining about this, but they are complaining for the wrong reasons.  Don't listen to them. The only point Josh/Brennon have is tooling support.   I've already stopped waiting on Cider. I just couldn't understand what was taking so long, so I chose to build my own.  Unfortunately, my solution makes heavy use of a large MarkupExtension library, and I can't use it for SL2.  (I've complained about this on the SL2 forums before, pronouncing the XAML Silverlight data format to be "XAML without the X".)

  • Anonymous
    October 07, 2008
    The comment has been removed

  • Anonymous
    December 10, 2008
    The comment has been removed

  • Anonymous
    September 27, 2012
    Good One.