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