From f2f77da95306193c70a25e2d7458e42dc1a61986 Mon Sep 17 00:00:00 2001 From: Robert Date: Wed, 12 Aug 2020 23:32:30 +0200 Subject: [PATCH] Display conditions - Implemented lists in the core, UI needs more work --- .../Abstract/DisplayConditionPart.cs | 11 ++ .../Conditions/DisplayConditionGroup.cs | 28 +++- .../DisplayConditionListPredicate.cs | 38 ++++- .../Conditions/DisplayConditionPredicate.cs | 131 +++++++++++++----- .../Plugins/Abstract/DataModels/DataModel.cs | 66 ++++++++- .../DataModelListPropertiesViewModel.cs | 4 + .../Shared/DataModelListViewModel.cs | 45 +++--- .../Shared/DataModelVisualizationViewModel.cs | 16 +-- .../DisplayConditions.xaml | 4 +- .../Abstract/DisplayConditionViewModel.cs | 6 +- .../DisplayConditionListPredicateViewModel.cs | 10 +- .../DisplayConditionPredicateViewModel.cs | 6 + .../DataModel/GeneralDataModel.cs | 7 + 13 files changed, 288 insertions(+), 84 deletions(-) diff --git a/src/Artemis.Core/Models/Profile/Conditions/Abstract/DisplayConditionPart.cs b/src/Artemis.Core/Models/Profile/Conditions/Abstract/DisplayConditionPart.cs index 534f48926..0531e1a5b 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/Abstract/DisplayConditionPart.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/Abstract/DisplayConditionPart.cs @@ -35,8 +35,19 @@ namespace Artemis.Core.Models.Profile.Conditions.Abstract } } + /// + /// Evaluates the condition part on the data model + /// + /// public abstract bool Evaluate(); + /// + /// Evaluates the condition part on the given target (currently only for lists) + /// + /// + /// + public abstract bool EvaluateObject(object target); + internal abstract void Initialize(IDataModelService dataModelService); internal abstract void ApplyToEntity(); internal abstract DisplayConditionPartEntity GetEntity(); diff --git a/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionGroup.cs b/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionGroup.cs index e6d2591f7..627571163 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionGroup.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionGroup.cs @@ -37,9 +37,12 @@ namespace Artemis.Core.Models.Profile.Conditions public override bool Evaluate() { - // If there are less than two children, ignore the boolean operator - if (Children.Count <= 2) - return Children.All(c => c.Evaluate()); + // Empty groups are always true + if (Children.Count == 0) + return true; + // Groups with only one child ignore the boolean operator + if (Children.Count == 1) + return Children[0].Evaluate(); switch (BooleanOperator) { @@ -56,6 +59,25 @@ namespace Artemis.Core.Models.Profile.Conditions } } + public override bool EvaluateObject(object target) + { + // Empty groups are always true + if (Children.Count == 0) + return true; + // Groups with only one child ignore the boolean operator + if (Children.Count == 1) + return Children[0].EvaluateObject(target); + + return BooleanOperator switch + { + BooleanOperator.And => Children.All(c => c.EvaluateObject(target)), + BooleanOperator.Or => Children.Any(c => c.EvaluateObject(target)), + BooleanOperator.AndNot => Children.All(c => !c.EvaluateObject(target)), + BooleanOperator.OrNot => Children.Any(c => !c.EvaluateObject(target)), + _ => throw new ArgumentOutOfRangeException() + }; + } + internal override void ApplyToEntity() { DisplayConditionGroupEntity.BooleanOperator = (int) BooleanOperator; diff --git a/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionListPredicate.cs b/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionListPredicate.cs index b40560add..d79c2efa1 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionListPredicate.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionListPredicate.cs @@ -1,4 +1,7 @@ -using System.Linq; +using System; +using System.Collections; +using System.Linq; +using System.Linq.Expressions; using Artemis.Core.Exceptions; using Artemis.Core.Models.Profile.Conditions.Abstract; using Artemis.Core.Plugins.Abstract.DataModels; @@ -44,7 +47,23 @@ namespace Artemis.Core.Models.Profile.Conditions public override bool Evaluate() { - return true; + return EvaluateObject(CompiledListAccessor(ListDataModel)); + } + + public override bool EvaluateObject(object target) + { + if (!(target is IList list)) + return false; + + var objectList = list.Cast(); + return ListOperator switch + { + ListOperator.Any => objectList.Any(o => Children[0].EvaluateObject(o)), + ListOperator.All => objectList.All(o => Children[0].EvaluateObject(o)), + ListOperator.None => objectList.Any(o => !Children[0].EvaluateObject(o)), + ListOperator.Count => false, + _ => throw new ArgumentOutOfRangeException() + }; } internal override void ApplyToEntity() @@ -94,11 +113,26 @@ namespace Artemis.Core.Models.Profile.Conditions { if (!dataModel.ContainsPath(path)) throw new ArtemisCoreException($"Data model of type {dataModel.GetType().Name} does not contain a property at path '{path}'"); + if (dataModel.GetListTypeAtPath(path) == null) + throw new ArtemisCoreException($"The path '{path}' does not contain a list"); } ListDataModel = dataModel; ListPropertyPath = path; + + if (dataModel != null) + { + var parameter = Expression.Parameter(typeof(object), "listDataModel"); + var accessor = path.Split('.').Aggregate( + Expression.Convert(parameter, dataModel.GetType()), + (expression, s) => Expression.Convert(Expression.Property(expression, s), typeof(IList))); + + var lambda = Expression.Lambda>(accessor, parameter); + CompiledListAccessor = lambda.Compile(); + } } + + public Func CompiledListAccessor { get; set; } } public enum ListOperator diff --git a/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionPredicate.cs b/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionPredicate.cs index 0e08216d0..8e42f97a6 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionPredicate.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionPredicate.cs @@ -39,10 +39,9 @@ namespace Artemis.Core.Models.Profile.Conditions public string RightPropertyPath { get; private set; } public object RightStaticValue { get; private set; } - public Expression> DynamicConditionLambda { get; private set; } - public Func CompiledDynamicConditionLambda { get; private set; } - public Expression> StaticConditionLambda { get; private set; } - public Func CompiledStaticConditionLambda { get; private set; } + public Func CompiledDynamicPredicate { get; private set; } + public Func CompiledStaticPredicate { get; private set; } + public Func CompiledListPredicate { get; private set; } public void UpdateLeftSide(DataModel dataModel, string path) { @@ -120,10 +119,9 @@ namespace Artemis.Core.Models.Profile.Conditions private void CreateExpression() { - DynamicConditionLambda = null; - CompiledDynamicConditionLambda = null; - StaticConditionLambda = null; - CompiledStaticConditionLambda = null; + CompiledDynamicPredicate = null; + CompiledStaticPredicate = null; + CompiledListPredicate = null; if (Operator == null) return; @@ -137,7 +135,7 @@ namespace Artemis.Core.Models.Profile.Conditions internal override void ApplyToEntity() { - DisplayConditionPredicateEntity.PredicateType = (int)PredicateType; + DisplayConditionPredicateEntity.PredicateType = (int) PredicateType; DisplayConditionPredicateEntity.LeftDataModelGuid = LeftDataModel?.PluginInfo?.Guid; DisplayConditionPredicateEntity.LeftPropertyPath = LeftPropertyPath; @@ -151,10 +149,18 @@ namespace Artemis.Core.Models.Profile.Conditions public override bool Evaluate() { - if (CompiledDynamicConditionLambda != null) - return CompiledDynamicConditionLambda(LeftDataModel, RightDataModel); - if (CompiledStaticConditionLambda != null) - return CompiledStaticConditionLambda(LeftDataModel); + 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; } @@ -194,7 +200,7 @@ namespace Artemis.Core.Models.Profile.Conditions // Use the left side type so JSON.NET has a better idea what to do var leftSideType = LeftDataModel.GetTypeAtPath(LeftPropertyPath); object rightSideValue; - + try { rightSideValue = JsonConvert.DeserializeObject(DisplayConditionPredicateEntity.RightStaticValue, leftSideType); @@ -286,26 +292,42 @@ namespace Artemis.Core.Models.Profile.Conditions if (LeftDataModel == null || RightDataModel == null || Operator == null) return; - var leftSideParameter = Expression.Parameter(typeof(DataModel), "leftDataModel"); - var leftSideAccessor = LeftPropertyPath.Split('.').Aggregate( - Expression.Convert(leftSideParameter, LeftDataModel.GetType()), // Cast to the appropriate type - Expression.Property - ); - var rightSideParameter = Expression.Parameter(typeof(DataModel), "rightDataModel"); - var rightSideAccessor = RightPropertyPath.Split('.').Aggregate( - Expression.Convert(rightSideParameter, LeftDataModel.GetType()), // Cast to the appropriate type - Expression.Property - ); + var isListExpression = LeftDataModel.GetListTypeAtPath(LeftPropertyPath) != null; + 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 dynamicConditionExpression = Operator.CreateExpression(leftSideAccessor, rightSideAccessor); + var conditionExpression = Operator.CreateExpression(leftSideAccessor, rightSideAccessor); - DynamicConditionLambda = Expression.Lambda>(dynamicConditionExpression, leftSideParameter, rightSideParameter); - CompiledDynamicConditionLambda = DynamicConditionLambda.Compile(); + if (isListExpression) + { + var lambda = Expression.Lambda>(conditionExpression, leftSideParameter); + CompiledListPredicate = lambda.Compile(); + } + else + { + var lambda = Expression.Lambda>(conditionExpression, leftSideParameter, rightSideParameter); + CompiledDynamicPredicate = lambda.Compile(); + } } private void CreateStaticExpression() @@ -313,11 +335,18 @@ namespace Artemis.Core.Models.Profile.Conditions if (LeftDataModel == null || Operator == null) return; - var leftSideParameter = Expression.Parameter(typeof(DataModel), "leftDataModel"); - var leftSideAccessor = LeftPropertyPath.Split('.').Aggregate( - Expression.Convert(leftSideParameter, LeftDataModel.GetType()), // Cast to the appropriate type - Expression.Property - ); + var isListExpression = LeftDataModel.GetListTypeAtPath(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); // 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) @@ -330,8 +359,42 @@ namespace Artemis.Core.Models.Profile.Conditions var conditionExpression = Operator.CreateExpression(leftSideAccessor, rightSideConstant); - StaticConditionLambda = Expression.Lambda>(conditionExpression, leftSideParameter); - CompiledStaticConditionLambda = StaticConditionLambda.Compile(); + if (isListExpression) + { + var lambda = Expression.Lambda>(conditionExpression, leftSideParameter); + CompiledListPredicate = lambda.Compile(); + } + else + { + var lambda = Expression.Lambda>(conditionExpression, leftSideParameter); + CompiledStaticPredicate = lambda.Compile(); + } + } + + private Expression CreateAccessor(DataModel dataModel, string path, string parameterName, out ParameterExpression parameter) + { + var listType = dataModel.GetListTypeAtPath(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 + ); + } + + private Expression CreateListAccessor(DataModel dataModel, string path, ParameterExpression listParameter) + { + var listType = dataModel.GetListTypeAtPath(path); + if (listType == null) + throw new ArtemisCoreException($"Cannot create a list accessor at path {path} because the path does not contain a list"); + + path = dataModel.GetListInnerPath(path); + return path.Split('.').Aggregate( + Expression.Convert(listParameter, listType), // Cast to the appropriate type + Expression.Property + ); } public override string ToString() diff --git a/src/Artemis.Core/Plugins/Abstract/DataModels/DataModel.cs b/src/Artemis.Core/Plugins/Abstract/DataModels/DataModel.cs index 69c22364a..5aaee3fee 100644 --- a/src/Artemis.Core/Plugins/Abstract/DataModels/DataModel.cs +++ b/src/Artemis.Core/Plugins/Abstract/DataModels/DataModel.cs @@ -1,7 +1,11 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Linq; using System.Reflection; +using System.Text; +using Artemis.Core.Exceptions; using Artemis.Core.Plugins.Abstract.DataModels.Attributes; using Artemis.Core.Plugins.Models; @@ -27,8 +31,14 @@ namespace Artemis.Core.Plugins.Abstract.DataModels var current = GetType(); foreach (var part in parts) { - var property = current?.GetProperty(part); - current = property?.PropertyType; + var property = current.GetProperty(part); + + // For lists, look into the list type instead of the list itself + if (property != null && typeof(IList).IsAssignableFrom(property.PropertyType)) + current = property.PropertyType.GetGenericArguments()[0]; + else + current = property?.PropertyType; + if (property == null) return false; } @@ -48,13 +58,63 @@ namespace Artemis.Core.Plugins.Abstract.DataModels foreach (var part in parts) { var property = current.GetProperty(part); - current = property.PropertyType; + + // For lists, look into the list type instead of the list itself + if (typeof(IList).IsAssignableFrom(property.PropertyType)) + current = property.PropertyType.GetGenericArguments()[0]; + else + current = property.PropertyType; + result = property.PropertyType; } return result; } + public Type GetListTypeAtPath(string path) + { + if (!ContainsPath(path)) + return null; + + var parts = path.Split('.'); + var current = GetType(); + + foreach (var part in parts) + { + var property = current.GetProperty(part); + + // For lists, look into the list type instead of the list itself + if (typeof(IList).IsAssignableFrom(property.PropertyType)) + return property.PropertyType.GetGenericArguments()[0]; + + current = property.PropertyType; + } + + return null; + } + + public string GetListInnerPath(string path) + { + if (GetListTypeAtPath(path) == null) + throw new ArtemisCoreException($"Cannot determine inner list path at {path} because it does not contain a list"); + + var parts = path.Split('.'); + var current = GetType(); + + for (var index = 0; index < parts.Length; index++) + { + var part = parts[index]; + var property = current.GetProperty(part); + + if (typeof(IList).IsAssignableFrom(property.PropertyType)) + return string.Join('.', parts.Skip(index + 1).ToList()); + + current = property.PropertyType; + } + + return null; + } + /// /// Returns a read-only list of all properties in this datamodel that are to be ignored /// diff --git a/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListPropertiesViewModel.cs b/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListPropertiesViewModel.cs index e41083e86..a766b91d7 100644 --- a/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListPropertiesViewModel.cs +++ b/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListPropertiesViewModel.cs @@ -34,6 +34,10 @@ namespace Artemis.UI.Shared.DataModelVisualization.Shared set => SetAndNotify(ref _displayValue, value); } + public override string PropertyPath => Parent?.PropertyPath; + + public override string DisplayPropertyPath => Parent?.DisplayPropertyPath; + public override void Update(IDataModelVisualizationService dataModelVisualizationService) { // Display value gets updated by parent, don't do anything if it is null diff --git a/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListViewModel.cs b/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListViewModel.cs index 4807135fb..0beaa714b 100644 --- a/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListViewModel.cs +++ b/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListViewModel.cs @@ -3,7 +3,6 @@ using System.Collections; using System.Reflection; using Artemis.Core.Extensions; using Artemis.Core.Plugins.Abstract.DataModels; -using Artemis.Core.Plugins.Abstract.DataModels.Attributes; using Artemis.UI.Shared.Services; using Stylet; @@ -26,12 +25,6 @@ namespace Artemis.UI.Shared.DataModelVisualization.Shared set => SetAndNotify(ref _list, value); } - public DataModelVisualizationViewModel ListTypePropertyViewModel - { - get => _listTypePropertyViewModel; - set => SetAndNotify(ref _listTypePropertyViewModel, value); - } - public BindableCollection ListChildren { get; set; } public string Count @@ -40,6 +33,30 @@ namespace Artemis.UI.Shared.DataModelVisualization.Shared set => SetAndNotify(ref _count, value); } + public DataModelPropertiesViewModel GetListTypeViewModel(IDataModelVisualizationService dataModelVisualizationService) + { + // Create a property VM describing the type of the list + var viewModel = CreateListChild(dataModelVisualizationService, List.GetType().GenericTypeArguments[0]); + + // Put an empty value into the list type property view model + if (viewModel is DataModelListPropertiesViewModel dataModelListClassViewModel) + { + dataModelListClassViewModel.DisplayValue = Activator.CreateInstance(dataModelListClassViewModel.ListType); + dataModelListClassViewModel.Update(dataModelVisualizationService); + return dataModelListClassViewModel; + } + + if (viewModel is DataModelListPropertyViewModel dataModelListPropertyViewModel) + { + dataModelListPropertyViewModel.DisplayValue = Activator.CreateInstance(dataModelListPropertyViewModel.ListType); + var wrapper = new DataModelPropertiesViewModel(null,null,null); + wrapper.Children.Add(dataModelListPropertyViewModel); + return wrapper; + } + + return null; + } + public override void Update(IDataModelVisualizationService dataModelVisualizationService) { if (Parent != null && !Parent.IsVisualizationExpanded) @@ -49,20 +66,6 @@ namespace Artemis.UI.Shared.DataModelVisualization.Shared if (List == null) return; - if (ListTypePropertyViewModel == null) - { - // Create a property VM describing the type of the list - ListTypePropertyViewModel = CreateListChild(dataModelVisualizationService, List.GetType().GenericTypeArguments[0]); - - // Put an empty value into the list type property view model - if (ListTypePropertyViewModel is DataModelListPropertiesViewModel dataModelListClassViewModel) - dataModelListClassViewModel.DisplayValue = Activator.CreateInstance(dataModelListClassViewModel.ListType); - else if (ListTypePropertyViewModel is DataModelListPropertyViewModel dataModelListPropertyViewModel) - dataModelListPropertyViewModel.DisplayValue = Activator.CreateInstance(dataModelListPropertyViewModel.ListType); - - ListTypePropertyViewModel.Update(dataModelVisualizationService); - } - var index = 0; foreach (var item in List) { diff --git a/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelVisualizationViewModel.cs b/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelVisualizationViewModel.cs index b31edb00a..3281a1e5c 100644 --- a/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelVisualizationViewModel.cs +++ b/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelVisualizationViewModel.cs @@ -1,12 +1,11 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Linq; using System.Reflection; +using System.Windows.Documents; using Artemis.Core.Extensions; using Artemis.Core.Models.Profile.Conditions; -using Artemis.Core.Plugins.Abstract; using Artemis.Core.Plugins.Abstract.DataModels; using Artemis.Core.Plugins.Abstract.DataModels.Attributes; using Artemis.UI.Shared.Exceptions; @@ -26,7 +25,6 @@ namespace Artemis.UI.Shared.DataModelVisualization.Shared private DataModelVisualizationViewModel _parent; private DataModelPropertyAttribute _propertyDescription; private PropertyInfo _propertyInfo; - private bool _isIgnored; internal DataModelVisualizationViewModel(DataModel dataModel, DataModelVisualizationViewModel parent, PropertyInfo propertyInfo) { @@ -91,7 +89,7 @@ namespace Artemis.UI.Shared.DataModelVisualization.Shared } } - public string PropertyPath + public virtual string PropertyPath { get { @@ -106,7 +104,7 @@ namespace Artemis.UI.Shared.DataModelVisualization.Shared } } - public string DisplayPropertyPath + public virtual string DisplayPropertyPath { get { @@ -176,7 +174,7 @@ namespace Artemis.UI.Shared.DataModelVisualization.Shared IsMatchingFilteredTypes = false; return; } - + if (looseMatch) IsMatchingFilteredTypes = filteredTypes.Any(t => t.IsCastableFrom(PropertyInfo.PropertyType)); else @@ -213,12 +211,14 @@ namespace Artemis.UI.Shared.DataModelVisualization.Shared if (IsRootViewModel) { - var child = Children.FirstOrDefault(c => c.DataModel.PluginInfo.Guid == dataModelGuid); + var child = Children.FirstOrDefault(c => c.DataModel != null && + c.DataModel.PluginInfo.Guid == dataModelGuid); return child?.GetChildByPath(dataModelGuid, propertyPath); } else { - var child = Children.FirstOrDefault(c => c.DataModel.PluginInfo.Guid == dataModelGuid && c.PropertyInfo?.Name == currentPart); + var child = Children.FirstOrDefault(c => c.DataModel != null && + c.DataModel.PluginInfo.Guid == dataModelGuid && c.PropertyInfo?.Name == currentPart); if (child == null) return null; diff --git a/src/Artemis.UI/ResourceDictionaries/DisplayConditions.xaml b/src/Artemis.UI/ResourceDictionaries/DisplayConditions.xaml index fb6938622..8aa41835a 100644 --- a/src/Artemis.UI/ResourceDictionaries/DisplayConditions.xaml +++ b/src/Artemis.UI/ResourceDictionaries/DisplayConditions.xaml @@ -2,7 +2,8 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:dataModel="clr-namespace:Artemis.UI.Shared.DataModelVisualization.Shared;assembly=Artemis.UI.Shared" xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" - xmlns:s="https://github.com/canton7/Stylet"> + xmlns:s="https://github.com/canton7/Stylet" + xmlns:converters="clr-namespace:Artemis.UI.Converters">