Files
Umbraco-CMS/src/Umbraco.Core/Models/Property.cs

426 lines
17 KiB
C#
Raw Normal View History

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using Umbraco.Core.Collections;
using Umbraco.Core.Models.Entities;
namespace Umbraco.Core.Models
{
/// <summary>
/// Represents a property.
/// </summary>
[Serializable]
[DataContract(IsReference = true)]
public class Property : EntityBase
{
private List<PropertyValue> _values = new List<PropertyValue>();
private PropertyValue _pvalue;
2018-04-21 09:57:28 +02:00
private Dictionary<CompositeStringStringKey, PropertyValue> _vvalues;
private static readonly Lazy<PropertySelectors> Ps = new Lazy<PropertySelectors>();
protected Property()
2017-08-24 21:24:14 +02:00
{ }
public Property(PropertyType propertyType)
{
PropertyType = propertyType;
}
public Property(int id, PropertyType propertyType)
{
Id = id;
PropertyType = propertyType;
}
public class PropertyValue
{
2018-04-21 09:57:28 +02:00
private string _culture;
private string _segment;
2018-04-21 09:57:28 +02:00
public string Culture
{
get => _culture;
internal set => _culture = value?.ToLowerInvariant();
}
public string Segment
{
get => _segment;
internal set => _segment = value?.ToLowerInvariant();
}
public object EditedValue { get; internal set; }
2017-11-15 08:53:20 +01:00
public object PublishedValue { get; internal set; }
2017-11-10 11:27:12 +01:00
public PropertyValue Clone()
2018-04-21 09:57:28 +02:00
=> new PropertyValue { _culture = _culture, _segment = _segment, PublishedValue = PublishedValue, EditedValue = EditedValue };
2017-12-15 11:58:11 +01:00
}
// ReSharper disable once ClassNeverInstantiated.Local
private class PropertySelectors
{
public readonly PropertyInfo ValuesSelector = ExpressionHelper.GetPropertyInfo<Property, object>(x => x.Values);
2016-06-22 13:10:37 +02:00
2017-08-24 21:24:14 +02:00
public readonly DelegateEqualityComparer<object> PropertyValueComparer = new DelegateEqualityComparer<object>(
(o, o1) =>
2016-06-22 13:10:37 +02:00
{
2017-08-24 21:24:14 +02:00
if (o == null && o1 == null) return true;
2017-11-10 11:27:12 +01:00
// custom comparer for strings.
// if one is null and another is empty then they are the same
2017-08-24 21:24:14 +02:00
if (o is string || o1 is string)
2017-11-10 11:27:12 +01:00
return ((o as string).IsNullOrWhiteSpace() && (o1 as string).IsNullOrWhiteSpace()) || (o != null && o1 != null && o.Equals(o1));
2016-06-22 13:10:37 +02:00
2017-08-24 21:24:14 +02:00
if (o == null || o1 == null) return false;
2016-06-22 13:10:37 +02:00
2017-11-10 11:27:12 +01:00
// custom comparer for enumerable
// ReSharper disable once MergeCastWithTypeCheck
if (o is IEnumerable && o1 is IEnumerable)
return ((IEnumerable) o).Cast<object>().UnsortedSequenceEqual(((IEnumerable) o1).Cast<object>());
2017-08-24 21:24:14 +02:00
return o.Equals(o1);
}, o => o.GetHashCode());
}
2017-07-20 11:21:28 +02:00
/// <summary>
/// Returns the PropertyType, which this Property is based on
/// </summary>
[IgnoreDataMember]
public PropertyType PropertyType { get; private set; }
/// <summary>
/// Gets the list of values.
/// </summary>
[DataMember]
public IReadOnlyCollection<PropertyValue> Values
{
get => _values;
set
{
// make sure we filter out invalid variations
// make sure we leave _vvalues null if possible
2018-04-21 09:57:28 +02:00
_values = value.Where(x => PropertyType.ValidateVariation(x.Culture, x.Segment, false)).ToList();
_pvalue = _values.FirstOrDefault(x => x.Culture == null && x.Segment == null);
_vvalues = _values.Count > (_pvalue == null ? 0 : 1)
2018-04-21 09:57:28 +02:00
? _values.Where(x => x != _pvalue).ToDictionary(x => new CompositeStringStringKey(x.Culture, x.Segment), x => x)
: null;
}
}
/// <summary>
/// Returns the Alias of the PropertyType, which this Property is based on
/// </summary>
[DataMember]
public string Alias => PropertyType.Alias;
/// <summary>
/// Returns the Id of the PropertyType, which this Property is based on
/// </summary>
[IgnoreDataMember]
internal int PropertyTypeId => PropertyType.Id;
/// <summary>
/// Returns the DatabaseType that the underlaying DataType is using to store its values
/// </summary>
/// <remarks>
/// Only used internally when saving the property value.
/// </remarks>
[IgnoreDataMember]
2018-01-24 11:44:44 +01:00
internal ValueStorageType ValueStorageType => PropertyType.ValueStorageType;
/// <summary>
2017-11-30 13:56:29 +01:00
/// Gets the value.
/// </summary>
2018-04-21 09:57:28 +02:00
public object GetValue(string culture = null, string segment = null, bool published = false)
{
2018-04-21 09:57:28 +02:00
if (!PropertyType.ValidateVariation(culture, segment, false)) return null;
if (culture == null && segment == null) return GetPropertyValue(_pvalue, published);
if (_vvalues == null) return null;
2018-04-21 09:57:28 +02:00
return _vvalues.TryGetValue(new CompositeStringStringKey(culture, segment), out var pvalue)
? GetPropertyValue(pvalue, published)
: null;
2017-11-15 08:53:20 +01:00
}
private object GetPropertyValue(PropertyValue pvalue, bool published)
{
2017-12-01 19:29:54 +01:00
if (pvalue == null) return null;
return PropertyType.IsPublishing
? (published ? pvalue.PublishedValue : pvalue.EditedValue)
: pvalue.EditedValue;
}
2017-11-30 13:56:29 +01:00
// internal - must be invoked by the content item
2017-12-02 13:13:24 +01:00
// does *not* validate the value - content item must validate first
2017-11-30 13:56:29 +01:00
internal void PublishAllValues()
{
// if invariant-neutral is supported, publish invariant-neutral
if (PropertyType.ValidateVariation(null, null, false))
2017-11-30 13:56:29 +01:00
PublishPropertyValue(_pvalue);
// publish everything not invariant-neutral that is supported
if (_vvalues != null)
{
var pvalues = _vvalues
2018-04-21 09:57:28 +02:00
.Where(x => PropertyType.ValidateVariation(x.Value.Culture, x.Value.Segment, false))
2017-11-30 13:56:29 +01:00
.Select(x => x.Value);
foreach (var pvalue in pvalues)
PublishPropertyValue(pvalue);
}
}
// internal - must be invoked by the content item
2017-12-02 13:13:24 +01:00
// does *not* validate the value - content item must validate first
2018-04-21 09:57:28 +02:00
internal void PublishValue(string culture = null, string segment = null)
2017-11-10 11:27:12 +01:00
{
2018-04-21 09:57:28 +02:00
PropertyType.ValidateVariation(culture, segment, true);
2017-11-30 13:56:29 +01:00
2018-04-21 09:57:28 +02:00
(var pvalue, _) = GetPValue(culture, segment, false);
2017-11-10 11:27:12 +01:00
if (pvalue == null) return;
PublishPropertyValue(pvalue);
}
2017-11-30 13:56:29 +01:00
// internal - must be invoked by the content item
2017-12-02 13:13:24 +01:00
// does *not* validate the value - content item must validate first
2018-04-21 09:57:28 +02:00
internal void PublishCultureValues(string culture = null)
2017-11-10 11:27:12 +01:00
{
2017-11-30 13:56:29 +01:00
// if invariant and invariant-neutral is supported, publish invariant-neutral
2018-04-21 09:57:28 +02:00
if (culture == null && PropertyType.ValidateVariation(null, null, false))
2017-11-30 13:56:29 +01:00
PublishPropertyValue(_pvalue);
// publish everything not invariant-neutral that matches the culture and is supported
if (_vvalues != null)
{
var pvalues = _vvalues
2018-04-21 09:57:28 +02:00
.Where(x => x.Value.Culture.InvariantEquals(culture))
.Where(x => PropertyType.ValidateVariation(culture, x.Value.Segment, false))
2017-11-30 13:56:29 +01:00
.Select(x => x.Value);
foreach (var pvalue in pvalues)
PublishPropertyValue(pvalue);
}
2017-11-10 11:27:12 +01:00
}
2017-11-30 13:56:29 +01:00
// internal - must be invoked by the content item
internal void ClearPublishedAllValues()
2017-11-10 11:27:12 +01:00
{
if (PropertyType.ValidateVariation(null, null, false))
2017-11-30 13:56:29 +01:00
ClearPublishedPropertyValue(_pvalue);
if (_vvalues != null)
{
var pvalues = _vvalues
2018-04-21 09:57:28 +02:00
.Where(x => PropertyType.ValidateVariation(x.Value.Culture, x.Value.Segment, false))
2017-11-30 13:56:29 +01:00
.Select(x => x.Value);
foreach (var pvalue in pvalues)
ClearPublishedPropertyValue(pvalue);
}
2017-11-10 11:27:12 +01:00
}
2017-11-30 13:56:29 +01:00
// internal - must be invoked by the content item
2018-04-21 09:57:28 +02:00
internal void ClearPublishedValue(string culture = null, string segment = null)
2016-06-27 15:21:21 +02:00
{
2018-04-21 09:57:28 +02:00
PropertyType.ValidateVariation(culture, segment, true);
(var pvalue, _) = GetPValue(culture, segment, false);
2017-11-30 13:56:29 +01:00
if (pvalue == null) return;
ClearPublishedPropertyValue(pvalue);
2016-06-27 15:21:21 +02:00
}
2017-11-30 13:56:29 +01:00
// internal - must be invoked by the content item
2018-04-21 09:57:28 +02:00
internal void ClearPublishedCultureValues(string culture = null)
{
2018-04-21 09:57:28 +02:00
if (culture == null && PropertyType.ValidateVariation(null, null, false))
2017-11-30 13:56:29 +01:00
ClearPublishedPropertyValue(_pvalue);
if (_vvalues != null)
{
var pvalues = _vvalues
2018-04-21 09:57:28 +02:00
.Where(x => x.Value.Culture.InvariantEquals(culture))
.Where(x => PropertyType.ValidateVariation(culture, x.Value.Segment, false))
2017-11-30 13:56:29 +01:00
.Select(x => x.Value);
foreach (var pvalue in pvalues)
ClearPublishedPropertyValue(pvalue);
}
}
2016-07-05 15:11:10 +02:00
2017-11-30 13:56:29 +01:00
private void PublishPropertyValue(PropertyValue pvalue)
{
2017-12-01 19:29:54 +01:00
if (pvalue == null) return;
if (!PropertyType.IsPublishing)
2017-11-30 13:56:29 +01:00
throw new NotSupportedException("Property type does not support publishing.");
var origValue = pvalue.PublishedValue;
2018-02-09 14:34:28 +01:00
pvalue.PublishedValue = PropertyType.ConvertAssignedValue(pvalue.EditedValue);
2017-11-30 13:56:29 +01:00
DetectChanges(pvalue.EditedValue, origValue, Ps.Value.ValuesSelector, Ps.Value.PropertyValueComparer, false);
}
private void ClearPublishedPropertyValue(PropertyValue pvalue)
{
2017-12-01 19:29:54 +01:00
if (pvalue == null) return;
if (!PropertyType.IsPublishing)
2017-11-30 13:56:29 +01:00
throw new NotSupportedException("Property type does not support publishing.");
var origValue = pvalue.PublishedValue;
2018-02-09 14:34:28 +01:00
pvalue.PublishedValue = PropertyType.ConvertAssignedValue(null);
2017-11-30 13:56:29 +01:00
DetectChanges(pvalue.EditedValue, origValue, Ps.Value.ValuesSelector, Ps.Value.PropertyValueComparer, false);
}
/// <summary>
2017-11-30 13:56:29 +01:00
/// Sets a value.
/// </summary>
2018-04-21 09:57:28 +02:00
public void SetValue(object value, string culture = null, string segment = null)
{
if (PropertyType.Variations == ContentVariation.InvariantNeutral)
{
culture = null;
}
2018-04-21 09:57:28 +02:00
PropertyType.ValidateVariation(culture, segment, true);
(var pvalue, var change) = GetPValue(culture, segment, true);
2017-11-15 08:53:20 +01:00
var origValue = pvalue.EditedValue;
2018-02-09 14:34:28 +01:00
var setValue = PropertyType.ConvertAssignedValue(value);
2017-11-15 08:53:20 +01:00
pvalue.EditedValue = setValue;
2017-11-15 08:53:20 +01:00
DetectChanges(setValue, origValue, Ps.Value.ValuesSelector, Ps.Value.PropertyValueComparer, change);
}
2017-11-30 13:56:29 +01:00
// bypasses all changes detection and is the *only* way to set the published value
2018-04-21 09:57:28 +02:00
internal void FactorySetValue(string culture, string segment, bool published, object value)
2017-11-15 08:53:20 +01:00
{
2018-04-21 09:57:28 +02:00
(var pvalue, _) = GetPValue(culture, segment, true);
2017-11-10 11:27:12 +01:00
if (published && PropertyType.IsPublishing)
2017-11-15 08:53:20 +01:00
pvalue.PublishedValue = value;
else
pvalue.EditedValue = value;
2017-11-10 11:27:12 +01:00
}
private (PropertyValue, bool) GetPValue(bool create)
2017-11-10 11:27:12 +01:00
{
var change = false;
if (_pvalue == null)
{
if (!create) return (null, false);
_pvalue = new PropertyValue();
_values.Add(_pvalue);
change = true;
}
return (_pvalue, change);
}
2018-04-21 09:57:28 +02:00
private (PropertyValue, bool) GetPValue(string culture, string segment, bool create)
2017-11-10 11:27:12 +01:00
{
2018-04-21 09:57:28 +02:00
if (culture == null && segment == null)
return GetPValue(create);
2017-11-10 11:27:12 +01:00
var change = false;
if (_vvalues == null)
{
2017-11-10 11:27:12 +01:00
if (!create) return (null, false);
2018-04-21 09:57:28 +02:00
_vvalues = new Dictionary<CompositeStringStringKey, PropertyValue>();
change = true;
}
2018-04-21 09:57:28 +02:00
var k = new CompositeStringStringKey(culture, segment);
2017-12-15 11:58:11 +01:00
if (!_vvalues.TryGetValue(k, out var pvalue))
{
2017-11-10 11:27:12 +01:00
if (!create) return (null, false);
2017-12-15 11:58:11 +01:00
pvalue = _vvalues[k] = new PropertyValue();
2018-04-21 09:57:28 +02:00
pvalue.Culture = culture;
pvalue.Segment = segment;
_values.Add(pvalue);
change = true;
}
2017-11-10 11:27:12 +01:00
return (pvalue, change);
}
/// <summary>
2017-11-30 13:56:29 +01:00
/// Gets a value indicating whether everything is valid.
/// </summary>
2017-11-30 13:56:29 +01:00
/// <returns></returns>
public bool IsAllValid()
{
2017-12-02 13:13:24 +01:00
// invariant-neutral is supported, validate invariant-neutral
// includes mandatory validation
if (PropertyType.ValidateVariation(null, null, false) && !IsValidValue(_pvalue)) return false;
2017-11-30 13:56:29 +01:00
2017-12-02 13:13:24 +01:00
// either invariant-neutral is not supported, or it is valid
// for anything else, validate the existing values (including mandatory),
// but we cannot validate mandatory globally (we don't know the possible cultures and segments)
2017-11-30 13:56:29 +01:00
2017-12-02 13:13:24 +01:00
if (_vvalues == null) return true;
2017-11-30 13:56:29 +01:00
var pvalues = _vvalues
2018-04-21 09:57:28 +02:00
.Where(x => PropertyType.ValidateVariation(x.Value.Culture, x.Value.Segment, false))
2017-11-30 13:56:29 +01:00
.Select(x => x.Value)
.ToArray();
2017-12-02 13:13:24 +01:00
return pvalues.Length == 0 || pvalues.All(x => IsValidValue(x.EditedValue));
}
/// <summary>
2017-11-30 13:56:29 +01:00
/// Gets a value indicating whether the culture/any values are valid.
2017-11-23 16:30:04 +01:00
/// </summary>
/// <remarks>An invalid value can be saved, but only valid values can be published.</remarks>
2018-04-21 09:57:28 +02:00
public bool IsCultureValid(string culture)
2017-11-23 16:30:04 +01:00
{
2017-12-02 13:13:24 +01:00
// culture-neutral is supported, validate culture-neutral
// includes mandatory validation
2018-04-21 09:57:28 +02:00
if (PropertyType.ValidateVariation(culture, null, false) && !IsValidValue(GetValue(culture)))
2017-12-02 13:13:24 +01:00
return false;
2017-11-30 13:56:29 +01:00
2017-12-02 13:13:24 +01:00
// either culture-neutral is not supported, or it is valid
// for anything non-neutral, validate the existing values (including mandatory),
// but we cannot validate mandatory globally (we don't know the possible segments)
2017-11-30 13:56:29 +01:00
2017-12-02 13:13:24 +01:00
if (_vvalues == null) return true;
2017-11-30 13:56:29 +01:00
var pvalues = _vvalues
2018-04-21 09:57:28 +02:00
.Where(x => x.Value.Culture.InvariantEquals(culture))
.Where(x => PropertyType.ValidateVariation(culture, x.Value.Segment, false))
2017-11-30 13:56:29 +01:00
.Select(x => x.Value)
.ToArray();
2017-12-02 13:13:24 +01:00
return pvalues.Length == 0 || pvalues.All(x => IsValidValue(x.EditedValue));
2017-11-23 16:30:04 +01:00
}
/// <summary>
2017-11-30 13:56:29 +01:00
/// Gets a value indicating whether the value is valid.
/// </summary>
/// <remarks>An invalid value can be saved, but only valid values can be published.</remarks>
2018-04-21 09:57:28 +02:00
public bool IsValid(string culture = null, string segment = null)
{
2017-12-02 13:13:24 +01:00
// single value -> validates mandatory
2018-04-21 09:57:28 +02:00
return IsValidValue(GetValue(culture, segment));
}
/// <summary>
/// Boolean indicating whether the passed in value is valid
/// </summary>
/// <param name="value"></param>
/// <returns>True is property value is valid, otherwise false</returns>
private bool IsValidValue(object value)
{
2018-02-09 14:34:28 +01:00
return PropertyType.IsPropertyValueValid(value);
}
2014-02-20 22:34:54 +11:00
public override object DeepClone()
2014-02-20 22:34:54 +11:00
{
var clone = (Property) base.DeepClone();
//turn off change tracking
clone.DisableChangeTracking();
//need to manually assign since this is a readonly property
clone.PropertyType = (PropertyType) PropertyType.DeepClone();
//re-enable tracking
clone.ResetDirtyProperties(false); // not needed really, since we're not tracking
clone.EnableChangeTracking();
2017-07-20 11:21:28 +02:00
2014-02-20 22:34:54 +11:00
return clone;
}
}
2017-07-20 11:21:28 +02:00
}