diff --git a/src/Artemis.Core/Models/Profile/DataModel/DataModelPath.cs b/src/Artemis.Core/Models/Profile/DataModel/DataModelPath.cs index 7b301001b..3dfdd5271 100644 --- a/src/Artemis.Core/Models/Profile/DataModel/DataModelPath.cs +++ b/src/Artemis.Core/Models/Profile/DataModel/DataModelPath.cs @@ -1,7 +1,9 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; +using System.Reflection; using Artemis.Core.DataModelExpansions; namespace Artemis.Core @@ -11,22 +13,29 @@ namespace Artemis.Core /// public class DataModelPath { - private readonly LinkedList _parts; + private readonly LinkedList _segments; - internal DataModelPath(DataModel dataModel, string path) + /// + /// Creates a new instance of the class + /// + /// The data model at which this path starts + /// A string representation of the + public DataModelPath(object dataModel, string path) { - DataModel = dataModel ?? throw new ArgumentNullException(nameof(dataModel)); + Target = dataModel ?? throw new ArgumentNullException(nameof(dataModel)); Path = path ?? throw new ArgumentNullException(nameof(path)); - DataModelGuid = dataModel.PluginInfo.Guid; - _parts = new LinkedList(); + if (string.IsNullOrWhiteSpace(Path)) + throw new ArgumentException("Path cannot be empty"); + + _segments = new LinkedList(); Initialize(path); } /// /// Gets the data model at which this path starts /// - public DataModel DataModel { get; } + public object Target { get; } /// /// Gets a string representation of the @@ -34,49 +43,111 @@ namespace Artemis.Core public string Path { get; } /// - /// Gets a boolean indicating whether all are valid + /// Gets a boolean indicating whether all are valid /// - public bool IsValid => Parts.All(p => p.Type != DataModelPathPartType.Invalid); + public bool IsValid => Segments.All(p => p.Type != DataModelPathSegmentType.Invalid); /// - /// Gets a read-only list of all parts of this path + /// Gets a read-only list of all segments of this path /// - public IReadOnlyCollection Parts => _parts.ToList().AsReadOnly(); + public IReadOnlyCollection Segments => _segments.ToList().AsReadOnly(); - internal Func Accessor { get; private set; } + /// + /// Gets a boolean indicating whether this data model path can have an inner path because it points to a list + /// + public bool CanHaveInnerPath => Segments.LastOrDefault()?.GetPropertyType()?.IsAssignableFrom(typeof(IList)) ?? false; - internal Guid DataModelGuid { get; set; } + /// + /// Gets the inner path of this path, only available if this path points to a list + /// + public DataModelPath InnerPath { get; internal set; } + + internal Func Accessor { get; private set; } + + public void SetInnerPath(string path) + { + if (!CanHaveInnerPath) + { + var type = Segments.LastOrDefault()?.GetPropertyType(); + throw new ArtemisCoreException($"Cannot set an inner path on a data model path if it does not point to a list (value is of type: {type?.Name})"); + } + + InnerPath = new DataModelPath(GetValue(), path); + } + + /// + /// Gets the current value of the path + /// + public object GetValue() + { + return Accessor?.Invoke(Target); + } + + /// + /// Gets the property info of the property this path points to + /// + /// If static, the property info. If dynamic, null + public PropertyInfo GetPropertyInfo() + { + if (InnerPath != null) + return InnerPath.GetPropertyInfo(); + return Segments.LastOrDefault()?.GetPropertyInfo(); + } + + /// + /// Gets the type of the property this path points to + /// + /// If possible, the property type + public Type GetPropertyType() + { + if (InnerPath != null) + return InnerPath.GetPropertyType(); + return Segments.LastOrDefault()?.GetPropertyType(); + } + + /// + /// Gets the property description of the property this path points to + /// + /// If found, the data model property description + public DataModelPropertyAttribute GetPropertyDescription() + { + if (InnerPath != null) + return InnerPath.GetPropertyDescription(); + return Segments.LastOrDefault()?.GetPropertyDescription(); + } /// public override string ToString() { + if (InnerPath != null) + return $"{Path} > {InnerPath}"; return Path; } private void Initialize(string path) { - var parts = path.Split("."); - for (var index = 0; index < parts.Length; index++) + var segments = path.Split("."); + for (var index = 0; index < segments.Length; index++) { - var identifier = parts[index]; - var node = _parts.AddLast(new DataModelPathPart(this, identifier, string.Join('.', parts.Take(index + 1)))); + var identifier = segments[index]; + var node = _segments.AddLast(new DataModelPathSegment(this, identifier, string.Join('.', segments.Take(index + 1)))); node.Value.Node = node; } - var parameter = Expression.Parameter(typeof(DataModel), "dm"); - Expression expression = Expression.Convert(parameter, DataModel.GetType()); + var parameter = Expression.Parameter(typeof(object), "t"); + Expression expression = Expression.Convert(parameter, Target.GetType()); Expression nullCondition = null; - foreach (var part in _parts) + foreach (var segment in _segments) { - var notNull = Expression.NotEqual(expression, Expression.Constant(null)); + var notNull = Expression.NotEqual(expression, Expression.Default(expression.Type)); nullCondition = nullCondition != null ? Expression.AndAlso(nullCondition, notNull) : notNull; - expression = part.Initialize(parameter, expression, nullCondition); + expression = segment.Initialize(parameter, expression, nullCondition); if (expression == null) return; } - Accessor = Expression.Lambda>( + Accessor = Expression.Lambda>( // Wrap with a null check Expression.Condition( nullCondition, diff --git a/src/Artemis.Core/Models/Profile/DataModel/DataModelPathPart.cs b/src/Artemis.Core/Models/Profile/DataModel/DataModelPathPart.cs deleted file mode 100644 index 131c0b3cc..000000000 --- a/src/Artemis.Core/Models/Profile/DataModel/DataModelPathPart.cs +++ /dev/null @@ -1,151 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq.Expressions; -using System.Reflection; -using Artemis.Core.DataModelExpansions; - -namespace Artemis.Core -{ - /// - /// Represents a part of a data model path - /// - public class DataModelPathPart - { - internal DataModelPathPart(DataModelPath dataModelPath, string identifier, string path) - { - DataModelPath = dataModelPath; - Identifier = identifier; - Path = path; - } - - /// - /// Gets the data model path this is a part of - /// - public DataModelPath DataModelPath { get; } - - /// - /// Gets the identifier that is associated with this part - /// - public string Identifier { get; } - - /// - /// Gets the path that leads to this part - /// - public string Path { get; } - - /// - /// Gets the type of data model this part of the path points to - /// - public DataModelPathPartType Type { get; private set; } - - /// - /// Gets the type of dynamic data model this path points to - /// Not used if the is - /// - public Type DynamicDataModelType { get; private set; } - - /// - /// Gets the previous part in the path - /// - public DataModelPathPart Previous => Node.Previous?.Value; - - /// - /// Gets the next part in the path - /// - public DataModelPathPart Next => Node.Next?.Value; - - internal Func Accessor { get; set; } - internal LinkedListNode Node { get; set; } - - /// - /// Returns the current value of the path up to this part - /// - /// - public object GetValue() - { - return Type == DataModelPathPartType.Invalid ? null : Accessor(DataModelPath.DataModel); - } - - /// - public override string ToString() - { - return $"[{Type}] {Path}"; - } - - internal Expression Initialize(ParameterExpression parameter, Expression expression, Expression nullCondition) - { - var previousValue = Previous != null ? Previous.GetValue() : DataModelPath.DataModel; - if (previousValue == null) - { - Type = DataModelPathPartType.Invalid; - return null; - } - - // Determine this part's type by looking for a dynamic data model with the identifier - if (previousValue is DataModel dataModel) - { - var hasDynamicDataModel = dataModel.DynamicDataModels.TryGetValue(Identifier, out var dynamicDataModel); - // If a dynamic data model is found the use that - if (hasDynamicDataModel) - DetermineDynamicType(dynamicDataModel); - // Otherwise look for a static type - else - DetermineStaticType(previousValue); - } - // Only data models can have dynamic types so if it is something else, its always going to be static - else - DetermineStaticType(previousValue); - - return CreateExpression(parameter, expression, nullCondition); - } - - private Expression CreateExpression(ParameterExpression parameter, Expression expression, Expression nullCondition) - { - if (Type == DataModelPathPartType.Invalid) - { - Accessor = null; - return null; - } - - Expression accessorExpression; - // A static part just needs to access the property or filed - if (Type == DataModelPathPartType.Static) - accessorExpression = Expression.PropertyOrField(expression, Identifier); - // A dynamic part calls the generic method DataModel.DynamicChild and provides the identifier as an argument - else - { - accessorExpression = Expression.Call( - expression, - nameof(DataModel.DynamicChild), - new[] {DynamicDataModelType}, - Expression.Constant(Identifier) - ); - } - - Accessor = Expression.Lambda>( - // Wrap with a null check - Expression.Condition( - nullCondition, - Expression.Convert(accessorExpression, typeof(object)), - Expression.Convert(Expression.Default(accessorExpression.Type), typeof(object)) - ), - parameter - ).Compile(); - - return accessorExpression; - } - - private void DetermineDynamicType(DataModel dynamicDataModel) - { - Type = DataModelPathPartType.Dynamic; - DynamicDataModelType = dynamicDataModel.GetType(); - } - - private void DetermineStaticType(object previous) - { - var previousType = previous.GetType(); - var property = previousType.GetProperty(Identifier, BindingFlags.Public | BindingFlags.Instance); - Type = property == null ? DataModelPathPartType.Invalid : DataModelPathPartType.Static; - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataModel/DataModelPathSegment.cs b/src/Artemis.Core/Models/Profile/DataModel/DataModelPathSegment.cs new file mode 100644 index 000000000..a82b3d1b3 --- /dev/null +++ b/src/Artemis.Core/Models/Profile/DataModel/DataModelPathSegment.cs @@ -0,0 +1,208 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Reflection; +using Artemis.Core.DataModelExpansions; +using Humanizer; + +namespace Artemis.Core +{ + /// + /// Represents a segment of a data model path + /// + public class DataModelPathSegment + { + internal DataModelPathSegment(DataModelPath dataModelPath, string identifier, string path) + { + DataModelPath = dataModelPath; + Identifier = identifier; + Path = path; + } + + /// + /// Gets the data model path this is a segment of + /// + public DataModelPath DataModelPath { get; } + + /// + /// Gets the identifier that is associated with this segment + /// + public string Identifier { get; } + + /// + /// Gets the path that leads to this segment + /// + public string Path { get; } + + /// + /// Gets the type of data model this segment of the path points to + /// + public DataModelPathSegmentType Type { get; private set; } + + /// + /// Gets the type of dynamic data model this path points to + /// Not used if the is + /// + public Type DynamicDataModelType { get; private set; } + + /// + /// Gets the previous segment in the path + /// + public DataModelPathSegment Previous => Node.Previous?.Value; + + /// + /// Gets the next segment in the path + /// + public DataModelPathSegment Next => Node.Next?.Value; + + internal Func Accessor { get; set; } + internal LinkedListNode Node { get; set; } + + /// + /// Returns the current value of the path up to this segment + /// + /// + public object GetValue() + { + return Type == DataModelPathSegmentType.Invalid ? null : Accessor(DataModelPath.Target); + } + + /// + public override string ToString() + { + return $"[{Type}] {Path}"; + } + + /// + /// Gets the property info of the property this segment points to + /// + /// If static, the property info. If dynamic, null + public PropertyInfo GetPropertyInfo() + { + // Dynamic types have no property and therefore no property info + if (Type == DataModelPathSegmentType.Dynamic) + return null; + + // If this is the first segment in a path, the property is located on the data model + if (Previous == null) + return DataModelPath.Target.GetType().GetProperty(Identifier); + // If this is not the first segment in a path, the property is located on the previous segment + return Previous.GetValue()?.GetType().GetProperty(Identifier); + } + + /// + /// Gets the property description of the property this segment points to + /// + /// If found, the data model property description + public DataModelPropertyAttribute GetPropertyDescription() + { + // Dynamic types have a data model description + if (Type == DataModelPathSegmentType.Dynamic) + return ((DataModel) GetValue())?.DataModelDescription; + + var propertyInfo = GetPropertyInfo(); + if (propertyInfo == null) + return null; + + // Static types may have one as an attribute + return (DataModelPropertyAttribute) Attribute.GetCustomAttribute(propertyInfo, typeof(DataModelPropertyAttribute)) ?? + new DataModelPropertyAttribute {Name = propertyInfo.Name.Humanize(), ResetsDepth = false}; + } + + /// + /// Gets the type of the property this path points to + /// + /// If possible, the property type + public Type GetPropertyType() + { + // Prefer basing the type on the property info + var propertyInfo = GetPropertyInfo(); + var type = propertyInfo?.PropertyType; + // Property info is not available on dynamic paths though, so fall back on the current value + if (propertyInfo == null) + { + var currentValue = GetValue(); + if (currentValue != null) + type = currentValue.GetType(); + } + + return type; + } + + internal Expression Initialize(ParameterExpression parameter, Expression expression, Expression nullCondition) + { + var previousValue = Previous != null ? Previous.GetValue() : DataModelPath.Target; + if (previousValue == null) + { + Type = DataModelPathSegmentType.Invalid; + return null; + } + + // Determine this segment's type by looking for a dynamic data model with the identifier + if (previousValue is DataModel dataModel) + { + var hasDynamicDataModel = dataModel.DynamicDataModels.TryGetValue(Identifier, out var dynamicDataModel); + // If a dynamic data model is found the use that + if (hasDynamicDataModel) + DetermineDynamicType(dynamicDataModel); + // Otherwise look for a static type + else + DetermineStaticType(previousValue); + } + // Only data models can have dynamic types so if it is something else, its always going to be static + else + DetermineStaticType(previousValue); + + return CreateExpression(parameter, expression, nullCondition); + } + + private Expression CreateExpression(ParameterExpression parameter, Expression expression, Expression nullCondition) + { + if (Type == DataModelPathSegmentType.Invalid) + { + Accessor = null; + return null; + } + + Expression accessorExpression; + // A static segment just needs to access the property or filed + if (Type == DataModelPathSegmentType.Static) + accessorExpression = Expression.PropertyOrField(expression, Identifier); + // A dynamic segment calls the generic method DataModel.DynamicChild and provides the identifier as an argument + else + { + accessorExpression = Expression.Call( + expression, + nameof(DataModel.DynamicChild), + new[] {DynamicDataModelType}, + Expression.Constant(Identifier) + ); + } + + Accessor = Expression.Lambda>( + // Wrap with a null check + Expression.Condition( + nullCondition, + Expression.Convert(accessorExpression, typeof(object)), + Expression.Convert(Expression.Default(accessorExpression.Type), typeof(object)) + ), + parameter + ).Compile(); + + return accessorExpression; + } + + private void DetermineDynamicType(DataModel dynamicDataModel) + { + Type = DataModelPathSegmentType.Dynamic; + DynamicDataModelType = dynamicDataModel.GetType(); + } + + private void DetermineStaticType(object previous) + { + var previousType = previous.GetType(); + var property = previousType.GetProperty(Identifier, BindingFlags.Public | BindingFlags.Instance); + Type = property == null ? DataModelPathSegmentType.Invalid : DataModelPathSegmentType.Static; + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataModel/DataModelPathPartType.cs b/src/Artemis.Core/Models/Profile/DataModel/DataModelPathSegmentType.cs similarity index 93% rename from src/Artemis.Core/Models/Profile/DataModel/DataModelPathPartType.cs rename to src/Artemis.Core/Models/Profile/DataModel/DataModelPathSegmentType.cs index d3e494264..565f0c037 100644 --- a/src/Artemis.Core/Models/Profile/DataModel/DataModelPathPartType.cs +++ b/src/Artemis.Core/Models/Profile/DataModel/DataModelPathSegmentType.cs @@ -3,7 +3,7 @@ /// /// Represents a type of data model path /// - public enum DataModelPathPartType + public enum DataModelPathSegmentType { /// /// Represents an invalid data model type that points to a missing data model diff --git a/src/Artemis.Core/Plugins/DataModelExpansions/DataModel.cs b/src/Artemis.Core/Plugins/DataModelExpansions/DataModel.cs index 04d7dffc6..3fbff7206 100644 --- a/src/Artemis.Core/Plugins/DataModelExpansions/DataModel.cs +++ b/src/Artemis.Core/Plugins/DataModelExpansions/DataModel.cs @@ -68,7 +68,7 @@ namespace Artemis.Core.DataModelExpansions if (_dynamicDataModels.ContainsKey(key)) { throw new ArtemisCoreException($"Cannot add a dynamic data model with key '{key}' " + - "because the key is already in use on this data model."); + "because the key is already in use on by another dynamic property this data model."); } if (_dynamicDataModels.ContainsValue(dynamicDataModel)) @@ -78,6 +78,12 @@ namespace Artemis.Core.DataModelExpansions $"because the dynamic data model is already added with key '{existingKey}."); } + if (GetType().GetProperty(key) != null) + { + throw new ArtemisCoreException($"Cannot add a dynamic data model with key '{key}' " + + "because the key is already in use by a static property on this data model."); + } + dynamicDataModel.PluginInfo = PluginInfo; dynamicDataModel.DataModelDescription = new DataModelPropertyAttribute { diff --git a/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelDynamicView.xaml b/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelDynamicView.xaml index c28daabc1..b260726fc 100644 --- a/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelDynamicView.xaml +++ b/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelDynamicView.xaml @@ -30,7 +30,7 @@