1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-13 05:48:35 +00:00

Conditions - Added null-checks to accessors

Conditions - Simplified operators, removing unnecessary expression trees
This commit is contained in:
SpoinkyNL 2020-09-24 00:02:53 +02:00
parent ad3581a93a
commit 33373bda57
24 changed files with 253 additions and 311 deletions

View File

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
namespace Artemis.Core.DefaultTypes
{
@ -11,9 +10,9 @@ namespace Artemis.Core.DefaultTypes
public override string Description => "Equals";
public override string Icon => "Equal";
public override BinaryExpression CreateExpression(Expression leftSide, Expression rightSide)
public override bool Evaluate(object a, object b)
{
return Expression.Equal(leftSide, rightSide);
return Equals(a, b);
}
}
}

View File

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
namespace Artemis.Core.DefaultTypes
{
@ -11,9 +10,9 @@ namespace Artemis.Core.DefaultTypes
public override string Description => "Is greater than";
public override string Icon => "GreaterThan";
public override BinaryExpression CreateExpression(Expression leftSide, Expression rightSide)
public override bool Evaluate(object a, object b)
{
return Expression.GreaterThan(leftSide, rightSide);
return Convert.ToSingle(a) > Convert.ToSingle(b);
}
}
}

View File

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
namespace Artemis.Core.DefaultTypes
{
@ -11,9 +10,9 @@ namespace Artemis.Core.DefaultTypes
public override string Description => "Is greater than or equal to";
public override string Icon => "GreaterThanOrEqual";
public override BinaryExpression CreateExpression(Expression leftSide, Expression rightSide)
public override bool Evaluate(object a, object b)
{
return Expression.GreaterThanOrEqual(leftSide, rightSide);
return Convert.ToSingle(a) >= Convert.ToSingle(b);
}
}
}

View File

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
namespace Artemis.Core.DefaultTypes
{
@ -11,9 +10,9 @@ namespace Artemis.Core.DefaultTypes
public override string Description => "Is less than";
public override string Icon => "LessThan";
public override BinaryExpression CreateExpression(Expression leftSide, Expression rightSide)
public override bool Evaluate(object a, object b)
{
return Expression.LessThan(leftSide, rightSide);
return Convert.ToSingle(a) < Convert.ToSingle(b);
}
}
}

View File

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
namespace Artemis.Core.DefaultTypes
{
@ -11,9 +10,9 @@ namespace Artemis.Core.DefaultTypes
public override string Description => "Is less than or equal to";
public override string Icon => "LessThanOrEqual";
public override BinaryExpression CreateExpression(Expression leftSide, Expression rightSide)
public override bool Evaluate(object a, object b)
{
return Expression.LessThanOrEqual(leftSide, rightSide);
return Convert.ToSingle(a) <= Convert.ToSingle(b);
}
}
}

View File

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
namespace Artemis.Core.DefaultTypes
{
@ -11,9 +10,9 @@ namespace Artemis.Core.DefaultTypes
public override string Description => "Does not equal";
public override string Icon => "NotEqualVariant";
public override BinaryExpression CreateExpression(Expression leftSide, Expression rightSide)
public override bool Evaluate(object a, object b)
{
return Expression.NotEqual(leftSide, rightSide);
return !Equals(a, b);
}
}
}

View File

@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
namespace Artemis.Core.DefaultTypes
{
internal class NotNullConditionOperator : ConditionOperator
{
public NotNullConditionOperator()
{
SupportsRightSide = false;
}
public override IReadOnlyCollection<Type> CompatibleTypes => new List<Type> {typeof(object)};
public override string Description => "Is not null";
public override string Icon => "CheckboxMarkedCircleOutline";
public override bool Evaluate(object a, object b)
{
return a != null;
}
}
}

View File

@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
namespace Artemis.Core.DefaultTypes
{
internal class NullConditionOperator : ConditionOperator
{
public NullConditionOperator()
{
SupportsRightSide = false;
}
public override IReadOnlyCollection<Type> CompatibleTypes => new List<Type> {typeof(object)};
public override string Description => "Is null";
public override string Icon => "Null";
public override bool Evaluate(object a, object b)
{
return a == null;
}
}
}

View File

@ -1,33 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;
namespace Artemis.Core.DefaultTypes
{
internal class StringContainsConditionOperator : ConditionOperator
{
private readonly MethodInfo _contains;
private readonly MethodInfo _toLower;
public StringContainsConditionOperator()
{
_toLower = typeof(string).GetMethod("ToLower", new Type[] { });
_contains = typeof(string).GetMethod("Contains", new[] {typeof(string)});
}
public override IReadOnlyCollection<Type> CompatibleTypes => new List<Type> {typeof(string)};
public override string Description => "Contains";
public override string Icon => "Contain";
public override BinaryExpression CreateExpression(Expression leftSide, Expression rightSide)
public override bool Evaluate(object a, object b)
{
var contains = Expression.Equal(
Expression.Call(Expression.Call(leftSide, _toLower), _contains, Expression.Call(rightSide, _toLower)),
Expression.Constant(true)
);
return AddNullChecks(leftSide, rightSide, contains);
var aString = (string) a;
var bString = (string) b;
return bString != null && aString != null && aString.Contains(bString, StringComparison.InvariantCultureIgnoreCase);
}
}
}

View File

@ -1,33 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;
namespace Artemis.Core.DefaultTypes
{
internal class StringEndsWithConditionOperator : ConditionOperator
{
private readonly MethodInfo _endsWith;
private readonly MethodInfo _toLower;
public StringEndsWithConditionOperator()
{
_toLower = typeof(string).GetMethod("ToLower", new Type[] { });
_endsWith = typeof(string).GetMethod("EndsWith", new[] {typeof(string)});
}
public override IReadOnlyCollection<Type> CompatibleTypes => new List<Type> {typeof(string)};
public override string Description => "Ends with";
public override string Icon => "ContainEnd";
public override BinaryExpression CreateExpression(Expression leftSide, Expression rightSide)
public override bool Evaluate(object a, object b)
{
var endsWith = Expression.Equal(
Expression.Call(Expression.Call(leftSide, _toLower), _endsWith, Expression.Call(rightSide, _toLower)),
Expression.Constant(true)
);
return AddNullChecks(leftSide, rightSide, endsWith);
var aString = (string) a;
var bString = (string) b;
return bString != null && aString != null && aString.EndsWith(bString, StringComparison.InvariantCultureIgnoreCase);
}
}
}

View File

@ -1,27 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;
namespace Artemis.Core.DefaultTypes
{
internal class StringEqualsConditionOperator : ConditionOperator
{
private readonly MethodInfo _toLower;
public StringEqualsConditionOperator()
{
_toLower = typeof(string).GetMethod("ToLower", new Type[] { });
}
public override IReadOnlyCollection<Type> CompatibleTypes => new List<Type> {typeof(string)};
public override string Description => "Equals";
public override string Icon => "Equal";
public override BinaryExpression CreateExpression(Expression leftSide, Expression rightSide)
public override bool Evaluate(object a, object b)
{
return Expression.Equal(Expression.Call(leftSide, _toLower), Expression.Call(rightSide, _toLower));
var aString = (string) a;
var bString = (string) b;
return string.Equals(aString, bString, StringComparison.InvariantCultureIgnoreCase);
}
}
}

View File

@ -1,33 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;
namespace Artemis.Core.DefaultTypes
{
internal class StringNotContainsConditionOperator : ConditionOperator
{
private readonly MethodInfo _contains;
private readonly MethodInfo _toLower;
public StringNotContainsConditionOperator()
{
_toLower = typeof(string).GetMethod("ToLower", new Type[] { });
_contains = typeof(string).GetMethod("Contains", new[] {typeof(string)});
}
public override IReadOnlyCollection<Type> CompatibleTypes => new List<Type> {typeof(string)};
public override string Description => "Does not contain";
public override string Icon => "FormatStrikethrough";
public override BinaryExpression CreateExpression(Expression leftSide, Expression rightSide)
public override bool Evaluate(object a, object b)
{
var notContains = Expression.Equal(
Expression.Call(Expression.Call(leftSide, _toLower), _contains, Expression.Call(rightSide, _toLower)),
Expression.Constant(false)
);
return AddNullChecks(leftSide, rightSide, notContains);
var aString = (string) a;
var bString = (string) b;
return bString != null && aString != null && !aString.Contains(bString, StringComparison.InvariantCultureIgnoreCase);
}
}
}

View File

@ -1,27 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;
namespace Artemis.Core.DefaultTypes
{
internal class StringNotEqualConditionOperator : ConditionOperator
{
private readonly MethodInfo _toLower;
public StringNotEqualConditionOperator()
{
_toLower = typeof(string).GetMethod("ToLower", new Type[] { });
}
public override IReadOnlyCollection<Type> CompatibleTypes => new List<Type> {typeof(string)};
public override string Description => "Does not equal";
public override string Icon => "NotEqualVariant";
public override BinaryExpression CreateExpression(Expression leftSide, Expression rightSide)
public override bool Evaluate(object a, object b)
{
return Expression.NotEqual(Expression.Call(leftSide, _toLower), Expression.Call(rightSide, _toLower));
var aString = (string) a;
var bString = (string) b;
return !string.Equals(aString, bString, StringComparison.InvariantCultureIgnoreCase);
}
}
}

View File

@ -1,24 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
namespace Artemis.Core.DefaultTypes
{
internal class StringNullConditionOperator : ConditionOperator
{
public StringNullConditionOperator()
{
SupportsRightSide = false;
}
public override IReadOnlyCollection<Type> CompatibleTypes => new List<Type> {typeof(string)};
public override string Description => "Is null";
public override string Icon => "Null";
public override BinaryExpression CreateExpression(Expression leftSide, Expression rightSide)
{
return Expression.Equal(leftSide, Expression.Constant(null, leftSide.Type));
}
}
}

View File

@ -1,33 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;
namespace Artemis.Core.DefaultTypes
{
internal class StringStartsWithConditionOperator : ConditionOperator
{
private readonly MethodInfo _startsWith;
private readonly MethodInfo _toLower;
public StringStartsWithConditionOperator()
{
_toLower = typeof(string).GetMethod("ToLower", new Type[] { });
_startsWith = typeof(string).GetMethod("StartsWith", new[] {typeof(string)});
}
public override IReadOnlyCollection<Type> CompatibleTypes => new List<Type> {typeof(string)};
public override string Description => "Starts with";
public override string Icon => "ContainStart";
public override BinaryExpression CreateExpression(Expression leftSide, Expression rightSide)
public override bool Evaluate(object a, object b)
{
var startsWith = Expression.Equal(
Expression.Call(Expression.Call(leftSide, _toLower), _startsWith, Expression.Call(rightSide, _toLower)),
Expression.Constant(true)
);
return AddNullChecks(leftSide, rightSide, startsWith);
var aString = (string) a;
var bString = (string) b;
return bString != null && aString != null && aString.StartsWith(bString, StringComparison.InvariantCultureIgnoreCase);
}
}
}

View File

@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
namespace Artemis.Core
{
@ -47,40 +46,10 @@ namespace Artemis.Core
}
/// <summary>
/// Creates a binary expression comparing two types
/// Evaluates the operator on a and b
/// </summary>
/// <param name="leftSide">The parameter on the left side of the expression</param>
/// <param name="rightSide">The parameter on the right side of the expression</param>
/// <returns></returns>
public abstract BinaryExpression CreateExpression(Expression leftSide, Expression rightSide);
/// <summary>
/// Wraps the provided expression in null-checks for the left and right side
/// <para>
/// The resulting expression body looks like:
/// <code>(a == null &amp;&amp; b == null) || ((a != null &amp;&amp; b != null) &amp;&amp; &lt;expression&gt;)</code>
/// </para>
/// </summary>
/// <param name="leftSide">The left side to check for nulls</param>
/// <param name="rightSide">The right side to check for nulls</param>
/// <param name="expression">The expression to wrap</param>
/// <returns>The wrapped expression</returns>
protected BinaryExpression AddNullChecks(Expression leftSide, Expression rightSide, BinaryExpression expression)
{
var nullConst = Expression.Constant(null);
return Expression.OrElse(
Expression.AndAlso(
Expression.Equal(leftSide, nullConst),
Expression.Equal(rightSide, nullConst)
),
Expression.AndAlso(
Expression.AndAlso(
Expression.NotEqual(leftSide, nullConst),
Expression.NotEqual(rightSide, nullConst)
),
expression
)
);
}
/// <param name="a">The parameter on the left side of the expression</param>
/// <param name="b">The parameter on the right side of the expression</param>
public abstract bool Evaluate(object a, object b);
}
}

View File

@ -77,15 +77,8 @@ namespace Artemis.Core
/// </summary>
public object RightStaticValue { get; private set; }
/// <summary>
/// Gets the compiled expression that evaluates this predicate
/// </summary>
public Func<object, bool> CompiledListPredicate { get; private set; }
/// <summary>
/// Gets the compiled expression that evaluates this predicate on an external right-side data model
/// </summary>
public Func<object, DataModel, bool> CompiledExternalListPredicate { get; set; }
public Func<object, object> LeftSideAccessor { get; set; }
public Func<object, object> RightSideAccessor { get; set; }
/// <summary>
/// Updates the left side of the predicate
@ -215,28 +208,26 @@ namespace Artemis.Core
return true;
}
#region IDisposable
/// <inheritdoc />
protected override void Dispose(bool disposing)
{
ConditionOperatorStore.ConditionOperatorAdded -= ConditionOperatorStoreOnConditionOperatorAdded;
ConditionOperatorStore.ConditionOperatorRemoved -= ConditionOperatorStoreOnConditionOperatorRemoved;
base.Dispose(disposing);
}
#endregion
internal override bool EvaluateObject(object target)
{
if (PredicateType == ListRightSideType.Static && CompiledListPredicate != null)
return CompiledListPredicate(target);
if (PredicateType == ListRightSideType.DynamicList && CompiledListPredicate != null)
return CompiledListPredicate(target);
if (PredicateType == ListRightSideType.Dynamic && CompiledExternalListPredicate != null)
return CompiledExternalListPredicate(target, RightDataModel);
if (Operator == null || LeftSideAccessor == null || PredicateType != ListRightSideType.Static && RightSideAccessor == null)
return false;
// Compare with a static value
if (PredicateType == ListRightSideType.Static)
{
if (!DataModelConditionList.ListType.IsValueType && RightStaticValue == null)
return false;
return Operator.Evaluate(LeftSideAccessor(target), RightStaticValue);
}
// Compare with dynamic values
if (PredicateType == ListRightSideType.Dynamic)
return Operator.Evaluate(LeftSideAccessor(target), RightSideAccessor(RightDataModel));
if (PredicateType == ListRightSideType.DynamicList)
return Operator.Evaluate(LeftSideAccessor(target), RightSideAccessor(target));
return false;
}
@ -308,6 +299,8 @@ namespace Artemis.Core
private void Initialize()
{
DataModelStore.DataModelAdded += DataModelStoreOnDataModelAdded;
DataModelStore.DataModelRemoved += DataModelStoreOnDataModelRemoved;
ConditionOperatorStore.ConditionOperatorAdded += ConditionOperatorStoreOnConditionOperatorAdded;
ConditionOperatorStore.ConditionOperatorRemoved += ConditionOperatorStoreOnConditionOperatorRemoved;
@ -375,18 +368,16 @@ namespace Artemis.Core
private void CreateExpression()
{
CompiledListPredicate = null;
if (Operator == null)
return;
// If the operator does not support a right side, create a static expression because the right side will simply be null
if (PredicateType == ListRightSideType.DynamicList && Operator.SupportsRightSide)
CreateDynamicListExpression();
CreateDynamicListAccessors();
else if (PredicateType == ListRightSideType.Dynamic && Operator.SupportsRightSide)
CreateDynamicExpression();
CreateDynamicAccessors();
else
CreateStaticExpression();
CreateStaticAccessors();
}
private void ValidateOperator()
@ -452,7 +443,7 @@ namespace Artemis.Core
RightStaticValue = null;
}
private void CreateDynamicListExpression()
private void CreateDynamicListAccessors()
{
if (LeftPropertyPath == null || RightPropertyPath == null || Operator == null)
return;
@ -467,12 +458,11 @@ namespace Artemis.Core
if (rightSideAccessor.Type != leftSideAccessor.Type)
rightSideAccessor = Expression.Convert(rightSideAccessor, leftSideAccessor.Type);
var conditionExpression = Operator.CreateExpression(leftSideAccessor, rightSideAccessor);
var lambda = Expression.Lambda<Func<object, bool>>(conditionExpression, leftSideParameter);
CompiledListPredicate = lambda.Compile();
LeftSideAccessor = Expression.Lambda<Func<object, object>>(leftSideAccessor, leftSideParameter).Compile();
RightSideAccessor = Expression.Lambda<Func<object, object>>(rightSideAccessor, leftSideParameter).Compile();
}
private void CreateDynamicExpression()
private void CreateDynamicAccessors()
{
if (LeftPropertyPath == null || RightPropertyPath == null || RightDataModel == null || Operator == null)
return;
@ -480,19 +470,18 @@ namespace Artemis.Core
// List accessors share the same parameter because a list always contains one item per entry
var leftSideParameter = Expression.Parameter(typeof(object), "listItem");
var leftSideAccessor = CreateListAccessor(LeftPropertyPath, leftSideParameter);
var rightSideAccessor = CreateAccessor(RightDataModel, RightPropertyPath, "right", out var rightSideParameter);
var rightSideAccessor = ExpressionUtilities.CreateDataModelAccessor(RightDataModel, RightPropertyPath, "right", out var rightSideParameter);
// A conversion may be required if the types differ
// This can cause issues if the DataModelConditionOperator wasn't accurate in it's supported types but that is not a concern here
if (rightSideAccessor.Type != leftSideAccessor.Type)
rightSideAccessor = Expression.Convert(rightSideAccessor, leftSideAccessor.Type);
var conditionExpression = Operator.CreateExpression(leftSideAccessor, rightSideAccessor);
var lambda = Expression.Lambda<Func<object, DataModel, bool>>(conditionExpression, leftSideParameter, rightSideParameter);
CompiledExternalListPredicate = lambda.Compile();
LeftSideAccessor = Expression.Lambda<Func<object, object>>(leftSideAccessor, leftSideParameter).Compile();
RightSideAccessor = Expression.Lambda<Func<object, object>>(rightSideAccessor, rightSideParameter).Compile();
}
private void CreateStaticExpression()
private void CreateStaticAccessors()
{
if (!DataModelConditionList.IsPrimitiveList && LeftPropertyPath == null || Operator == null)
return;
@ -503,43 +492,51 @@ namespace Artemis.Core
? Expression.Convert(leftSideParameter, DataModelConditionList.ListType)
: CreateListAccessor(LeftPropertyPath, leftSideParameter);
// If the left side is a value type but the input is empty, this isn't a valid expression
if (leftSideAccessor.Type.IsValueType && RightStaticValue == null)
return;
// If the right side value is null, the constant type cannot be inferred and must be provided manually
var rightSideConstant = RightStaticValue != null
? Expression.Constant(Convert.ChangeType(RightStaticValue, leftSideAccessor.Type))
: Expression.Constant(null, leftSideAccessor.Type);
var conditionExpression = Operator.CreateExpression(leftSideAccessor, rightSideConstant);
var lambda = Expression.Lambda<Func<object, bool>>(conditionExpression, leftSideParameter);
CompiledListPredicate = lambda.Compile();
LeftSideAccessor = Expression.Lambda<Func<object, object>>(leftSideAccessor, leftSideParameter).Compile();
RightSideAccessor = null;
}
private Expression CreateListAccessor(string path, ParameterExpression listParameter)
{
return path.Split('.').Aggregate<string, Expression>(
Expression.Convert(listParameter, DataModelConditionList.ListType), // Cast to the appropriate type
Expression.Property
);
// Create an expression that checks every part of the path for null
// In the same iteration, create the accessor
Expression source = Expression.Convert(listParameter, DataModelConditionList.ListType);
return ExpressionUtilities.CreateNullCheckedAccessor(source, path);
}
private Expression CreateAccessor(DataModel dataModel, string path, string parameterName, out ParameterExpression parameter)
#region IDisposable
/// <inheritdoc />
protected override void Dispose(bool disposing)
{
var listType = dataModel.GetListTypeInPath(path);
if (listType != null)
throw new ArtemisCoreException($"Cannot create a regular accessor at path {path} because the path contains a list");
DataModelStore.DataModelAdded -= DataModelStoreOnDataModelAdded;
DataModelStore.DataModelRemoved -= DataModelStoreOnDataModelRemoved;
ConditionOperatorStore.ConditionOperatorAdded -= ConditionOperatorStoreOnConditionOperatorAdded;
ConditionOperatorStore.ConditionOperatorRemoved -= ConditionOperatorStoreOnConditionOperatorRemoved;
parameter = Expression.Parameter(typeof(object), parameterName + "DataModel");
return path.Split('.').Aggregate<string, Expression>(
Expression.Convert(parameter, dataModel.GetType()), // Cast to the appropriate type
Expression.Property
);
base.Dispose(disposing);
}
#endregion
#region Event handlers
private void DataModelStoreOnDataModelAdded(object sender, DataModelStoreEvent e)
{
var dataModel = e.Registration.DataModel;
if (dataModel.PluginInfo.Guid == Entity.RightDataModelGuid && dataModel.ContainsPath(Entity.RightPropertyPath))
UpdateRightSideDynamic(dataModel, Entity.RightPropertyPath);
}
private void DataModelStoreOnDataModelRemoved(object sender, DataModelStoreEvent e)
{
if (RightDataModel == e.Registration.DataModel)
{
RightSideAccessor = null;
RightDataModel = null;
}
}
private void ConditionOperatorStoreOnConditionOperatorAdded(object sender, ConditionOperatorStoreEvent e)
{
var conditionOperator = e.Registration.ConditionOperator;
@ -552,7 +549,6 @@ namespace Artemis.Core
if (e.Registration.ConditionOperator != Operator)
return;
Operator = null;
CompiledListPredicate = null;
}
#endregion

View File

@ -73,15 +73,8 @@ namespace Artemis.Core
/// </summary>
public object RightStaticValue { get; private set; }
/// <summary>
/// Gets the compiled function that evaluates this predicate if it of a dynamic <see cref="PredicateType" />
/// </summary>
public Func<DataModel, DataModel, bool> CompiledDynamicPredicate { get; private set; }
/// <summary>
/// Gets the compiled function that evaluates this predicate if it is of a static <see cref="PredicateType" />
/// </summary>
public Func<DataModel, bool> CompiledStaticPredicate { get; private set; }
public Func<object, object> LeftSideAccessor { get; set; }
public Func<object, object> RightSideAccessor { get; set; }
internal DataModelConditionPredicateEntity Entity { get; set; }
@ -109,7 +102,7 @@ namespace Artemis.Core
ValidateOperator();
ValidateRightSide();
CreateExpression();
CreateAccessors();
}
/// <summary>
@ -134,7 +127,7 @@ namespace Artemis.Core
RightDataModel = dataModel;
RightPropertyPath = path;
CreateExpression();
CreateAccessors();
}
/// <summary>
@ -167,7 +160,7 @@ namespace Artemis.Core
else
RightStaticValue = null;
CreateExpression();
CreateAccessors();
}
/// <summary>
@ -180,7 +173,7 @@ namespace Artemis.Core
if (conditionOperator == null)
{
Operator = null;
CreateExpression();
CreateAccessors();
return;
}
@ -199,16 +192,28 @@ namespace Artemis.Core
}
Operator = conditionOperator;
CreateExpression();
CreateAccessors();
}
/// <inheritdoc />
public override bool Evaluate()
{
if (CompiledDynamicPredicate != null)
return CompiledDynamicPredicate(LeftDataModel, RightDataModel);
if (CompiledStaticPredicate != null)
return CompiledStaticPredicate(LeftDataModel);
if (Operator == null || LeftSideAccessor == null || PredicateType != ProfileRightSideType.Static && RightSideAccessor == null)
return false;
// Compare with a static value
if (PredicateType == ProfileRightSideType.Static)
{
var leftSideValue = LeftSideAccessor(LeftDataModel);
if (leftSideValue.GetType().IsValueType && RightStaticValue == null)
return false;
return Operator.Evaluate(leftSideValue, RightStaticValue);
}
// Compare with dynamic values
if (PredicateType == ProfileRightSideType.Dynamic)
return Operator.Evaluate(LeftSideAccessor(LeftDataModel), RightSideAccessor(RightDataModel));
return false;
}
@ -314,17 +319,14 @@ namespace Artemis.Core
return Entity;
}
private void CreateExpression()
private void CreateAccessors()
{
CompiledDynamicPredicate = null;
CompiledStaticPredicate = null;
if (Operator == null)
return;
// If the operator does not support a right side, create a static expression because the right side will simply be null
if (PredicateType == ProfileRightSideType.Dynamic && Operator.SupportsRightSide)
CreateDynamicExpression();
CreateDynamicAccessors();
else
CreateStaticExpression();
}
@ -360,22 +362,21 @@ namespace Artemis.Core
}
}
private void CreateDynamicExpression()
private void CreateDynamicAccessors()
{
if (LeftDataModel == null || RightDataModel == null || Operator == null)
return;
var leftSideAccessor = CreateAccessor(LeftDataModel, LeftPropertyPath, "left", out var leftSideParameter);
var rightSideAccessor = CreateAccessor(RightDataModel, RightPropertyPath, "right", out var rightSideParameter);
var leftSideAccessor = ExpressionUtilities.CreateDataModelAccessor(LeftDataModel, LeftPropertyPath, "left", out var leftSideParameter);
var rightSideAccessor = ExpressionUtilities.CreateDataModelAccessor(RightDataModel, RightPropertyPath, "right", out var rightSideParameter);
// A conversion may be required if the types differ
// This can cause issues if the DataModelConditionOperator wasn't accurate in it's supported types but that is not a concern here
if (rightSideAccessor.Type != leftSideAccessor.Type)
rightSideAccessor = Expression.Convert(rightSideAccessor, leftSideAccessor.Type);
var conditionExpression = Operator.CreateExpression(leftSideAccessor, rightSideAccessor);
var lambda = Expression.Lambda<Func<DataModel, DataModel, bool>>(conditionExpression, leftSideParameter, rightSideParameter);
CompiledDynamicPredicate = lambda.Compile();
LeftSideAccessor = Expression.Lambda<Func<object, object>>(leftSideAccessor, leftSideParameter).Compile();
RightSideAccessor = Expression.Lambda<Func<object, object>>(rightSideAccessor, rightSideParameter).Compile();
}
private void CreateStaticExpression()
@ -383,34 +384,19 @@ namespace Artemis.Core
if (LeftDataModel == null || Operator == null)
return;
var leftSideAccessor = CreateAccessor(LeftDataModel, LeftPropertyPath, "left", out var leftSideParameter);
var leftSideAccessor = Expression.Convert(
ExpressionUtilities.CreateDataModelAccessor(LeftDataModel, LeftPropertyPath, "left", out var leftSideParameter),
typeof(object)
);
// If the left side is a value type but the input is empty, this isn't a valid expression
if (leftSideAccessor.Type.IsValueType && RightStaticValue == null)
return;
// If the right side value is null, the constant type cannot be inferred and must be provided manually
var rightSideConstant = RightStaticValue != null
? Expression.Constant(Convert.ChangeType(RightStaticValue, leftSideAccessor.Type))
: Expression.Constant(null, leftSideAccessor.Type);
var conditionExpression = Operator.CreateExpression(leftSideAccessor, rightSideConstant);
var lambda = Expression.Lambda<Func<DataModel, bool>>(conditionExpression, leftSideParameter);
CompiledStaticPredicate = lambda.Compile();
LeftSideAccessor = Expression.Lambda<Func<object, object>>(leftSideAccessor, leftSideParameter).Compile();
RightSideAccessor = null;
}
private Expression CreateAccessor(DataModel dataModel, string path, string parameterName, out ParameterExpression parameter)
{
var listType = dataModel.GetListTypeInPath(path);
if (listType != null)
throw new ArtemisCoreException($"Cannot create a regular accessor at path {path} because the path contains a list");
parameter = Expression.Parameter(typeof(object), parameterName + "DataModel");
return path.Split('.').Aggregate<string, Expression>(
Expression.Convert(parameter, dataModel.GetType()), // Cast to the appropriate type
Expression.Property
);
}
#region Event handlers
@ -427,13 +413,13 @@ namespace Artemis.Core
{
if (LeftDataModel == e.Registration.DataModel)
{
CompiledDynamicPredicate = null;
LeftSideAccessor = null;
LeftDataModel = null;
}
if (RightDataModel == e.Registration.DataModel)
{
CompiledDynamicPredicate = null;
RightSideAccessor = null;
RightDataModel = null;
}
}
@ -451,8 +437,6 @@ namespace Artemis.Core
return;
Operator = null;
CompiledStaticPredicate = null;
CompiledDynamicPredicate = null;
}
#endregion

View File

@ -37,6 +37,8 @@ namespace Artemis.Core
throw new ObjectDisposedException("ConditionalDataBinding");
var condition = Conditions.FirstOrDefault(c => c.Evaluate());
if (condition != null)
Console.WriteLine();
return condition == null ? baseValue : condition.Value;
}
@ -108,8 +110,7 @@ namespace Artemis.Core
}
#endregion
#region Storage
/// <inheritdoc />

View File

@ -300,25 +300,14 @@ namespace Artemis.Core
return;
// If the right side value is null, the constant type cannot be inferred and must be provided based on the data binding target
var parameterAccessor = CreateAccessor(ParameterDataModel, ParameterPropertyPath, "parameter", out var rightSideParameter);
var parameterAccessor = ExpressionUtilities.CreateDataModelAccessor(
ParameterDataModel, ParameterPropertyPath, "parameter", out var rightSideParameter
);
var lambda = Expression.Lambda<Func<DataModel, object>>(Expression.Convert(parameterAccessor, typeof(object)), rightSideParameter);
CompiledParameterAccessor = lambda.Compile();
}
}
private Expression CreateAccessor(DataModel dataModel, string path, string parameterName, out ParameterExpression parameter)
{
var listType = dataModel.GetListTypeInPath(path);
if (listType != null)
throw new ArtemisCoreException($"Cannot create a regular accessor at path {path} because the path contains a list");
parameter = Expression.Parameter(typeof(object), parameterName + "DataModel");
return path.Split('.').Aggregate<string, Expression>(
Expression.Convert(parameter, dataModel.GetType()), // Cast to the appropriate type
Expression.Property
);
}
#region Event handlers
private void DataBindingModifierTypeStoreOnDataBindingModifierAdded(object sender, DataBindingModifierTypeStoreEvent e)

View File

@ -59,7 +59,11 @@ namespace Artemis.Core.Services
RegisterConditionOperator(Constants.CorePluginInfo, new StringNotContainsConditionOperator());
RegisterConditionOperator(Constants.CorePluginInfo, new StringStartsWithConditionOperator());
RegisterConditionOperator(Constants.CorePluginInfo, new StringEndsWithConditionOperator());
RegisterConditionOperator(Constants.CorePluginInfo, new StringNullConditionOperator());
// Null checks, at the bottom
// TODO: Implement a priority mechanism
RegisterConditionOperator(Constants.CorePluginInfo, new NullConditionOperator());
RegisterConditionOperator(Constants.CorePluginInfo, new NotNullConditionOperator());
}
}
}

View File

@ -0,0 +1,37 @@
using System.Linq.Expressions;
using Artemis.Core.DataModelExpansions;
namespace Artemis.Core
{
internal static class ExpressionUtilities
{
internal static Expression CreateDataModelAccessor(DataModel dataModel, string path, string parameterName, out ParameterExpression parameter)
{
parameter = Expression.Parameter(typeof(object), parameterName + "DataModel");
// Create an expression that checks every part of the path for null
// In the same iteration, create the accessor
Expression source = Expression.Convert(parameter, dataModel.GetType());
return CreateNullCheckedAccessor(source, path);
}
internal static Expression CreateNullCheckedAccessor(Expression source, string path)
{
// Create an expression that checks every part of the path for null
// In the same iteration, create the accessor
Expression condition = null;
foreach (var memberName in path.Split('.'))
{
var notNull = Expression.NotEqual(source, Expression.Constant(null));
condition = condition != null ? Expression.AndAlso(condition, notNull) : notNull;
source = Expression.PropertyOrField(source, memberName);
}
if (condition == null)
throw new ArtemisCoreException($"Failed to create a null-check for path {path}");
// Combine the null check and the accessor in a conditional statement that returns the default for the type if null
return Expression.Condition(condition, source, Expression.Default(source.Type));
}
}
}

View File

@ -1,4 +1,5 @@
using System;
using System.Linq;
using Artemis.Core.DataModelExpansions;
using Artemis.Plugins.DataModelExpansions.TestData.DataModels;
using SkiaSharp;

View File

@ -128,10 +128,15 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
if (DataModelConditionPredicate.Operator == null)
DataModelConditionPredicate.UpdateOperator(Operators.FirstOrDefault(o => o.SupportsType(leftSideType)));
SelectedOperator = DataModelConditionPredicate.Operator;
if (!SelectedOperator.SupportsRightSide)
{
DisposeRightSideStatic();
DisposeRightSideDynamic();
}
// Ensure the right side has the proper VM
var targetType = LeftSideSelectionViewModel?.SelectedPropertyViewModel?.PropertyInfo?.PropertyType;
if (DataModelConditionPredicate.PredicateType == ProfileRightSideType.Dynamic)
if (DataModelConditionPredicate.PredicateType == ProfileRightSideType.Dynamic && SelectedOperator.SupportsRightSide)
{
DisposeRightSideStatic();
if (RightSideSelectionViewModel == null)
@ -147,7 +152,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
);
RightSideSelectionViewModel.FilterTypes = new[] {targetType};
}
else
else if (SelectedOperator.SupportsRightSide)
{
DisposeRightSideDynamic();
if (RightSideInputViewModel == null)