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?? <DataTrigger Binding="{l:ComparisonBinding Age, LT, 65}" Value="{x:Null}" > 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?? <DataTrigger Binding="{l:ComparisonBinding Age, LT, 65}" Value="{x:Null}" > 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 removedAnonymous
December 10, 2008
The comment has been removedAnonymous
September 27, 2012
Good One.