using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Runtime.Serialization; using System.Web; using Umbraco.Core.Models.Entities; 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 : TreeEntityBase, IContentBase { protected static readonly Dictionary NoNames = new Dictionary(); private static readonly Lazy Ps = new Lazy(); private int _contentTypeId; protected IContentTypeComposition ContentTypeBase; private int _writerId; private PropertyCollection _properties; private Dictionary _names; /// /// 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 = 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)); SetParent(parent); } private ContentBase(string name, IContentTypeComposition contentType, PropertyCollection properties) { ContentTypeBase = contentType ?? throw new ArgumentNullException(nameof(contentType)); // initially, all new instances have Id = 0; // no identity VersionId = 0; // no versions 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 DefaultContentTypeIdSelector = ExpressionHelper.GetPropertyInfo(x => x.ContentTypeId); public readonly PropertyInfo PropertyCollectionSelector = ExpressionHelper.GetPropertyInfo(x => x.Properties); public readonly PropertyInfo WriterSelector = ExpressionHelper.GetPropertyInfo(x => x.WriterId); public readonly PropertyInfo NamesSelector = ExpressionHelper.GetPropertyInfo>(x => x.Names); } protected void PropertiesChanged(object sender, NotifyCollectionChangedEventArgs e) { OnPropertyChanged(Ps.Value.PropertyCollectionSelector); } /// /// Id of the user who wrote/updated this entity /// [DataMember] public virtual int WriterId { get => _writerId; set => SetPropertyValueAndDetectChanges(value, ref _writerId, Ps.Value.WriterSelector); } [IgnoreDataMember] public int VersionId { 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; } } /// [DataMember] public virtual IReadOnlyDictionary Names { get => _names ?? NoNames; set { foreach (var (languageId, name) in value) SetName(languageId, name); } } /// public virtual void SetName(int? languageId, string name) { if (languageId == null) { Name = name; return; } if ((ContentTypeBase.Variations & (ContentVariation.CultureNeutral | ContentVariation.CultureSegment)) == 0) throw new NotSupportedException("Content type does not support varying name by culture."); if (_names == null) _names = new Dictionary(); _names[languageId.Value] = name; OnPropertyChanged(Ps.Value.NamesSelector); } protected virtual void ClearNames() { _names = null; OnPropertyChanged(Ps.Value.NamesSelector); } /// public virtual string GetName(int? languageId) { if (languageId == null) return Name; if (_names == null) return null; return _names.TryGetValue(languageId.Value, out var name) ? name : null; } /// /// 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 /// public virtual bool HasProperty(string propertyTypeAlias) => Properties.Contains(propertyTypeAlias); /// public virtual object GetValue(string propertyTypeAlias, int? languageId = null, string segment = null, bool published = false) { return Properties.TryGetValue(propertyTypeAlias, out var property) ? property.GetValue(languageId, segment, published) : null; } /// public virtual TValue GetValue(string propertyTypeAlias, int? languageId = null, string segment = null, bool published = false) { if (!Properties.TryGetValue(propertyTypeAlias, out var property)) return default; var convertAttempt = property.GetValue(languageId, segment, published).TryConvertTo(); return convertAttempt.Success ? convertAttempt.Result : default; } /// public virtual void SetValue(string propertyTypeAlias, object value, int? languageId = null, string segment = null) { if (Properties.Contains(propertyTypeAlias)) { Properties[propertyTypeAlias].SetValue(value, languageId, segment); 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, languageId, segment); Properties.Add(property); } // HttpPostedFileBase is the base class that can be mocked // HttpPostedFile is what we get in ASP.NET // HttpPostedFileWrapper wraps sealed HttpPostedFile as HttpPostedFileBase /// /// Sets the posted file value of a property. /// public virtual void SetValue(string propertyTypeAlias, HttpPostedFile value, int? languageId = null, string segment = null) { ContentExtensions.SetValue(this, propertyTypeAlias, new HttpPostedFileWrapper(value), languageId, segment); } /// /// Sets the posted file value of a property. /// public virtual void SetValue(string propertyTypeAlias, HttpPostedFileBase value, int? languageId = null, string segment = null) { ContentExtensions.SetValue(this, propertyTypeAlias, value, languageId, segment); } #endregion #region Validation public virtual Property[] ValidateAll() { return Properties.Where(x => !x.IsAllValid()).ToArray(); } public virtual Property[] Validate(int? languageId = null, string segment = null) { return Properties.Where(x => !x.IsValid(languageId, segment)).ToArray(); } public virtual Property[] ValidateCulture(int? languageId = null) { return Properties.Where(x => !x.IsCultureValid(languageId)).ToArray(); } #endregion #region Dirty /// /// Overriden to include user 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); } /// /// Overriden to include user properties. public override bool IsDirty() { return IsEntityDirty() || this.IsAnyUserPropertyDirty(); } /// /// Overriden to include user properties. 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(); } /// /// Overriden to include user properties. public override bool IsPropertyDirty(string propertyName) { if (base.IsPropertyDirty(propertyName)) return true; return Properties.Contains(propertyName) && Properties[propertyName].IsDirty(); } /// /// Overriden to include user properties. public override bool WasPropertyDirty(string propertyName) { if (base.WasPropertyDirty(propertyName)) return true; return Properties.Contains(propertyName) && Properties[propertyName].WasDirty(); } /// /// Overriden to include user properties. public override IEnumerable GetDirtyProperties() { var instanceProperties = base.GetDirtyProperties(); var propertyTypes = Properties.Where(x => x.IsDirty()).Select(x => x.Alias); return instanceProperties.Concat(propertyTypes); } /// /// Overriden to include user properties. public override IEnumerable GetWereDirtyProperties() { var instanceProperties = base.GetWereDirtyProperties(); var propertyTypes = Properties.Where(x => x.WasDirty()).Select(x => x.Alias); return instanceProperties.Concat(propertyTypes); } #endregion } }