Data See, Data Do

Mike Hillberg's Blog on Wpf and Silverlight

A Comparable DataTrigger

A Comparable DataTrigger

Rate This
  • Comments 7

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

    }

 

}

 

 

  • PingBack from http://www.easycoded.com/a-comparable-datatrigger/

  • 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.

  • 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.

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

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

  • 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 :)

  • Good One.

Page 1 of 1 (7 items)
Leave a Comment
  • Please add 4 and 4 and type the answer here:
  • Post