diff --git a/src/Umbraco.Core/Models/ContentTypeBase.cs b/src/Umbraco.Core/Models/ContentTypeBase.cs index 0877354bba..c0550bec19 100644 --- a/src/Umbraco.Core/Models/ContentTypeBase.cs +++ b/src/Umbraco.Core/Models/ContentTypeBase.cs @@ -31,6 +31,8 @@ namespace Umbraco.Core.Models private PropertyGroupCollection _propertyGroups; private PropertyTypeCollection _propertyTypes; private IEnumerable _allowedContentTypes; + private bool _hasPropertyTypeBeenRemoved; + protected ContentTypeBase(int parentId) { @@ -68,6 +70,8 @@ namespace Umbraco.Core.Models private static readonly PropertyInfo AllowedContentTypesSelector = ExpressionHelper.GetPropertyInfo>(x => x.AllowedContentTypes); private static readonly PropertyInfo PropertyGroupCollectionSelector = ExpressionHelper.GetPropertyInfo(x => x.PropertyGroups); private static readonly PropertyInfo PropertyTypeCollectionSelector = ExpressionHelper.GetPropertyInfo>(x => x.PropertyTypes); + private static readonly PropertyInfo HasPropertyTypeBeenRemovedSelector = ExpressionHelper.GetPropertyInfo(x => x.HasPropertyTypeBeenRemoved); + protected void PropertyGroupsChanged(object sender, NotifyCollectionChangedEventArgs e) { @@ -153,8 +157,11 @@ namespace Umbraco.Core.Models get { return _alias; } set { - _alias = value.ToSafeAlias(); - OnPropertyChanged(AliasSelector); + SetPropertyValueAndDetectChanges(o => + { + _alias = value.ToSafeAlias(); + return _alias; + }, _alias, AliasSelector); } } @@ -322,6 +329,24 @@ namespace Umbraco.Core.Models } } + /// + /// A boolean flag indicating if a property type has been removed from this instance. + /// + /// + /// This is currently (specifically) used in order to know that we need to refresh the content cache which + /// needs to occur when a property has been removed from a content type + /// + [IgnoreDataMember] + internal bool HasPropertyTypeBeenRemoved + { + get { return _hasPropertyTypeBeenRemoved; } + private set + { + _hasPropertyTypeBeenRemoved = value; + OnPropertyChanged(HasPropertyTypeBeenRemovedSelector); + } + } + /// /// Checks whether a PropertyType with a given alias already exists /// @@ -396,6 +421,15 @@ namespace Umbraco.Core.Models /// Alias of the to remove public void RemovePropertyType(string propertyTypeAlias) { + + //check if the property exist in one of our collections + if (PropertyGroups.Any(group => group.PropertyTypes.Any(pt => pt.Alias == propertyTypeAlias)) + || _propertyTypes.Any(x => x.Alias == propertyTypeAlias)) + { + //set the flag that a property has been removed + HasPropertyTypeBeenRemoved = true; + } + foreach (var propertyGroup in PropertyGroups) { propertyGroup.PropertyTypes.RemoveItem(propertyTypeAlias); @@ -403,7 +437,7 @@ namespace Umbraco.Core.Models if (_propertyTypes.Any(x => x.Alias == propertyTypeAlias)) { - _propertyTypes.RemoveItem(propertyTypeAlias); + _propertyTypes.RemoveItem(propertyTypeAlias); } } diff --git a/src/Umbraco.Core/Models/EntityBase/Entity.cs b/src/Umbraco.Core/Models/EntityBase/Entity.cs index 95be4851e7..71caad7174 100644 --- a/src/Umbraco.Core/Models/EntityBase/Entity.cs +++ b/src/Umbraco.Core/Models/EntityBase/Entity.cs @@ -14,7 +14,7 @@ namespace Umbraco.Core.Models.EntityBase [Serializable] [DataContract(IsReference = true)] [DebuggerDisplay("Id: {Id}")] - public abstract class Entity : IEntity, ICanBeDirty + public abstract class Entity : IEntity, IRememberBeingDirty, ICanBeDirty { private bool _hasIdentity; private int? _hash; @@ -115,6 +115,11 @@ namespace Umbraco.Core.Models.EntityBase /// private readonly IDictionary _propertyChangedInfo = new Dictionary(); + /// + /// Tracks the properties that we're changed before the last commit (or last call to ResetDirtyProperties) + /// + private IDictionary _lastPropertyChangedInfo = null; + /// /// Indicates whether a specific property on the current entity is dirty. /// @@ -134,6 +139,33 @@ namespace Umbraco.Core.Models.EntityBase return _propertyChangedInfo.Any(); } + /// + /// Indicates that the entity had been changed and the changes were committed + /// + /// + public bool WasDirty() + { + return _lastPropertyChangedInfo != null && _lastPropertyChangedInfo.Any(); + } + + /// + /// Indicates whether a specific property on the current entity was changed and the changes were committed + /// + /// Name of the property to check + /// True if Property was changed, otherwise False. Returns false if the entity had not been previously changed. + public virtual bool WasPropertyDirty(string propertyName) + { + return WasDirty() && _lastPropertyChangedInfo.Any(x => x.Key == propertyName); + } + + /// + /// Resets the remembered dirty properties from before the last commit + /// + public void ForgetPreviouslyDirtyProperties() + { + _lastPropertyChangedInfo.Clear(); + } + /// /// Resets dirty properties by clearing the dictionary used to track changes. /// @@ -143,9 +175,37 @@ namespace Umbraco.Core.Models.EntityBase /// public virtual void ResetDirtyProperties() { + //copy the changed properties to the last changed properties + _lastPropertyChangedInfo = _propertyChangedInfo.ToDictionary(v => v.Key, v => v.Value); + _propertyChangedInfo.Clear(); } + /// + /// Used by inheritors to set the value of properties, this will detect if the property value actually changed and if it did + /// it will ensure that the property has a dirty flag set. + /// + /// + /// + /// + /// returns true if the value changed + /// + /// This is required because we don't want a property to show up as "dirty" if the value is the same. For example, when we + /// save a document type, nearly all properties are flagged as dirty just because we've 'reset' them, but they are all set + /// to the same value, so it's really not dirty. + /// + internal bool SetPropertyValueAndDetectChanges(Func setValue, T value, PropertyInfo propertySelector) + { + var initVal = value; + var newVal = setValue(value); + if (!Equals(initVal, newVal)) + { + OnPropertyChanged(propertySelector); + return true; + } + return false; + } + /// /// Indicates whether the current entity has an identity, eg. Id. /// diff --git a/src/Umbraco.Core/Models/EntityBase/IRememberBeingDirty.cs b/src/Umbraco.Core/Models/EntityBase/IRememberBeingDirty.cs new file mode 100644 index 0000000000..94a88d312e --- /dev/null +++ b/src/Umbraco.Core/Models/EntityBase/IRememberBeingDirty.cs @@ -0,0 +1,13 @@ +namespace Umbraco.Core.Models.EntityBase +{ + /// + /// An interface that defines if the object is tracking property changes and that is is also + /// remembering what property changes had been made after the changes were committed. + /// + public interface IRememberBeingDirty : ICanBeDirty + { + bool WasDirty(); + bool WasPropertyDirty(string propertyName); + void ForgetPreviouslyDirtyProperties(); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 1b4eab601a..713e1af374 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -166,6 +166,7 @@ + diff --git a/src/Umbraco.Tests/Models/ContentTests.cs b/src/Umbraco.Tests/Models/ContentTests.cs index 96a6b3acc8..33af9c30e6 100644 --- a/src/Umbraco.Tests/Models/ContentTests.cs +++ b/src/Umbraco.Tests/Models/ContentTests.cs @@ -404,6 +404,51 @@ namespace Umbraco.Tests.Models //Assert.That(contentType.IsPropertyDirty("PropertyGroups"), Is.True); } + [Test] + public void After_Committing_Changes_Was_Dirty_Is_True() + { + // Arrange + var contentType = MockedContentTypes.CreateTextpageContentType(); + contentType.ResetDirtyProperties(); //reset + + // Act + contentType.Alias = "newAlias"; + contentType.ResetDirtyProperties(); //this would be like committing the entity + + // Assert + Assert.That(contentType.IsDirty(), Is.False); + Assert.That(contentType.WasDirty(), Is.True); + Assert.That(contentType.WasPropertyDirty("Alias"), Is.True); + } + + [Test] + public void If_Not_Committed_Was_Dirty_Is_False() + { + // Arrange + var contentType = MockedContentTypes.CreateTextpageContentType(); + + // Act + contentType.Alias = "newAlias"; + + // Assert + Assert.That(contentType.IsDirty(), Is.True); + Assert.That(contentType.WasDirty(), Is.False); + } + + [Test] + public void Detect_That_A_Property_Is_Removed() + { + // Arrange + var contentType = MockedContentTypes.CreateTextpageContentType(); + Assert.That(contentType.WasPropertyDirty("HasPropertyTypeBeenRemoved"), Is.False); + + // Act + contentType.RemovePropertyType("title"); + + // Assert + Assert.That(contentType.IsPropertyDirty("HasPropertyTypeBeenRemoved"), Is.True); + } + [Test] public void Adding_PropertyType_To_PropertyGroup_On_ContentType_Results_In_Dirty_Entity() { diff --git a/src/Umbraco.Web/Cache/ContentTypeCacheRefresher.cs b/src/Umbraco.Web/Cache/ContentTypeCacheRefresher.cs index 7145c72ea7..2a37efed59 100644 --- a/src/Umbraco.Web/Cache/ContentTypeCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/ContentTypeCacheRefresher.cs @@ -5,6 +5,7 @@ using System.Text; using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Models; +using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Models.Rdbms; using Umbraco.Core.Persistence.Caching; @@ -87,12 +88,34 @@ namespace Umbraco.Web.Cache /// - InMemoryCacheProvider.Current.Clear(); /// - RuntimeCacheProvider.Current.Clear(); /// - /// TODO: Needs to update any content items that this effects for the xml cache... currently it would seem that this is not handled! - /// it is only handled in the ContentTypeControlNew.ascx, not by business logic/events. - The xml cache needs to be updated when the doc type alias changes or when a property type is removed, the ContentService.RePublishAll should be executed anytime either of these happens. + /// TODO: Needs to update any content items that this effects for the xml cache... + /// it is only handled in the ContentTypeControlNew.ascx, not by business logic/events. - The xml cache needs to be updated + /// when the doc type alias changes or when a property type is removed, the ContentService.RePublishAll should be executed anytime either of these happens. /// private static void ClearContentTypeCache(params IContentTypeBase[] contentTypes) { - contentTypes.ForEach(ClearContentTypeCache); + var needsContentRefresh = false; + + contentTypes.ForEach(contentType => + { + //clear the cache for each item + ClearContentTypeCache(contentType); + + //here we need to check if the alias of the content type changed or if one of the properties was removed. + var dirty = contentType as IRememberBeingDirty; + if (dirty == null) return; + if (dirty.WasPropertyDirty("Alias") || dirty.WasPropertyDirty("HasPropertyTypeBeenRemoved")) + { + needsContentRefresh = true; + } + }); + + //need to refresh the xml content cache if required + if (needsContentRefresh) + { + var pageRefresher = CacheRefreshersResolver.Current.GetById(new Guid(DistributedCache.PageCacheRefresherId)); + pageRefresher.RefreshAll(); + } //clear the cache providers if there were any content types to clear if (contentTypes.Any()) @@ -110,6 +133,9 @@ namespace Umbraco.Web.Cache /// See notes for the other overloaded ClearContentTypeCache for /// full details on clearing cache. /// + /// + /// Return true if the alias of the content type changed + /// private static void ClearContentTypeCache(IContentTypeBase contentType) { //clears the cache for each property type associated with the content type @@ -131,7 +157,7 @@ namespace Umbraco.Web.Cache foreach (var dto in dtos) { ClearContentTypeCache(dto.ChildId); - } + } } ///