From 68b61cbcb209936af5f07450dab77740a46522f3 Mon Sep 17 00:00:00 2001 From: SpoinkyNL Date: Sun, 30 Aug 2020 23:09:38 +0200 Subject: [PATCH] Condition operators - Marked built-in operators as internal Display conditions - Added docs to the service and profile types Color gradient - Added docs Storage - Moved profile entities to separate namespaces Data bindings - Added entities to storage Data bindings - Started implementing in the core --- .../Models/Profile/Colors/ColorGradient.cs | 44 ++- .../Profile/Colors/ColorGradientStop.cs | 13 + .../Abstract/DisplayConditionPart.cs | 32 +- .../Profile/Conditions/DisplayCondition.cs | 12 - .../Conditions/DisplayConditionGroup.cs | 22 +- .../Conditions/DisplayConditionList.cs | 1 + .../DisplayConditionListPredicate.cs | 133 ++++---- .../Conditions/DisplayConditionOperator.cs | 18 +- .../Conditions/DisplayConditionPredicate.cs | 279 +++++++++-------- .../Operators/EqualsConditionOperator.cs | 2 +- .../Operators/GreaterThanConditionOperator.cs | 2 +- .../GreaterThanOrEqualConditionOperator.cs | 2 +- .../Operators/LessThanConditionOperator.cs | 2 +- .../LessThanOrEqualConditionOperator.cs | 2 +- .../Operators/NotEqualConditionOperator.cs | 2 +- .../StringContainsConditionOperator.cs | 2 +- .../StringEndsWithConditionOperator.cs | 2 +- .../StringEqualsConditionOperator.cs | 2 +- .../StringNotContainsConditionOperator.cs | 2 +- .../StringNotEqualConditionOperator.cs | 2 +- .../Operators/StringNullConditionOperator.cs | 2 +- .../StringStartsWithConditionOperator.cs | 2 +- .../Profile/DataBindings/DataBinding.cs | 60 ++++ .../DataBindings/DataBindingModifier.cs | 287 ++++++++++++++++++ .../Modifiers/DataBindingModifierType.cs | 99 ++++++ .../Models/Profile/ProfileRightSideType.cs | 18 ++ .../Services/DataBindingService.cs | 100 +++++- .../Interfaces/IDataBindingService.cs | 47 ++- .../Services/Interfaces/IDataModelService.cs | 28 ++ .../Profile/Abstract/RenderElementEntity.cs | 1 + .../DisplayConditionGroupEntity.cs | 6 +- .../DisplayConditionListEntity.cs | 2 +- .../DisplayConditionListPredicateEntity.cs | 2 +- .../DisplayConditionPredicateEntity.cs | 2 +- .../ProfileConditionEntity.cs | 2 +- .../Profile/DataBindings/DataBindingEntity.cs | 21 ++ .../DataBindings/DataBindingModifierEntity.cs | 19 ++ .../Entities/Profile/KeyframeEntity.cs | 12 + .../Entities/Profile/PropertyEntity.cs | 10 +- .../DisplayConditionGroupViewModel.cs | 9 +- .../DisplayConditionListPredicateViewModel.cs | 10 +- .../DisplayConditionListViewModel.cs | 52 ++-- .../DisplayConditionPredicateViewModel.cs | 16 +- 43 files changed, 1059 insertions(+), 324 deletions(-) delete mode 100644 src/Artemis.Core/Models/Profile/Conditions/DisplayCondition.cs create mode 100644 src/Artemis.Core/Models/Profile/DataBindings/DataBinding.cs create mode 100644 src/Artemis.Core/Models/Profile/DataBindings/DataBindingModifier.cs create mode 100644 src/Artemis.Core/Models/Profile/DataBindings/Modifiers/DataBindingModifierType.cs create mode 100644 src/Artemis.Core/Models/Profile/ProfileRightSideType.cs rename src/Artemis.Storage/Entities/Profile/{ => Conditions}/DisplayConditionGroupEntity.cs (73%) rename src/Artemis.Storage/Entities/Profile/{ => Conditions}/DisplayConditionListEntity.cs (89%) rename src/Artemis.Storage/Entities/Profile/{ => Conditions}/DisplayConditionListPredicateEntity.cs (92%) rename src/Artemis.Storage/Entities/Profile/{ => Conditions}/DisplayConditionPredicateEntity.cs (92%) rename src/Artemis.Storage/Entities/Profile/{ => Conditions}/ProfileConditionEntity.cs (67%) create mode 100644 src/Artemis.Storage/Entities/Profile/DataBindings/DataBindingEntity.cs create mode 100644 src/Artemis.Storage/Entities/Profile/DataBindings/DataBindingModifierEntity.cs create mode 100644 src/Artemis.Storage/Entities/Profile/KeyframeEntity.cs diff --git a/src/Artemis.Core/Models/Profile/Colors/ColorGradient.cs b/src/Artemis.Core/Models/Profile/Colors/ColorGradient.cs index fc7108775..3307dcac6 100644 --- a/src/Artemis.Core/Models/Profile/Colors/ColorGradient.cs +++ b/src/Artemis.Core/Models/Profile/Colors/ColorGradient.cs @@ -3,35 +3,35 @@ using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Runtime.CompilerServices; -using System.Windows.Documents; using Artemis.Core.Annotations; using SkiaSharp; using Stylet; namespace Artemis.Core.Models.Profile.Colors { + /// + /// A gradient containing a list of s + /// public class ColorGradient : INotifyPropertyChanged { - private float _rotation; - + /// + /// Creates a new instance of the class + /// public ColorGradient() { Stops = new BindableCollection(); } + /// + /// Gets a list of all the s in the gradient + /// public BindableCollection Stops { get; } - public float Rotation - { - get => _rotation; - set - { - if (value.Equals(_rotation)) return; - _rotation = value; - OnPropertyChanged(); - } - } - + /// + /// Gets all the colors in the color gradient + /// + /// The amount of times to repeat the colors + /// public SKColor[] GetColorsArray(int timesToRepeat = 0) { if (timesToRepeat == 0) @@ -46,6 +46,14 @@ namespace Artemis.Core.Models.Profile.Colors return result.ToArray(); } + /// + /// Gets all the positions in the color gradient + /// + /// + /// The amount of times to repeat the positions, positions will get squished together and + /// always stay between 0.0 and 1.0 + /// + /// public float[] GetPositionsArray(int timesToRepeat = 0) { if (timesToRepeat == 0) @@ -65,11 +73,18 @@ namespace Artemis.Core.Models.Profile.Colors return result.ToArray(); } + /// + /// Triggers a property changed event of the collection + /// public void OnColorValuesUpdated() { OnPropertyChanged(nameof(Stops)); } + /// + /// Gets a color at any position between 0.0 and 1.0 using interpolation + /// + /// A position between 0.0 and 1.0 public SKColor GetColor(float position) { if (!Stops.Any()) @@ -119,6 +134,7 @@ namespace Artemis.Core.Models.Profile.Colors #region PropertyChanged + /// public event PropertyChangedEventHandler PropertyChanged; [NotifyPropertyChangedInvocator] diff --git a/src/Artemis.Core/Models/Profile/Colors/ColorGradientStop.cs b/src/Artemis.Core/Models/Profile/Colors/ColorGradientStop.cs index abc89947a..71bc35f52 100644 --- a/src/Artemis.Core/Models/Profile/Colors/ColorGradientStop.cs +++ b/src/Artemis.Core/Models/Profile/Colors/ColorGradientStop.cs @@ -5,17 +5,26 @@ using SkiaSharp; namespace Artemis.Core.Models.Profile.Colors { + /// + /// A color with a position, usually contained in a + /// public class ColorGradientStop : INotifyPropertyChanged { private SKColor _color; private float _position; + /// + /// Creates a new instance of the class + /// public ColorGradientStop(SKColor color, float position) { Color = color; Position = position; } + /// + /// Gets or sets the color of the stop + /// public SKColor Color { get => _color; @@ -27,6 +36,9 @@ namespace Artemis.Core.Models.Profile.Colors } } + /// + /// Gets or sets the position of the stop + /// public float Position { get => _position; @@ -40,6 +52,7 @@ namespace Artemis.Core.Models.Profile.Colors #region PropertyChanged + /// public event PropertyChangedEventHandler PropertyChanged; [NotifyPropertyChangedInvocator] diff --git a/src/Artemis.Core/Models/Profile/Conditions/Abstract/DisplayConditionPart.cs b/src/Artemis.Core/Models/Profile/Conditions/Abstract/DisplayConditionPart.cs index 0531e1a5b..898e824cc 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/Abstract/DisplayConditionPart.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/Abstract/DisplayConditionPart.cs @@ -1,22 +1,30 @@ using System.Collections.Generic; -using System.Linq.Expressions; using Artemis.Core.Services.Interfaces; using Artemis.Storage.Entities.Profile.Abstract; namespace Artemis.Core.Models.Profile.Conditions.Abstract { + /// + /// An abstract class for display condition parts + /// public abstract class DisplayConditionPart { - private readonly List _children; + private readonly List _children = new List(); + + /// + /// Gets the parent of this part + /// + public DisplayConditionPart Parent { get; internal set; } - protected DisplayConditionPart() - { - _children = new List(); - } - - public DisplayConditionPart Parent { get; set; } + /// + /// Gets the children of this part + /// public IReadOnlyList Children => _children.AsReadOnly(); + /// + /// Adds a child to the display condition part's collection + /// + /// public void AddChild(DisplayConditionPart displayConditionPart) { if (!_children.Contains(displayConditionPart)) @@ -26,6 +34,10 @@ namespace Artemis.Core.Models.Profile.Conditions.Abstract } } + /// + /// Removes a child from the display condition part's collection + /// + /// The child to remove public void RemoveChild(DisplayConditionPart displayConditionPart) { if (_children.Contains(displayConditionPart)) @@ -36,13 +48,13 @@ namespace Artemis.Core.Models.Profile.Conditions.Abstract } /// - /// Evaluates the condition part on the data model + /// Evaluates the condition part on the data model /// /// public abstract bool Evaluate(); /// - /// Evaluates the condition part on the given target (currently only for lists) + /// Evaluates the condition part on the given target (currently only for lists) /// /// /// diff --git a/src/Artemis.Core/Models/Profile/Conditions/DisplayCondition.cs b/src/Artemis.Core/Models/Profile/Conditions/DisplayCondition.cs deleted file mode 100644 index 96bf9e33f..000000000 --- a/src/Artemis.Core/Models/Profile/Conditions/DisplayCondition.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Linq.Expressions; -using Artemis.Core.Plugins.DataModelExpansions; - -namespace Artemis.Core.Models.Profile.Conditions -{ - public class DisplayCondition - { - public Expression> ExpressionTree { get; set; } - public DisplayConditionGroup RootGroup { get; set; } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionGroup.cs b/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionGroup.cs index 8250aa911..640216e7e 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionGroup.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionGroup.cs @@ -4,17 +4,31 @@ using Artemis.Core.Models.Profile.Conditions.Abstract; using Artemis.Core.Services.Interfaces; using Artemis.Storage.Entities.Profile; using Artemis.Storage.Entities.Profile.Abstract; +using Artemis.Storage.Entities.Profile.Conditions; namespace Artemis.Core.Models.Profile.Conditions { + /// + /// A group containing zero to many s which it evaluates using a boolean specific + /// operator + /// public class DisplayConditionGroup : DisplayConditionPart { + /// + /// Creates a new instance of the class + /// + /// public DisplayConditionGroup(DisplayConditionPart parent) { Parent = parent; Entity = new DisplayConditionGroupEntity(); } + /// + /// Creates a new instance of the class + /// + /// + /// public DisplayConditionGroup(DisplayConditionPart parent, DisplayConditionGroupEntity entity) { Parent = parent; @@ -34,9 +48,14 @@ namespace Artemis.Core.Models.Profile.Conditions } } + /// + /// Gets or sets the boolean operator of this group + /// public BooleanOperator BooleanOperator { get; set; } - public DisplayConditionGroupEntity Entity { get; set; } + internal DisplayConditionGroupEntity Entity { get; set; } + + /// public override bool Evaluate() { // Empty groups are always true @@ -61,6 +80,7 @@ namespace Artemis.Core.Models.Profile.Conditions } } + /// public override bool EvaluateObject(object target) { // Empty groups are always true diff --git a/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionList.cs b/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionList.cs index 42b7b33ed..1bcd9f271 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionList.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionList.cs @@ -8,6 +8,7 @@ using Artemis.Core.Plugins.DataModelExpansions; using Artemis.Core.Services.Interfaces; using Artemis.Storage.Entities.Profile; using Artemis.Storage.Entities.Profile.Abstract; +using Artemis.Storage.Entities.Profile.Conditions; namespace Artemis.Core.Models.Profile.Conditions { diff --git a/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionListPredicate.cs b/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionListPredicate.cs index cff1a145b..4c0f9dbb9 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionListPredicate.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionListPredicate.cs @@ -6,15 +6,15 @@ using Artemis.Core.Extensions; using Artemis.Core.Models.Profile.Conditions.Abstract; using Artemis.Core.Plugins.DataModelExpansions; using Artemis.Core.Services.Interfaces; -using Artemis.Storage.Entities.Profile; using Artemis.Storage.Entities.Profile.Abstract; +using Artemis.Storage.Entities.Profile.Conditions; using Newtonsoft.Json; namespace Artemis.Core.Models.Profile.Conditions { public class DisplayConditionListPredicate : DisplayConditionPart { - public DisplayConditionListPredicate(DisplayConditionPart parent, PredicateType predicateType) + public DisplayConditionListPredicate(DisplayConditionPart parent, ProfileRightSideType predicateType) { Parent = parent; PredicateType = predicateType; @@ -27,14 +27,14 @@ namespace Artemis.Core.Models.Profile.Conditions { Parent = parent; Entity = entity; - PredicateType = (PredicateType) entity.PredicateType; + PredicateType = (ProfileRightSideType) entity.PredicateType; ApplyParentList(); } public DisplayConditionListPredicateEntity Entity { get; set; } - public PredicateType PredicateType { get; set; } + public ProfileRightSideType PredicateType { get; set; } public DisplayConditionOperator Operator { get; private set; } public Type ListType { get; private set; } @@ -57,6 +57,7 @@ namespace Artemis.Core.Models.Profile.Conditions UpdateList(parentList.ListDataModel, parentList.ListPropertyPath); return; } + current = current.Parent; } } @@ -77,9 +78,7 @@ namespace Artemis.Core.Models.Profile.Conditions ListType = listType; } else - { ListType = null; - } ListDataModel = dataModel; ListPropertyPath = path; @@ -109,7 +108,7 @@ namespace Artemis.Core.Models.Profile.Conditions if (!ListContainsInnerPath(path)) throw new ArtemisCoreException($"List type {ListType.Name} does not contain path {path}"); - PredicateType = PredicateType.Dynamic; + PredicateType = ProfileRightSideType.Dynamic; RightPropertyPath = path; CreateExpression(); @@ -117,7 +116,7 @@ namespace Artemis.Core.Models.Profile.Conditions public void UpdateRightSideStatic(object staticValue) { - PredicateType = PredicateType.Static; + PredicateType = ProfileRightSideType.Static; RightPropertyPath = null; SetStaticValue(staticValue); @@ -145,18 +144,52 @@ namespace Artemis.Core.Models.Profile.Conditions CreateExpression(); } - private void CreateExpression() + public override bool Evaluate() { - CompiledListPredicate = null; + return false; + } - if (Operator == null) - return; + public override bool EvaluateObject(object target) + { + return CompiledListPredicate != null && CompiledListPredicate(target); + } - // If the operator does not support a right side, create a static expression because the right side will simply be null - if (PredicateType == PredicateType.Dynamic && Operator.SupportsRightSide) - CreateDynamicExpression(); + public bool ListContainsInnerPath(string path) + { + if (ListType == null) + return false; - CreateStaticExpression(); + var parts = path.Split('.'); + var current = ListType; + foreach (var part in parts) + { + var property = current.GetProperty(part); + current = property?.PropertyType; + + if (property == null) + return false; + } + + return true; + } + + public Type GetTypeAtInnerPath(string path) + { + if (!ListContainsInnerPath(path)) + return null; + + var parts = path.Split('.'); + var current = ListType; + + Type result = null; + foreach (var part in parts) + { + var property = current.GetProperty(part); + current = property.PropertyType; + result = property.PropertyType; + } + + return result; } internal override void ApplyToEntity() @@ -173,16 +206,6 @@ namespace Artemis.Core.Models.Profile.Conditions Entity.OperatorType = Operator?.GetType().Name; } - public override bool Evaluate() - { - return false; - } - - public override bool EvaluateObject(object target) - { - return CompiledListPredicate != null && CompiledListPredicate(target); - } - internal override void Initialize(IDataModelService dataModelService) { // Left side @@ -198,13 +221,13 @@ namespace Artemis.Core.Models.Profile.Conditions } // Right side dynamic - if (PredicateType == PredicateType.Dynamic && Entity.RightPropertyPath != null) + if (PredicateType == ProfileRightSideType.Dynamic && Entity.RightPropertyPath != null) { if (ListContainsInnerPath(Entity.RightPropertyPath)) UpdateLeftSide(Entity.LeftPropertyPath); } // Right side static - else if (PredicateType == PredicateType.Static && Entity.RightStaticValue != null) + else if (PredicateType == ProfileRightSideType.Static && Entity.RightStaticValue != null) { try { @@ -245,6 +268,20 @@ namespace Artemis.Core.Models.Profile.Conditions return Entity; } + 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 == ProfileRightSideType.Dynamic && Operator.SupportsRightSide) + CreateDynamicExpression(); + + CreateStaticExpression(); + } + private void ValidateOperator() { if (LeftPropertyPath == null || Operator == null) @@ -258,7 +295,7 @@ namespace Artemis.Core.Models.Profile.Conditions private void ValidateRightSide() { var leftSideType = GetTypeAtInnerPath(LeftPropertyPath); - if (PredicateType == PredicateType.Dynamic) + if (PredicateType == ProfileRightSideType.Dynamic) { if (RightPropertyPath == null) return; @@ -349,43 +386,5 @@ namespace Artemis.Core.Models.Profile.Conditions Expression.Property ); } - - public bool ListContainsInnerPath(string path) - { - if (ListType == null) - return false; - - var parts = path.Split('.'); - var current = ListType; - foreach (var part in parts) - { - var property = current.GetProperty(part); - current = property?.PropertyType; - - if (property == null) - return false; - } - - return true; - } - - public Type GetTypeAtInnerPath(string path) - { - if (!ListContainsInnerPath(path)) - return null; - - var parts = path.Split('.'); - var current = ListType; - - Type result = null; - foreach (var part in parts) - { - var property = current.GetProperty(part); - current = property.PropertyType; - result = property.PropertyType; - } - - return result; - } } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionOperator.cs b/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionOperator.cs index 535168b46..235fe13f0 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionOperator.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionOperator.cs @@ -8,6 +8,9 @@ using Artemis.Core.Services.Interfaces; namespace Artemis.Core.Models.Profile.Conditions { + /// + /// A display condition operator is used by the conditions system to perform a specific boolean check + /// public abstract class DisplayConditionOperator { private IDataModelService _dataModelService; @@ -39,6 +42,9 @@ namespace Artemis.Core.Models.Profile.Conditions /// 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) @@ -54,16 +60,6 @@ namespace Artemis.Core.Models.Profile.Conditions /// public abstract BinaryExpression CreateExpression(Expression leftSide, Expression rightSide); - /// - /// Returns an expression that checks the given expression for null - /// - /// - /// - protected Expression CreateNullCheckExpression(Expression expression) - { - return Expression.NotEqual(expression, Expression.Constant(null)); - } - internal void Register(PluginInfo pluginInfo, IDataModelService dataModelService) { if (_registered) @@ -90,7 +86,7 @@ namespace Artemis.Core.Models.Profile.Conditions private void InstanceOnPluginDisabled(object sender, EventArgs e) { - // Profile editor service will call Unsubscribe + // The service will call Unsubscribe _dataModelService.RemoveConditionOperator(this); } } diff --git a/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionPredicate.cs b/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionPredicate.cs index 0b8e5eb85..a1e34f885 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionPredicate.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionPredicate.cs @@ -6,45 +6,95 @@ using Artemis.Core.Extensions; using Artemis.Core.Models.Profile.Conditions.Abstract; using Artemis.Core.Plugins.DataModelExpansions; using Artemis.Core.Services.Interfaces; -using Artemis.Storage.Entities.Profile; using Artemis.Storage.Entities.Profile.Abstract; +using Artemis.Storage.Entities.Profile.Conditions; using Newtonsoft.Json; namespace Artemis.Core.Models.Profile.Conditions { + /// + /// A predicate in a display condition using either two data model values or one data model value and a + /// static value + /// public class DisplayConditionPredicate : DisplayConditionPart { - public DisplayConditionPredicate(DisplayConditionPart parent, PredicateType predicateType) + /// + /// Creates a new instance of the class + /// + /// + /// + public DisplayConditionPredicate(DisplayConditionPart parent, ProfileRightSideType predicateType) { Parent = parent; PredicateType = predicateType; Entity = new DisplayConditionPredicateEntity(); } + /// + /// Creates a new instance of the class + /// + /// + /// public DisplayConditionPredicate(DisplayConditionPart parent, DisplayConditionPredicateEntity entity) { Parent = parent; Entity = entity; - PredicateType = (PredicateType) entity.PredicateType; + PredicateType = (ProfileRightSideType) entity.PredicateType; } - public DisplayConditionPredicateEntity Entity { get; set; } + /// + /// Gets or sets the predicate type + /// + public ProfileRightSideType PredicateType { get; set; } - public PredicateType PredicateType { get; set; } + /// + /// Gets the operator + /// public DisplayConditionOperator Operator { get; private set; } + /// + /// Gets the currently used instance of the left data model + /// public DataModel LeftDataModel { get; private set; } + + /// + /// Gets the path of the left property in the + /// public string LeftPropertyPath { get; private set; } + + /// + /// Gets the currently used instance of the right data model + /// public DataModel RightDataModel { get; private set; } + + /// + /// Gets the path of the right property in the + /// public string RightPropertyPath { get; private set; } + + /// + /// Gets the right static value, only used it is + /// + /// public object RightStaticValue { get; private set; } - public DataModel ListDataModel { get; private set; } - public string ListPropertyPath { get; private set; } + /// + /// Gets the compiled function that evaluates this predicate if it of a dynamic + /// public Func CompiledDynamicPredicate { get; private set; } - public Func CompiledStaticPredicate { get; private set; } - public Func CompiledListPredicate { get; private set; } + /// + /// Gets the compiled function that evaluates this predicate if it is of a static + /// + public Func CompiledStaticPredicate { get; private set; } + + internal DisplayConditionPredicateEntity Entity { get; set; } + + /// + /// Updates the left side of the predicate + /// + /// The data model of the left side value + /// The path pointing to the left side value inside the data model public void UpdateLeftSide(DataModel dataModel, string path) { if (dataModel != null && path == null) @@ -67,6 +117,11 @@ namespace Artemis.Core.Models.Profile.Conditions CreateExpression(); } + /// + /// Updates the right side of the predicate, makes the predicate dynamic and re-compiles the expression + /// + /// The data model of the right side value + /// The path pointing to the right side value inside the data model public void UpdateRightSide(DataModel dataModel, string path) { if (dataModel != null && path == null) @@ -80,32 +135,61 @@ namespace Artemis.Core.Models.Profile.Conditions throw new ArtemisCoreException($"Data model of type {dataModel.GetType().Name} does not contain a property at path '{path}'"); } - PredicateType = PredicateType.Dynamic; + PredicateType = ProfileRightSideType.Dynamic; RightDataModel = dataModel; RightPropertyPath = path; CreateExpression(); } + /// + /// Updates the right side of the predicate, makes the predicate static and re-compiles the expression + /// + /// The right side value to use public void UpdateRightSide(object staticValue) { - PredicateType = PredicateType.Static; + PredicateType = ProfileRightSideType.Static; RightDataModel = null; RightPropertyPath = null; - SetStaticValue(staticValue); + // If the left side is empty simply apply the value, any validation will wait + if (LeftDataModel == null) + { + RightStaticValue = staticValue; + return; + } + + var leftSideType = LeftDataModel.GetTypeAtPath(LeftPropertyPath); + + // If not null ensure the types match and if not, convert it + if (staticValue != null && staticValue.GetType() == leftSideType) + RightStaticValue = staticValue; + else if (staticValue != null) + RightStaticValue = Convert.ChangeType(staticValue, leftSideType); + // 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 + RightStaticValue = null; CreateExpression(); } + /// + /// Updates the operator of the predicate and re-compiles the expression + /// + /// public void UpdateOperator(DisplayConditionOperator displayConditionOperator) { + // Calling CreateExpression will clear compiled expressions if (displayConditionOperator == null) { Operator = null; + CreateExpression(); return; } + // No need to clear compiled expressions, without a left data model they are already null if (LeftDataModel == null) { Operator = displayConditionOperator; @@ -113,26 +197,37 @@ namespace Artemis.Core.Models.Profile.Conditions } var leftType = LeftDataModel.GetTypeAtPath(LeftPropertyPath); - if (displayConditionOperator.SupportsType(leftType)) - Operator = displayConditionOperator; + if (!displayConditionOperator.SupportsType(leftType)) + throw new ArtemisCoreException($"Cannot apply operator {displayConditionOperator.GetType().Name} to this predicate because " + + $"it does not support left side type {leftType.Name}"); + Operator = displayConditionOperator; CreateExpression(); } - private void CreateExpression() + /// + public override bool Evaluate() { - CompiledDynamicPredicate = null; - CompiledStaticPredicate = null; - CompiledListPredicate = null; + if (CompiledDynamicPredicate != null) + return CompiledDynamicPredicate(LeftDataModel, RightDataModel); + if (CompiledStaticPredicate != null) + return CompiledStaticPredicate(LeftDataModel); - if (Operator == null) - return; + return false; + } - // If the operator does not support a right side, create a static expression because the right side will simply be null - if (PredicateType == PredicateType.Dynamic && Operator.SupportsRightSide) - CreateDynamicExpression(); + /// + public override bool EvaluateObject(object target) + { + return false; + } - CreateStaticExpression(); + /// + public override string ToString() + { + if (PredicateType == ProfileRightSideType.Dynamic) + return $"[Dynamic] {LeftPropertyPath} {Operator.Description} {RightPropertyPath}"; + return $"[Static] {LeftPropertyPath} {Operator.Description} {RightStaticValue}"; } internal override void ApplyToEntity() @@ -149,24 +244,6 @@ namespace Artemis.Core.Models.Profile.Conditions Entity.OperatorType = Operator?.GetType().Name; } - public override bool Evaluate() - { - if (CompiledDynamicPredicate != null) - return CompiledDynamicPredicate(LeftDataModel, RightDataModel); - if (CompiledStaticPredicate != null) - return CompiledStaticPredicate(LeftDataModel); - - return false; - } - - public override bool EvaluateObject(object target) - { - if (CompiledListPredicate != null) - return CompiledListPredicate(target); - - return false; - } - internal override void Initialize(IDataModelService dataModelService) { // Left side @@ -186,14 +263,14 @@ namespace Artemis.Core.Models.Profile.Conditions } // Right side dynamic - if (PredicateType == PredicateType.Dynamic && Entity.RightDataModelGuid != null) + if (PredicateType == ProfileRightSideType.Dynamic && Entity.RightDataModelGuid != null) { var dataModel = dataModelService.GetPluginDataModelByGuid(Entity.RightDataModelGuid.Value); if (dataModel != null && dataModel.ContainsPath(Entity.RightPropertyPath)) UpdateRightSide(dataModel, Entity.RightPropertyPath); } // Right side static - else if (PredicateType == PredicateType.Static && Entity.RightStaticValue != null) + else if (PredicateType == ProfileRightSideType.Static && Entity.RightStaticValue != null) { try { @@ -235,6 +312,21 @@ namespace Artemis.Core.Models.Profile.Conditions return Entity; } + private void CreateExpression() + { + 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(); + else + CreateStaticExpression(); + } + private void ValidateOperator() { if (LeftDataModel == null || Operator == null) @@ -248,7 +340,7 @@ namespace Artemis.Core.Models.Profile.Conditions private void ValidateRightSide() { var leftSideType = LeftDataModel.GetTypeAtPath(LeftPropertyPath); - if (PredicateType == PredicateType.Dynamic) + if (PredicateType == ProfileRightSideType.Dynamic) { if (RightDataModel == null) return; @@ -266,70 +358,22 @@ namespace Artemis.Core.Models.Profile.Conditions } } - private void SetStaticValue(object staticValue) - { - // If the left side is empty simply apply the value, any validation will wait - if (LeftDataModel == null) - { - RightStaticValue = staticValue; - return; - } - - var leftSideType = LeftDataModel.GetTypeAtPath(LeftPropertyPath); - - // If not null ensure the types match and if not, convert it - if (staticValue != null && staticValue.GetType() == leftSideType) - RightStaticValue = staticValue; - else if (staticValue != null) - RightStaticValue = Convert.ChangeType(staticValue, leftSideType); - // 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 - RightStaticValue = null; - } - private void CreateDynamicExpression() { if (LeftDataModel == null || RightDataModel == null || Operator == null) return; - var isListExpression = LeftDataModel.GetListTypeInPath(LeftPropertyPath) != null; + var leftSideAccessor = CreateAccessor(LeftDataModel, LeftPropertyPath, "left", out var leftSideParameter); + var rightSideAccessor = CreateAccessor(RightDataModel, RightPropertyPath, "right", out var rightSideParameter); - Expression leftSideAccessor; - Expression rightSideAccessor; - ParameterExpression leftSideParameter; - ParameterExpression rightSideParameter = null; - if (isListExpression) - { - // List accessors share the same parameter because a list always contains one item per entry - leftSideParameter = Expression.Parameter(typeof(object), "listItem"); - leftSideAccessor = CreateListAccessor(LeftDataModel, LeftPropertyPath, leftSideParameter); - rightSideAccessor = CreateListAccessor(RightDataModel, RightPropertyPath, leftSideParameter); - } - else - { - leftSideAccessor = CreateAccessor(LeftDataModel, LeftPropertyPath, "left", out leftSideParameter); - rightSideAccessor = CreateAccessor(RightDataModel, RightPropertyPath, "right", out rightSideParameter); - } - // A conversion may be required if the types differ // This can cause issues if the DisplayConditionOperator 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); - - if (isListExpression) - { - var lambda = Expression.Lambda>(conditionExpression, leftSideParameter); - CompiledListPredicate = lambda.Compile(); - } - else - { - var lambda = Expression.Lambda>(conditionExpression, leftSideParameter, rightSideParameter); - CompiledDynamicPredicate = lambda.Compile(); - } + var lambda = Expression.Lambda>(conditionExpression, leftSideParameter, rightSideParameter); + CompiledDynamicPredicate = lambda.Compile(); } private void CreateStaticExpression() @@ -337,18 +381,7 @@ namespace Artemis.Core.Models.Profile.Conditions if (LeftDataModel == null || Operator == null) return; - var isListExpression = LeftDataModel.GetListTypeInPath(LeftPropertyPath) != null; - - Expression leftSideAccessor; - ParameterExpression leftSideParameter; - if (isListExpression) - { - // List accessors share the same parameter because a list always contains one item per entry - leftSideParameter = Expression.Parameter(typeof(object), "listItem"); - leftSideAccessor = CreateListAccessor(LeftDataModel, LeftPropertyPath, leftSideParameter); - } - else - leftSideAccessor = CreateAccessor(LeftDataModel, LeftPropertyPath, "left", out leftSideParameter); + var leftSideAccessor = CreateAccessor(LeftDataModel, LeftPropertyPath, "left", out var 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) @@ -360,19 +393,10 @@ namespace Artemis.Core.Models.Profile.Conditions : Expression.Constant(null, leftSideAccessor.Type); var conditionExpression = Operator.CreateExpression(leftSideAccessor, rightSideConstant); - - if (isListExpression) - { - var lambda = Expression.Lambda>(conditionExpression, leftSideParameter); - CompiledListPredicate = lambda.Compile(); - } - else - { - var lambda = Expression.Lambda>(conditionExpression, leftSideParameter); - CompiledStaticPredicate = lambda.Compile(); - } + var lambda = Expression.Lambda>(conditionExpression, leftSideParameter); + CompiledStaticPredicate = lambda.Compile(); } - + private Expression CreateAccessor(DataModel dataModel, string path, string parameterName, out ParameterExpression parameter) { var listType = dataModel.GetListTypeInPath(path); @@ -398,18 +422,5 @@ namespace Artemis.Core.Models.Profile.Conditions Expression.Property ); } - - public override string ToString() - { - if (PredicateType == PredicateType.Dynamic) - return $"[Dynamic] {LeftPropertyPath} {Operator.Description} {RightPropertyPath}"; - return $"[Static] {LeftPropertyPath} {Operator.Description} {RightStaticValue}"; - } - } - - public enum PredicateType - { - Static, - Dynamic } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Conditions/Operators/EqualsConditionOperator.cs b/src/Artemis.Core/Models/Profile/Conditions/Operators/EqualsConditionOperator.cs index 4868ad996..b70f5dc01 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/Operators/EqualsConditionOperator.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/Operators/EqualsConditionOperator.cs @@ -4,7 +4,7 @@ using System.Linq.Expressions; namespace Artemis.Core.Models.Profile.Conditions.Operators { - public class EqualsConditionOperator : DisplayConditionOperator + internal class EqualsConditionOperator : DisplayConditionOperator { public override IReadOnlyCollection CompatibleTypes => new List {typeof(object)}; diff --git a/src/Artemis.Core/Models/Profile/Conditions/Operators/GreaterThanConditionOperator.cs b/src/Artemis.Core/Models/Profile/Conditions/Operators/GreaterThanConditionOperator.cs index b8618a2b6..121e7f10a 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/Operators/GreaterThanConditionOperator.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/Operators/GreaterThanConditionOperator.cs @@ -4,7 +4,7 @@ using System.Linq.Expressions; namespace Artemis.Core.Models.Profile.Conditions.Operators { - public class GreaterThanConditionOperator : DisplayConditionOperator + internal class GreaterThanConditionOperator : DisplayConditionOperator { public override IReadOnlyCollection CompatibleTypes => Constants.NumberTypes; diff --git a/src/Artemis.Core/Models/Profile/Conditions/Operators/GreaterThanOrEqualConditionOperator.cs b/src/Artemis.Core/Models/Profile/Conditions/Operators/GreaterThanOrEqualConditionOperator.cs index d4955ee34..444ac15c6 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/Operators/GreaterThanOrEqualConditionOperator.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/Operators/GreaterThanOrEqualConditionOperator.cs @@ -4,7 +4,7 @@ using System.Linq.Expressions; namespace Artemis.Core.Models.Profile.Conditions.Operators { - public class GreaterThanOrEqualConditionOperator : DisplayConditionOperator + internal class GreaterThanOrEqualConditionOperator : DisplayConditionOperator { public override IReadOnlyCollection CompatibleTypes => Constants.NumberTypes; diff --git a/src/Artemis.Core/Models/Profile/Conditions/Operators/LessThanConditionOperator.cs b/src/Artemis.Core/Models/Profile/Conditions/Operators/LessThanConditionOperator.cs index fc083f400..0ce64ff72 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/Operators/LessThanConditionOperator.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/Operators/LessThanConditionOperator.cs @@ -4,7 +4,7 @@ using System.Linq.Expressions; namespace Artemis.Core.Models.Profile.Conditions.Operators { - public class LessThanConditionOperator : DisplayConditionOperator + internal class LessThanConditionOperator : DisplayConditionOperator { public override IReadOnlyCollection CompatibleTypes => Constants.NumberTypes; diff --git a/src/Artemis.Core/Models/Profile/Conditions/Operators/LessThanOrEqualConditionOperator.cs b/src/Artemis.Core/Models/Profile/Conditions/Operators/LessThanOrEqualConditionOperator.cs index ebc9dd871..092c5d7af 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/Operators/LessThanOrEqualConditionOperator.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/Operators/LessThanOrEqualConditionOperator.cs @@ -4,7 +4,7 @@ using System.Linq.Expressions; namespace Artemis.Core.Models.Profile.Conditions.Operators { - public class LessThanOrEqualConditionOperator : DisplayConditionOperator + internal class LessThanOrEqualConditionOperator : DisplayConditionOperator { public override IReadOnlyCollection CompatibleTypes => Constants.NumberTypes; diff --git a/src/Artemis.Core/Models/Profile/Conditions/Operators/NotEqualConditionOperator.cs b/src/Artemis.Core/Models/Profile/Conditions/Operators/NotEqualConditionOperator.cs index f4dc78c71..84a025987 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/Operators/NotEqualConditionOperator.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/Operators/NotEqualConditionOperator.cs @@ -4,7 +4,7 @@ using System.Linq.Expressions; namespace Artemis.Core.Models.Profile.Conditions.Operators { - public class NotEqualConditionOperator : DisplayConditionOperator + internal class NotEqualConditionOperator : DisplayConditionOperator { public override IReadOnlyCollection CompatibleTypes => new List { typeof(object) }; diff --git a/src/Artemis.Core/Models/Profile/Conditions/Operators/StringContainsConditionOperator.cs b/src/Artemis.Core/Models/Profile/Conditions/Operators/StringContainsConditionOperator.cs index f82d7e367..cb8f326f3 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/Operators/StringContainsConditionOperator.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/Operators/StringContainsConditionOperator.cs @@ -5,7 +5,7 @@ using System.Reflection; namespace Artemis.Core.Models.Profile.Conditions.Operators { - public class StringContainsConditionOperator : DisplayConditionOperator + internal class StringContainsConditionOperator : DisplayConditionOperator { private readonly MethodInfo _toLower; private readonly MethodInfo _contains; diff --git a/src/Artemis.Core/Models/Profile/Conditions/Operators/StringEndsWithConditionOperator.cs b/src/Artemis.Core/Models/Profile/Conditions/Operators/StringEndsWithConditionOperator.cs index 69bcdc5b3..b117b3a4e 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/Operators/StringEndsWithConditionOperator.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/Operators/StringEndsWithConditionOperator.cs @@ -5,7 +5,7 @@ using System.Reflection; namespace Artemis.Core.Models.Profile.Conditions.Operators { - public class StringEndsWithConditionOperator : DisplayConditionOperator + internal class StringEndsWithConditionOperator : DisplayConditionOperator { private readonly MethodInfo _toLower; private readonly MethodInfo _endsWith; diff --git a/src/Artemis.Core/Models/Profile/Conditions/Operators/StringEqualsConditionOperator.cs b/src/Artemis.Core/Models/Profile/Conditions/Operators/StringEqualsConditionOperator.cs index 56fce09f7..4c3006e32 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/Operators/StringEqualsConditionOperator.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/Operators/StringEqualsConditionOperator.cs @@ -5,7 +5,7 @@ using System.Reflection; namespace Artemis.Core.Models.Profile.Conditions.Operators { - public class StringEqualsConditionOperator : DisplayConditionOperator + internal class StringEqualsConditionOperator : DisplayConditionOperator { private readonly MethodInfo _toLower; diff --git a/src/Artemis.Core/Models/Profile/Conditions/Operators/StringNotContainsConditionOperator.cs b/src/Artemis.Core/Models/Profile/Conditions/Operators/StringNotContainsConditionOperator.cs index 11ee0144b..682269b9c 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/Operators/StringNotContainsConditionOperator.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/Operators/StringNotContainsConditionOperator.cs @@ -5,7 +5,7 @@ using System.Reflection; namespace Artemis.Core.Models.Profile.Conditions.Operators { - public class StringNotContainsConditionOperator : DisplayConditionOperator + internal class StringNotContainsConditionOperator : DisplayConditionOperator { private readonly MethodInfo _toLower; private readonly MethodInfo _contains; diff --git a/src/Artemis.Core/Models/Profile/Conditions/Operators/StringNotEqualConditionOperator.cs b/src/Artemis.Core/Models/Profile/Conditions/Operators/StringNotEqualConditionOperator.cs index 7235dd5bc..ebef98d93 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/Operators/StringNotEqualConditionOperator.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/Operators/StringNotEqualConditionOperator.cs @@ -5,7 +5,7 @@ using System.Reflection; namespace Artemis.Core.Models.Profile.Conditions.Operators { - public class StringNotEqualConditionOperator : DisplayConditionOperator + internal class StringNotEqualConditionOperator : DisplayConditionOperator { private readonly MethodInfo _toLower; diff --git a/src/Artemis.Core/Models/Profile/Conditions/Operators/StringNullConditionOperator.cs b/src/Artemis.Core/Models/Profile/Conditions/Operators/StringNullConditionOperator.cs index 589b31223..0fc2c8394 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/Operators/StringNullConditionOperator.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/Operators/StringNullConditionOperator.cs @@ -4,7 +4,7 @@ using System.Linq.Expressions; namespace Artemis.Core.Models.Profile.Conditions.Operators { - public class StringNullConditionOperator : DisplayConditionOperator + internal class StringNullConditionOperator : DisplayConditionOperator { public StringNullConditionOperator() { diff --git a/src/Artemis.Core/Models/Profile/Conditions/Operators/StringStartsWithConditionOperator.cs b/src/Artemis.Core/Models/Profile/Conditions/Operators/StringStartsWithConditionOperator.cs index 06259737c..3049d1ec8 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/Operators/StringStartsWithConditionOperator.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/Operators/StringStartsWithConditionOperator.cs @@ -5,7 +5,7 @@ using System.Reflection; namespace Artemis.Core.Models.Profile.Conditions.Operators { - public class StringStartsWithConditionOperator : DisplayConditionOperator + internal class StringStartsWithConditionOperator : DisplayConditionOperator { private readonly MethodInfo _toLower; private readonly MethodInfo _startsWith; diff --git a/src/Artemis.Core/Models/Profile/DataBindings/DataBinding.cs b/src/Artemis.Core/Models/Profile/DataBindings/DataBinding.cs new file mode 100644 index 000000000..e15a79785 --- /dev/null +++ b/src/Artemis.Core/Models/Profile/DataBindings/DataBinding.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using Artemis.Core.Models.Profile.LayerProperties; +using Artemis.Core.Plugins.DataModelExpansions; + +namespace Artemis.Core.Models.Profile.DataBindings +{ + /// + /// A data binding that binds a certain to a value inside a + /// + public class DataBinding + { + private readonly List _modifiers = new List(); + + /// + /// The that the data binding targets + /// + public BaseLayerProperty Target { get; set; } // BIG FAT TODO: Take into account X and Y of SkPosition etc., forgot about it again :> + + /// + /// Gets the currently used instance of the data model that contains the source of the data binding + /// + public DataModel SourceDataModel { get; private set; } + + /// + /// Gets the path of the source property in the + /// + public string SourcePropertyPath { get; private set; } + + /// + /// Gets a list of modifiers applied to this data binding + /// + public IReadOnlyList Modifiers => _modifiers.AsReadOnly(); + + /// + /// Adds a modifier to the data binding's collection + /// + public void AddModifier(DataBindingModifier modifier) + { + if (!_modifiers.Contains(modifier)) + { + modifier.DataBinding = this; + modifier.CreateExpression(); + _modifiers.Add(modifier); + } + } + + /// + /// Removes a modifier from the data binding's collection + /// + public void RemoveModifier(DataBindingModifier modifier) + { + if (_modifiers.Contains(modifier)) + { + modifier.DataBinding = null; + modifier.CreateExpression(); + _modifiers.Remove(modifier); + } + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataBindings/DataBindingModifier.cs b/src/Artemis.Core/Models/Profile/DataBindings/DataBindingModifier.cs new file mode 100644 index 000000000..4e34c3682 --- /dev/null +++ b/src/Artemis.Core/Models/Profile/DataBindings/DataBindingModifier.cs @@ -0,0 +1,287 @@ +using System; +using System.Linq; +using System.Linq.Expressions; +using Artemis.Core.Exceptions; +using Artemis.Core.Models.Profile.DataBindings.Modifiers; +using Artemis.Core.Plugins.DataModelExpansions; +using Artemis.Core.Services.Interfaces; +using Artemis.Storage.Entities.Profile.DataBindings; +using Newtonsoft.Json; + +namespace Artemis.Core.Models.Profile.DataBindings +{ + /// + /// Modifies a data model value in a way defined by the modifier type + /// + public class DataBindingModifier + { + /// + /// Creates a new instance of the class + /// + public DataBindingModifier(DataBinding dataBinding, ProfileRightSideType parameterType) + { + DataBinding = dataBinding; + ParameterType = parameterType; + Entity = new DataBindingModifierEntity(); + } + + /// + /// Creates a new instance of the class + /// + public DataBindingModifier(DataBinding dataBinding, DataBindingModifierEntity entity) + { + DataBinding = dataBinding; + ParameterType = (ProfileRightSideType) entity.ParameterType; + Order = entity.Order; + Entity = entity; + } + + /// + /// Gets the data binding this modifier is applied to + /// + public DataBinding DataBinding { get; internal set; } + + /// + /// Gets the type of modifier that is being applied + /// + public DataBindingModifierType ModifierType { get; private set; } + + /// + /// Gets the type of the parameter, can either be dynamic (based on a data model value) or static + /// + public ProfileRightSideType ParameterType { get; private set; } + + /// + /// Gets the position at which the modifier appears on the data binding + /// + public int Order { get; internal set; } + + /// + /// Gets the currently used instance of the parameter data model + /// + public DataModel ParameterDataModel { get; private set; } + + /// + /// Gets the path of the parameter property in the + /// + public string ParameterPropertyPath { get; private set; } + + /// + /// Gets the parameter static value, only used it is + /// + /// + public object ParameterStaticValue { get; private set; } + + /// + /// Gets the compiled function that evaluates this predicate if it of a dynamic + /// + public Func CompiledDynamicPredicate { get; private set; } + + /// + /// Gets the compiled function that evaluates this predicate if it is of a static + /// + public Func CompiledStaticPredicate { get; private set; } + + internal DataBindingModifierEntity Entity { get; set; } + + /// + /// Applies the modifier to the provided value + /// + /// The value to apply the modifier to, should be of the same type as the data binding target + /// The modified value + public object Apply(object currentValue) + { + var targetType = DataBinding.Target.GetPropertyType(); + if (currentValue.GetType() != targetType) + { + throw new ArtemisCoreException("The current value of the data binding does not match the type of the target property." + + $" {targetType.Name} expected, received {currentValue.GetType().Name}."); + } + + if (CompiledDynamicPredicate != null) + return CompiledDynamicPredicate(currentValue, ParameterDataModel); + if (CompiledStaticPredicate != null) + return CompiledStaticPredicate(currentValue); + + return currentValue; + } + + /// + /// Updates the modifier type of the modifier and re-compiles the expression + /// + /// + public void UpdateModifierType(DataBindingModifierType modifierType) + { + // Calling CreateExpression will clear compiled expressions + if (modifierType == null) + { + ModifierType = null; + CreateExpression(); + return; + } + + var targetType = DataBinding.Target.GetPropertyType(); + if (!modifierType.SupportsType(targetType)) + { + throw new ArtemisCoreException($"Cannot apply modifier type {modifierType.GetType().Name} to this modifier because " + + $"it does not support this data binding's type {targetType.Name}"); + } + + ModifierType = modifierType; + CreateExpression(); + } + + /// + /// Updates the parameter of the modifier, makes the modifier dynamic and re-compiles the expression + /// + /// The data model of the parameter + /// The path pointing to the parameter inside the data model + public void UpdateParameter(DataModel dataModel, string path) + { + if (dataModel != null && path == null) + throw new ArtemisCoreException("If a data model is provided, a path is also required"); + if (dataModel == null && path != null) + throw new ArtemisCoreException("If path is provided, a data model is also required"); + + if (dataModel != null) + { + if (!dataModel.ContainsPath(path)) + throw new ArtemisCoreException($"Data model of type {dataModel.GetType().Name} does not contain a property at path '{path}'"); + } + + ParameterType = ProfileRightSideType.Dynamic; + ParameterDataModel = dataModel; + ParameterPropertyPath = path; + + CreateExpression(); + } + + /// + /// Updates the parameter of the modifier, makes the modifier static and re-compiles the expression + /// + /// The static value to use as a parameter + public void UpdateParameter(object staticValue) + { + ParameterType = ProfileRightSideType.Static; + ParameterDataModel = null; + ParameterPropertyPath = null; + + var targetType = DataBinding.Target.GetPropertyType(); + + // If not null ensure the types match and if not, convert it + if (staticValue != null && staticValue.GetType() == targetType) + ParameterStaticValue = staticValue; + else if (staticValue != null) + ParameterStaticValue = Convert.ChangeType(staticValue, targetType); + // If null create a default instance for value types or simply make it null for reference types + else if (targetType.IsValueType) + ParameterStaticValue = Activator.CreateInstance(targetType); + else + ParameterStaticValue = null; + + CreateExpression(); + } + + internal void Initialize(IDataModelService dataModelService, IDataBindingService dataBindingService) + { + // Modifier type + if (Entity.ModifierTypePluginGuid != null) + { + var modifierType = dataBindingService.GetModifierType(Entity.ModifierTypePluginGuid.Value, Entity.ModifierType); + if (modifierType != null) + UpdateModifierType(modifierType); + } + + // Dynamic parameter + if (ParameterType == ProfileRightSideType.Dynamic && Entity.ParameterDataModelGuid != null) + { + var dataModel = dataModelService.GetPluginDataModelByGuid(Entity.ParameterDataModelGuid.Value); + if (dataModel != null && dataModel.ContainsPath(Entity.ParameterPropertyPath)) + UpdateParameter(dataModel, Entity.ParameterPropertyPath); + } + else if (ParameterType == ProfileRightSideType.Static && Entity.ParameterStaticValue != null) + { + // Use the target type so JSON.NET has a better idea what to do + var targetType = DataBinding.Target.GetPropertyType(); + object staticValue; + + try + { + staticValue = JsonConvert.DeserializeObject(Entity.ParameterStaticValue, targetType); + } + // If deserialization fails, use the type's default + catch (JsonSerializationException e) + { + dataBindingService.LogModifierDeserializationFailure(this, e); + staticValue = Activator.CreateInstance(targetType); + } + + UpdateParameter(staticValue); + } + + // Static parameter + } + + + internal void CreateExpression() + { + CompiledDynamicPredicate = null; + CompiledStaticPredicate = null; + + if (ModifierType == null || DataBinding == null) + return; + + if (ParameterType == ProfileRightSideType.Dynamic && ModifierType.SupportsParameter) + CreateDynamicExpression(); + else + CreateStaticExpression(); + } + + private void CreateDynamicExpression() + { + if (ParameterDataModel == null) + return; + + var currentValueParameter = Expression.Parameter(DataBinding.Target.GetPropertyType()); + + // If the right side value is null, the constant type cannot be inferred and must be provided based on the data binding target + var rightSideAccessor = CreateAccessor(ParameterDataModel, ParameterPropertyPath, "right", out var rightSideParameter); + + // A conversion may be required if the types differ + // This can cause issues if the DisplayConditionOperator wasn't accurate in it's supported types but that is not a concern here + if (rightSideAccessor.Type != DataBinding.Target.GetPropertyType()) + rightSideAccessor = Expression.Convert(rightSideAccessor, DataBinding.Target.GetPropertyType()); + + var modifierExpression = ModifierType.CreateExpression(currentValueParameter, rightSideAccessor); + var lambda = Expression.Lambda>(modifierExpression, currentValueParameter, rightSideParameter); + CompiledDynamicPredicate = lambda.Compile(); + } + + private void CreateStaticExpression() + { + var currentValueParameter = Expression.Parameter(DataBinding.Target.GetPropertyType()); + + // If the right side value is null, the constant type cannot be inferred and must be provided based on the data binding target + var rightSideConstant = ParameterStaticValue != null + ? Expression.Constant(ParameterStaticValue) + : Expression.Constant(null, DataBinding.Target.GetPropertyType()); + + var modifierExpression = ModifierType.CreateExpression(currentValueParameter, rightSideConstant); + var lambda = Expression.Lambda>(modifierExpression, currentValueParameter); + CompiledStaticPredicate = 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( + Expression.Convert(parameter, dataModel.GetType()), // Cast to the appropriate type + Expression.Property + ); + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataBindings/Modifiers/DataBindingModifierType.cs b/src/Artemis.Core/Models/Profile/DataBindings/Modifiers/DataBindingModifierType.cs new file mode 100644 index 000000000..c6ee9ce9e --- /dev/null +++ b/src/Artemis.Core/Models/Profile/DataBindings/Modifiers/DataBindingModifierType.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using Artemis.Core.Extensions; +using Artemis.Core.Plugins; +using Artemis.Core.Services; +using Artemis.Core.Services.Interfaces; + +namespace Artemis.Core.Models.Profile.DataBindings.Modifiers +{ + /// + /// A modifier that changes the source value of a data binding in some way + /// + public abstract class DataBindingModifierType + { + private bool _registered; + private IDataBindingService _dataBindingService; + + /// + /// Gets the plugin info this data binding modifier belongs to + /// Note: Not set until after registering + /// + public PluginInfo PluginInfo { get; internal set; } + + /// + /// Gets the data binding modifier this modifier type is applied to + /// + public DataBindingModifier Modifier { get; internal set; } + + /// + /// Gets the types this modifier supports + /// + public abstract IReadOnlyCollection CompatibleTypes { get; } + + /// + /// Gets or sets the description of this modifier + /// + public abstract string Description { get; } + + /// + /// Gets or sets the icon of this modifier + /// + public abstract string Icon { get; } + + /// + /// Gets or sets whether this modifier supports a parameter, defaults to true + /// + public bool SupportsParameter { get; protected set; } = true; + + /// + /// Returns whether the given type is supported by the modifier + /// + public bool SupportsType(Type type) + { + if (type == null) + return true; + return CompatibleTypes.Any(t => t.IsCastableFrom(type)); + } + + /// + /// Creates a binary expression comparing two types + /// + /// The current value of the data binding + /// An argument passed to the modifier, either static of dynamic + /// + public abstract Expression CreateExpression(ParameterExpression currentValue, Expression modifierArgument); + + internal void Register(PluginInfo pluginInfo, IDataBindingService dataBindingService) + { + if (_registered) + return; + + PluginInfo = pluginInfo; + _dataBindingService = dataBindingService; + + if (PluginInfo != Constants.CorePluginInfo) + PluginInfo.Instance.PluginDisabled += InstanceOnPluginDisabled; + + _registered = true; + } + + internal void Unsubscribe() + { + if (!_registered) + return; + + if (PluginInfo != Constants.CorePluginInfo) + PluginInfo.Instance.PluginDisabled -= InstanceOnPluginDisabled; + _registered = false; + } + + private void InstanceOnPluginDisabled(object sender, EventArgs e) + { + // The service will call Unsubscribe + _dataBindingService.RemoveModifierType(this); + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/ProfileRightSideType.cs b/src/Artemis.Core/Models/Profile/ProfileRightSideType.cs new file mode 100644 index 000000000..55f95795c --- /dev/null +++ b/src/Artemis.Core/Models/Profile/ProfileRightSideType.cs @@ -0,0 +1,18 @@ +namespace Artemis.Core.Models.Profile +{ + /// + /// An enum defining the right side type of a profile entity + /// + public enum ProfileRightSideType + { + /// + /// A static right side value + /// + Static, + + /// + /// A dynamic right side value based on a path in a data model + /// + Dynamic + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Services/DataBindingService.cs b/src/Artemis.Core/Services/DataBindingService.cs index 1fa8e0e10..c6d1f5e44 100644 --- a/src/Artemis.Core/Services/DataBindingService.cs +++ b/src/Artemis.Core/Services/DataBindingService.cs @@ -1,11 +1,107 @@ using System; using System.Collections.Generic; -using System.Text; +using System.Linq; +using Artemis.Core.Extensions; +using Artemis.Core.Models.Profile.DataBindings; +using Artemis.Core.Models.Profile.DataBindings.Modifiers; +using Artemis.Core.Plugins; using Artemis.Core.Services.Interfaces; +using Newtonsoft.Json; +using Serilog; namespace Artemis.Core.Services { - public class DataBindingService : IDataBindingService + internal class DataBindingService : IDataBindingService { + private readonly ILogger _logger; + private readonly List _registeredDataBindingModifierTypes; + + public DataBindingService(ILogger logger) + { + _logger = logger; + _registeredDataBindingModifierTypes = new List(); + } + + public IReadOnlyCollection RegisteredDataBindingModifierTypes + { + get + { + lock (_registeredDataBindingModifierTypes) + { + return _registeredDataBindingModifierTypes.AsReadOnly(); + } + } + } + + public void RegisterModifierType(PluginInfo pluginInfo, DataBindingModifierType dataBindingModifierType) + { + if (pluginInfo == null) + throw new ArgumentNullException(nameof(pluginInfo)); + if (dataBindingModifierType == null) + throw new ArgumentNullException(nameof(dataBindingModifierType)); + + lock (_registeredDataBindingModifierTypes) + { + if (_registeredDataBindingModifierTypes.Contains(dataBindingModifierType)) + return; + + dataBindingModifierType.Register(pluginInfo, this); + _registeredDataBindingModifierTypes.Add(dataBindingModifierType); + } + } + + public void RemoveModifierType(DataBindingModifierType dataBindingModifierType) + { + if (dataBindingModifierType == null) + throw new ArgumentNullException(nameof(dataBindingModifierType)); + + lock (_registeredDataBindingModifierTypes) + { + if (!_registeredDataBindingModifierTypes.Contains(dataBindingModifierType)) + return; + + dataBindingModifierType.Unsubscribe(); + _registeredDataBindingModifierTypes.Remove(dataBindingModifierType); + } + } + + public List GetCompatibleModifierTypes(Type type) + { + lock (_registeredDataBindingModifierTypes) + { + if (type == null) + return new List(_registeredDataBindingModifierTypes); + + var candidates = _registeredDataBindingModifierTypes.Where(c => c.CompatibleTypes.Any(t => t.IsCastableFrom(type))).ToList(); + + // If there are multiple modifier types with the same description, use the closest match + foreach (var dataBindingModifierTypes in candidates.GroupBy(c => c.Description).Where(g => g.Count() > 1).ToList()) + { + var bestCandidate = dataBindingModifierTypes.OrderByDescending(c => c.CompatibleTypes.Contains(type)).FirstOrDefault(); + foreach (var dataBindingModifierType in dataBindingModifierTypes) + { + if (dataBindingModifierType != bestCandidate) + candidates.Remove(dataBindingModifierType); + } + } + + return candidates; + } + } + + public DataBindingModifierType GetModifierType(Guid modifierTypePluginGuid, string modifierType) + { + return RegisteredDataBindingModifierTypes.FirstOrDefault(o => o.PluginInfo.Guid == modifierTypePluginGuid && o.GetType().Name == modifierType); + } + + public void LogModifierDeserializationFailure(DataBindingModifier dataBindingModifier, JsonSerializationException exception) + { + _logger.Warning( + exception, + "Failed to deserialize static parameter for operator {order}. {operatorType}", + dataBindingModifier.Entity.Order, + dataBindingModifier.Entity.ModifierType + ); + } } } \ No newline at end of file diff --git a/src/Artemis.Core/Services/Interfaces/IDataBindingService.cs b/src/Artemis.Core/Services/Interfaces/IDataBindingService.cs index b8caaf2ed..1ac5018b9 100644 --- a/src/Artemis.Core/Services/Interfaces/IDataBindingService.cs +++ b/src/Artemis.Core/Services/Interfaces/IDataBindingService.cs @@ -1,6 +1,51 @@ -namespace Artemis.Core.Services.Interfaces +using System; +using System.Collections.Generic; +using Artemis.Core.Annotations; +using Artemis.Core.Models.Profile.DataBindings; +using Artemis.Core.Models.Profile.DataBindings.Modifiers; +using Artemis.Core.Plugins; +using Newtonsoft.Json; + +namespace Artemis.Core.Services.Interfaces { public interface IDataBindingService : IArtemisService { + /// + /// Gets a read-only collection of all registered modifier types + /// + IReadOnlyCollection RegisteredDataBindingModifierTypes { get; } + + /// + /// Registers a new modifier type for use in data bindings + /// + /// The PluginInfo of the plugin this modifier type belongs to + /// The modifier type to register + void RegisterModifierType([NotNull] PluginInfo pluginInfo, [NotNull] DataBindingModifierType dataBindingModifierType); + + /// + /// Removes a modifier type so it is no longer available for use in data bindings + /// + /// The modifier type to remove + void RemoveModifierType([NotNull] DataBindingModifierType dataBindingModifierType); + + /// + /// Returns all the data binding modifier types compatible with the provided type + /// + List GetCompatibleModifierTypes(Type type); + + /// + /// Gets a modifier type by its plugin GUID and type name + /// + /// The modifier type's plugin GUID + /// The type name of the modifier type + /// + DataBindingModifierType GetModifierType(Guid modifierTypePluginGuid, string modifierType); + + /// + /// Logs a modifier deserialization failure + /// + /// The modifier that failed to deserialize + /// The JSON exception that occurred + void LogModifierDeserializationFailure(DataBindingModifier dataBindingModifier, JsonSerializationException exception); } } \ No newline at end of file diff --git a/src/Artemis.Core/Services/Interfaces/IDataModelService.cs b/src/Artemis.Core/Services/Interfaces/IDataModelService.cs index f1586204d..512fc8710 100644 --- a/src/Artemis.Core/Services/Interfaces/IDataModelService.cs +++ b/src/Artemis.Core/Services/Interfaces/IDataModelService.cs @@ -10,7 +10,14 @@ namespace Artemis.Core.Services.Interfaces { public interface IDataModelService : IArtemisService { + /// + /// Gets a read-only collection of all registered condition operators + /// IReadOnlyCollection RegisteredConditionOperators { get; } + + /// + /// Gets a read-only collection of all registered data model expansions + /// IReadOnlyCollection DataModelExpansions { get; } /// @@ -57,9 +64,30 @@ namespace Artemis.Core.Services.Interfaces /// The layer condition operator to remove void RemoveConditionOperator([NotNull] DisplayConditionOperator displayConditionOperator); + /// + /// Returns all the display condition operators compatible with the provided type + /// List GetCompatibleConditionOperators(Type type); + + /// + /// Gets a condition operator by its plugin GUID and type name + /// + /// The operator's plugin GUID + /// The type name of the operator DisplayConditionOperator GetConditionOperator(Guid operatorPluginGuid, string operatorType); + + /// + /// Logs a predicate deserialization failure + /// + /// The predicate that failed to deserialize + /// The JSON exception that occurred void LogPredicateDeserializationFailure(DisplayConditionPredicate displayConditionPredicate, JsonException exception); + + /// + /// Logs a list predicate deserialization failure + /// + /// The list predicate that failed to deserialize + /// The JSON exception that occurred void LogListPredicateDeserializationFailure(DisplayConditionListPredicate displayConditionListPredicate, JsonException exception); } } \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Profile/Abstract/RenderElementEntity.cs b/src/Artemis.Storage/Entities/Profile/Abstract/RenderElementEntity.cs index f76182c77..41af59cd9 100644 --- a/src/Artemis.Storage/Entities/Profile/Abstract/RenderElementEntity.cs +++ b/src/Artemis.Storage/Entities/Profile/Abstract/RenderElementEntity.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Artemis.Storage.Entities.Profile.Conditions; namespace Artemis.Storage.Entities.Profile.Abstract { diff --git a/src/Artemis.Storage/Entities/Profile/DisplayConditionGroupEntity.cs b/src/Artemis.Storage/Entities/Profile/Conditions/DisplayConditionGroupEntity.cs similarity index 73% rename from src/Artemis.Storage/Entities/Profile/DisplayConditionGroupEntity.cs rename to src/Artemis.Storage/Entities/Profile/Conditions/DisplayConditionGroupEntity.cs index b99d01170..a862a79e4 100644 --- a/src/Artemis.Storage/Entities/Profile/DisplayConditionGroupEntity.cs +++ b/src/Artemis.Storage/Entities/Profile/Conditions/DisplayConditionGroupEntity.cs @@ -1,9 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Text; +using System.Collections.Generic; using Artemis.Storage.Entities.Profile.Abstract; -namespace Artemis.Storage.Entities.Profile +namespace Artemis.Storage.Entities.Profile.Conditions { public class DisplayConditionGroupEntity : DisplayConditionPartEntity { diff --git a/src/Artemis.Storage/Entities/Profile/DisplayConditionListEntity.cs b/src/Artemis.Storage/Entities/Profile/Conditions/DisplayConditionListEntity.cs similarity index 89% rename from src/Artemis.Storage/Entities/Profile/DisplayConditionListEntity.cs rename to src/Artemis.Storage/Entities/Profile/Conditions/DisplayConditionListEntity.cs index 34907ebc7..925c9a055 100644 --- a/src/Artemis.Storage/Entities/Profile/DisplayConditionListEntity.cs +++ b/src/Artemis.Storage/Entities/Profile/Conditions/DisplayConditionListEntity.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using Artemis.Storage.Entities.Profile.Abstract; -namespace Artemis.Storage.Entities.Profile +namespace Artemis.Storage.Entities.Profile.Conditions { public class DisplayConditionListEntity : DisplayConditionPartEntity { diff --git a/src/Artemis.Storage/Entities/Profile/DisplayConditionListPredicateEntity.cs b/src/Artemis.Storage/Entities/Profile/Conditions/DisplayConditionListPredicateEntity.cs similarity index 92% rename from src/Artemis.Storage/Entities/Profile/DisplayConditionListPredicateEntity.cs rename to src/Artemis.Storage/Entities/Profile/Conditions/DisplayConditionListPredicateEntity.cs index 289a7db5e..8a2521389 100644 --- a/src/Artemis.Storage/Entities/Profile/DisplayConditionListPredicateEntity.cs +++ b/src/Artemis.Storage/Entities/Profile/Conditions/DisplayConditionListPredicateEntity.cs @@ -1,7 +1,7 @@ using System; using Artemis.Storage.Entities.Profile.Abstract; -namespace Artemis.Storage.Entities.Profile +namespace Artemis.Storage.Entities.Profile.Conditions { public class DisplayConditionListPredicateEntity : DisplayConditionPartEntity { diff --git a/src/Artemis.Storage/Entities/Profile/DisplayConditionPredicateEntity.cs b/src/Artemis.Storage/Entities/Profile/Conditions/DisplayConditionPredicateEntity.cs similarity index 92% rename from src/Artemis.Storage/Entities/Profile/DisplayConditionPredicateEntity.cs rename to src/Artemis.Storage/Entities/Profile/Conditions/DisplayConditionPredicateEntity.cs index 2e29e01ad..f31c160f9 100644 --- a/src/Artemis.Storage/Entities/Profile/DisplayConditionPredicateEntity.cs +++ b/src/Artemis.Storage/Entities/Profile/Conditions/DisplayConditionPredicateEntity.cs @@ -1,7 +1,7 @@ using System; using Artemis.Storage.Entities.Profile.Abstract; -namespace Artemis.Storage.Entities.Profile +namespace Artemis.Storage.Entities.Profile.Conditions { public class DisplayConditionPredicateEntity : DisplayConditionPartEntity { diff --git a/src/Artemis.Storage/Entities/Profile/ProfileConditionEntity.cs b/src/Artemis.Storage/Entities/Profile/Conditions/ProfileConditionEntity.cs similarity index 67% rename from src/Artemis.Storage/Entities/Profile/ProfileConditionEntity.cs rename to src/Artemis.Storage/Entities/Profile/Conditions/ProfileConditionEntity.cs index 99ca1dbe3..686300b58 100644 --- a/src/Artemis.Storage/Entities/Profile/ProfileConditionEntity.cs +++ b/src/Artemis.Storage/Entities/Profile/Conditions/ProfileConditionEntity.cs @@ -1,6 +1,6 @@ using System; -namespace Artemis.Storage.Entities.Profile +namespace Artemis.Storage.Entities.Profile.Conditions { public class ProfileConditionEntity { diff --git a/src/Artemis.Storage/Entities/Profile/DataBindings/DataBindingEntity.cs b/src/Artemis.Storage/Entities/Profile/DataBindings/DataBindingEntity.cs new file mode 100644 index 000000000..da9d85c7c --- /dev/null +++ b/src/Artemis.Storage/Entities/Profile/DataBindings/DataBindingEntity.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; + +namespace Artemis.Storage.Entities.Profile.DataBindings +{ + public class DataBindingEntity + { + public DataBindingEntity() + { + Modifiers = new List(); + } + + public Guid? SourceDataModelGuid { get; set; } + public string SourcePropertyPath { get; set; } + public int DataBindingMode { get; set; } + public TimeSpan EasingTime { get; set; } + public int EasingFunction { get; set; } + + public List Modifiers { get; set; } + } +} \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Profile/DataBindings/DataBindingModifierEntity.cs b/src/Artemis.Storage/Entities/Profile/DataBindings/DataBindingModifierEntity.cs new file mode 100644 index 000000000..cef799f14 --- /dev/null +++ b/src/Artemis.Storage/Entities/Profile/DataBindings/DataBindingModifierEntity.cs @@ -0,0 +1,19 @@ +using System; + +namespace Artemis.Storage.Entities.Profile.DataBindings +{ + public class DataBindingModifierEntity + { + public string ModifierType { get; set; } + public Guid? ModifierTypePluginGuid { get; set; } + + public int Order { get; set; } + public int ParameterType { get; set; } + + public Guid? ParameterDataModelGuid { get; set; } + public string ParameterPropertyPath { get; set; } + + // Stored as a string to be able to control serialization and deserialization ourselves + public string ParameterStaticValue { get; set; } + } +} \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Profile/KeyframeEntity.cs b/src/Artemis.Storage/Entities/Profile/KeyframeEntity.cs new file mode 100644 index 000000000..32762d4c5 --- /dev/null +++ b/src/Artemis.Storage/Entities/Profile/KeyframeEntity.cs @@ -0,0 +1,12 @@ +using System; + +namespace Artemis.Storage.Entities.Profile +{ + public class KeyframeEntity + { + public TimeSpan Position { get; set; } + public int Timeline { get; set; } + public string Value { get; set; } + public int EasingFunction { get; set; } + } +} \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Profile/PropertyEntity.cs b/src/Artemis.Storage/Entities/Profile/PropertyEntity.cs index 610bf0910..d2c82aef7 100644 --- a/src/Artemis.Storage/Entities/Profile/PropertyEntity.cs +++ b/src/Artemis.Storage/Entities/Profile/PropertyEntity.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Artemis.Storage.Entities.Profile.DataBindings; namespace Artemis.Storage.Entities.Profile { @@ -17,13 +18,6 @@ namespace Artemis.Storage.Entities.Profile public bool KeyframesEnabled { get; set; } public List KeyframeEntities { get; set; } - } - - public class KeyframeEntity - { - public TimeSpan Position { get; set; } - public int Timeline { get; set; } - public string Value { get; set; } - public int EasingFunction { get; set; } + public DataBindingEntity DataBindingEntity { get; set; } } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionGroupViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionGroupViewModel.cs index 279149129..92835d7bd 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionGroupViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionGroupViewModel.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Threading.Tasks; using System.Windows; +using Artemis.Core.Models.Profile; using Artemis.Core.Models.Profile.Conditions; using Artemis.UI.Ninject.Factories; using Artemis.UI.Screens.ProfileEditor.DisplayConditions.Abstract; @@ -74,16 +75,16 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions if (type == "Static") { if (!IsListGroup) - DisplayConditionGroup.AddChild(new DisplayConditionPredicate(DisplayConditionGroup, PredicateType.Static)); + DisplayConditionGroup.AddChild(new DisplayConditionPredicate(DisplayConditionGroup, ProfileRightSideType.Static)); else - DisplayConditionGroup.AddChild(new DisplayConditionListPredicate(DisplayConditionGroup, PredicateType.Static)); + DisplayConditionGroup.AddChild(new DisplayConditionListPredicate(DisplayConditionGroup, ProfileRightSideType.Static)); } else if (type == "Dynamic") { if (!IsListGroup) - DisplayConditionGroup.AddChild(new DisplayConditionPredicate(DisplayConditionGroup, PredicateType.Dynamic)); + DisplayConditionGroup.AddChild(new DisplayConditionPredicate(DisplayConditionGroup, ProfileRightSideType.Dynamic)); else - DisplayConditionGroup.AddChild(new DisplayConditionListPredicate(DisplayConditionGroup, PredicateType.Dynamic)); + DisplayConditionGroup.AddChild(new DisplayConditionListPredicate(DisplayConditionGroup, ProfileRightSideType.Dynamic)); } else if (type == "List" && !IsListGroup) DisplayConditionGroup.AddChild(new DisplayConditionList(DisplayConditionGroup)); diff --git a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionListPredicateViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionListPredicateViewModel.cs index c8eee0de2..f4cc5c236 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionListPredicateViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionListPredicateViewModel.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using System.Timers; using System.Windows; using System.Windows.Input; +using Artemis.Core.Models.Profile; using Artemis.Core.Models.Profile.Conditions; using Artemis.Core.Plugins.Settings; using Artemis.Core.Services; @@ -14,7 +15,6 @@ using Artemis.UI.Exceptions; using Artemis.UI.Screens.ProfileEditor.DisplayConditions.Abstract; using Artemis.UI.Shared.DataModelVisualization; using Artemis.UI.Shared.DataModelVisualization.Shared; -using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services.Interfaces; using Artemis.UI.Utilities; using Stylet; @@ -69,7 +69,7 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions } public DisplayConditionListPredicate DisplayConditionListPredicate => (DisplayConditionListPredicate) Model; - public bool ShowRightSidePropertySelection => DisplayConditionListPredicate.PredicateType == PredicateType.Dynamic; + public bool ShowRightSidePropertySelection => DisplayConditionListPredicate.PredicateType == ProfileRightSideType.Dynamic; public bool CanActivateRightSideInputViewModel => SelectedLeftSideProperty?.PropertyInfo != null; public PluginSetting ShowDataModelValues { get; } @@ -201,7 +201,7 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions var listDataModelGuid = DisplayConditionListPredicate.ListDataModel.PluginInfo.Guid; // If static, only allow selecting properties also supported by input - if (DisplayConditionListPredicate.PredicateType == PredicateType.Static) + if (DisplayConditionListPredicate.PredicateType == ProfileRightSideType.Static) LeftSideDataModel.ApplyTypeFilter(false, _supportedInputTypes.ToArray()); // Determine the left side property first @@ -216,7 +216,7 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions SelectedOperator = DisplayConditionListPredicate.Operator; // Determine the right side - if (DisplayConditionListPredicate.PredicateType == PredicateType.Dynamic) + if (DisplayConditionListPredicate.PredicateType == ProfileRightSideType.Dynamic) { SelectedRightSideProperty = RightSideDataModel.GetChildByPath(listDataModelGuid, DisplayConditionListPredicate.RightPropertyPath); RightSideDataModel.ApplyTypeFilter(true, leftSideType); @@ -310,7 +310,7 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions private void LeftDataModelUpdateRequested(object sender, EventArgs e) { - if (DisplayConditionListPredicate.PredicateType == PredicateType.Static) + if (DisplayConditionListPredicate.PredicateType == ProfileRightSideType.Static) LeftSideDataModel.ApplyTypeFilter(false, _supportedInputTypes.ToArray()); } diff --git a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionListViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionListViewModel.cs index 7127bf517..1ce2ca740 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionListViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionListViewModel.cs @@ -3,13 +3,13 @@ using System.Collections; using System.Linq; using System.Threading.Tasks; using System.Timers; +using Artemis.Core.Models.Profile; using Artemis.Core.Models.Profile.Conditions; using Artemis.Core.Plugins.Settings; using Artemis.Core.Services; using Artemis.UI.Ninject.Factories; using Artemis.UI.Screens.ProfileEditor.DisplayConditions.Abstract; using Artemis.UI.Shared.DataModelVisualization.Shared; -using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services.Interfaces; using Artemis.UI.Utilities; using Humanizer; @@ -18,13 +18,13 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions { public class DisplayConditionListViewModel : DisplayConditionViewModel { - private readonly IProfileEditorService _profileEditorService; private readonly IDataModelUIService _dataModelUIService; private readonly IDisplayConditionsVmFactory _displayConditionsVmFactory; + private readonly IProfileEditorService _profileEditorService; + private readonly Timer _updateTimer; private bool _isInitialized; private DataModelListViewModel _selectedListProperty; private DataModelPropertiesViewModel _targetDataModel; - private readonly Timer _updateTimer; public DisplayConditionListViewModel( DisplayConditionList displayConditionList, @@ -73,7 +73,7 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions } public string SelectedListOperator => DisplayConditionList.ListOperator.Humanize(); - + public void SelectListOperator(string type) { var enumValue = Enum.Parse(type); @@ -86,9 +86,9 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions public void AddCondition(string type) { if (type == "Static") - DisplayConditionList.AddChild(new DisplayConditionPredicate(DisplayConditionList, PredicateType.Static)); + DisplayConditionList.AddChild(new DisplayConditionPredicate(DisplayConditionList, ProfileRightSideType.Static)); else if (type == "Dynamic") - DisplayConditionList.AddChild(new DisplayConditionPredicate(DisplayConditionList, PredicateType.Dynamic)); + DisplayConditionList.AddChild(new DisplayConditionPredicate(DisplayConditionList, ProfileRightSideType.Dynamic)); Update(); _profileEditorService.UpdateSelectedProfileElement(); @@ -125,26 +125,6 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions IsInitialized = true; } - protected override void Dispose(bool disposing) - { - _updateTimer.Stop(); - _updateTimer.Elapsed -= OnUpdateTimerOnElapsed; - } - - private void OnUpdateTimerOnElapsed(object sender, ElapsedEventArgs e) - { - if (TargetDataModelOpen) - { - TargetDataModel?.Update(_dataModelUIService); - SelectedListProperty?.Update(_dataModelUIService); - } - } - - private void TargetDataModelUpdateRequested(object sender, EventArgs e) - { - TargetDataModel.ApplyTypeFilter(true, typeof(IList)); - } - public void ApplyList() { DisplayConditionList.UpdateList(SelectedListProperty.DataModel, SelectedListProperty.PropertyPath); @@ -198,6 +178,26 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions childViewModel.Update(); } + protected override void Dispose(bool disposing) + { + _updateTimer.Stop(); + _updateTimer.Elapsed -= OnUpdateTimerOnElapsed; + } + + private void OnUpdateTimerOnElapsed(object sender, ElapsedEventArgs e) + { + if (TargetDataModelOpen) + { + TargetDataModel?.Update(_dataModelUIService); + SelectedListProperty?.Update(_dataModelUIService); + } + } + + private void TargetDataModelUpdateRequested(object sender, EventArgs e) + { + TargetDataModel.ApplyTypeFilter(true, typeof(IList)); + } + private void ExecuteSelectListProperty(object context) { diff --git a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionPredicateViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionPredicateViewModel.cs index f132c696a..5e0779be3 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionPredicateViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionPredicateViewModel.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using System.Timers; using System.Windows; using System.Windows.Input; +using Artemis.Core.Models.Profile; using Artemis.Core.Models.Profile.Conditions; using Artemis.Core.Plugins.Settings; using Artemis.Core.Services; @@ -13,7 +14,6 @@ using Artemis.UI.Events; using Artemis.UI.Screens.ProfileEditor.DisplayConditions.Abstract; using Artemis.UI.Shared.DataModelVisualization; using Artemis.UI.Shared.DataModelVisualization.Shared; -using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services.Interfaces; using Artemis.UI.Utilities; using Stylet; @@ -26,6 +26,7 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions private readonly IDataModelUIService _dataModelUIService; private readonly IEventAggregator _eventAggregator; private readonly IProfileEditorService _profileEditorService; + private readonly Timer _updateTimer; private bool _isInitialized; private DataModelPropertiesViewModel _leftSideDataModel; private BindableCollection _operators; @@ -38,7 +39,6 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions private DataModelVisualizationViewModel _selectedRightSideProperty; private List _supportedInputTypes; - private readonly Timer _updateTimer; public DisplayConditionPredicateViewModel( DisplayConditionPredicate displayConditionPredicate, @@ -68,7 +68,7 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions } public DisplayConditionPredicate DisplayConditionPredicate => (DisplayConditionPredicate) Model; - public bool ShowRightSidePropertySelection => DisplayConditionPredicate.PredicateType == PredicateType.Dynamic; + public bool ShowRightSidePropertySelection => DisplayConditionPredicate.PredicateType == ProfileRightSideType.Dynamic; public bool CanActivateRightSideInputViewModel => SelectedLeftSideProperty?.PropertyInfo != null; public PluginSetting ShowDataModelValues { get; } @@ -199,11 +199,11 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions public override void Update() { - if (LeftSideDataModel == null || DisplayConditionPredicate.PredicateType == PredicateType.Dynamic && RightSideDataModel == null) + if (LeftSideDataModel == null || DisplayConditionPredicate.PredicateType == ProfileRightSideType.Dynamic && RightSideDataModel == null) return; // If static, only allow selecting properties also supported by input - if (DisplayConditionPredicate.PredicateType == PredicateType.Static) + if (DisplayConditionPredicate.PredicateType == ProfileRightSideType.Static) LeftSideDataModel.ApplyTypeFilter(false, _supportedInputTypes.ToArray()); // Determine the left side property first @@ -218,7 +218,7 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions SelectedOperator = DisplayConditionPredicate.Operator; // Determine the right side - if (DisplayConditionPredicate.PredicateType == PredicateType.Dynamic) + if (DisplayConditionPredicate.PredicateType == ProfileRightSideType.Dynamic) { SelectedRightSideProperty = LeftSideDataModel.GetChildForCondition(DisplayConditionPredicate, DisplayConditionSide.Right); RightSideDataModel.ApplyTypeFilter(true, leftSideType); @@ -300,7 +300,7 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions private void RightDataModelUpdateRequested(object sender, EventArgs e) { var leftSideType = SelectedLeftSideProperty?.PropertyInfo?.PropertyType; - if (DisplayConditionPredicate.PredicateType == PredicateType.Dynamic) + if (DisplayConditionPredicate.PredicateType == ProfileRightSideType.Dynamic) SelectedRightSideProperty = LeftSideDataModel.GetChildForCondition(DisplayConditionPredicate, DisplayConditionSide.Right); // With the data model updated, also reapply the filter @@ -309,7 +309,7 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions private void LeftDataModelUpdateRequested(object sender, EventArgs e) { - if (DisplayConditionPredicate.PredicateType == PredicateType.Static) + if (DisplayConditionPredicate.PredicateType == ProfileRightSideType.Static) LeftSideDataModel.ApplyTypeFilter(false, _supportedInputTypes.ToArray()); }