diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs index 49f679d1f4..e019f9a5d1 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs @@ -194,17 +194,19 @@ namespace Umbraco.Core.Models.PublishedContent } /// - /// Determines whether a source value is an actual value, or not a value. + /// Determines whether a value is an actual value, or not a value. /// /// Used by property.HasValue and, for instance, in fallback scenarios. - public bool IsValue(object value) + public bool? IsValue(object value, PropertyValueLevel level) { - // if we have a converter, use the converter, - // otherwise use the old magic null & string comparisons + if (!_initialized) Initialize(); - return _converter == null - ? value != null && (!(value is string) || string.IsNullOrWhiteSpace((string) value) == false) - : _converter.IsValue(value); + // if we have a converter, use the converter + if (_converter != null) + return _converter.IsValue(value, level); + + // otherwise use the old magic null & string comparisons + return value != null && (!(value is string) || string.IsNullOrWhiteSpace((string) value) == false); } /// diff --git a/src/Umbraco.Core/PropertyEditors/IPropertyValueConverter.cs b/src/Umbraco.Core/PropertyEditors/IPropertyValueConverter.cs index 53851f8653..258febe813 100644 --- a/src/Umbraco.Core/PropertyEditors/IPropertyValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/IPropertyValueConverter.cs @@ -18,9 +18,16 @@ namespace Umbraco.Core.PropertyEditors bool IsConverter(PublishedPropertyType propertyType); /// - /// Determines whether a source value is an actual value, or not a value. + /// Determines whether a value is an actual value, or not a value. /// - bool IsValue(object value); + /// + /// Called for Source, Inter and Object levels, until one does not return null. + /// Can return true (is a value), false (is not a value), or null to indicate that it + /// cannot be determined at the specified level. For instance, if source is a string that + /// could contain JSON, the decision could be made on the intermediate value. Or, if it is + /// a picker, it could be made on the object value (the actual picked object). + /// + bool? IsValue(object value, PropertyValueLevel level); /// /// Gets the type of values returned by the converter. diff --git a/src/Umbraco.Core/PropertyEditors/PropertyValueConverterBase.cs b/src/Umbraco.Core/PropertyEditors/PropertyValueConverterBase.cs index 9a82446edd..4c20016318 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyValueConverterBase.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyValueConverterBase.cs @@ -11,8 +11,25 @@ namespace Umbraco.Core.PropertyEditors public virtual bool IsConverter(PublishedPropertyType propertyType) => false; - public bool IsValue(object value) - => value != null && (!(value is string) || string.IsNullOrWhiteSpace((string) value) == false); + public virtual bool? IsValue(object value, PropertyValueLevel level) + { + switch (level) + { + case PropertyValueLevel.Source: + return value != null && (!(value is string) || string.IsNullOrWhiteSpace((string) value) == false); + default: + throw new NotSupportedException($"Invalid level: {level}."); + } + } + + public virtual bool HasValue(IPublishedProperty property, string culture, string segment) + { + // the default implementation uses the old magic null & string comparisons, + // other implementations may be more clever, and/or test the final converted object values + // fixme - cannot access the intermediate value here? + var value = property.GetSourceValue(culture, segment); + return value != null && (!(value is string) || string.IsNullOrWhiteSpace((string) value) == false); + } public virtual Type GetPropertyValueType(PublishedPropertyType propertyType) => typeof (object); diff --git a/src/Umbraco.Core/PropertyEditors/PropertyValueLevel.cs b/src/Umbraco.Core/PropertyEditors/PropertyValueLevel.cs new file mode 100644 index 0000000000..956ce03b30 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/PropertyValueLevel.cs @@ -0,0 +1,23 @@ +namespace Umbraco.Core.PropertyEditors +{ + /// + /// Indicates the level of a value. + /// + public enum PropertyValueLevel + { + /// + /// The source value, i.e. what is in the database. + /// + Source, + + /// + /// The conversion intermediate value. + /// + Inter, + + /// + /// The converted value. + /// + Object + } +} diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 1689be9b85..68b7142789 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -402,6 +402,7 @@ + diff --git a/src/Umbraco.Tests/Published/ConvertersTests.cs b/src/Umbraco.Tests/Published/ConvertersTests.cs index 39cb37f160..edc4face17 100644 --- a/src/Umbraco.Tests/Published/ConvertersTests.cs +++ b/src/Umbraco.Tests/Published/ConvertersTests.cs @@ -43,12 +43,30 @@ namespace Umbraco.Tests.Published var element1 = new PublishedElement(elementType1, Guid.NewGuid(), new Dictionary { { "prop1", "1234" } }, false); Assert.AreEqual(1234, element1.Value("prop1")); + + // 'null' would be considered a 'missing' value by the default, magic logic + var e = new PublishedElement(elementType1, Guid.NewGuid(), new Dictionary { { "prop1", null } }, false); + Assert.IsFalse(e.HasValue("prop1")); + + // '0' would not - it's a valid integer - but the converter knows better + e = new PublishedElement(elementType1, Guid.NewGuid(), new Dictionary { { "prop1", "0" } }, false); + Assert.IsFalse(e.HasValue("prop1")); } private class SimpleConverter1 : IPropertyValueConverter { - public bool IsValue(object value) - => value != null && (!(value is string) || string.IsNullOrWhiteSpace((string) value) == false); + public bool? IsValue(object value, PropertyValueLevel level) + { + switch (level) + { + case PropertyValueLevel.Source: + return null; + case PropertyValueLevel.Inter: + return value is int ivalue && ivalue != 0; + default: + throw new NotSupportedException($"Invalid level: {level}."); + } + } public bool IsConverter(PublishedPropertyType propertyType) => propertyType.EditorAlias.InvariantEquals("Umbraco.Void"); @@ -120,7 +138,7 @@ namespace Umbraco.Tests.Published _cacheLevel = cacheLevel; } - public bool IsValue(object value) + public bool? IsValue(object value, PropertyValueLevel level) => value != null && (!(value is string) || string.IsNullOrWhiteSpace((string) value) == false); public bool IsConverter(PublishedPropertyType propertyType) diff --git a/src/Umbraco.Tests/Published/PropertyCacheLevelTests.cs b/src/Umbraco.Tests/Published/PropertyCacheLevelTests.cs index 617429d205..33a595626e 100644 --- a/src/Umbraco.Tests/Published/PropertyCacheLevelTests.cs +++ b/src/Umbraco.Tests/Published/PropertyCacheLevelTests.cs @@ -210,7 +210,7 @@ namespace Umbraco.Tests.Published public int SourceConverts { get; private set; } public int InterConverts { get; private set; } - public bool IsValue(object value) + public bool? IsValue(object value, PropertyValueLevel level) => value != null && (!(value is string) || string.IsNullOrWhiteSpace((string) value) == false); public bool IsConverter(PublishedPropertyType propertyType) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/Property.cs b/src/Umbraco.Web/PublishedCache/NuCache/Property.cs index 73a159cb19..8dd3bb8dc7 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/Property.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/Property.cs @@ -87,10 +87,34 @@ namespace Umbraco.Web.PublishedCache.NuCache _variations = origin._variations; } + // determines whether a property has value public override bool HasValue(string culture = null, string segment = null) { ContextualizeVariation(ref culture, ref segment); - return PropertyType.IsValue(GetSourceValue(culture, segment)); + + var value = GetSourceValue(culture, segment); + var hasValue = PropertyType.IsValue(value, PropertyValueLevel.Source); + if (hasValue.HasValue) return hasValue.Value; + + lock (_locko) + { + value = GetInterValue(culture, segment); + hasValue = PropertyType.IsValue(value, PropertyValueLevel.Inter); + if (hasValue.HasValue) return hasValue.Value; + + var cacheValues = GetCacheValues(PropertyType.CacheLevel).For(culture, segment); + + // initial reference cache level always is .Content + const PropertyCacheLevel initialCacheLevel = PropertyCacheLevel.Element; + + if (!cacheValues.ObjectInitialized) + { + cacheValues.ObjectValue = PropertyType.ConvertInterToObject(_content, initialCacheLevel, value, _isPreviewing); + cacheValues.ObjectInitialized = true; + } + value = cacheValues.ObjectValue; + return PropertyType.IsValue(value, PropertyValueLevel.Object) ?? false; + } } // used to cache the CacheValues of this property diff --git a/src/Umbraco.Web/PublishedCache/PublishedElementPropertyBase.cs b/src/Umbraco.Web/PublishedCache/PublishedElementPropertyBase.cs index d8db937ca8..cff1e40b69 100644 --- a/src/Umbraco.Web/PublishedCache/PublishedElementPropertyBase.cs +++ b/src/Umbraco.Web/PublishedCache/PublishedElementPropertyBase.cs @@ -37,7 +37,28 @@ namespace Umbraco.Web.PublishedCache } public override bool HasValue(string culture = null, string segment = null) - => _sourceValue != null && (!(_sourceValue is string s) || !string.IsNullOrWhiteSpace(s)); + { + var hasValue = PropertyType.IsValue(_sourceValue, PropertyValueLevel.Source); + if (hasValue.HasValue) return hasValue.Value; + + GetCacheLevels(out var cacheLevel, out var referenceCacheLevel); + + lock (_locko) + { + var value = GetInterValue(); + hasValue = PropertyType.IsValue(value, PropertyValueLevel.Inter); + if (hasValue.HasValue) return hasValue.Value; + + var cacheValues = GetCacheValues(cacheLevel); + if (!cacheValues.ObjectInitialized) + { + cacheValues.ObjectValue = PropertyType.ConvertInterToObject(Element, referenceCacheLevel, value, IsPreviewing); + cacheValues.ObjectInitialized = true; + } + value = cacheValues.ObjectValue; + return PropertyType.IsValue(value, PropertyValueLevel.Object) ?? false; + } + } // used to cache the CacheValues of this property // ReSharper disable InconsistentlySynchronizedField