Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docfx/articles/interactions/data-trigger-behavior.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# DataTriggerBehavior

When a string `Value` cannot be converted to the runtime type of `Binding`, the comparison uses non-equal semantics instead of throwing: `NotEqual` evaluates to `true`, while the other comparison operators evaluate to `false`.

`DataTriggerBehavior` is a behavior that listens for changes to a bound value and invokes actions when the value meets a specified condition.

## Properties
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,16 @@ public static bool Compare(object? leftOperand, ComparisonConditionType operator
if (rightOperand is string rightOperandString)
{
var leftOperandType = leftOperand.GetType();
var convertedRightOperand = TypeConverterHelper.Convert(rightOperandString, leftOperandType);
object? convertedRightOperand = null;
try
{
convertedRightOperand = TypeConverterHelper.Convert(rightOperandString, leftOperandType);
}
catch (Exception exception) when (IsConversionException(exception))
{
// Keep the original operand so comparable evaluation can use non-equal semantics.
}

if (convertedRightOperand is not null)
{
rightOperand = convertedRightOperand;
Expand Down Expand Up @@ -74,21 +83,26 @@ private static bool EvaluateComparable(IComparable leftOperand, ComparisonCondit
{
convertedOperand = Convert.ChangeType(rightOperand, leftOperand.GetType(), CultureInfo.CurrentCulture);
}
catch (FormatException)
catch (Exception exception) when (IsConversionException(exception))
{
// FormatException: Convert.ChangeType("hello", typeof(double), ...);
// The operands cannot be converted to a common comparable type.
}
catch (InvalidCastException)

if (convertedOperand is null)
{
// InvalidCastException: Convert.ChangeType(4.0d, typeof(Rectangle), ...);
return IsNonEqualResult(operatorType);
}

if (convertedOperand is null)
int comparison;
try
{
return operatorType == ComparisonConditionType.NotEqual;
comparison = leftOperand.CompareTo((IComparable)convertedOperand);
}
catch (Exception exception) when (exception is ArgumentException or InvalidCastException)
{
return IsNonEqualResult(operatorType);
}

var comparison = leftOperand.CompareTo((IComparable)convertedOperand);
return operatorType switch
{
ComparisonConditionType.Equal => comparison == 0,
Expand All @@ -100,4 +114,17 @@ private static bool EvaluateComparable(IComparable leftOperand, ComparisonCondit
_ => false
};
}

private static bool IsConversionException(Exception exception)
{
return exception is FormatException
or InvalidCastException
or NotSupportedException
or OverflowException;
}

private static bool IsNonEqualResult(ComparisonConditionType operatorType)
{
return operatorType == ComparisonConditionType.NotEqual;
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using Avalonia.Controls;
using Avalonia.Headless;
using Avalonia.Headless.XUnit;
using Avalonia.Input;
Expand Down Expand Up @@ -86,6 +87,34 @@ public void DataTriggerBehavior_003()
Assert.Equal(50d, window.TargetSlider.Value);
}

[AvaloniaFact]
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Validates the reflection-based compatibility trigger.")]
public void DataTriggerBehavior_NonConvertibleValue_DoesNotThrowOrExecuteActions()
{
var commandCalls = 0;
var target = new Border();
var trigger = new DataTriggerBehavior
{
Binding = 42,
ComparisonCondition = ComparisonConditionType.Equal,
Value = "not-an-integer",
};
var action = new InvokeCommandAction
{
Command = new Command(_ => commandCalls++),
};
trigger.Actions ??= [];
trigger.Actions.Add(action);
Interaction.GetBehaviors(target).Add(trigger);
var window = new Window { Content = target };
window.Show();

var exception = Record.Exception(() => Dispatcher.UIThread.RunJobs());

Assert.Null(exception);
Assert.Equal(0, commandCalls);
}

[AvaloniaFact]
public void DataTriggerBehavior_004_Reverts_On_False()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Xunit;

namespace Avalonia.Xaml.Interactivity.UnitTests;

[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Validates the reflection-based compatibility comparison helper.")]
public class ComparisonConditionTypeHelperTests
{
private sealed class ThrowingComparable : IComparable
{
public int CompareCalls { get; private set; }

public int CompareTo(object? obj)
{
CompareCalls++;
throw new ArgumentException("Operands are not comparable.", nameof(obj));
}
}

[Theory]
[InlineData(ComparisonConditionType.Equal, false)]
[InlineData(ComparisonConditionType.NotEqual, true)]
[InlineData(ComparisonConditionType.LessThan, false)]
[InlineData(ComparisonConditionType.LessThanOrEqual, false)]
[InlineData(ComparisonConditionType.GreaterThan, false)]
[InlineData(ComparisonConditionType.GreaterThanOrEqual, false)]
public void Compare_NonConvertibleString_UsesNonEqualSemantics(
ComparisonConditionType operatorType,
bool expected)
{
var result = ComparisonConditionTypeHelper.Compare(42, operatorType, "not-an-integer");

Assert.Equal(expected, result);
}

[Fact]
public void Compare_OverflowingString_UsesNonEqualSemantics()
{
var result = ComparisonConditionTypeHelper.Compare(
42,
ComparisonConditionType.Equal,
"999999999999999999999999999999");

Assert.False(result);
}

[Theory]
[InlineData(ComparisonConditionType.Equal, false)]
[InlineData(ComparisonConditionType.NotEqual, true)]
public void Compare_IncompatibleComparable_UsesNonEqualSemantics(
ComparisonConditionType operatorType,
bool expected)
{
var left = new ThrowingComparable();
var right = new ThrowingComparable();

var result = ComparisonConditionTypeHelper.Compare(left, operatorType, right);

Assert.Equal(expected, result);
Assert.Equal(1, left.CompareCalls);
}
}
Loading