using System; using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Runtime.Serialization; using System.Web; using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Services; namespace Umbraco.Core.Models { /// /// Represents an abstract class for base Content properties and methods /// [Serializable] [DataContract(IsReference = true)] [DebuggerDisplay("Id: {Id}, Name: {Name}, ContentType: {ContentTypeBase.Alias}")] public abstract class ContentBase : Entity, IContentBase { private static readonly Lazy Ps = new Lazy(); private int _contentTypeId; protected IContentTypeComposition ContentTypeBase; private Lazy _parentId; private int _level; private string _path; private int _sortOrder; private bool _trashed; private int _creatorId; private int _writerId; // fixme need to deal with localized names, how? // for the time being, this is the node text = unique private string _name; private PropertyCollection _properties; private readonly List _invalidProperties = new List(); [EditorBrowsable(EditorBrowsableState.Never)] IDictionary IUmbracoEntity.AdditionalData => _lazyAdditionalData.Value; private readonly Lazy> _lazyAdditionalData = new Lazy>(); /// /// Initializes a new instance of the class. /// protected ContentBase(string name, int parentId, IContentTypeComposition contentType, PropertyCollection properties) : this(name, contentType, properties) { if (parentId == 0) throw new ArgumentOutOfRangeException(nameof(parentId)); _parentId = new Lazy(() => parentId); } /// /// Initializes a new instance of the class. /// protected ContentBase(string name, IContentBase parent, IContentTypeComposition contentType, PropertyCollection properties) : this(name, contentType, properties) { if (parent == null) throw new ArgumentNullException(nameof(parent)); _parentId = new Lazy(() => parent.Id); } private ContentBase(string name, IContentTypeComposition contentType, PropertyCollection properties) { ContentTypeBase = contentType ?? throw new ArgumentNullException(nameof(contentType)); // initially, all new instances have Id = 0; // no identity Version = Guid.NewGuid(); // a new unique version id _name = name; _contentTypeId = contentType.Id; _properties = properties ?? throw new ArgumentNullException(nameof(properties)); _properties.EnsurePropertyTypes(PropertyTypes); } // ReSharper disable once ClassNeverInstantiated.Local private class PropertySelectors { public readonly PropertyInfo ParentIdSelector = ExpressionHelper.GetPropertyInfo(x => x.ParentId); public readonly PropertyInfo LevelSelector = ExpressionHelper.GetPropertyInfo(x => x.Level); public readonly PropertyInfo PathSelector = ExpressionHelper.GetPropertyInfo(x => x.Path); public readonly PropertyInfo SortOrderSelector = ExpressionHelper.GetPropertyInfo(x => x.SortOrder); public readonly PropertyInfo TrashedSelector = ExpressionHelper.GetPropertyInfo(x => x.Trashed); public readonly PropertyInfo NameSelector = ExpressionHelper.GetPropertyInfo(x => x.Name); public readonly PropertyInfo CreatorIdSelector = ExpressionHelper.GetPropertyInfo(x => x.CreatorId); public readonly PropertyInfo DefaultContentTypeIdSelector = ExpressionHelper.GetPropertyInfo(x => x.ContentTypeId); public readonly PropertyInfo PropertyCollectionSelector = ExpressionHelper.GetPropertyInfo(x => x.Properties); public readonly PropertyInfo WriterSelector = ExpressionHelper.GetPropertyInfo(x => x.WriterId); } protected void PropertiesChanged(object sender, NotifyCollectionChangedEventArgs e) { OnPropertyChanged(Ps.Value.PropertyCollectionSelector); } /// /// Gets or sets the identifier of the parent entity. /// [DataMember] public virtual int ParentId { get { var val = _parentId.Value; if (val == 0) { throw new InvalidOperationException("The ParentId cannot have a value of 0. Perhaps the parent object used to instantiate this object has not been persisted to the data store."); } return val; } set { _parentId = new Lazy(() => value); OnPropertyChanged(Ps.Value.ParentIdSelector); } } /// /// Sets the identifier of the parent entity. /// /// Id of the Parent protected internal void SetLazyParentId(Lazy parentId) { _parentId = parentId; OnPropertyChanged(Ps.Value.ParentIdSelector); } /// /// Gets or sets the level of the entity. /// [DataMember] public virtual int Level { get => _level; set => SetPropertyValueAndDetectChanges(value, ref _level, Ps.Value.LevelSelector); } /// /// Gets or sets the path of the entity. /// [DataMember] public virtual string Path //Setting this value should be handled by the class not the user { get => _path; set => SetPropertyValueAndDetectChanges(value, ref _path, Ps.Value.PathSelector); } /// /// Gets or sets the sort order of the entity. /// [DataMember] public virtual int SortOrder { get => _sortOrder; set => SetPropertyValueAndDetectChanges(value, ref _sortOrder, Ps.Value.SortOrderSelector); } /// /// Gets or sets a value indicating whether the entity is trashed. /// /// A trashed entity is unpublished and in the recycle bin. [DataMember] public virtual bool Trashed // fixme setting this value should be handled by the class not the user { get => _trashed; internal set => SetPropertyValueAndDetectChanges(value, ref _trashed, Ps.Value.TrashedSelector); } /// /// Gets or sets the name of the entity. /// [DataMember] public virtual string Name { get => _name; set => SetPropertyValueAndDetectChanges(value, ref _name, Ps.Value.NameSelector); } /// /// Gets or sets the identifier of the user who created the entity. /// [DataMember] public virtual int CreatorId { get => _creatorId; set => SetPropertyValueAndDetectChanges(value, ref _creatorId, Ps.Value.CreatorIdSelector); } /// /// Id of the user who wrote/updated this entity /// [DataMember] public virtual int WriterId { get => _writerId; set => SetPropertyValueAndDetectChanges(value, ref _writerId, Ps.Value.WriterSelector); } /// /// Gets or sets the identifier of the version. /// [DataMember] public Guid Version { get; internal set; } /// /// Integer Id of the default ContentType /// [DataMember] public virtual int ContentTypeId { get { //There will be cases where this has not been updated to reflect the true content type ID. //This will occur when inserting new content. if (_contentTypeId == 0 && ContentTypeBase != null && ContentTypeBase.HasIdentity) { _contentTypeId = ContentTypeBase.Id; } return _contentTypeId; } protected set { SetPropertyValueAndDetectChanges(value, ref _contentTypeId, Ps.Value.DefaultContentTypeIdSelector); } } /// /// Gets or sets the collection of properties for the entity. /// [DataMember] public virtual PropertyCollection Properties { get => _properties; set { _properties = value; _properties.CollectionChanged += PropertiesChanged; } } /// /// Gets the enumeration of property groups for the entity. /// fixme is a proxy, kill this /// [IgnoreDataMember] public IEnumerable PropertyGroups => ContentTypeBase.CompositionPropertyGroups; /// /// Gets the numeration of property types for the entity. /// fixme is a proxy, kill this /// [IgnoreDataMember] public IEnumerable PropertyTypes => ContentTypeBase.CompositionPropertyTypes; #region Has, Get, Set, Publish Property Value /// /// Gets a value indicating whether the content entity has a property with the supplied alias. /// fixme with a value, or just the property? /// public virtual bool HasProperty(string propertyTypeAlias) => Properties.Contains(propertyTypeAlias); /// /// Gets the neutral value of a property. /// public virtual object GetValue(string propertyTypeAlias, bool published = false) { return Properties.Contains(propertyTypeAlias) ? Properties[propertyTypeAlias].GetValue(published) : null; } /// /// Gets the culture value of a property. /// public virtual object GetValue(string propertyTypeAlias, int languageId, bool published = false) { return Properties.Contains(propertyTypeAlias) ? Properties[propertyTypeAlias].GetValue(languageId, published) : null; } /// /// Gets the segment value of a property. /// public virtual object GetValue(string propertyTypeAlias, int languageId, string segment, bool published = false) { return Properties.Contains(propertyTypeAlias) ? Properties[propertyTypeAlias].GetValue(languageId, segment, published) : null; } /// /// Gets the typed neutral value of a property. /// public virtual TPropertyValue GetValue(string propertyTypeAlias, bool published = false) { if (Properties.Contains(propertyTypeAlias) == false) return default; var convertAttempt = Properties[propertyTypeAlias].GetValue(published).TryConvertTo(); return convertAttempt.Success ? convertAttempt.Result : default; } /// /// Gets the typed culture value of a property. /// public virtual TPropertyValue GetValue(string propertyTypeAlias, int languageId, bool published = false) { if (Properties.Contains(propertyTypeAlias) == false) return default; var convertAttempt = Properties[propertyTypeAlias].GetValue(languageId, published).TryConvertTo(); return convertAttempt.Success ? convertAttempt.Result : default; } /// /// Gets the typed segment value of a property. /// public virtual TPropertyValue GetValue(string propertyTypeAlias, int languageId, string segment, bool published = false) { if (Properties.Contains(propertyTypeAlias) == false) return default; var convertAttempt = Properties[propertyTypeAlias].GetValue(languageId, segment, published).TryConvertTo(); return convertAttempt.Success ? convertAttempt.Result : default; } /// /// Publish the neutral value. /// internal virtual void PublishValues() { foreach (var property in Properties) property.PublishValues(); } /// /// Publish the culture value. /// internal virtual void PublishValues(int? nLanguageId) { foreach (var property in Properties) property.PublishValues(nLanguageId); } /// /// Publish the segment value. /// internal virtual void PublishValues(int? nLanguageId, string segment) { foreach (var property in Properties) property.PublishValues(nLanguageId, segment); } /// /// Publish all values. /// internal virtual void PublishAllValues() { foreach (var property in Properties) property.PublishAllValues(); } internal virtual void RollbackValues(IContentBase other) { // clear all existing properties ClearEditValues(null, null); // copy other properties var otherProperties = other.Properties; foreach (var otherProperty in otherProperties) { var alias = otherProperty.PropertyType.Alias; SetValue(alias, otherProperty.GetValue(true)); } } internal virtual void RollbackValues(IContentBase other, int? nLanguageId) { if (!nLanguageId.HasValue) { RollbackValues(other); return; } var languageId = nLanguageId.Value; // clear all existing properties ClearEditValues(nLanguageId, null); // copy other properties var otherProperties = other.Properties; foreach (var otherProperty in otherProperties) { var alias = otherProperty.PropertyType.Alias; SetValue(alias, languageId, otherProperty.GetValue(languageId, true)); } } internal virtual void RollbackValues(IContentBase other, int? nLanguageId, string segment) { if (segment == null) { RollbackValues(other, nLanguageId); return; } if (!nLanguageId.HasValue) throw new ArgumentException("Cannot be null when segment is not null.", nameof(nLanguageId)); var languageId = nLanguageId.Value; // clear all existing properties ClearEditValues(nLanguageId, segment); // copy other properties var otherProperties = other.Properties; foreach (var otherProperty in otherProperties) { var alias = otherProperty.PropertyType.Alias; SetValue(alias, languageId, segment, otherProperty.GetValue(languageId, segment, true)); } } private void ClearEditValues() { // clear all existing properties // note: use property.SetValue(), don't assign pvalue.EditValue, else change tracking fails foreach (var property in Properties) foreach (var pvalue in property.Values) property.SetValue(pvalue.LanguageId, pvalue.Segment, null); } private void ClearEditValues(int? nLanguageId, string segment) { // clear all existing properties // note: use property.SetValue(), don't assign pvalue.EditValue, else change tracking fails foreach (var property in Properties) foreach (var pvalue in property.Values) if (pvalue.LanguageId == nLanguageId && pvalue.Segment == segment) property.SetValue(pvalue.LanguageId, pvalue.Segment, null); } internal virtual void RollbackAllValues(IContentBase other) { // clear all existing properties ClearEditValues(); // copy other properties var otherProperties = other.Properties; foreach (var otherProperty in otherProperties) { var alias = otherProperty.PropertyType.Alias; foreach (var pvalue in otherProperty.Values) { // fixme can we update SetValue to accept null lang/segment and fallback? if (!pvalue.LanguageId.HasValue) SetValue(alias, pvalue.PublishedValue); else if (pvalue.Segment == null) SetValue(alias, pvalue.LanguageId.Value, pvalue.PublishedValue); else SetValue(alias, pvalue.LanguageId.Value, pvalue.Segment, pvalue.PublishedValue); } } } /// /// Sets the neutral (draft) value of a property. /// public virtual void SetValue(string propertyTypeAlias, object value) { if (value == null) { SetValueOnProperty(propertyTypeAlias, null); return; } // .NET magic to call one of the 'SetPropertyValue' handlers with matching signature ((dynamic) this).SetPropertyValue(propertyTypeAlias, (dynamic) value); } /// /// Sets the culture (draft) value of a property. /// public virtual void SetValue(string propertyTypeAlias, int languageId, object value) { if (value == null) { SetValueOnProperty(propertyTypeAlias, languageId, null); return; } // .NET magic to call one of the 'SetPropertyValue' handlers with matching signature ((dynamic) this).SetPropertyValue(propertyTypeAlias, languageId, (dynamic) value); } /// /// Sets the segment (draft) value of a property. /// public virtual void SetValue(string propertyTypeAlias, int languageId, string segment, object value) { if (value == null) { SetValueOnProperty(propertyTypeAlias, languageId, segment, null); return; } // .NET magic to call one of the 'SetPropertyValue' handlers with matching signature ((dynamic) this).SetPropertyValue(propertyTypeAlias, languageId, segment, (dynamic) value); } /// /// Sets the neutral (draft) string value of a Property /// public virtual void SetPropertyValue(string propertyTypeAlias, string value) { SetValueOnProperty(propertyTypeAlias, value); } /// /// Sets the culture (draft) string value of a Property /// public virtual void SetPropertyValue(string propertyTypeAlias, int languageId, string value) { SetValueOnProperty(propertyTypeAlias, languageId, value); } /// /// Sets the segment (draft) string value of a Property /// public virtual void SetPropertyValue(string propertyTypeAlias, int languageId, string segment, string value) { SetValueOnProperty(propertyTypeAlias, languageId, segment, value); } /// /// Sets the neutral (draft) int value of a Property /// public virtual void SetPropertyValue(string propertyTypeAlias, int value) { SetValueOnProperty(propertyTypeAlias, value); } /// /// Sets the culture (draft) int value of a Property /// public virtual void SetPropertyValue(string propertyTypeAlias, int languageId, int value) { SetValueOnProperty(propertyTypeAlias, languageId, value); } /// /// Sets the segment (draft) int value of a Property /// public virtual void SetPropertyValue(string propertyTypeAlias, int languageId, string segment, int value) { SetValueOnProperty(propertyTypeAlias, languageId, segment, value); } /// /// Sets the neutral (draft) long value of a Property /// public virtual void SetPropertyValue(string propertyTypeAlias, long value) { SetValueOnProperty(propertyTypeAlias, value); } /// /// Sets the culture (draft) long value of a Property /// public virtual void SetPropertyValue(string propertyTypeAlias, int languageId, long value) { SetValueOnProperty(propertyTypeAlias, languageId, value); } /// /// Sets the segment (draft) long value of a Property /// public virtual void SetPropertyValue(string propertyTypeAlias, int languageId, string segment, long value) { SetValueOnProperty(propertyTypeAlias, languageId, segment, value); } /// /// Sets the neutral (draft) decimal value of a Property /// public virtual void SetPropertyValue(string propertyTypeAlias, decimal value) { SetValueOnProperty(propertyTypeAlias, value); } /// /// Sets the culture (draft) decimal value of a Property /// public virtual void SetPropertyValue(string propertyTypeAlias, int languageId, decimal value) { SetValueOnProperty(propertyTypeAlias, languageId, value); } /// /// Sets the segment (draft) decimal value of a Property /// public virtual void SetPropertyValue(string propertyTypeAlias, int languageId, string segment, decimal value) { SetValueOnProperty(propertyTypeAlias, languageId, segment, value); } /// /// Sets the neutral (draft) double value of a Property /// public virtual void SetPropertyValue(string propertyTypeAlias, double value) { SetValueOnProperty(propertyTypeAlias, value); } /// /// Sets the culture (draft) double value of a Property /// public virtual void SetPropertyValue(string propertyTypeAlias, int languageId, double value) { SetValueOnProperty(propertyTypeAlias, languageId, value); } /// /// Sets the segment (draft) double value of a Property /// public virtual void SetPropertyValue(string propertyTypeAlias, int languageId, string segment, double value) { SetValueOnProperty(propertyTypeAlias, languageId, segment, value); } /// /// Sets the neutral (draft) boolean value of a Property /// public virtual void SetPropertyValue(string propertyTypeAlias, bool value) { var val = Convert.ToInt32(value); SetValueOnProperty(propertyTypeAlias, val); } /// /// Sets the culture (draft) boolean value of a Property /// public virtual void SetPropertyValue(string propertyTypeAlias, int languageId, bool value) { var val = Convert.ToInt32(value); SetValueOnProperty(propertyTypeAlias, languageId, val); } /// /// Sets the segment (draft) boolean value of a Property /// public virtual void SetPropertyValue(string propertyTypeAlias, int languageId, string segment, bool value) { var val = Convert.ToInt32(value); SetValueOnProperty(propertyTypeAlias, languageId, segment, val); } /// /// Sets the neutral (draft) DateTime value of a Property /// public virtual void SetPropertyValue(string propertyTypeAlias, DateTime value) { SetValueOnProperty(propertyTypeAlias, value); } /// /// Sets the culture (draft) DateTime value of a Property /// public virtual void SetPropertyValue(string propertyTypeAlias, int languageId, DateTime value) { SetValueOnProperty(propertyTypeAlias, languageId, value); } /// /// Sets the segment (draft) DateTime value of a Property /// public virtual void SetPropertyValue(string propertyTypeAlias, int languageId, string segment, DateTime value) { SetValueOnProperty(propertyTypeAlias, languageId, segment, value); } // fixme - these three use an extension method that needs to be adapted too /// /// Sets the posted file value of a Property /// public virtual void SetPropertyValue(string propertyTypeAlias, HttpPostedFile value) { ContentExtensions.SetValue(this, propertyTypeAlias, value); } /// /// Sets the posted file base value of a Property /// /// Alias of the PropertyType /// Value to set for the Property public virtual void SetPropertyValue(string propertyTypeAlias, HttpPostedFileBase value) { ContentExtensions.SetValue(this, propertyTypeAlias, value); } /// /// Sets the posted file wrapper value of a Property /// [Obsolete("There is no reason for this overload since HttpPostedFileWrapper inherits from HttpPostedFileBase")] public virtual void SetPropertyValue(string propertyTypeAlias, HttpPostedFileWrapper value) { ContentExtensions.SetValue(this, propertyTypeAlias, value); } /// /// Sets the neutral (draft) value of a property. /// private void SetValueOnProperty(string propertyTypeAlias, object value) { if (Properties.Contains(propertyTypeAlias)) { Properties[propertyTypeAlias].SetValue(value); return; } var propertyType = PropertyTypes.FirstOrDefault(x => x.Alias.InvariantEquals(propertyTypeAlias)); if (propertyType == null) throw new InvalidOperationException($"No PropertyType exists with the supplied alias \"{propertyTypeAlias}\"."); var property = propertyType.CreateProperty(); property.SetValue(value); Properties.Add(property); } /// /// Sets the culture (draft) value of a property. /// private void SetValueOnProperty(string propertyTypeAlias, int languageId, object value) { if (Properties.Contains(propertyTypeAlias)) { Properties[propertyTypeAlias].SetValue(languageId, value); return; } var propertyType = PropertyTypes.FirstOrDefault(x => x.Alias.InvariantEquals(propertyTypeAlias)); if (propertyType == null) throw new InvalidOperationException($"No PropertyType exists with the supplied alias \"{propertyTypeAlias}\"."); var property = propertyType.CreateProperty(); property.SetValue(languageId, value); Properties.Add(property); } /// /// Sets the segment (draft) value of a property. /// private void SetValueOnProperty(string propertyTypeAlias, int languageId, string segment, object value) { if (Properties.Contains(propertyTypeAlias)) { Properties[propertyTypeAlias].SetValue(languageId, segment, value); return; } var propertyType = PropertyTypes.FirstOrDefault(x => x.Alias.InvariantEquals(propertyTypeAlias)); if (propertyType == null) throw new InvalidOperationException($"No PropertyType exists with the supplied alias \"{propertyTypeAlias}\"."); var property = propertyType.CreateProperty(); property.SetValue(languageId, segment, value); Properties.Add(property); } #endregion #region Validation /// /// Gets a value indicating whether the content and its properties are valid. /// public virtual bool Validate() // fixme would it depends on the property varyBy? or would we validate for a given culture/segment? { _invalidProperties.Clear(); _invalidProperties.AddRange(Properties.Where(property => property.IsValid() == false)); return _invalidProperties.Any() == false; } /// /// Gets the properties marked as invalid during the last validation. /// [IgnoreDataMember] internal IEnumerable InvalidProperties => _invalidProperties; #endregion #region Dirty /// /// Resets dirty properties. /// public override void ResetDirtyProperties(bool rememberDirty) { base.ResetDirtyProperties(rememberDirty); // also reset dirty changes made to user's properties foreach (var prop in Properties) prop.ResetDirtyProperties(rememberDirty); } /// /// Gets a value indicating whether the current entity is dirty. /// public override bool IsDirty() { return IsEntityDirty() || this.IsAnyUserPropertyDirty(); } /// /// Gets a value indicating whether the current entity was dirty. /// public override bool WasDirty() { return WasEntityDirty() || this.WasAnyUserPropertyDirty(); } /// /// Gets a value indicating whether the current entity's own properties (not user) are dirty. /// public bool IsEntityDirty() { return base.IsDirty(); } /// /// Gets a value indicating whether the current entity's own properties (not user) were dirty. /// public bool WasEntityDirty() { return base.WasDirty(); } /// /// Gets a value indicating whether a user property is dirty. /// public override bool IsPropertyDirty(string propertyName) { if (base.IsPropertyDirty(propertyName)) return true; return Properties.Contains(propertyName) && Properties[propertyName].IsDirty(); } /// /// Gets a value indicating whether a user property was dirty. /// public override bool WasPropertyDirty(string propertyName) { if (base.WasPropertyDirty(propertyName)) return true; return Properties.Contains(propertyName) && Properties[propertyName].WasDirty(); } #endregion } }