using System; using System.Collections; using System.Linq; using System.Reflection; using System.Runtime.Serialization; using Umbraco.Core.Models.EntityBase; namespace Umbraco.Core.Models { /// /// A Property contains a single piece of data /// [Serializable] [DataContract(IsReference = true)] public class Property : Entity { private PropertyType _propertyType; private Guid _version; private object _value; private readonly PropertyTags _tagSupport = new PropertyTags(); protected Property() { } public Property(PropertyType propertyType) { _propertyType = propertyType; } public Property(PropertyType propertyType, object value) { _propertyType = propertyType; Value = value; } public Property(int id, Guid version, PropertyType propertyType, object value) { Id = id; _propertyType = propertyType; _version = version; Value = value; } private static readonly Lazy Ps = new Lazy(); private class PropertySelectors { public readonly PropertyInfo ValueSelector = ExpressionHelper.GetPropertyInfo(x => x.Value); public readonly PropertyInfo VersionSelector = ExpressionHelper.GetPropertyInfo(x => x.Version); public readonly DelegateEqualityComparer PropertyValueComparer = new DelegateEqualityComparer( (o, o1) => { if (o == null && o1 == null) return true; //custom comparer for strings. if (o is string || o1 is string) { //if one is null and another is empty then they are the same if ((o as string).IsNullOrWhiteSpace() && (o1 as string).IsNullOrWhiteSpace()) { return true; } if (o == null || o1 == null) return false; return o.Equals(o1); } if (o == null || o1 == null) return false; //Custom comparer for enumerable if it is enumerable var enum1 = o as IEnumerable; var enum2 = o1 as IEnumerable; if (enum1 != null && enum2 != null) { return enum1.Cast().UnsortedSequenceEqual(enum2.Cast()); } return o.Equals(o1); }, o => o.GetHashCode()); } /// /// Returns the instance of the tag support, by default tags are not enabled /// internal PropertyTags TagSupport { get { return _tagSupport; } } /// /// Returns the Alias of the PropertyType, which this Property is based on /// [DataMember] public string Alias { get { return _propertyType.Alias; } } /// /// Returns the Id of the PropertyType, which this Property is based on /// [IgnoreDataMember] internal int PropertyTypeId { get { return _propertyType.Id; } } /// /// Returns the DatabaseType that the underlaying DataType is using to store its values /// /// /// Only used internally when saving the property value. /// [IgnoreDataMember] internal DataTypeDatabaseType DataTypeDatabaseType { get { return _propertyType.DataTypeDatabaseType; } } /// /// Returns the PropertyType, which this Property is based on /// [IgnoreDataMember] public PropertyType PropertyType { get { return _propertyType; } } /// /// Gets or Sets the version id for the Property /// /// /// The version will be the same for all Property objects in a collection on a Content /// object, so not sure how much this makes sense but adding it to align with: /// umbraco.interfaces.IProperty /// [DataMember] public Guid Version { get { return _version; } set { SetPropertyValueAndDetectChanges(value, ref _version, Ps.Value.VersionSelector); } } private static void ThrowTypeException(object value, Type expected, string alias) { throw new InvalidOperationException(string.Format("Value \"{0}\" of type \"{1}\" could not be converted" + " to type \"{2}\" which is expected by property type \"{3}\".", value, value.GetType(), expected, alias)); } /// /// Gets or Sets the value of the Property /// /// /// Setting the value will trigger a type validation. /// The type of the value has to be valid in order to be saved. /// [DataMember] public object Value { get { return _value; } set { var isOfExpectedType = _propertyType.IsPropertyTypeValid(value); if (isOfExpectedType == false) // isOfExpectedType is true if value is null - so if false, value is *not* null { // "garbage-in", accept what we can & convert // throw only if conversion is not possible var s = value.ToString(); switch (_propertyType.DataTypeDatabaseType) { case DataTypeDatabaseType.Nvarchar: case DataTypeDatabaseType.Ntext: value = s; break; case DataTypeDatabaseType.Integer: if (s.IsNullOrWhiteSpace()) value = null; // assume empty means null else { var convInt = value.TryConvertTo(); if (convInt == false) ThrowTypeException(value, typeof(int), _propertyType.Alias); value = convInt.Result; } break; case DataTypeDatabaseType.Decimal: if (s.IsNullOrWhiteSpace()) value = null; // assume empty means null else { var convDecimal = value.TryConvertTo(); if (convDecimal == false) ThrowTypeException(value, typeof (decimal), _propertyType.Alias); // need to normalize the value (change the scaling factor and remove trailing zeroes) // because the underlying database is going to mess with the scaling factor anyways. value = convDecimal.Result.Normalize(); } break; case DataTypeDatabaseType.Date: if (s.IsNullOrWhiteSpace()) value = null; // assume empty means null else { var convDateTime = value.TryConvertTo(); if (convDateTime == false) ThrowTypeException(value, typeof (DateTime), _propertyType.Alias); value = convDateTime.Result; } break; } } SetPropertyValueAndDetectChanges(value, ref _value, Ps.Value.ValueSelector, Ps.Value.PropertyValueComparer); } } /// /// Boolean indicating whether the current value is valid /// /// /// A valid value implies that it is ready for publishing. /// Invalid property values can be saved, but not published. /// /// True is property value is valid, otherwise false public bool IsValid() { return IsValid(Value); } /// /// Boolean indicating whether the passed in value is valid /// /// /// True is property value is valid, otherwise false public bool IsValid(object value) { return _propertyType.IsPropertyValueValid(value); } public override object DeepClone() { 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(); //this shouldn't really be needed since we're not tracking clone.ResetDirtyProperties(false); //re-enable tracking clone.EnableChangeTracking(); return clone; } } }