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 (7)

  1. Anonymous says:

    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.

  2. Anonymous says:

    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.

  3. johnzabroski says:

    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".)

  4. johnzabroski says:

    Also, to drill home the point, your sample code for that ValueConverter object is brittle.  Storing "back references" in ValueConverter objects is a huge hack (in fact, it will not pass a code review for my project), and I feel sorry for the person who has to maintain that code long term.  Nothing is more fungible than user interface requirements – nothing!  Plan for change.

    I just don’t want people getting the wrong idea on this.  I saw Josh and Brennon upset about this, but felt it was for totally wrong reasons.  Rather than criticize your strategy, I am merely criticizing your tactics.  The idea is good, but the implementation is anti-exemplary.

    Moreover, my only "strategy" criticism is this: it’s a really bad idea to keep adding subclasses to Binding when clients cannot extend it for themselves.  You should open up Binding first, let the community contribute their own extensions to solve the problem.  Actually, I wish I had further access to BindingBase, because MultiBinding and Binding interfaces simply are not that well designed (I had to come up with a kludge workaround to this similar to M. Orcun Topdagi’s fix:Binding kludge).

  5. Nullable says:

    Good stuff… except returning "null" / "non-null" is pretty lame 🙂 Why not do "True" and "False". Then "remembering to set to null" won’t be necessary ethter, but rather your ‘converter’ will simply convert to True if matched, and False if not.

    Other than that… good work 🙂