Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve the usage experience with IsInRangeConverter in XAML #1983

Merged
Merged
Show file tree
Hide file tree
Changes from 10 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
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ public class IsInRangeConverterTests : BaseOneWayConverterTest<IsInRangeConverte
{
public const string FalseTestObject = nameof(FalseTestObject);
public const string TrueTestObject = nameof(TrueTestObject);
public const DateTimeKind LocalDateTimeKind = DateTimeKind.Utc;
GeorgeLeithead marked this conversation as resolved.
Show resolved Hide resolved
public const DateTimeKind UnspecifiedDateTimeKind = DateTimeKind.Unspecified;

public enum Days { Sun, Mon, Tue, Wed, Thu, Fri, Sat };

Expand All @@ -32,17 +34,17 @@ public enum Days { Sun, Mon, Tue, Wed, Thu, Fri, Sat };
[new DateOnly(2022, 01, 02), new DateOnly(2022, 5, 5), new DateOnly(2022, 12, 25), TrueTestObject, FalseTestObject, FalseTestObject],
[new DateOnly(2022, 12, 26), new DateOnly(2022, 5, 5), new DateOnly(2022, 12, 25), TrueTestObject, FalseTestObject, FalseTestObject],
// System.DateTime
[new DateTime(2022, 07, 07), new DateTime(2022, 5, 5), new DateTime(2022, 12, 25), TrueTestObject, FalseTestObject, TrueTestObject],
[new DateTime(2022, 05, 05), new DateTime(2022, 5, 5), new DateTime(2022, 12, 25), TrueTestObject, FalseTestObject, TrueTestObject],
[new DateTime(2022, 12, 25), new DateTime(2022, 5, 5), new DateTime(2022, 12, 25), TrueTestObject, FalseTestObject, TrueTestObject],
[new DateTime(2022, 01, 02), new DateTime(2022, 5, 5), new DateTime(2022, 12, 25), TrueTestObject, FalseTestObject, FalseTestObject],
[new DateTime(2022, 12, 26), new DateTime(2022, 5, 5), new DateTime(2022, 12, 25), TrueTestObject, FalseTestObject, FalseTestObject],
[new DateTime(2022, 07, 07, 0, 0, 0, LocalDateTimeKind), new DateTime(2022, 5, 5, 0, 0, 0, LocalDateTimeKind), new DateTime(2022, 12, 25, 0, 0, 0, LocalDateTimeKind), TrueTestObject, FalseTestObject, TrueTestObject],
[new DateTime(2022, 05, 05, 0, 0, 0, LocalDateTimeKind), new DateTime(2022, 5, 5, 0, 0, 0, LocalDateTimeKind), new DateTime(2022, 12, 25, 0, 0, 0, LocalDateTimeKind), TrueTestObject, FalseTestObject, TrueTestObject],
[new DateTime(2022, 12, 25, 0, 0, 0, LocalDateTimeKind), new DateTime(2022, 5, 5, 0, 0, 0, LocalDateTimeKind), new DateTime(2022, 12, 25, 0, 0, 0, LocalDateTimeKind), TrueTestObject, FalseTestObject, TrueTestObject],
[new DateTime(2022, 01, 02, 0, 0, 0, LocalDateTimeKind), new DateTime(2022, 5, 5, 0, 0, 0, LocalDateTimeKind), new DateTime(2022, 12, 25, 0, 0, 0, LocalDateTimeKind), TrueTestObject, FalseTestObject, FalseTestObject],
[new DateTime(2022, 12, 26, 0, 0, 0, LocalDateTimeKind), new DateTime(2022, 5, 5, 0, 0, 0, LocalDateTimeKind), new DateTime(2022, 12, 25, 0, 0, 0, LocalDateTimeKind), TrueTestObject, FalseTestObject, FalseTestObject],
// System.DateTimeOffset
[new DateTimeOffset(new DateTime(1973, 1, 1), new TimeSpan(3, 0, 0)), new DateTimeOffset(new DateTime(1973, 1, 1), new TimeSpan(4, 0, 0)), new DateTimeOffset(new DateTime(1973, 1, 1), new TimeSpan(2, 0, 0)), TrueTestObject, FalseTestObject, TrueTestObject],
[new DateTimeOffset(new DateTime(1973, 1, 1), new TimeSpan(4, 0, 0)), new DateTimeOffset(new DateTime(1973, 1, 1), new TimeSpan(4, 0, 0)), new DateTimeOffset(new DateTime(1973, 1, 1), new TimeSpan(2, 0, 0)), TrueTestObject, FalseTestObject, TrueTestObject],
[new DateTimeOffset(new DateTime(1973, 1, 1), new TimeSpan(2, 0, 0)), new DateTimeOffset(new DateTime(1973, 1, 1), new TimeSpan(4, 0, 0)), new DateTimeOffset(new DateTime(1973, 1, 1), new TimeSpan(2, 0, 0)), TrueTestObject, FalseTestObject, TrueTestObject],
[new DateTimeOffset(new DateTime(1973, 1, 1), new TimeSpan(1, 0, 0)), new DateTimeOffset(new DateTime(1973, 1, 1), new TimeSpan(4, 0, 0)), new DateTimeOffset(new DateTime(1973, 1, 1), new TimeSpan(2, 0, 0)), TrueTestObject, FalseTestObject, FalseTestObject],
[new DateTimeOffset(new DateTime(1973, 1, 1), new TimeSpan(5, 0, 0)), new DateTimeOffset(new DateTime(1973, 1, 1), new TimeSpan(4, 0, 0)), new DateTimeOffset(new DateTime(1973, 1, 1), new TimeSpan(2, 0, 0)), TrueTestObject, FalseTestObject, FalseTestObject],
[new DateTimeOffset(DateTime.SpecifyKind(new DateTime(1973, 1, 1, 0, 0, 0, UnspecifiedDateTimeKind), UnspecifiedDateTimeKind), new TimeSpan(3, 0, 0)), new DateTimeOffset(DateTime.SpecifyKind(new DateTime(1973, 1, 1, 0, 0, 0, UnspecifiedDateTimeKind), UnspecifiedDateTimeKind), new TimeSpan(4, 0, 0)), new DateTimeOffset(DateTime.SpecifyKind(new DateTime(1973, 1, 1, 0, 0, 0, UnspecifiedDateTimeKind), UnspecifiedDateTimeKind), new TimeSpan(2, 0, 0)), TrueTestObject, FalseTestObject, TrueTestObject],
[new DateTimeOffset(DateTime.SpecifyKind(new DateTime(1973, 1, 1, 0, 0, 0, UnspecifiedDateTimeKind), UnspecifiedDateTimeKind), new TimeSpan(4, 0, 0)), new DateTimeOffset(DateTime.SpecifyKind(new DateTime(1973, 1, 1, 0, 0, 0, UnspecifiedDateTimeKind), UnspecifiedDateTimeKind), new TimeSpan(4, 0, 0)), new DateTimeOffset(DateTime.SpecifyKind(new DateTime(1973, 1, 1, 0, 0, 0, UnspecifiedDateTimeKind), UnspecifiedDateTimeKind), new TimeSpan(2, 0, 0)), TrueTestObject, FalseTestObject, TrueTestObject],
[new DateTimeOffset(DateTime.SpecifyKind(new DateTime(1973, 1, 1, 0, 0, 0, UnspecifiedDateTimeKind), UnspecifiedDateTimeKind), new TimeSpan(2, 0, 0)), new DateTimeOffset(DateTime.SpecifyKind(new DateTime(1973, 1, 1, 0, 0, 0, UnspecifiedDateTimeKind), UnspecifiedDateTimeKind), new TimeSpan(4, 0, 0)), new DateTimeOffset(DateTime.SpecifyKind(new DateTime(1973, 1, 1, 0, 0, 0, UnspecifiedDateTimeKind), UnspecifiedDateTimeKind), new TimeSpan(2, 0, 0)), TrueTestObject, FalseTestObject, TrueTestObject],
[new DateTimeOffset(DateTime.SpecifyKind(new DateTime(1973, 1, 1, 0, 0, 0, UnspecifiedDateTimeKind), UnspecifiedDateTimeKind), new TimeSpan(1, 0, 0)), new DateTimeOffset(DateTime.SpecifyKind(new DateTime(1973, 1, 1, 0, 0, 0, UnspecifiedDateTimeKind), UnspecifiedDateTimeKind), new TimeSpan(4, 0, 0)), new DateTimeOffset(DateTime.SpecifyKind(new DateTime(1973, 1, 1, 0, 0, 0, UnspecifiedDateTimeKind), UnspecifiedDateTimeKind), new TimeSpan(2, 0, 0)), TrueTestObject, FalseTestObject, FalseTestObject],
[new DateTimeOffset(DateTime.SpecifyKind(new DateTime(1973, 1, 1, 0, 0, 0, UnspecifiedDateTimeKind), UnspecifiedDateTimeKind), new TimeSpan(5, 0, 0)), new DateTimeOffset(DateTime.SpecifyKind(new DateTime(1973, 1, 1, 0, 0, 0, UnspecifiedDateTimeKind), UnspecifiedDateTimeKind), new TimeSpan(4, 0, 0)), new DateTimeOffset(DateTime.SpecifyKind(new DateTime(1973, 1, 1, 0, 0, 0, UnspecifiedDateTimeKind), UnspecifiedDateTimeKind), new TimeSpan(2, 0, 0)), TrueTestObject, FalseTestObject, FalseTestObject],
// System.Decimal
[new decimal(037.73), new decimal(7.73), new decimal(73.37), TrueTestObject, FalseTestObject, TrueTestObject],
[new decimal(007.73), new decimal(7.73), new decimal(73.37), TrueTestObject, FalseTestObject, TrueTestObject],
Expand Down Expand Up @@ -181,7 +183,7 @@ public enum Days { Sun, Mon, Tue, Wed, Thu, Fri, Sat };
[InlineData(20d, "A", 'B', TrueTestObject, FalseTestObject)]
[InlineData(20d, 1d, 'B', TrueTestObject, FalseTestObject)]
[InlineData(20d, "A", 1d, TrueTestObject, FalseTestObject)]
public void InvalidIComparableThrowArgumentException(IComparable value, IComparable comparingMinValue, IComparable comparingMaxValue, object trueObject, object falseObject)
public void InvalidArgumentException(IComparable value, IComparable comparingMinValue, IComparable comparingMaxValue, object trueObject, object falseObject)
{
IsInRangeConverter isInRangeConverter = new()
{
Expand Down Expand Up @@ -223,7 +225,7 @@ public void InvalidValuesThrowArgumentException(IComparable value, IComparable?
};

Assert.Throws<ArgumentException>(() => ((ICommunityToolkitValueConverter)isInRangeConverter).Convert(value, typeof(object), null, CultureInfo.CurrentCulture));
Assert.Throws<ArgumentException>(() => isInRangeConverter.ConvertFrom(value));
Assert.Throws<ArgumentException>(() => isInRangeConverter.ConvertFrom(value, CultureInfo.CurrentCulture));
}

[Theory]
Expand All @@ -239,7 +241,7 @@ public void IsInRangeConverterConvertFrom(IComparable value, IComparable compari
};

object? convertResult = ((ICommunityToolkitValueConverter)isInRangeConverter).Convert(value, typeof(object), null, CultureInfo.CurrentCulture);
object convertFromResult = isInRangeConverter.ConvertFrom(value);
object convertFromResult = isInRangeConverter.ConvertFrom(value, CultureInfo.CurrentCulture);

Assert.Equal(expectedResult, convertResult);
Assert.Equal(expectedResult, convertFromResult);
Expand All @@ -260,7 +262,7 @@ public void NullToMinValueIsInRangeConverterConvertFrom(IComparable value, IComp
TrueObject = trueObject,
};

object convertFromResult = isInRangeConverter.ConvertFrom(value);
object convertFromResult = isInRangeConverter.ConvertFrom(value, CultureInfo.CurrentCulture);
Assert.Equal(expectedResult, convertFromResult);
}

Expand All @@ -277,7 +279,7 @@ public void ReturnObjectsNullExpectBoolReturn(IComparable value, IComparable com
TrueObject = trueObject,
};

object convertFromResult = isInRangeConverter.ConvertFrom(value);
object convertFromResult = isInRangeConverter.ConvertFrom(value, CultureInfo.CurrentCulture);
Assert.Equal(expectedResult, convertFromResult);
}
}
104 changes: 29 additions & 75 deletions src/CommunityToolkit.Maui/Converters/IsInRangeConverter.shared.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,69 +3,31 @@
namespace CommunityToolkit.Maui.Converters;

/// <summary>Converts the incoming value to a <see cref="bool"/> indicating whether or not the value is within a range.</summary>
public sealed class IsInRangeConverter : IsInRangeConverter<object>
{
}
public sealed class IsInRangeConverter : IsInRangeConverter<IComparable, object>;

/// <summary>Converts the incoming value to a <see cref="bool"/> indicating whether or not the value is within a range.</summary>
public abstract class IsInRangeConverter<TObject> : BaseConverterOneWay<IComparable, object>
public abstract class IsInRangeConverter<TValue, TReturnObject> : BaseConverterOneWay<TValue, object> where TValue : IComparable
{
/// <summary>
/// Bindable property for <see cref="MinValue"/>
/// </summary>
public static readonly BindableProperty MinValueProperty = BindableProperty.Create(nameof(MinValue), typeof(IComparable), typeof(IsInRangeConverter<TObject>));

/// <summary>
/// Bindable property for <see cref="MaxValue"/>
/// </summary>
public static readonly BindableProperty MaxValueProperty = BindableProperty.Create(nameof(MaxValue), typeof(IComparable), typeof(IsInRangeConverter<TObject>));

/// <summary>
/// Bindable property for <see cref="TrueObject"/>
/// </summary>
public static readonly BindableProperty TrueObjectProperty = BindableProperty.Create(nameof(TrueObject), typeof(TObject?), typeof(IsInRangeConverter<TObject>));

/// <summary>
/// Bindable property for <see cref="FalseObject"/>
/// </summary>
public static readonly BindableProperty FalseObjectProperty = BindableProperty.Create(nameof(FalseObject), typeof(TObject?), typeof(IsInRangeConverter<TObject>));

/// <inheritdoc/>
public override object DefaultConvertReturnValue { get; set; } = new();

/// <summary>Minimum value.</summary>
public IComparable? MinValue
{
get => (IComparable?)GetValue(MinValueProperty);
set => SetValue(MinValueProperty, value);
}
/// <summary>If supplied this value will be returned when the converter receives an input value that is <b>outside</b> of the <see cref="MinValue" /> and <see cref="MaxValue" />s.</summary>
public TReturnObject? FalseObject { get; set; }
bijington marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>Maximum value.</summary>
public IComparable? MaxValue
{
get => (IComparable?)GetValue(MaxValueProperty);
set => SetValue(MaxValueProperty, value);
}
/// <summary>The upper bounds of the range to compare against when determining whether the input value to the convert is within range.</summary>
public TValue? MaxValue { get; set; }

/// <summary>The object that corresponds to True value.</summary>
public TObject? TrueObject
{
get => (TObject?)GetValue(TrueObjectProperty);
set => SetValue(TrueObjectProperty, value);
}
/// <summary>The lower bounds of the range to compare against when determining whether the input value to the convert is within range.</summary>
public TValue? MinValue { get; set; }

/// <summary>The object that corresponds to False value.</summary>
public TObject? FalseObject
{
get => (TObject?)GetValue(FalseObjectProperty);
set => SetValue(FalseObjectProperty, value);
}
/// <summary>If supplied this value will be returned when the converter receives an input value that is <b>inside</b> (inclusive) of the <see cref="MinValue" /> and <see cref="MaxValue" />s.</summary>
public TReturnObject? TrueObject { get; set; }

/// <summary>Converts an object that implemented IComparable to a <see cref="bool"/> based on the object being within a <see cref="MinValue"/> and <see cref="MaxValue"/> range.</summary>
/// <param name="value">The value to convert.</param>
/// <param name="culture">(Not Used)</param>
/// <returns>The object assigned to <see cref="TrueObject"/> if value is between <see cref="MinValue"/> and <see cref="MaxValue"/> then <see cref="TrueObject"/> returns true, otherwise the value assigned to <see cref="FalseObject"/>.</returns>
public override object ConvertFrom(IComparable value, CultureInfo? culture = null)
/// <returns>The object assigned to <see cref="TrueObject"/> if value is between <see cref="MinValue"/> and <see cref="MaxValue"/> returns true, otherwise the value assigned to <see cref="FalseObject"/>.</returns>
public override object ConvertFrom(TValue value, CultureInfo? culture)
{
ArgumentNullException.ThrowIfNull(value);

Expand All @@ -79,37 +41,29 @@ public override object ConvertFrom(IComparable value, CultureInfo? culture = nul
throw new InvalidOperationException($"{nameof(TrueObject)} and {nameof(FalseObject)} should either be both defined or both omitted.");
}

var valueType = value.GetType();
if (MinValue is not null && MinValue.GetType() != valueType)
{
throw new ArgumentException($"{nameof(value)} is expected to be of type {nameof(MinValue)}, but is {valueType}", nameof(value));
}

if (MaxValue is not null && MaxValue.GetType() != valueType)
{
throw new ArgumentException($"{nameof(value)} is expected to be of type {nameof(MaxValue)}, but is {valueType}", nameof(value));
}

var shouldReturnObjectResult = TrueObject is not null && FalseObject is not null;

bool shouldReturnObjectResult = TrueObject is not null && FalseObject is not null;
if (MaxValue is null)
{
return EvaluateCondition(value.CompareTo(MinValue) >= 0, shouldReturnObjectResult);
}

if (MinValue is null)
{
return EvaluateCondition(value.CompareTo(MaxValue) <= 0, shouldReturnObjectResult);
}

return EvaluateCondition(value.CompareTo(MinValue) >= 0 && value.CompareTo(MaxValue) <= 0, shouldReturnObjectResult);
return MinValue is null
? EvaluateCondition(value.CompareTo(MaxValue) <= 0, shouldReturnObjectResult)
: EvaluateCondition(value.CompareTo(MinValue) >= 0 && value.CompareTo(MaxValue) <= 0, shouldReturnObjectResult);
}

object EvaluateCondition(bool comparisonResult, bool shouldReturnObject) => (comparisonResult, shouldReturnObject) switch
/// <summary>Evaluates a condition based on the given comparison result and returns an object.</summary>
/// <param name="comparisonResult">The result of the comparison.</param>
/// <param name="shouldReturnObject">Indicates whether an object should be returned.</param>
/// <returns>The result of the evaluation.</returns>
object EvaluateCondition(bool comparisonResult, bool shouldReturnObject)
{
(true, true) => TrueObject ?? throw new InvalidOperationException($"{nameof(TrueObject)} cannot be null"),
(false, true) => FalseObject ?? throw new InvalidOperationException($"{nameof(FalseObject)} cannot be null"),
(true, false) => true,
(false, false) => false
};
return (comparisonResult, shouldReturnObject) switch
{
(true, true) => TrueObject ?? throw new InvalidOperationException($"{nameof(TrueObject)} cannot be null"),
(false, true) => FalseObject ?? throw new InvalidOperationException($"{nameof(FalseObject)} cannot be null"),
(true, false) => true,
(false, false) => false
};
}
}