diff --git a/src/Artemis.Core/DefaultTypes/Conditions/Operators/EqualsConditionOperator.cs b/src/Artemis.Core/DefaultTypes/Conditions/Operators/EqualsConditionOperator.cs index 602afcd18..dc66e12a3 100644 --- a/src/Artemis.Core/DefaultTypes/Conditions/Operators/EqualsConditionOperator.cs +++ b/src/Artemis.Core/DefaultTypes/Conditions/Operators/EqualsConditionOperator.cs @@ -1,12 +1,7 @@ -using System; -using System.Collections.Generic; - -namespace Artemis.Core.DefaultTypes +namespace Artemis.Core.DefaultTypes { - internal class EqualsConditionOperator : ConditionOperator + internal class EqualsConditionOperator : ConditionOperator { - public override IReadOnlyCollection CompatibleTypes => new List {typeof(object)}; - public override string Description => "Equals"; public override string Icon => "Equal"; diff --git a/src/Artemis.Core/DefaultTypes/Conditions/Operators/GreaterThanConditionOperator.cs b/src/Artemis.Core/DefaultTypes/Conditions/Operators/GreaterThanConditionOperator.cs index c33b0ab47..de5e61ea3 100644 --- a/src/Artemis.Core/DefaultTypes/Conditions/Operators/GreaterThanConditionOperator.cs +++ b/src/Artemis.Core/DefaultTypes/Conditions/Operators/GreaterThanConditionOperator.cs @@ -1,18 +1,13 @@ -using System; -using System.Collections.Generic; - -namespace Artemis.Core.DefaultTypes +namespace Artemis.Core.DefaultTypes { - internal class GreaterThanConditionOperator : ConditionOperator + internal class GreaterThanConditionOperator : ConditionOperator { - public override IReadOnlyCollection CompatibleTypes => Constants.NumberTypes; - public override string Description => "Is greater than"; public override string Icon => "GreaterThan"; - public override bool Evaluate(object a, object b) + public override bool Evaluate(double a, double b) { - return Convert.ToSingle(a) > Convert.ToSingle(b); + return a > b; } } } \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/Conditions/Operators/GreaterThanOrEqualConditionOperator.cs b/src/Artemis.Core/DefaultTypes/Conditions/Operators/GreaterThanOrEqualConditionOperator.cs index ca09d2fdd..a62b19fda 100644 --- a/src/Artemis.Core/DefaultTypes/Conditions/Operators/GreaterThanOrEqualConditionOperator.cs +++ b/src/Artemis.Core/DefaultTypes/Conditions/Operators/GreaterThanOrEqualConditionOperator.cs @@ -1,18 +1,13 @@ -using System; -using System.Collections.Generic; - -namespace Artemis.Core.DefaultTypes +namespace Artemis.Core.DefaultTypes { - internal class GreaterThanOrEqualConditionOperator : ConditionOperator + internal class GreaterThanOrEqualConditionOperator : ConditionOperator { - public override IReadOnlyCollection CompatibleTypes => Constants.NumberTypes; - public override string Description => "Is greater than or equal to"; public override string Icon => "GreaterThanOrEqual"; - public override bool Evaluate(object a, object b) + public override bool Evaluate(double a, double b) { - return Convert.ToSingle(a) >= Convert.ToSingle(b); + return a >= b; } } } \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/Conditions/Operators/LessThanConditionOperator.cs b/src/Artemis.Core/DefaultTypes/Conditions/Operators/LessThanConditionOperator.cs index 9a05a487b..bad93babc 100644 --- a/src/Artemis.Core/DefaultTypes/Conditions/Operators/LessThanConditionOperator.cs +++ b/src/Artemis.Core/DefaultTypes/Conditions/Operators/LessThanConditionOperator.cs @@ -1,18 +1,13 @@ -using System; -using System.Collections.Generic; - -namespace Artemis.Core.DefaultTypes +namespace Artemis.Core.DefaultTypes { - internal class LessThanConditionOperator : ConditionOperator + internal class LessThanConditionOperator : ConditionOperator { - public override IReadOnlyCollection CompatibleTypes => Constants.NumberTypes; - public override string Description => "Is less than"; public override string Icon => "LessThan"; - public override bool Evaluate(object a, object b) + public override bool Evaluate(double a, double b) { - return Convert.ToSingle(a) < Convert.ToSingle(b); + return a < b; } } } \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/Conditions/Operators/LessThanOrEqualConditionOperator.cs b/src/Artemis.Core/DefaultTypes/Conditions/Operators/LessThanOrEqualConditionOperator.cs index 0cf0c6b17..98cb11fca 100644 --- a/src/Artemis.Core/DefaultTypes/Conditions/Operators/LessThanOrEqualConditionOperator.cs +++ b/src/Artemis.Core/DefaultTypes/Conditions/Operators/LessThanOrEqualConditionOperator.cs @@ -1,18 +1,13 @@ -using System; -using System.Collections.Generic; - -namespace Artemis.Core.DefaultTypes +namespace Artemis.Core.DefaultTypes { - internal class LessThanOrEqualConditionOperator : ConditionOperator + internal class LessThanOrEqualConditionOperator : ConditionOperator { - public override IReadOnlyCollection CompatibleTypes => Constants.NumberTypes; - public override string Description => "Is less than or equal to"; public override string Icon => "LessThanOrEqual"; - public override bool Evaluate(object a, object b) + public override bool Evaluate(double a, double b) { - return Convert.ToSingle(a) <= Convert.ToSingle(b); + return a <= b; } } } \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/Conditions/Operators/NotEqualConditionOperator.cs b/src/Artemis.Core/DefaultTypes/Conditions/Operators/NotEqualConditionOperator.cs index 9d264e5c8..502888ee0 100644 --- a/src/Artemis.Core/DefaultTypes/Conditions/Operators/NotEqualConditionOperator.cs +++ b/src/Artemis.Core/DefaultTypes/Conditions/Operators/NotEqualConditionOperator.cs @@ -1,12 +1,7 @@ -using System; -using System.Collections.Generic; - -namespace Artemis.Core.DefaultTypes +namespace Artemis.Core.DefaultTypes { - internal class NotEqualConditionOperator : ConditionOperator + internal class NotEqualConditionOperator : ConditionOperator { - public override IReadOnlyCollection CompatibleTypes => new List {typeof(object)}; - public override string Description => "Does not equal"; public override string Icon => "NotEqualVariant"; diff --git a/src/Artemis.Core/DefaultTypes/Conditions/Operators/NotNullConditionOperator.cs b/src/Artemis.Core/DefaultTypes/Conditions/Operators/NotNullConditionOperator.cs index c14ebaa77..faf7e7054 100644 --- a/src/Artemis.Core/DefaultTypes/Conditions/Operators/NotNullConditionOperator.cs +++ b/src/Artemis.Core/DefaultTypes/Conditions/Operators/NotNullConditionOperator.cs @@ -1,21 +1,11 @@ -using System; -using System.Collections.Generic; - -namespace Artemis.Core.DefaultTypes +namespace Artemis.Core.DefaultTypes { - internal class NotNullConditionOperator : ConditionOperator + internal class NotNullConditionOperator : ConditionOperator { - public NotNullConditionOperator() - { - SupportsRightSide = false; - } - - public override IReadOnlyCollection CompatibleTypes => new List {typeof(object)}; - public override string Description => "Is not null"; public override string Icon => "CheckboxMarkedCircleOutline"; - public override bool Evaluate(object a, object b) + public override bool Evaluate(object a) { return a != null; } diff --git a/src/Artemis.Core/DefaultTypes/Conditions/Operators/NullConditionOperator.cs b/src/Artemis.Core/DefaultTypes/Conditions/Operators/NullConditionOperator.cs index ffd8c0639..27a29508f 100644 --- a/src/Artemis.Core/DefaultTypes/Conditions/Operators/NullConditionOperator.cs +++ b/src/Artemis.Core/DefaultTypes/Conditions/Operators/NullConditionOperator.cs @@ -1,21 +1,11 @@ -using System; -using System.Collections.Generic; - -namespace Artemis.Core.DefaultTypes +namespace Artemis.Core.DefaultTypes { - internal class NullConditionOperator : ConditionOperator + internal class NullConditionOperator : ConditionOperator { - public NullConditionOperator() - { - SupportsRightSide = false; - } - - public override IReadOnlyCollection CompatibleTypes => new List {typeof(object)}; - public override string Description => "Is null"; public override string Icon => "Null"; - public override bool Evaluate(object a, object b) + public override bool Evaluate(object a) { return a == null; } diff --git a/src/Artemis.Core/DefaultTypes/Conditions/Operators/NumberEqualsConditionOperator.cs b/src/Artemis.Core/DefaultTypes/Conditions/Operators/NumberEqualsConditionOperator.cs new file mode 100644 index 000000000..c0f303553 --- /dev/null +++ b/src/Artemis.Core/DefaultTypes/Conditions/Operators/NumberEqualsConditionOperator.cs @@ -0,0 +1,16 @@ +using System; + +namespace Artemis.Core.DefaultTypes +{ + internal class NumberEqualsConditionOperator : ConditionOperator + { + public override string Description => "Equals"; + public override string Icon => "Equal"; + + public override bool Evaluate(double a, double b) + { + // Numbers can be tricky, an epsilon like this is close enough + return Math.Abs(a - b) < 0.000001; + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/Conditions/Operators/NumberNotEqualConditionOperator.cs b/src/Artemis.Core/DefaultTypes/Conditions/Operators/NumberNotEqualConditionOperator.cs new file mode 100644 index 000000000..6f5208cea --- /dev/null +++ b/src/Artemis.Core/DefaultTypes/Conditions/Operators/NumberNotEqualConditionOperator.cs @@ -0,0 +1,16 @@ +using System; + +namespace Artemis.Core.DefaultTypes +{ + internal class NumberNotEqualConditionOperator : ConditionOperator + { + public override string Description => "Does not equal"; + public override string Icon => "NotEqualVariant"; + + public override bool Evaluate(double a, double b) + { + // Numbers can be tricky, an epsilon like this is close enough + return Math.Abs(a - b) > 0.000001; + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/Conditions/Operators/StringContainsConditionOperator.cs b/src/Artemis.Core/DefaultTypes/Conditions/Operators/StringContainsConditionOperator.cs index 66df52bd9..c108e7b5b 100644 --- a/src/Artemis.Core/DefaultTypes/Conditions/Operators/StringContainsConditionOperator.cs +++ b/src/Artemis.Core/DefaultTypes/Conditions/Operators/StringContainsConditionOperator.cs @@ -1,21 +1,15 @@ using System; -using System.Collections.Generic; namespace Artemis.Core.DefaultTypes { - internal class StringContainsConditionOperator : ConditionOperator + internal class StringContainsConditionOperator : ConditionOperator { - public override IReadOnlyCollection CompatibleTypes => new List {typeof(string)}; - public override string Description => "Contains"; public override string Icon => "Contain"; - public override bool Evaluate(object a, object b) + public override bool Evaluate(string a, string b) { - string aString = (string) a; - string bString = (string) b; - - return bString != null && aString != null && aString.Contains(bString, StringComparison.InvariantCultureIgnoreCase); + return a != null && b != null && a.Contains(b, StringComparison.InvariantCultureIgnoreCase); } } } \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/Conditions/Operators/StringEndsWithConditionOperator.cs b/src/Artemis.Core/DefaultTypes/Conditions/Operators/StringEndsWithConditionOperator.cs index 8041809f4..603660672 100644 --- a/src/Artemis.Core/DefaultTypes/Conditions/Operators/StringEndsWithConditionOperator.cs +++ b/src/Artemis.Core/DefaultTypes/Conditions/Operators/StringEndsWithConditionOperator.cs @@ -1,21 +1,15 @@ using System; -using System.Collections.Generic; namespace Artemis.Core.DefaultTypes { - internal class StringEndsWithConditionOperator : ConditionOperator + internal class StringEndsWithConditionOperator : ConditionOperator { - public override IReadOnlyCollection CompatibleTypes => new List {typeof(string)}; - public override string Description => "Ends with"; public override string Icon => "ContainEnd"; - public override bool Evaluate(object a, object b) + public override bool Evaluate(string a, string b) { - string aString = (string) a; - string bString = (string) b; - - return bString != null && aString != null && aString.EndsWith(bString, StringComparison.InvariantCultureIgnoreCase); + return a != null && b != null && a.EndsWith(b, StringComparison.InvariantCultureIgnoreCase); } } } \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/Conditions/Operators/StringEqualsConditionOperator.cs b/src/Artemis.Core/DefaultTypes/Conditions/Operators/StringEqualsConditionOperator.cs index 5857ce81f..09bdbb502 100644 --- a/src/Artemis.Core/DefaultTypes/Conditions/Operators/StringEqualsConditionOperator.cs +++ b/src/Artemis.Core/DefaultTypes/Conditions/Operators/StringEqualsConditionOperator.cs @@ -1,21 +1,15 @@ using System; -using System.Collections.Generic; namespace Artemis.Core.DefaultTypes { - internal class StringEqualsConditionOperator : ConditionOperator + internal class StringEqualsConditionOperator : ConditionOperator { - public override IReadOnlyCollection CompatibleTypes => new List {typeof(string)}; - public override string Description => "Equals"; public override string Icon => "Equal"; - public override bool Evaluate(object a, object b) + public override bool Evaluate(string a, string b) { - string aString = (string) a; - string bString = (string) b; - - return string.Equals(aString, bString, StringComparison.InvariantCultureIgnoreCase); + return string.Equals(a, b, StringComparison.InvariantCultureIgnoreCase); } } } \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/Conditions/Operators/StringNotContainsConditionOperator.cs b/src/Artemis.Core/DefaultTypes/Conditions/Operators/StringNotContainsConditionOperator.cs index 9b7f45f8f..a15271341 100644 --- a/src/Artemis.Core/DefaultTypes/Conditions/Operators/StringNotContainsConditionOperator.cs +++ b/src/Artemis.Core/DefaultTypes/Conditions/Operators/StringNotContainsConditionOperator.cs @@ -1,21 +1,15 @@ using System; -using System.Collections.Generic; namespace Artemis.Core.DefaultTypes { - internal class StringNotContainsConditionOperator : ConditionOperator + internal class StringNotContainsConditionOperator : ConditionOperator { - public override IReadOnlyCollection CompatibleTypes => new List {typeof(string)}; - public override string Description => "Does not contain"; public override string Icon => "FormatStrikethrough"; - public override bool Evaluate(object a, object b) + public override bool Evaluate(string a, string b) { - string aString = (string) a; - string bString = (string) b; - - return bString != null && aString != null && !aString.Contains(bString, StringComparison.InvariantCultureIgnoreCase); + return a != null && b != null && !a.Contains(b, StringComparison.InvariantCultureIgnoreCase); } } } \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/Conditions/Operators/StringNotEqualConditionOperator.cs b/src/Artemis.Core/DefaultTypes/Conditions/Operators/StringNotEqualConditionOperator.cs index aef95e7e4..17b99227b 100644 --- a/src/Artemis.Core/DefaultTypes/Conditions/Operators/StringNotEqualConditionOperator.cs +++ b/src/Artemis.Core/DefaultTypes/Conditions/Operators/StringNotEqualConditionOperator.cs @@ -1,21 +1,15 @@ using System; -using System.Collections.Generic; namespace Artemis.Core.DefaultTypes { - internal class StringNotEqualConditionOperator : ConditionOperator + internal class StringNotEqualConditionOperator : ConditionOperator { - public override IReadOnlyCollection CompatibleTypes => new List {typeof(string)}; - public override string Description => "Does not equal"; public override string Icon => "NotEqualVariant"; - public override bool Evaluate(object a, object b) + public override bool Evaluate(string a, string b) { - string aString = (string) a; - string bString = (string) b; - - return !string.Equals(aString, bString, StringComparison.InvariantCultureIgnoreCase); + return !string.Equals(a, b, StringComparison.InvariantCultureIgnoreCase); } } } \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/Conditions/Operators/StringStartsWithConditionOperator.cs b/src/Artemis.Core/DefaultTypes/Conditions/Operators/StringStartsWithConditionOperator.cs index d6f86eba3..538692baa 100644 --- a/src/Artemis.Core/DefaultTypes/Conditions/Operators/StringStartsWithConditionOperator.cs +++ b/src/Artemis.Core/DefaultTypes/Conditions/Operators/StringStartsWithConditionOperator.cs @@ -1,21 +1,15 @@ using System; -using System.Collections.Generic; namespace Artemis.Core.DefaultTypes { - internal class StringStartsWithConditionOperator : ConditionOperator + internal class StringStartsWithConditionOperator : ConditionOperator { - public override IReadOnlyCollection CompatibleTypes => new List {typeof(string)}; - public override string Description => "Starts with"; public override string Icon => "ContainStart"; - public override bool Evaluate(object a, object b) + public override bool Evaluate(string a, string b) { - string aString = (string) a; - string bString = (string) b; - - return bString != null && aString != null && aString.StartsWith(bString, StringComparison.InvariantCultureIgnoreCase); + return a != null && b != null && a.StartsWith(b, StringComparison.InvariantCultureIgnoreCase); } } } \ No newline at end of file diff --git a/src/Artemis.Core/Extensions/TypeExtensions.cs b/src/Artemis.Core/Extensions/TypeExtensions.cs index d6001d7ba..b485a4865 100644 --- a/src/Artemis.Core/Extensions/TypeExtensions.cs +++ b/src/Artemis.Core/Extensions/TypeExtensions.cs @@ -21,6 +21,25 @@ namespace Artemis.Core {typeof(short), new List {typeof(byte)}} }; + private static readonly Dictionary TypeKeywords = new Dictionary + { + {typeof(bool), "bool"}, + {typeof(byte), "byte"}, + {typeof(sbyte), "sbyte"}, + {typeof(char), "char"}, + {typeof(decimal), "decimal"}, + {typeof(double), "double"}, + {typeof(float), "float"}, + {typeof(int), "int"}, + {typeof(uint), "uint"}, + {typeof(long), "long"}, + {typeof(ulong), "ulong"}, + {typeof(short), "short"}, + {typeof(ushort), "ushort"}, + {typeof(object), "object"}, + {typeof(string), "string"} + }; + public static bool IsGenericType(this Type type, Type genericType) { if (type == null) @@ -64,31 +83,10 @@ namespace Artemis.Core || value is decimal; } - private static readonly Dictionary TypeKeywords = new Dictionary() - { - {typeof(bool), "bool"}, - {typeof(byte), "byte"}, - {typeof(sbyte), "sbyte"}, - {typeof(char), "char"}, - {typeof(decimal), "decimal"}, - {typeof(double), "double"}, - {typeof(float), "float"}, - {typeof(int), "int"}, - {typeof(uint), "uint"}, - {typeof(long), "long"}, - {typeof(ulong), "ulong"}, - {typeof(short), "short"}, - {typeof(ushort), "ushort"}, - {typeof(object), "object"}, - {typeof(string), "string"}, - }; - // From https://stackoverflow.com/a/2224421/5015269 but inverted and renamed to match similar framework methods /// /// Determines whether an instance of a specified type can be casted to a variable of the current type /// - /// - /// /// public static bool IsCastableFrom(this Type to, Type from) { @@ -99,14 +97,32 @@ namespace Artemis.Core if (PrimitiveTypeConversions.ContainsKey(to) && PrimitiveTypeConversions[to].Contains(from)) return true; bool castable = from.GetMethods(BindingFlags.Public | BindingFlags.Static) - .Any( - m => m.ReturnType == to && - (m.Name == "op_Implicit" || - m.Name == "op_Explicit") - ); + .Any(m => m.ReturnType == to && (m.Name == "op_Implicit" || m.Name == "op_Explicit")); return castable; } + /// + /// Scores how well the two types can be casted from one to another, 5 being a perfect match and 0 being not castable + /// at all + /// + /// + public static int ScoreCastability(this Type to, Type from) + { + if (to == from) + return 5; + if (to.TypeIsNumber() && from.TypeIsNumber()) + return 4; + if (PrimitiveTypeConversions.ContainsKey(to) && PrimitiveTypeConversions[to].Contains(from)) + return 3; + bool castable = from.GetMethods(BindingFlags.Public | BindingFlags.Static) + .Any(m => m.ReturnType == to && (m.Name == "op_Implicit" || m.Name == "op_Explicit")); + if (castable) + return 2; + if (to.IsAssignableFrom(from)) + return 1; + return 0; + } + /// /// Returns the default value of the given type /// @@ -149,7 +165,7 @@ namespace Artemis.Core } /// - /// Determines a display name for the given type + /// Determines a display name for the given type /// /// The type to determine the name for /// Whether or not to humanize the result, defaults to false diff --git a/src/Artemis.Core/Models/Profile/Conditions/Abstract/BaseConditionOperator.cs b/src/Artemis.Core/Models/Profile/Conditions/Abstract/BaseConditionOperator.cs new file mode 100644 index 000000000..f95557020 --- /dev/null +++ b/src/Artemis.Core/Models/Profile/Conditions/Abstract/BaseConditionOperator.cs @@ -0,0 +1,65 @@ +using System; + +namespace Artemis.Core +{ + /// + /// Represents a condition operator that performs a boolean operation + /// + /// To implement your own condition operator, inherit or + /// + /// + /// + public abstract class BaseConditionOperator + { + /// + /// Gets or sets the description of this logical operator + /// + public abstract string Description { get; } + + /// + /// Gets or sets the icon of this logical operator + /// + public abstract string Icon { get; } + + /// + /// Gets the plugin info this condition operator belongs to + /// Note: Not set until after registering + /// + public PluginInfo PluginInfo { get; internal set; } + + /// + /// Gets the left side type of this condition operator + /// + public abstract Type LeftSideType { get; } + + /// + /// Gets the right side type of this condition operator. May be null if the operator does not support a left side type + /// + public abstract Type? RightSideType { get; } + + /// + /// Returns whether the given type is supported by the operator + /// + /// The type to check for, must be either the same or be castable to the target type + /// Which side of the operator to check, left or right + public abstract bool SupportsType(Type type, ConditionParameterSide side); + + /// + /// Evaluates the condition with the input types being provided as objects + /// + /// This leaves the caller responsible for the types matching and + /// + /// + /// + /// The left side value, type should match + /// The right side value, type should match + /// The result of the boolean condition's evaluation + internal abstract bool InternalEvaluate(object? leftSideValue, object? rightSideValue); + } + + public enum ConditionParameterSide + { + Left, + Right + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Conditions/Abstract/ConditionOperator.cs b/src/Artemis.Core/Models/Profile/Conditions/Abstract/ConditionOperator.cs new file mode 100644 index 000000000..16d42e147 --- /dev/null +++ b/src/Artemis.Core/Models/Profile/Conditions/Abstract/ConditionOperator.cs @@ -0,0 +1,95 @@ +using System; + +namespace Artemis.Core +{ + /// + /// Represents a condition operator that performs a boolean operation using a left- and right-side + /// + public abstract class ConditionOperator : BaseConditionOperator + { + /// + /// Evaluates the operator on a and b + /// + /// The parameter on the left side of the expression + /// The parameter on the right side of the expression + public abstract bool Evaluate(TLeftSide a, TRightSide b); + + /// + public override bool SupportsType(Type type, ConditionParameterSide side) + { + if (type == null) + return true; + if (side == ConditionParameterSide.Left) + return LeftSideType.IsCastableFrom(type); + return RightSideType.IsCastableFrom(type); + } + + /// + internal override bool InternalEvaluate(object? leftSideValue, object? rightSideValue) + { + // TODO: Can we avoid boxing/unboxing? + TLeftSide leftSide; + if (leftSideValue != null) + leftSide = (TLeftSide) Convert.ChangeType(leftSideValue, typeof(TLeftSide)); + else + leftSide = default; + + TRightSide rightSide; + if (rightSideValue != null) + rightSide = (TRightSide) Convert.ChangeType(rightSideValue, typeof(TRightSide)); + else + rightSide = default; + + return Evaluate(leftSide, rightSide); + } + + /// + public override Type LeftSideType => typeof(TLeftSide); + + /// + public override Type RightSideType => typeof(TRightSide); + } + + /// + /// Represents a condition operator that performs a boolean operation using only a left side + /// + public abstract class ConditionOperator : BaseConditionOperator + { + /// + /// Evaluates the operator on a and b + /// + /// The parameter on the left side of the expression + public abstract bool Evaluate(TLeftSide a); + + /// + public override bool SupportsType(Type type, ConditionParameterSide side) + { + if (type == null) + return true; + if (side == ConditionParameterSide.Left) + return LeftSideType.IsCastableFrom(type); + return false; + } + + /// + internal override bool InternalEvaluate(object? leftSideValue, object? rightSideValue) + { + // TODO: Can we avoid boxing/unboxing? + TLeftSide leftSide; + if (leftSideValue != null) + leftSide = (TLeftSide) Convert.ChangeType(leftSideValue, typeof(TLeftSide)); + else + leftSide = default; + + return Evaluate(leftSide); + } + + /// + public override Type LeftSideType => typeof(TLeftSide); + + /// + /// Always null, not applicable to this type of condition operator + /// + public override Type? RightSideType => null; + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Conditions/ConditionOperator.cs b/src/Artemis.Core/Models/Profile/Conditions/ConditionOperator.cs deleted file mode 100644 index 9fe9461bd..000000000 --- a/src/Artemis.Core/Models/Profile/Conditions/ConditionOperator.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Artemis.Core -{ - /// - /// A condition operator is used by the conditions system to perform a specific boolean check - /// - public abstract class ConditionOperator - { - /// - /// Gets the plugin info this condition operator belongs to - /// Note: Not set until after registering - /// - public PluginInfo PluginInfo { get; internal set; } - - /// - /// Gets the types this operator supports - /// - public abstract IReadOnlyCollection CompatibleTypes { get; } - - /// - /// Gets or sets the description of this logical operator - /// - public abstract string Description { get; } - - /// - /// Gets or sets the icon of this logical operator - /// - public abstract string Icon { get; } - - /// - /// Gets or sets whether this condition operator supports a right side, defaults to true - /// - public bool SupportsRightSide { get; protected set; } = true; - - /// - /// Returns whether the given type is supported by the operator - /// - public bool SupportsType(Type type) - { - if (type == null) - return true; - return CompatibleTypes.Any(t => t.IsCastableFrom(type)); - } - - /// - /// Evaluates the operator on a and b - /// - /// The parameter on the left side of the expression - /// The parameter on the right side of the expression - public abstract bool Evaluate(object? a, object? b); - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Conditions/DataModelConditionListPredicate.cs b/src/Artemis.Core/Models/Profile/Conditions/DataModelConditionListPredicate.cs index cf35171cf..dcacd42b8 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/DataModelConditionListPredicate.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/DataModelConditionListPredicate.cs @@ -47,7 +47,7 @@ namespace Artemis.Core /// /// Gets the operator /// - public ConditionOperator? Operator { get; private set; } + public BaseConditionOperator? Operator { get; private set; } /// /// Gets the data model condition list this predicate belongs to @@ -99,6 +99,8 @@ namespace Artemis.Core { if (path != null && !path.IsValid) throw new ArtemisCoreException("Cannot update right side of predicate to an invalid path"); + if (Operator != null && path != null && !Operator.SupportsType(path.GetPropertyType()!, ConditionParameterSide.Right)) + throw new ArtemisCoreException($"Selected operator does not support right side of type {path.GetPropertyType()!.Name}"); RightPath?.Dispose(); RightPath = path != null ? new DataModelPath(path) : null; @@ -123,7 +125,7 @@ namespace Artemis.Core /// Updates the operator of the predicate and re-compiles the expression /// /// - public void UpdateOperator(ConditionOperator? conditionOperator) + public void UpdateOperator(BaseConditionOperator? conditionOperator) { if (conditionOperator == null) { @@ -139,12 +141,13 @@ namespace Artemis.Core } Type leftType = LeftPath.GetPropertyType()!; - if (!conditionOperator.SupportsType(leftType)) + // Left side can't go empty so enforce a match + if (!conditionOperator.SupportsType(leftType, ConditionParameterSide.Left)) throw new ArtemisCoreException($"Cannot apply operator {conditionOperator.GetType().Name} to this predicate because " + $"it does not support left side type {leftType.Name}"); - if (conditionOperator.SupportsType(leftType)) - Operator = conditionOperator; + Operator = conditionOperator; + ValidateRightSide(); } /// @@ -206,7 +209,7 @@ namespace Artemis.Core if (leftSideValue != null && leftSideValue.GetType().IsValueType && RightStaticValue == null) return false; - return Operator.Evaluate(leftSideValue, RightStaticValue); + return Operator.InternalEvaluate(leftSideValue, RightStaticValue); } if (RightPath == null || !RightPath.IsValid) @@ -217,8 +220,8 @@ namespace Artemis.Core { // If the path targets a property inside the list, evaluate on the list path value instead of the right path value if (RightPath.Target is ListPredicateWrapperDataModel) - return Operator.Evaluate(GetListPathValue(LeftPath, target), GetListPathValue(RightPath, target)); - return Operator.Evaluate(GetListPathValue(LeftPath, target), RightPath.GetValue()); + return Operator.InternalEvaluate(GetListPathValue(LeftPath, target), GetListPathValue(RightPath, target)); + return Operator.InternalEvaluate(GetListPathValue(LeftPath, target), RightPath.GetValue()); } return false; @@ -300,7 +303,7 @@ namespace Artemis.Core // Operator if (Entity.OperatorPluginGuid != null) { - ConditionOperator? conditionOperator = ConditionOperatorStore.Get(Entity.OperatorPluginGuid.Value, Entity.OperatorType)?.ConditionOperator; + BaseConditionOperator? conditionOperator = ConditionOperatorStore.Get(Entity.OperatorPluginGuid.Value, Entity.OperatorType)?.ConditionOperator; if (conditionOperator != null) UpdateOperator(conditionOperator); } @@ -361,28 +364,31 @@ namespace Artemis.Core return; Type leftType = LeftPath.GetPropertyType()!; - if (!Operator.SupportsType(leftType)) + if (!Operator.SupportsType(leftType, ConditionParameterSide.Left)) Operator = null; } private void ValidateRightSide() { - Type? leftType = LeftPath?.GetPropertyType(); + if (Operator == null) + return; + if (PredicateType == ProfileRightSideType.Dynamic) { if (RightPath == null || !RightPath.IsValid) return; Type rightSideType = RightPath.GetPropertyType()!; - if (leftType != null && !leftType.IsCastableFrom(rightSideType)) + if (!Operator.SupportsType(rightSideType, ConditionParameterSide.Right)) UpdateRightSideDynamic(null); } else { - if (RightStaticValue != null && (leftType == null || leftType.IsCastableFrom(RightStaticValue.GetType()))) - UpdateRightSideStatic(RightStaticValue); - else - UpdateRightSideStatic(null); + if (RightStaticValue == null) + return; + + if (!Operator.SupportsType(RightStaticValue.GetType(), ConditionParameterSide.Right)) + UpdateRightSideDynamic(null); } } @@ -391,24 +397,27 @@ namespace Artemis.Core RightPath?.Dispose(); RightPath = null; - // If the left side is empty simply apply the value, any validation will wait - if (LeftPath == null || !LeftPath.IsValid) + // If the operator is null simply apply the value, any validation will wait + if (Operator == null) { RightStaticValue = staticValue; return; } - - // If the left path is valid we can expect a type - Type leftSideType = LeftPath.GetPropertyType()!; + // If the operator does not support a right side, always set it to null + if (Operator.RightSideType == null) + { + RightStaticValue = null; + return; + } // If not null ensure the types match and if not, convert it - if (staticValue != null && staticValue.GetType() == leftSideType) + if (staticValue != null && staticValue.GetType() == Operator.RightSideType) RightStaticValue = staticValue; else if (staticValue != null) - RightStaticValue = Convert.ChangeType(staticValue, leftSideType); + RightStaticValue = Convert.ChangeType(staticValue, Operator.RightSideType); // If null create a default instance for value types or simply make it null for reference types - else if (leftSideType.IsValueType) - RightStaticValue = Activator.CreateInstance(leftSideType); + else if (Operator.RightSideType.IsValueType) + RightStaticValue = Activator.CreateInstance(Operator.RightSideType); else RightStaticValue = null; } @@ -426,7 +435,7 @@ namespace Artemis.Core private void ConditionOperatorStoreOnConditionOperatorAdded(object sender, ConditionOperatorStoreEvent e) { - ConditionOperator conditionOperator = e.Registration.ConditionOperator; + BaseConditionOperator conditionOperator = e.Registration.ConditionOperator; if (Entity.OperatorPluginGuid == conditionOperator.PluginInfo.Guid && Entity.OperatorType == conditionOperator.GetType().Name) UpdateOperator(conditionOperator); } diff --git a/src/Artemis.Core/Models/Profile/Conditions/DataModelConditionPredicate.cs b/src/Artemis.Core/Models/Profile/Conditions/DataModelConditionPredicate.cs index d7b107c74..27cdcc4b2 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/DataModelConditionPredicate.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/DataModelConditionPredicate.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using Artemis.Core.DataModelExpansions; using Artemis.Storage.Entities.Profile.Abstract; using Artemis.Storage.Entities.Profile.Conditions; @@ -43,7 +44,7 @@ namespace Artemis.Core /// /// Gets the operator /// - public ConditionOperator? Operator { get; private set; } + public BaseConditionOperator? Operator { get; private set; } /// /// Gets the path of the left property @@ -87,6 +88,8 @@ namespace Artemis.Core { if (path != null && !path.IsValid) throw new ArtemisCoreException("Cannot update right side of predicate to an invalid path"); + if (Operator != null && path != null && !Operator.SupportsType(path.GetPropertyType()!, ConditionParameterSide.Right)) + throw new ArtemisCoreException($"Selected operator does not support right side of type {path.GetPropertyType()!.Name}"); RightPath?.Dispose(); RightPath = path != null ? new DataModelPath(path) : null; @@ -104,24 +107,27 @@ namespace Artemis.Core RightPath?.Dispose(); RightPath = null; - // If the left side is empty simply apply the value, any validation will wait - if (LeftPath == null || !LeftPath.IsValid) + // If the operator is null simply apply the value, any validation will wait + if (Operator == null) { RightStaticValue = staticValue; return; } - - // If the left path is valid we can expect a type - Type leftSideType = LeftPath.GetPropertyType()!; + // If the operator does not support a right side, always set it to null + if (Operator.RightSideType == null) + { + RightStaticValue = null; + return; + } // If not null ensure the types match and if not, convert it - if (staticValue != null && staticValue.GetType() == leftSideType) + if (staticValue != null && staticValue.GetType() == Operator.RightSideType) RightStaticValue = staticValue; else if (staticValue != null) - RightStaticValue = Convert.ChangeType(staticValue, leftSideType); + RightStaticValue = Convert.ChangeType(staticValue, Operator.RightSideType); // If null create a default instance for value types or simply make it null for reference types - else if (leftSideType.IsValueType) - RightStaticValue = Activator.CreateInstance(leftSideType); + else if (Operator.RightSideType.IsValueType) + RightStaticValue = Activator.CreateInstance(Operator.RightSideType); else RightStaticValue = null; } @@ -130,9 +136,8 @@ namespace Artemis.Core /// Updates the operator of the predicate and re-compiles the expression /// /// - public void UpdateOperator(ConditionOperator? conditionOperator) + public void UpdateOperator(BaseConditionOperator? conditionOperator) { - // Calling CreateExpression will clear compiled expressions if (conditionOperator == null) { Operator = null; @@ -147,11 +152,13 @@ namespace Artemis.Core } Type leftType = LeftPath.GetPropertyType()!; - if (!conditionOperator.SupportsType(leftType)) + // Left side can't go empty so enforce a match + if (!conditionOperator.SupportsType(leftType, ConditionParameterSide.Left)) throw new ArtemisCoreException($"Cannot apply operator {conditionOperator.GetType().Name} to this predicate because " + $"it does not support left side type {leftType.Name}"); Operator = conditionOperator; + ValidateRightSide(); } /// @@ -167,14 +174,14 @@ namespace Artemis.Core if (leftSideValue != null && leftSideValue.GetType().IsValueType && RightStaticValue == null) return false; - return Operator.Evaluate(leftSideValue, RightStaticValue); + return Operator.InternalEvaluate(leftSideValue, RightStaticValue); } if (RightPath == null || !RightPath.IsValid) return false; // Compare with dynamic values - return Operator.Evaluate(LeftPath.GetValue(), RightPath.GetValue()); + return Operator.InternalEvaluate(LeftPath.GetValue(), RightPath.GetValue()); } /// @@ -237,7 +244,7 @@ namespace Artemis.Core // Operator if (Entity.OperatorPluginGuid != null) { - ConditionOperator? conditionOperator = ConditionOperatorStore.Get(Entity.OperatorPluginGuid.Value, Entity.OperatorType)?.ConditionOperator; + BaseConditionOperator? conditionOperator = ConditionOperatorStore.Get(Entity.OperatorPluginGuid.Value, Entity.OperatorType)?.ConditionOperator; if (conditionOperator != null) UpdateOperator(conditionOperator); } @@ -292,28 +299,31 @@ namespace Artemis.Core return; Type leftType = LeftPath.GetPropertyType()!; - if (!Operator.SupportsType(leftType)) + if (!Operator.SupportsType(leftType, ConditionParameterSide.Left)) Operator = null; } private void ValidateRightSide() { - Type? leftType = LeftPath?.GetPropertyType(); + if (Operator == null) + return; + if (PredicateType == ProfileRightSideType.Dynamic) { if (RightPath == null || !RightPath.IsValid) return; Type rightSideType = RightPath.GetPropertyType()!; - if (leftType != null && !leftType.IsCastableFrom(rightSideType)) + if (!Operator.SupportsType(rightSideType, ConditionParameterSide.Right)) UpdateRightSideDynamic(null); } else { - if (RightStaticValue != null && (leftType == null || leftType.IsCastableFrom(RightStaticValue.GetType()))) - UpdateRightSideStatic(RightStaticValue); - else - UpdateRightSideStatic(null); + if (RightStaticValue == null) + return; + + if (!Operator.SupportsType(RightStaticValue.GetType(), ConditionParameterSide.Right)) + UpdateRightSideDynamic(null); } } @@ -321,7 +331,7 @@ namespace Artemis.Core private void ConditionOperatorStoreOnConditionOperatorAdded(object? sender, ConditionOperatorStoreEvent e) { - ConditionOperator conditionOperator = e.Registration.ConditionOperator; + BaseConditionOperator conditionOperator = e.Registration.ConditionOperator; if (Entity.OperatorPluginGuid == conditionOperator.PluginInfo.Guid && Entity.OperatorType == conditionOperator.GetType().Name) UpdateOperator(conditionOperator); } diff --git a/src/Artemis.Core/Services/Registration/ConditionOperatorService.cs b/src/Artemis.Core/Services/Registration/ConditionOperatorService.cs index bf99c5e1c..2c009b33d 100644 --- a/src/Artemis.Core/Services/Registration/ConditionOperatorService.cs +++ b/src/Artemis.Core/Services/Registration/ConditionOperatorService.cs @@ -12,7 +12,7 @@ namespace Artemis.Core.Services RegisterBuiltInConditionOperators(); } - public ConditionOperatorRegistration RegisterConditionOperator(PluginInfo pluginInfo, ConditionOperator conditionOperator) + public ConditionOperatorRegistration RegisterConditionOperator(PluginInfo pluginInfo, BaseConditionOperator conditionOperator) { if (pluginInfo == null) throw new ArgumentNullException(nameof(pluginInfo)); @@ -30,12 +30,12 @@ namespace Artemis.Core.Services ConditionOperatorStore.Remove(registration); } - public List GetConditionOperatorsForType(Type type) + public List GetConditionOperatorsForType(Type type, ConditionParameterSide side) { - return ConditionOperatorStore.GetForType(type).Select(r => r.ConditionOperator).ToList(); + return ConditionOperatorStore.GetForType(type, side).Select(r => r.ConditionOperator).ToList(); } - public ConditionOperator GetConditionOperator(Guid operatorPluginGuid, string operatorType) + public BaseConditionOperator GetConditionOperator(Guid operatorPluginGuid, string operatorType) { return ConditionOperatorStore.Get(operatorPluginGuid, operatorType)?.ConditionOperator; } @@ -47,6 +47,8 @@ namespace Artemis.Core.Services RegisterConditionOperator(Constants.CorePluginInfo, new NotEqualConditionOperator()); // Numeric operators + RegisterConditionOperator(Constants.CorePluginInfo, new NumberEqualsConditionOperator()); + RegisterConditionOperator(Constants.CorePluginInfo, new NumberNotEqualConditionOperator()); RegisterConditionOperator(Constants.CorePluginInfo, new LessThanConditionOperator()); RegisterConditionOperator(Constants.CorePluginInfo, new GreaterThanConditionOperator()); RegisterConditionOperator(Constants.CorePluginInfo, new LessThanOrEqualConditionOperator()); diff --git a/src/Artemis.Core/Services/Registration/Interfaces/IConditionOperatorService.cs b/src/Artemis.Core/Services/Registration/Interfaces/IConditionOperatorService.cs index f59eb587d..49cf60b16 100644 --- a/src/Artemis.Core/Services/Registration/Interfaces/IConditionOperatorService.cs +++ b/src/Artemis.Core/Services/Registration/Interfaces/IConditionOperatorService.cs @@ -14,7 +14,7 @@ namespace Artemis.Core.Services /// /// The PluginInfo of the plugin this condition operator belongs to /// The condition operator to register - ConditionOperatorRegistration RegisterConditionOperator([NotNull] PluginInfo pluginInfo, [NotNull] ConditionOperator conditionOperator); + ConditionOperatorRegistration RegisterConditionOperator([NotNull] PluginInfo pluginInfo, [NotNull] BaseConditionOperator conditionOperator); /// /// Removes a condition operator so it is no longer available for use in layer conditions @@ -25,13 +25,13 @@ namespace Artemis.Core.Services /// /// Returns all the condition operators compatible with the provided type /// - List GetConditionOperatorsForType(Type type); + List GetConditionOperatorsForType(Type type, ConditionParameterSide side); /// /// Gets a condition operator by its plugin GUID and type name /// /// The operator's plugin GUID /// The type name of the operator - ConditionOperator GetConditionOperator(Guid operatorPluginGuid, string operatorType); + BaseConditionOperator GetConditionOperator(Guid operatorPluginGuid, string operatorType); } } \ No newline at end of file diff --git a/src/Artemis.Core/Stores/ConditionOperatorStore.cs b/src/Artemis.Core/Stores/ConditionOperatorStore.cs index f7d49161d..48125ac6a 100644 --- a/src/Artemis.Core/Stores/ConditionOperatorStore.cs +++ b/src/Artemis.Core/Stores/ConditionOperatorStore.cs @@ -8,7 +8,7 @@ namespace Artemis.Core { private static readonly List Registrations = new List(); - public static ConditionOperatorRegistration Add(ConditionOperator conditionOperator) + public static ConditionOperatorRegistration Add(BaseConditionOperator conditionOperator) { ConditionOperatorRegistration registration; lock (Registrations) @@ -46,19 +46,21 @@ namespace Artemis.Core } } - public static List GetForType(Type type) + public static List GetForType(Type type, ConditionParameterSide side) { lock (Registrations) { if (type == null) return new List(Registrations); - List candidates = Registrations.Where(r => r.ConditionOperator.CompatibleTypes.Any(t => t.IsCastableFrom(type))).ToList(); + List candidates = Registrations.Where(r => r.ConditionOperator.SupportsType(type, side)).ToList(); // If there are multiple operators with the same description, use the closest match foreach (IGrouping candidate in candidates.GroupBy(r => r.ConditionOperator.Description).Where(g => g.Count() > 1).ToList()) { - ConditionOperatorRegistration closest = candidate.OrderByDescending(r => r.ConditionOperator.CompatibleTypes.Contains(type)).FirstOrDefault(); + ConditionOperatorRegistration closest = side == ConditionParameterSide.Left + ? candidate.OrderByDescending(r => r.ConditionOperator.LeftSideType.ScoreCastability(type)).First() + : candidate.OrderByDescending(r => r.ConditionOperator.RightSideType!.ScoreCastability(type)).First(); foreach (ConditionOperatorRegistration conditionOperator in candidate) { if (conditionOperator != closest) diff --git a/src/Artemis.Core/Stores/Registrations/ConditionOperatorRegistration.cs b/src/Artemis.Core/Stores/Registrations/ConditionOperatorRegistration.cs index babd6da7e..1b0e6c109 100644 --- a/src/Artemis.Core/Stores/Registrations/ConditionOperatorRegistration.cs +++ b/src/Artemis.Core/Stores/Registrations/ConditionOperatorRegistration.cs @@ -7,7 +7,7 @@ namespace Artemis.Core /// public class ConditionOperatorRegistration { - internal ConditionOperatorRegistration(ConditionOperator conditionOperator, Plugin plugin) + internal ConditionOperatorRegistration(BaseConditionOperator conditionOperator, Plugin plugin) { ConditionOperator = conditionOperator; Plugin = plugin; @@ -18,7 +18,7 @@ namespace Artemis.Core /// /// Gets the condition operator that has been registered /// - public ConditionOperator ConditionOperator { get; } + public BaseConditionOperator ConditionOperator { get; } /// /// Gets the plugin the condition operator is associated with diff --git a/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionListPredicateViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionListPredicateViewModel.cs index 829ddfcca..ee9f93de5 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionListPredicateViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionListPredicateViewModel.cs @@ -21,10 +21,10 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions private readonly IProfileEditorService _profileEditorService; private bool _isPrimitiveList; private DataModelDynamicViewModel _leftSideSelectionViewModel; - private BindableCollection _operators; + private BindableCollection _operators; private DataModelStaticViewModel _rightSideInputViewModel; private DataModelDynamicViewModel _rightSideSelectionViewModel; - private ConditionOperator _selectedOperator; + private BaseConditionOperator _selectedOperator; private List _supportedInputTypes; @@ -40,14 +40,14 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions _supportedInputTypes = new List(); SelectOperatorCommand = new DelegateCommand(ExecuteSelectOperatorCommand); - Operators = new BindableCollection(); + Operators = new BindableCollection(); Initialize(); } public DataModelConditionListPredicate DataModelConditionListPredicate => (DataModelConditionListPredicate) Model; - public BindableCollection Operators + public BindableCollection Operators { get => _operators; set => SetAndNotify(ref _operators, value); @@ -71,7 +71,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions set => SetAndNotify(ref _rightSideInputViewModel, value); } - public ConditionOperator SelectedOperator + public BaseConditionOperator SelectedOperator { get => _selectedOperator; set => SetAndNotify(ref _selectedOperator, value); @@ -129,35 +129,41 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions // Get the supported operators Operators.Clear(); - Operators.AddRange(_conditionOperatorService.GetConditionOperatorsForType(leftSideType ?? typeof(object))); + Operators.AddRange(_conditionOperatorService.GetConditionOperatorsForType(leftSideType ?? typeof(object), ConditionParameterSide.Left)); if (DataModelConditionListPredicate.Operator == null) - DataModelConditionListPredicate.UpdateOperator(Operators.FirstOrDefault(o => o.SupportsType(leftSideType ?? typeof(object)))); + DataModelConditionListPredicate.UpdateOperator(Operators.FirstOrDefault(o => o.SupportsType(leftSideType ?? typeof(object), ConditionParameterSide.Left))); SelectedOperator = DataModelConditionListPredicate.Operator; - if (SelectedOperator == null || !SelectedOperator.SupportsRightSide) + + // Without a selected operator or one that supports a right side, leave the right side input empty + if (SelectedOperator == null || SelectedOperator.RightSideType == null) { DisposeRightSideStaticViewModel(); DisposeRightSideDynamicViewModel(); + return; } // Ensure the right side has the proper VM - if (DataModelConditionListPredicate.PredicateType == ProfileRightSideType.Dynamic && SelectedOperator.SupportsRightSide) + if (DataModelConditionListPredicate.PredicateType == ProfileRightSideType.Dynamic) { DisposeRightSideStaticViewModel(); if (RightSideSelectionViewModel == null) CreateRightSideSelectionViewModel(); - RightSideSelectionViewModel.FilterTypes = new[] {leftSideType}; RightSideSelectionViewModel.ChangeDataModelPath(DataModelConditionListPredicate.RightPath); + RightSideSelectionViewModel.FilterTypes = new[] {SelectedOperator.RightSideType}; } - else if (SelectedOperator.SupportsRightSide) + else { DisposeRightSideDynamicViewModel(); if (RightSideInputViewModel == null) - CreateRightSideInputViewModel(leftSideType); + CreateRightSideInputViewModel(SelectedOperator.RightSideType); - RightSideInputViewModel.Value = DataModelConditionListPredicate.RightStaticValue; - if (RightSideInputViewModel.TargetType != leftSideType) - RightSideInputViewModel.UpdateTargetType(leftSideType); + if (SelectedOperator.RightSideType.IsValueType && DataModelConditionListPredicate.RightStaticValue == null) + RightSideInputViewModel.Value = SelectedOperator.RightSideType.GetDefault(); + else + RightSideInputViewModel.Value = DataModelConditionListPredicate.RightStaticValue; + if (RightSideInputViewModel.TargetType != SelectedOperator.RightSideType) + RightSideInputViewModel.UpdateTargetType(SelectedOperator.RightSideType); } } @@ -210,7 +216,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions private void ExecuteSelectOperatorCommand(object context) { - if (!(context is ConditionOperator dataModelConditionOperator)) + if (!(context is BaseConditionOperator dataModelConditionOperator)) return; SelectedOperator = dataModelConditionOperator; @@ -276,7 +282,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions // Add an extra data model to the selection VM to allow self-referencing the current item // The safe cast prevents adding this extra VM on primitive lists where they serve no purpose - if (GetListDataModel()?.Children?.FirstOrDefault() is DataModelPropertiesViewModel listValue) + if (GetListDataModel()?.Children?.FirstOrDefault() is DataModelPropertiesViewModel listValue) RightSideSelectionViewModel.ExtraDataModelViewModels.Add(listValue); } diff --git a/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionPredicateViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionPredicateViewModel.cs index e7579968f..40793422f 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionPredicateViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionPredicateViewModel.cs @@ -19,10 +19,10 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions private readonly IDataModelUIService _dataModelUIService; private readonly IProfileEditorService _profileEditorService; private DataModelDynamicViewModel _leftSideSelectionViewModel; - private BindableCollection _operators; + private BindableCollection _operators; private DataModelStaticViewModel _rightSideInputViewModel; private DataModelDynamicViewModel _rightSideSelectionViewModel; - private ConditionOperator _selectedOperator; + private BaseConditionOperator _selectedOperator; private List _supportedInputTypes; @@ -39,7 +39,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions _supportedInputTypes = new List(); SelectOperatorCommand = new DelegateCommand(ExecuteSelectOperatorCommand); - Operators = new BindableCollection(); + Operators = new BindableCollection(); ShowDataModelValues = settingsService.GetSetting("ProfileEditor.ShowDataModelValues"); @@ -50,7 +50,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions public PluginSetting ShowDataModelValues { get; } - public BindableCollection Operators + public BindableCollection Operators { get => _operators; set => SetAndNotify(ref _operators, value); @@ -62,7 +62,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions set => SetAndNotify(ref _leftSideSelectionViewModel, value); } - public ConditionOperator SelectedOperator + public BaseConditionOperator SelectedOperator { get => _selectedOperator; set => SetAndNotify(ref _selectedOperator, value); @@ -109,12 +109,13 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions // Get the supported operators Operators.Clear(); - Operators.AddRange(_conditionOperatorService.GetConditionOperatorsForType(leftSideType ?? typeof(object))); + Operators.AddRange(_conditionOperatorService.GetConditionOperatorsForType(leftSideType ?? typeof(object), ConditionParameterSide.Left)); if (DataModelConditionPredicate.Operator == null) - DataModelConditionPredicate.UpdateOperator(Operators.FirstOrDefault(o => o.SupportsType(leftSideType ?? typeof(object)))); + DataModelConditionPredicate.UpdateOperator(Operators.FirstOrDefault(o => o.SupportsType(leftSideType ?? typeof(object), ConditionParameterSide.Left))); SelectedOperator = DataModelConditionPredicate.Operator; - if (SelectedOperator == null || !SelectedOperator.SupportsRightSide) + // Without a selected operator or one that supports a right side, leave the right side input empty + if (SelectedOperator == null || SelectedOperator.RightSideType == null) { DisposeRightSideStaticViewModel(); DisposeRightSideDynamicViewModel(); @@ -129,20 +130,20 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions CreateRightSideSelectionViewModel(); RightSideSelectionViewModel.ChangeDataModelPath(DataModelConditionPredicate.RightPath); - RightSideSelectionViewModel.FilterTypes = new[] {leftSideType}; + RightSideSelectionViewModel.FilterTypes = new[] {SelectedOperator.RightSideType}; } else { DisposeRightSideDynamicViewModel(); if (RightSideInputViewModel == null) - CreateRightSideInputViewModel(leftSideType); + CreateRightSideInputViewModel(SelectedOperator.RightSideType); - if (leftSideType != null && leftSideType.IsValueType && DataModelConditionPredicate.RightStaticValue == null) - RightSideInputViewModel.Value = leftSideType.GetDefault(); + if (SelectedOperator.RightSideType.IsValueType && DataModelConditionPredicate.RightStaticValue == null) + RightSideInputViewModel.Value = SelectedOperator.RightSideType.GetDefault(); else RightSideInputViewModel.Value = DataModelConditionPredicate.RightStaticValue; - if (RightSideInputViewModel.TargetType != leftSideType) - RightSideInputViewModel.UpdateTargetType(leftSideType); + if (RightSideInputViewModel.TargetType != SelectedOperator.RightSideType) + RightSideInputViewModel.UpdateTargetType(SelectedOperator.RightSideType); } } @@ -188,7 +189,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions private void ExecuteSelectOperatorCommand(object context) { - if (!(context is ConditionOperator DataModelConditionOperator)) + if (!(context is BaseConditionOperator DataModelConditionOperator)) return; SelectedOperator = DataModelConditionOperator;