diff --git a/src/Umbraco.Core/Collections/IReadOnlyKeyedCollection.cs b/src/Umbraco.Core/Collections/IReadOnlyKeyedCollection.cs deleted file mode 100644 index 8d78241275..0000000000 --- a/src/Umbraco.Core/Collections/IReadOnlyKeyedCollection.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Collections.Generic; - -namespace Umbraco.Core.Collections -{ - /// - /// A readonly keyed collection - /// - /// - public interface IReadOnlyKeyedCollection : IReadOnlyList - { - IEnumerable Keys { get; } - bool TryGetValue(TKey key, out TVal val); - TVal this[string key] { get; } - bool Contains(TKey key); - } -} diff --git a/src/Umbraco.Core/Collections/ObservableDictionary.cs b/src/Umbraco.Core/Collections/ObservableDictionary.cs index caa2be92a8..ded87c30a6 100644 --- a/src/Umbraco.Core/Collections/ObservableDictionary.cs +++ b/src/Umbraco.Core/Collections/ObservableDictionary.cs @@ -14,20 +14,21 @@ namespace Umbraco.Core.Collections /// /// The type of elements contained in the BindableCollection /// The type of the indexing key - public class ObservableDictionary : ObservableCollection + public class ObservableDictionary : ObservableCollection, IReadOnlyDictionary, IDictionary { - protected Dictionary Indecies = new Dictionary(); - protected Func KeySelector; + protected Dictionary Indecies { get; } + protected Func KeySelector { get; } /// /// Create new ObservableDictionary /// /// Selector function to create key from value - public ObservableDictionary(Func keySelector) + public ObservableDictionary(Func keySelector, IEqualityComparer equalityComparer = null) : base() { if (keySelector == null) throw new ArgumentException("keySelector"); KeySelector = keySelector; + Indecies = new Dictionary(equalityComparer); } #region Protected Methods @@ -73,7 +74,7 @@ namespace Umbraco.Core.Collections } #endregion - public virtual bool ContainsKey(TKey key) + public bool ContainsKey(TKey key) { return Indecies.ContainsKey(key); } @@ -83,7 +84,7 @@ namespace Umbraco.Core.Collections /// /// Key of element to replace /// - public virtual TValue this[TKey key] + public TValue this[TKey key] { get { return this[Indecies[key]]; } @@ -112,7 +113,7 @@ namespace Umbraco.Core.Collections /// /// /// False if key not found - public virtual bool Replace(TKey key, TValue value) + public bool Replace(TKey key, TValue value) { if (!Indecies.ContainsKey(key)) return false; //confirm key matches @@ -124,7 +125,7 @@ namespace Umbraco.Core.Collections } - public virtual bool Remove(TKey key) + public bool Remove(TKey key) { if (!Indecies.ContainsKey(key)) return false; @@ -138,7 +139,7 @@ namespace Umbraco.Core.Collections /// /// /// - public virtual void ChangeKey(TKey currentKey, TKey newKey) + public void ChangeKey(TKey currentKey, TKey newKey) { if (!Indecies.ContainsKey(currentKey)) { @@ -155,6 +156,75 @@ namespace Umbraco.Core.Collections Indecies.Add(newKey, currentIndex); } + #region IDictionary and IReadOnlyDictionary implementation + + public bool TryGetValue(TKey key, out TValue val) + { + if (Indecies.TryGetValue(key, out var index)) + { + val = this[index]; + return true; + } + val = default; + return false; + } + + /// + /// Returns all keys + /// + public IEnumerable Keys => Indecies.Keys; + + /// + /// Returns all values + /// + public IEnumerable Values => base.Items; + + ICollection IDictionary.Keys => Indecies.Keys; + + //this will never be used + ICollection IDictionary.Values => Values.ToList(); + + bool ICollection>.IsReadOnly + { + get { return false; } + } + + IEnumerator> IEnumerable>.GetEnumerator() + { + foreach (var i in Values) + { + var key = KeySelector(i); + yield return new KeyValuePair(key, i); + } + } + + void IDictionary.Add(TKey key, TValue value) + { + Add(value); + } + + void ICollection>.Add(KeyValuePair item) + { + Add(item.Value); + } + + bool ICollection>.Contains(KeyValuePair item) + { + return ContainsKey(item.Key); + } + + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) + { + throw new NotImplementedException(); + } + + bool ICollection>.Remove(KeyValuePair item) + { + return Remove(item.Key); + } + + #endregion + internal class DuplicateKeyException : Exception { diff --git a/src/Umbraco.Core/Models/Content.cs b/src/Umbraco.Core/Models/Content.cs index abe28522ba..25a30da6db 100644 --- a/src/Umbraco.Core/Models/Content.cs +++ b/src/Umbraco.Core/Models/Content.cs @@ -89,7 +89,7 @@ namespace Umbraco.Core.Models public readonly PropertyInfo PublishedSelector = ExpressionHelper.GetPropertyInfo(x => x.Published); public readonly PropertyInfo ReleaseDateSelector = ExpressionHelper.GetPropertyInfo(x => x.ReleaseDate); public readonly PropertyInfo ExpireDateSelector = ExpressionHelper.GetPropertyInfo(x => x.ExpireDate); - public readonly PropertyInfo PublishNamesSelector = ExpressionHelper.GetPropertyInfo>(x => x.PublishNames); + public readonly PropertyInfo PublishNamesSelector = ExpressionHelper.GetPropertyInfo>(x => x.PublishNames); } /// @@ -224,13 +224,13 @@ namespace Umbraco.Core.Models public bool IsCulturePublished(string culture) // just check _publishInfos // a non-available culture could not become published anyways - => _publishInfos != null && _publishInfos.Contains(culture); + => _publishInfos != null && _publishInfos.ContainsKey(culture); /// public bool WasCulturePublished(string culture) // just check _publishInfosOrig - a copy of _publishInfos // a non-available culture could not become published anyways - => _publishInfosOrig != null && _publishInfosOrig.Contains(culture); + => _publishInfosOrig != null && _publishInfosOrig.ContainsKey(culture); // adjust dates to sync between version, cultures etc // used by the repo when persisting @@ -260,7 +260,7 @@ namespace Umbraco.Core.Models /// [IgnoreDataMember] - public IReadOnlyKeyedCollection PublishNames => _publishInfos ?? NoNames; + public IReadOnlyDictionary PublishNames => _publishInfos ?? NoNames; /// public string GetPublishName(string culture) @@ -508,6 +508,9 @@ namespace Umbraco.Core.Models //turn off change tracking clone.DisableChangeTracking(); + //need to manually clone this since it's not settable + clone._contentType = (IContentType)ContentType.DeepClone(); + //if culture infos exist then deal with event bindings if (clone._publishInfos != null) { diff --git a/src/Umbraco.Core/Models/ContentBase.cs b/src/Umbraco.Core/Models/ContentBase.cs index 1c5ee18be6..a288f7fac9 100644 --- a/src/Umbraco.Core/Models/ContentBase.cs +++ b/src/Umbraco.Core/Models/ContentBase.cs @@ -70,7 +70,7 @@ namespace Umbraco.Core.Models 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 CultureNamesSelector = ExpressionHelper.GetPropertyInfo>(x => x.CultureNames); + public readonly PropertyInfo CultureNamesSelector = ExpressionHelper.GetPropertyInfo>(x => x.CultureNames); } protected void PropertiesChanged(object sender, NotifyCollectionChangedEventArgs e) @@ -156,11 +156,11 @@ namespace Umbraco.Core.Models /// public bool IsCultureAvailable(string culture) - => _cultureInfos != null && _cultureInfos.Contains(culture); + => _cultureInfos != null && _cultureInfos.ContainsKey(culture); /// [DataMember] - public virtual IReadOnlyKeyedCollection CultureNames => _cultureInfos ?? NoNames; + public virtual IReadOnlyDictionary CultureNames => _cultureInfos ?? NoNames; /// public string GetCultureName(string culture) @@ -373,7 +373,7 @@ namespace Umbraco.Core.Models foreach (var (otherCulture, otherName) in other.CultureNames) { if (culture == "*" || culture == otherCulture) - SetCultureName(otherName, otherCulture); + SetCultureName(otherName.Name, otherCulture); } } diff --git a/src/Umbraco.Core/Models/ContentTypeBase.cs b/src/Umbraco.Core/Models/ContentTypeBase.cs index 2a57420e9e..0a86f4cf09 100644 --- a/src/Umbraco.Core/Models/ContentTypeBase.cs +++ b/src/Umbraco.Core/Models/ContentTypeBase.cs @@ -28,7 +28,7 @@ namespace Umbraco.Core.Models private bool _allowedAsRoot; // note: only one that's not 'pure element type' private bool _isContainer; private PropertyGroupCollection _propertyGroups; - private PropertyTypeCollection _propertyTypes; + private PropertyTypeCollection _noGroupPropertyTypes; private IEnumerable _allowedContentTypes; private bool _hasPropertyTypeBeenRemoved; private ContentVariation _variations; @@ -43,8 +43,8 @@ namespace Umbraco.Core.Models // actually OK as IsPublishing is constant // ReSharper disable once VirtualMemberCallInConstructor - _propertyTypes = new PropertyTypeCollection(IsPublishing); - _propertyTypes.CollectionChanged += PropertyTypesChanged; + _noGroupPropertyTypes = new PropertyTypeCollection(IsPublishing); + _noGroupPropertyTypes.CollectionChanged += PropertyTypesChanged; _variations = ContentVariation.Nothing; } @@ -64,8 +64,8 @@ namespace Umbraco.Core.Models // actually OK as IsPublishing is constant // ReSharper disable once VirtualMemberCallInConstructor - _propertyTypes = new PropertyTypeCollection(IsPublishing); - _propertyTypes.CollectionChanged += PropertyTypesChanged; + _noGroupPropertyTypes = new PropertyTypeCollection(IsPublishing); + _noGroupPropertyTypes.CollectionChanged += PropertyTypesChanged; _variations = ContentVariation.Nothing; } @@ -248,7 +248,7 @@ namespace Umbraco.Core.Models { get { - return _propertyTypes.Union(PropertyGroups.SelectMany(x => x.PropertyTypes)); + return _noGroupPropertyTypes.Union(PropertyGroups.SelectMany(x => x.PropertyTypes)); } } @@ -261,12 +261,12 @@ namespace Umbraco.Core.Models [DoNotClone] public IEnumerable NoGroupPropertyTypes { - get => _propertyTypes; + get => _noGroupPropertyTypes; set { - _propertyTypes = new PropertyTypeCollection(IsPublishing, value); - _propertyTypes.CollectionChanged += PropertyTypesChanged; - PropertyTypesChanged(_propertyTypes, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + _noGroupPropertyTypes = new PropertyTypeCollection(IsPublishing, value); + _noGroupPropertyTypes.CollectionChanged += PropertyTypesChanged; + PropertyTypesChanged(_noGroupPropertyTypes, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); } } @@ -320,7 +320,7 @@ namespace Umbraco.Core.Models { if (PropertyTypeExists(propertyType.Alias) == false) { - _propertyTypes.Add(propertyType); + _noGroupPropertyTypes.Add(propertyType); return true; } @@ -384,7 +384,7 @@ namespace Umbraco.Core.Models } //check through each local property type collection (not assigned to a tab) - if (_propertyTypes.RemoveItem(propertyTypeAlias)) + if (_noGroupPropertyTypes.RemoveItem(propertyTypeAlias)) { if (!HasPropertyTypeBeenRemoved) { @@ -408,7 +408,7 @@ namespace Umbraco.Core.Models foreach (var property in group.PropertyTypes) { property.PropertyGroupId = null; - _propertyTypes.Add(property); + _noGroupPropertyTypes.Add(property); } // actually remove the group @@ -421,7 +421,7 @@ namespace Umbraco.Core.Models /// [IgnoreDataMember] //fixme should we mark this as EditorBrowsable hidden since it really isn't ever used? - internal PropertyTypeCollection PropertyTypeCollection => _propertyTypes; + internal PropertyTypeCollection PropertyTypeCollection => _noGroupPropertyTypes; /// /// Indicates whether the current entity is dirty. @@ -474,15 +474,15 @@ namespace Umbraco.Core.Models //turn off change tracking clone.DisableChangeTracking(); - if (clone._propertyTypes != null) + if (clone._noGroupPropertyTypes != null) { //need to manually wire up the event handlers for the property type collections - we've ensured // its ignored from the auto-clone process because its return values are unions, not raw and // we end up with duplicates, see: http://issues.umbraco.org/issue/U4-4842 - clone._propertyTypes.CollectionChanged -= this.PropertyTypesChanged; //clear this event handler if any - clone._propertyTypes = (PropertyTypeCollection)_propertyTypes.DeepClone(); //manually deep clone - clone._propertyTypes.CollectionChanged += clone.PropertyTypesChanged; //re-assign correct event handler + clone._noGroupPropertyTypes.CollectionChanged -= this.PropertyTypesChanged; //clear this event handler if any + clone._noGroupPropertyTypes = (PropertyTypeCollection)_noGroupPropertyTypes.DeepClone(); //manually deep clone + clone._noGroupPropertyTypes.CollectionChanged += clone.PropertyTypesChanged; //re-assign correct event handler } if (clone._propertyGroups != null) diff --git a/src/Umbraco.Core/Models/CultureNameCollection.cs b/src/Umbraco.Core/Models/CultureNameCollection.cs index be6540c399..406abc7c0f 100644 --- a/src/Umbraco.Core/Models/CultureNameCollection.cs +++ b/src/Umbraco.Core/Models/CultureNameCollection.cs @@ -12,13 +12,14 @@ namespace Umbraco.Core.Models /// /// The culture names of a content's variants /// - public class CultureNameCollection : KeyedCollection, INotifyCollectionChanged, IDeepCloneable, IReadOnlyKeyedCollection + public class CultureNameCollection : ObservableDictionary, IDeepCloneable { /// /// Creates a new collection from another collection /// /// - public CultureNameCollection(IEnumerable names) : base(StringComparer.InvariantCultureIgnoreCase) + public CultureNameCollection(IEnumerable names) + : base(x => x.Culture, StringComparer.InvariantCultureIgnoreCase) { foreach (var n in names) Add(n); @@ -27,21 +28,11 @@ namespace Umbraco.Core.Models /// /// Creates a new collection /// - public CultureNameCollection() : base(StringComparer.InvariantCultureIgnoreCase) + public CultureNameCollection() + : base(x => x.Culture, StringComparer.InvariantCultureIgnoreCase) { } - - /// - /// Returns all keys in the collection - /// - public IEnumerable Keys => Dictionary != null ? Dictionary.Keys : this.Select(x => x.Culture); - - public bool TryGetValue(string culture, out CultureName name) - { - name = this.FirstOrDefault(x => x.Culture.InvariantEquals(culture)); - return name != null; - } - + /// /// Add or update the /// @@ -63,78 +54,20 @@ namespace Umbraco.Core.Models }); } - /// - /// Gets the index for a specified culture - /// - public int IndexOfKey(string key) - { - for (var i = 0; i < Count; i++) - { - if (this[i].Culture.InvariantEquals(key)) - return i; - } - return -1; - } - - public event NotifyCollectionChangedEventHandler CollectionChanged; - - protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs args) - { - CollectionChanged?.Invoke(this, args); - } - public object DeepClone() { var clone = new CultureNameCollection(); foreach (var name in this) { - clone.Add((CultureName)name.DeepClone()); + name.DisableChangeTracking(); + var copy = (CultureName)name.DeepClone(); + copy.ResetDirtyProperties(false); + clone.Add(copy); + name.EnableChangeTracking(); } return clone; } - - protected override string GetKeyForItem(CultureName item) - { - return item.Culture; - } - - /// - /// Resets the collection to only contain the instances referenced in the parameter. - /// - /// The property groups. - /// - internal void Reset(IEnumerable names) - { - Clear(); - foreach (var name in names) - Add(name); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); - } - - protected override void SetItem(int index, CultureName item) - { - base.SetItem(index, item); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index)); - } - - protected override void RemoveItem(int index) - { - var removed = this[index]; - base.RemoveItem(index); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removed)); - } - - protected override void InsertItem(int index, CultureName item) - { - base.InsertItem(index, item); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item)); - } - - protected override void ClearItems() - { - base.ClearItems(); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); - } + } } diff --git a/src/Umbraco.Core/Models/IContent.cs b/src/Umbraco.Core/Models/IContent.cs index 3ddffe8f75..125c1a0f55 100644 --- a/src/Umbraco.Core/Models/IContent.cs +++ b/src/Umbraco.Core/Models/IContent.cs @@ -133,7 +133,7 @@ namespace Umbraco.Core.Models /// Because a dictionary key cannot be null this cannot get the invariant /// name, which must be get via the property. /// - IReadOnlyKeyedCollection PublishNames { get; } + IReadOnlyDictionary PublishNames { get; } /// /// Gets the published cultures. diff --git a/src/Umbraco.Core/Models/IContentBase.cs b/src/Umbraco.Core/Models/IContentBase.cs index 811cbf74f3..c84c768e9c 100644 --- a/src/Umbraco.Core/Models/IContentBase.cs +++ b/src/Umbraco.Core/Models/IContentBase.cs @@ -58,7 +58,7 @@ namespace Umbraco.Core.Models /// Because a dictionary key cannot be null this cannot contain the invariant /// culture name, which must be get or set via the property. /// - IReadOnlyKeyedCollection CultureNames { get; } + IReadOnlyDictionary CultureNames { get; } /// /// Gets the available cultures. diff --git a/src/Umbraco.Core/Models/PropertyGroupCollection.cs b/src/Umbraco.Core/Models/PropertyGroupCollection.cs index 80b663fa05..c5768c66db 100644 --- a/src/Umbraco.Core/Models/PropertyGroupCollection.cs +++ b/src/Umbraco.Core/Models/PropertyGroupCollection.cs @@ -14,10 +14,12 @@ namespace Umbraco.Core.Models /// [Serializable] [DataContract] + //TODO: Change this to ObservableDictionary so we can reduce the INotifyCollectionChanged implementation details public class PropertyGroupCollection : KeyedCollection, INotifyCollectionChanged, IDeepCloneable { private readonly ReaderWriterLockSlim _addLocker = new ReaderWriterLockSlim(); + //fixme: this doesn't seem to be used anywhere internal Action OnAdd; internal PropertyGroupCollection() diff --git a/src/Umbraco.Core/Models/PropertyTypeCollection.cs b/src/Umbraco.Core/Models/PropertyTypeCollection.cs index 47710e04cb..6053a6a5bf 100644 --- a/src/Umbraco.Core/Models/PropertyTypeCollection.cs +++ b/src/Umbraco.Core/Models/PropertyTypeCollection.cs @@ -13,11 +13,13 @@ namespace Umbraco.Core.Models /// [Serializable] [DataContract] + //TODO: Change this to ObservableDictionary so we can reduce the INotifyCollectionChanged implementation details public class PropertyTypeCollection : KeyedCollection, INotifyCollectionChanged, IDeepCloneable { [IgnoreDataMember] private readonly ReaderWriterLockSlim _addLocker = new ReaderWriterLockSlim(); + //fixme: This doesn't seem to be used [IgnoreDataMember] internal Action OnAdd; diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs index 3f1ea3116e..bea6eb9bce 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs @@ -326,9 +326,11 @@ AND umbracoNode.id <> @id", }); } - // delete property types - // ... by excepting entries from db with entries from collections - if (entity.IsPropertyDirty("PropertyTypes") || entity.PropertyTypes.Any(x => x.IsDirty())) + // Delete property types ... by excepting entries from db with entries from collections. + // We check if the entity's own PropertyTypes has been modified and then also check + // any of the property groups PropertyTypes has been modified. + // This specifically tells us if any property type collections have changed. + if (entity.IsPropertyDirty("PropertyTypes") || entity.PropertyGroups.Any(x => x.IsPropertyDirty("PropertyTypes"))) { var dbPropertyTypes = Database.Fetch("WHERE contentTypeId = @Id", new { entity.Id }); var dbPropertyTypeAlias = dbPropertyTypes.Select(x => x.Id); @@ -338,10 +340,11 @@ AND umbracoNode.id <> @id", DeletePropertyType(entity.Id, item); } - // delete tabs - // ... by excepting entries from db with entries from collections + // Delete tabs ... by excepting entries from db with entries from collections. + // We check if the entity's own PropertyGroups has been modified. + // This specifically tells us if the property group collections have changed. List orphanPropertyTypeIds = null; - if (entity.IsPropertyDirty("PropertyGroups") || entity.PropertyGroups.Any(x => x.IsDirty())) + if (entity.IsPropertyDirty("PropertyGroups")) { // todo // we used to try to propagate tabs renaming downstream, relying on ParentId, but diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs index 5cbc987ad0..1659ca6427 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs @@ -359,7 +359,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // names also impact 'edited' foreach (var (culture, name) in content.CultureNames) - if (name != content.GetPublishName(culture)) + if (name.Name != content.GetPublishName(culture)) (editedCultures ?? (editedCultures = new HashSet(StringComparer.OrdinalIgnoreCase))).Add(culture); // insert content variations @@ -521,7 +521,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // names also impact 'edited' foreach (var (culture, name) in content.CultureNames) - if (name != content.GetPublishName(culture)) + if (name.Name != content.GetPublishName(culture)) { edited = true; (editedCultures ?? (editedCultures = new HashSet(StringComparer.OrdinalIgnoreCase))).Add(culture); @@ -1120,7 +1120,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement VersionId = content.VersionId, LanguageId = LanguageRepository.GetIdByIsoCode(culture) ?? throw new InvalidOperationException("Not a valid culture."), Culture = culture, - Name = name, + Name = name.Name, UpdateDate = content.GetUpdateDate(culture) ?? DateTime.MinValue // we *know* there is a value }; @@ -1135,7 +1135,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement VersionId = content.PublishedVersionId, LanguageId = LanguageRepository.GetIdByIsoCode(culture) ?? throw new InvalidOperationException("Not a valid culture."), Culture = culture, - Name = name, + Name = name.Name, UpdateDate = content.GetPublishDate(culture) ?? DateTime.MinValue // we *know* there is a value }; } @@ -1210,7 +1210,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var defaultCulture = LanguageRepository.GetDefaultIsoCode(); content.Name = defaultCulture != null && content.CultureNames.TryGetValue(defaultCulture, out var cultureName) ? cultureName.Name - : content.CultureNames[0].Name; + : content.CultureNames.First().Value.Name; } else { @@ -1265,13 +1265,13 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // get a unique name var otherNames = cultureNames.Select(x => new SimilarNodeName { Id = x.Id, Name = x.Name }); - var uniqueName = SimilarNodeName.GetUniqueName(otherNames, 0, name); + var uniqueName = SimilarNodeName.GetUniqueName(otherNames, 0, name.Name); if (uniqueName == content.GetCultureName(culture)) continue; // update the name, and the publish name if published content.SetCultureName(uniqueName, culture); - if (publishing && content.PublishNames.Contains(culture)) + if (publishing && content.PublishNames.ContainsKey(culture)) content.SetPublishInfo(culture, uniqueName, DateTime.Now); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MacroRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MacroRepository.cs index 546be0b4a8..594f26fa72 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MacroRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MacroRepository.cs @@ -160,7 +160,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement //update the properties if they've changed var macro = (Macro)entity; - if (macro.IsPropertyDirty("Properties") || macro.Properties.Any(x => x.IsDirty())) + if (macro.IsPropertyDirty("Properties") || macro.Properties.Values.Any(x => x.IsDirty())) { var ids = dto.MacroPropertyDtos.Where(x => x.Id > 0).Select(x => x.Id).ToArray(); if (ids.Length > 0) @@ -173,7 +173,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var aliases = new Dictionary(); foreach (var propDto in dto.MacroPropertyDtos) { - var prop = macro.Properties.FirstOrDefault(x => x.Id == propDto.Id); + var prop = macro.Properties.Values.FirstOrDefault(x => x.Id == propDto.Id); if (prop == null) throw new Exception("oops: property."); if (propDto.Id == 0 || prop.IsPropertyDirty("Alias")) { @@ -195,7 +195,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement else { // update - var property = macro.Properties.FirstOrDefault(x => x.Id == propDto.Id); + var property = macro.Properties.Values.FirstOrDefault(x => x.Id == propDto.Id); if (property == null) throw new Exception("oops: property."); if (property.IsDirty()) Database.Update(propDto); diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index 9deadfa5af..a4fb535f04 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -836,7 +836,7 @@ namespace Umbraco.Core.Services.Implement //track the cultures that have changed var culturesChanging = content.ContentType.VariesByCulture() - ? string.Join(",", content.CultureNames.Where(x => x.IsDirty()).Select(x => x.Culture)) + ? string.Join(",", content.CultureNames.Where(x => x.Value.IsDirty()).Select(x => x.Key)) : null; //TODO: Currently there's no way to change track which variant properties have changed, we only have change // tracking enabled on all values on the Property which doesn't allow us to know which variants have changed. @@ -1069,7 +1069,7 @@ namespace Umbraco.Core.Services.Implement } else { - culturesChanging = string.Join(",", content.PublishNames.Where(x => x.IsDirty()).Select(x => x.Culture)); + culturesChanging = string.Join(",", content.PublishNames.Where(x => x.Value.IsDirty()).Select(x => x.Key)); } } @@ -1852,7 +1852,7 @@ namespace Umbraco.Core.Services.Implement //track the cultures changing for auditing var culturesChanging = content.ContentType.VariesByCulture() - ? string.Join(",", content.CultureNames.Where(x => x.IsDirty()).Select(x => x.Culture)) + ? string.Join(",", content.CultureNames.Where(x => x.Value.IsDirty()).Select(x => x.Key)) : null; //TODO: Currently there's no way to change track which variant properties have changed, we only have change // tracking enabled on all values on the Property which doesn't allow us to know which variants have changed. diff --git a/src/Umbraco.Core/Services/Implement/PackagingService.cs b/src/Umbraco.Core/Services/Implement/PackagingService.cs index c0e8f80337..fff865e097 100644 --- a/src/Umbraco.Core/Services/Implement/PackagingService.cs +++ b/src/Umbraco.Core/Services/Implement/PackagingService.cs @@ -1318,7 +1318,7 @@ namespace Umbraco.Core.Services.Implement sortOrder = int.Parse(sortOrderAttribute.Value); } - if (macro.Properties.Any(x => string.Equals(x.Alias, propertyAlias, StringComparison.OrdinalIgnoreCase))) continue; + if (macro.Properties.Values.Any(x => string.Equals(x.Alias, propertyAlias, StringComparison.OrdinalIgnoreCase))) continue; macro.Properties.Add(new MacroProperty(propertyAlias, propertyName, sortOrder, editorAlias)); sortOrder++; } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 8990b6d326..bc6c8fee54 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -109,7 +109,6 @@ - diff --git a/src/Umbraco.Tests/Composing/TypeLoaderTests.cs b/src/Umbraco.Tests/Composing/TypeLoaderTests.cs index 9b23ec3d6b..a7b3d0f446 100644 --- a/src/Umbraco.Tests/Composing/TypeLoaderTests.cs +++ b/src/Umbraco.Tests/Composing/TypeLoaderTests.cs @@ -272,7 +272,7 @@ AnotherContentFinder public void Resolves_Actions() { var actions = _typeLoader.GetActions(); - Assert.AreEqual(34, actions.Count()); + Assert.AreEqual(33, actions.Count()); } [Test] diff --git a/src/Umbraco.Tests/Models/VariationTests.cs b/src/Umbraco.Tests/Models/VariationTests.cs index 0e62c41f46..8a996f9782 100644 --- a/src/Umbraco.Tests/Models/VariationTests.cs +++ b/src/Umbraco.Tests/Models/VariationTests.cs @@ -237,9 +237,9 @@ namespace Umbraco.Tests.Models // variant dictionary of names work Assert.AreEqual(2, content.CultureNames.Count); - Assert.IsTrue(content.CultureNames.Contains(langFr)); + Assert.IsTrue(content.CultureNames.ContainsKey(langFr)); Assert.AreEqual("name-fr", content.CultureNames[langFr].Name); - Assert.IsTrue(content.CultureNames.Contains(langUk)); + Assert.IsTrue(content.CultureNames.ContainsKey(langUk)); Assert.AreEqual("name-uk", content.CultureNames[langUk].Name); } diff --git a/src/Umbraco.Tests/Persistence/Repositories/MacroRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MacroRepositoryTest.cs index a3b9035c8d..5ae25d629f 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MacroRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MacroRepositoryTest.cs @@ -175,7 +175,7 @@ namespace Umbraco.Tests.Persistence.Repositories // Assert Assert.That(macro.HasIdentity, Is.True); Assert.That(macro.Id, Is.EqualTo(4));//With 3 existing entries the Id should be 4 - Assert.Greater(macro.Properties.Single().Id, 0); + Assert.Greater(macro.Properties.Values.Single().Id, 0); } } @@ -268,15 +268,14 @@ namespace Umbraco.Tests.Persistence.Repositories repository.Save(macro); - // Assert - Assert.Greater(macro.Properties.First().Id, 0); //ensure id is returned + Assert.Greater(macro.Properties.Values.First().Id, 0); //ensure id is returned var result = repository.Get(1); - Assert.Greater(result.Properties.First().Id, 0); - Assert.AreEqual(1, result.Properties.Count()); - Assert.AreEqual("new1", result.Properties.First().Alias); - Assert.AreEqual("New1", result.Properties.First().Name); - Assert.AreEqual(3, result.Properties.First().SortOrder); + Assert.Greater(result.Properties.Values.First().Id, 0); + Assert.AreEqual(1, result.Properties.Values.Count()); + Assert.AreEqual("new1", result.Properties.Values.First().Alias); + Assert.AreEqual("New1", result.Properties.Values.First().Name); + Assert.AreEqual(3, result.Properties.Values.First().SortOrder); } } @@ -298,10 +297,10 @@ namespace Umbraco.Tests.Persistence.Repositories // Assert var result = repository.Get(macro.Id); - Assert.AreEqual(1, result.Properties.Count()); - Assert.AreEqual("blah1", result.Properties.First().Alias); - Assert.AreEqual("New1", result.Properties.First().Name); - Assert.AreEqual(4, result.Properties.First().SortOrder); + Assert.AreEqual(1, result.Properties.Values.Count()); + Assert.AreEqual("blah1", result.Properties.Values.First().Alias); + Assert.AreEqual("New1", result.Properties.Values.First().Name); + Assert.AreEqual(4, result.Properties.Values.First().SortOrder); } } @@ -325,7 +324,7 @@ namespace Umbraco.Tests.Persistence.Repositories // Assert result = repository.Get(macro.Id); - Assert.AreEqual(0, result.Properties.Count()); + Assert.AreEqual(0, result.Properties.Values.Count()); } } @@ -355,8 +354,8 @@ namespace Umbraco.Tests.Persistence.Repositories // Assert var result = repository.Get(macro.Id); - Assert.AreEqual(1, result.Properties.Count()); - Assert.AreEqual("blah2", result.Properties.Single().Alias); + Assert.AreEqual(1, result.Properties.Values.Count()); + Assert.AreEqual("blah2", result.Properties.Values.Single().Alias); } } @@ -382,8 +381,8 @@ namespace Umbraco.Tests.Persistence.Repositories // Assert var result = repository.Get(1); - Assert.AreEqual("new1", result.Properties.First().Alias); - Assert.AreEqual("this is a new name", result.Properties.First().Name); + Assert.AreEqual("new1", result.Properties.Values.First().Alias); + Assert.AreEqual("this is a new name", result.Properties.Values.First().Name); } } @@ -408,7 +407,7 @@ namespace Umbraco.Tests.Persistence.Repositories // Assert var result = repository.Get(1); - Assert.AreEqual("newAlias", result.Properties.First().Alias); + Assert.AreEqual("newAlias", result.Properties.Values.First().Alias); } } diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index d5660e708f..99aa9b788a 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -1308,7 +1308,7 @@ namespace Umbraco.Tests.Services var published = ServiceContext.ContentService.SavePublishing(content); //audit log will only show that french was published var lastLog = ServiceContext.AuditService.GetLogs(content.Id).Last(); - Assert.AreEqual($"Published culture fr-fr", lastLog.Comment); + Assert.AreEqual($"Published cultures: fr-fr", lastLog.Comment); //re-get content = ServiceContext.ContentService.GetById(content.Id); @@ -1317,7 +1317,7 @@ namespace Umbraco.Tests.Services published = ServiceContext.ContentService.SavePublishing(content); //audit log will only show that english was published lastLog = ServiceContext.AuditService.GetLogs(content.Id).Last(); - Assert.AreEqual($"Published culture en-uk", lastLog.Comment); + Assert.AreEqual($"Published cultures: en-uk", lastLog.Comment); } [Test] diff --git a/src/Umbraco.Tests/Services/Importing/PackageImportTests.cs b/src/Umbraco.Tests/Services/Importing/PackageImportTests.cs index 767ffd4fc2..b33ff83c4a 100644 --- a/src/Umbraco.Tests/Services/Importing/PackageImportTests.cs +++ b/src/Umbraco.Tests/Services/Importing/PackageImportTests.cs @@ -628,7 +628,7 @@ namespace Umbraco.Tests.Services.Importing // Assert Assert.That(macros.Any(), Is.True); - Assert.That(macros.First().Properties.Any(), Is.True); + Assert.That(macros.First().Properties.Values.Any(), Is.True); var allMacros = ServiceContext.MacroService.GetAll().ToList(); foreach (var macro in macros) diff --git a/src/Umbraco.Tests/Services/MacroServiceTests.cs b/src/Umbraco.Tests/Services/MacroServiceTests.cs index fa86f4baab..6539a37114 100644 --- a/src/Umbraco.Tests/Services/MacroServiceTests.cs +++ b/src/Umbraco.Tests/Services/MacroServiceTests.cs @@ -195,7 +195,7 @@ namespace Umbraco.Tests.Services macro.Properties["blah1"].EditorAlias = "new"; macro.Properties.Remove("blah3"); - var allPropKeys = macro.Properties.Select(x => new { x.Alias, x.Key }).ToArray(); + var allPropKeys = macro.Properties.Values.Select(x => new { x.Alias, x.Key }).ToArray(); macroService.Save(macro); @@ -228,10 +228,10 @@ namespace Umbraco.Tests.Services macroService.Save(macro); var result1 = macroService.GetById(macro.Id); - Assert.AreEqual(4, result1.Properties.Count()); + Assert.AreEqual(4, result1.Properties.Values.Count()); //simulate clearing the sections - foreach (var s in result1.Properties.ToArray()) + foreach (var s in result1.Properties.Values.ToArray()) { result1.Properties.Remove(s.Alias); } @@ -244,7 +244,7 @@ namespace Umbraco.Tests.Services //re-get result1 = macroService.GetById(result1.Id); - Assert.AreEqual(2, result1.Properties.Count()); + Assert.AreEqual(2, result1.Properties.Values.Count()); } diff --git a/src/Umbraco.Tests/Web/TemplateUtilitiesTests.cs b/src/Umbraco.Tests/Web/TemplateUtilitiesTests.cs index df21739c0b..f05e9525c2 100644 --- a/src/Umbraco.Tests/Web/TemplateUtilitiesTests.cs +++ b/src/Umbraco.Tests/Web/TemplateUtilitiesTests.cs @@ -22,6 +22,7 @@ using Umbraco.Web.Security; using Umbraco.Web.Templates; using System.Linq; using Umbraco.Core.Services; +using Umbraco.Core.Configuration; namespace Umbraco.Tests.Web { @@ -49,6 +50,8 @@ namespace Umbraco.Tests.Web Umbraco.Web.Composing.Current.UmbracoContextAccessor = new TestUmbracoContextAccessor(); Udi.ResetUdiTypes(); + + UmbracoConfig.For.SetUmbracoSettings(SettingsForTests.GetDefaultUmbracoSettings()); } [TearDown] @@ -90,7 +93,7 @@ namespace Umbraco.Tests.Web .Returns((UmbracoContext umbCtx, IPublishedContent content, UrlProviderMode mode, string culture, Uri url) => "/my-test-url"); var globalSettings = SettingsForTests.GenerateMockGlobalSettings(); - + var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.Nothing); var publishedContent = Mock.Of(); Mock.Get(publishedContent).Setup(x => x.Id).Returns(1234); diff --git a/src/Umbraco.Web.UI/Umbraco/developer/Macros/EditMacro.aspx.cs b/src/Umbraco.Web.UI/Umbraco/developer/Macros/EditMacro.aspx.cs index 96433be9cc..f2d2d5dcd3 100644 --- a/src/Umbraco.Web.UI/Umbraco/developer/Macros/EditMacro.aspx.cs +++ b/src/Umbraco.Web.UI/Umbraco/developer/Macros/EditMacro.aspx.cs @@ -94,7 +94,7 @@ namespace Umbraco.Web.UI.Umbraco.Developer.Macros { var macroPropertyId = (HtmlInputHidden)((Control)sender).Parent.FindControl("macroPropertyID"); - var property = _macro.Properties.Single(x => x.Id == int.Parse(macroPropertyId.Value)); + var property = _macro.Properties.Values.Single(x => x.Id == int.Parse(macroPropertyId.Value)); _macro.Properties.Remove(property); Services.MacroService.Save(_macro); @@ -104,7 +104,7 @@ namespace Umbraco.Web.UI.Umbraco.Developer.Macros public void MacroPropertyBind() { - macroProperties.DataSource = _macro.Properties.OrderBy(x => x.SortOrder); + macroProperties.DataSource = _macro.Properties.Values.OrderBy(x => x.SortOrder); macroProperties.DataBind(); } @@ -152,7 +152,7 @@ namespace Umbraco.Web.UI.Umbraco.Developer.Macros _macro.Properties.Add(new MacroProperty( macroPropertyAliasNew.Text.Trim(), macroPropertyNameNew.Text.Trim(), - _macro.Properties.Any() ? _macro.Properties.Max(x => x.SortOrder) + 1 : 0, + _macro.Properties.Values.Any() ? _macro.Properties.Values.Max(x => x.SortOrder) + 1 : 0, macroPropertyTypeNew.SelectedValue)); Services.MacroService.Save(_macro); @@ -246,7 +246,7 @@ namespace Umbraco.Web.UI.Umbraco.Developer.Macros var macroElementSortOrder = (TextBox)item.FindControl("macroPropertySortOrder"); var macroElementType = (DropDownList)item.FindControl("macroPropertyType"); - var prop = _macro.Properties.Single(x => x.Id == int.Parse(macroPropertyId.Value)); + var prop = _macro.Properties.Values.Single(x => x.Id == int.Parse(macroPropertyId.Value)); var sortOrder = 0; int.TryParse(macroElementSortOrder.Text, out sortOrder); diff --git a/src/Umbraco.Web/Models/Mapping/MacroMapperProfile.cs b/src/Umbraco.Web/Models/Mapping/MacroMapperProfile.cs index 9977b1cfb1..7bede52021 100644 --- a/src/Umbraco.Web/Models/Mapping/MacroMapperProfile.cs +++ b/src/Umbraco.Web/Models/Mapping/MacroMapperProfile.cs @@ -26,7 +26,7 @@ namespace Umbraco.Web.Models.Mapping .ForMember(dto => dto.AdditionalData, expression => expression.Ignore()); CreateMap>() - .ConvertUsing(macro => macro.Properties.Select(Mapper.Map).ToList()); + .ConvertUsing(macro => macro.Properties.Values.Select(Mapper.Map).ToList()); CreateMap() .ForMember(x => x.View, expression => expression.Ignore()) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index a9903669b9..355a4e7644 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -1199,7 +1199,7 @@ namespace Umbraco.Web.PublishedCache.NuCache foreach (var (culture, name) in names) { - cultureData[culture] = new CultureVariation { Name = name, Date = content.GetUpdateDate(culture) ?? DateTime.MinValue }; + cultureData[culture] = new CultureVariation { Name = name.Name, Date = content.GetUpdateDate(culture) ?? DateTime.MinValue }; } //the dictionary that will be serialized diff --git a/src/Umbraco.Web/Security/WebSecurity.cs b/src/Umbraco.Web/Security/WebSecurity.cs index 85d7128629..709f0d719a 100644 --- a/src/Umbraco.Web/Security/WebSecurity.cs +++ b/src/Umbraco.Web/Security/WebSecurity.cs @@ -33,9 +33,9 @@ namespace Umbraco.Web.Security public WebSecurity(HttpContextBase httpContext, IUserService userService, IGlobalSettings globalSettings) { - _httpContext = httpContext ?? throw new ArgumentNullException(nameof(httpContext)); - _userService = userService ?? throw new ArgumentNullException(nameof(userService)); - _globalSettings = globalSettings ?? throw new ArgumentNullException(nameof(globalSettings)); + _httpContext = httpContext; + _userService = userService; + _globalSettings = globalSettings; } /// diff --git a/src/Umbraco.Web/umbraco.presentation/page.cs b/src/Umbraco.Web/umbraco.presentation/page.cs index 91cf690996..ea1a563f9c 100644 --- a/src/Umbraco.Web/umbraco.presentation/page.cs +++ b/src/Umbraco.Web/umbraco.presentation/page.cs @@ -396,7 +396,7 @@ namespace umbraco return _cultureInfos; return _cultureInfos = _inner.PublishNames - .ToDictionary(x => x.Culture, x => new PublishedCultureInfo(x.Culture, x.Name, x.Date)); + .ToDictionary(x => x.Key, x => new PublishedCultureInfo(x.Key, x.Value.Name, x.Value.Date)); } }