Enhance implementation of ObservableDictionary so we can re-use this in more places, changes CultureNameCollection to ObservableDictionary, fixes issue with dirty tracking changed property type collections ... this was working by pure fluke before! Fixes more tests.

This commit is contained in:
Shannon
2018-10-19 15:41:28 +11:00
parent 8dfb0dc35c
commit abeb4e04e5
28 changed files with 190 additions and 192 deletions

View File

@@ -1,16 +0,0 @@
using System.Collections.Generic;
namespace Umbraco.Core.Collections
{
/// <summary>
/// A readonly keyed collection
/// </summary>
/// <typeparam name="T"></typeparam>
public interface IReadOnlyKeyedCollection<TKey, TVal> : IReadOnlyList<TVal>
{
IEnumerable<TKey> Keys { get; }
bool TryGetValue(TKey key, out TVal val);
TVal this[string key] { get; }
bool Contains(TKey key);
}
}

View File

@@ -14,20 +14,21 @@ namespace Umbraco.Core.Collections
/// </remarks>
/// <typeparam name="TValue">The type of elements contained in the BindableCollection</typeparam>
/// <typeparam name="TKey">The type of the indexing key</typeparam>
public class ObservableDictionary<TKey, TValue> : ObservableCollection<TValue>
public class ObservableDictionary<TKey, TValue> : ObservableCollection<TValue>, IReadOnlyDictionary<TKey, TValue>, IDictionary<TKey, TValue>
{
protected Dictionary<TKey, int> Indecies = new Dictionary<TKey, int>();
protected Func<TValue, TKey> KeySelector;
protected Dictionary<TKey, int> Indecies { get; }
protected Func<TValue, TKey> KeySelector { get; }
/// <summary>
/// Create new ObservableDictionary
/// </summary>
/// <param name="keySelector">Selector function to create key from value</param>
public ObservableDictionary(Func<TValue, TKey> keySelector)
public ObservableDictionary(Func<TValue, TKey> keySelector, IEqualityComparer<TKey> equalityComparer = null)
: base()
{
if (keySelector == null) throw new ArgumentException("keySelector");
KeySelector = keySelector;
Indecies = new Dictionary<TKey, int>(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
/// </summary>
/// <param name="key">Key of element to replace</param>
/// <returns></returns>
public virtual TValue this[TKey key]
public TValue this[TKey key]
{
get { return this[Indecies[key]]; }
@@ -112,7 +113,7 @@ namespace Umbraco.Core.Collections
///
/// <exception cref="InvalidOperationException"></exception>
/// <returns>False if key not found</returns>
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
/// </summary>
/// <param name="currentKey"></param>
/// <param name="newKey"></param>
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;
}
/// <summary>
/// Returns all keys
/// </summary>
public IEnumerable<TKey> Keys => Indecies.Keys;
/// <summary>
/// Returns all values
/// </summary>
public IEnumerable<TValue> Values => base.Items;
ICollection<TKey> IDictionary<TKey, TValue>.Keys => Indecies.Keys;
//this will never be used
ICollection<TValue> IDictionary<TKey, TValue>.Values => Values.ToList();
bool ICollection<KeyValuePair<TKey, TValue>>.IsReadOnly
{
get { return false; }
}
IEnumerator<KeyValuePair<TKey, TValue>> IEnumerable<KeyValuePair<TKey, TValue>>.GetEnumerator()
{
foreach (var i in Values)
{
var key = KeySelector(i);
yield return new KeyValuePair<TKey, TValue>(key, i);
}
}
void IDictionary<TKey, TValue>.Add(TKey key, TValue value)
{
Add(value);
}
void ICollection<KeyValuePair<TKey, TValue>>.Add(KeyValuePair<TKey, TValue> item)
{
Add(item.Value);
}
bool ICollection<KeyValuePair<TKey, TValue>>.Contains(KeyValuePair<TKey, TValue> item)
{
return ContainsKey(item.Key);
}
void ICollection<KeyValuePair<TKey, TValue>>.CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex)
{
throw new NotImplementedException();
}
bool ICollection<KeyValuePair<TKey, TValue>>.Remove(KeyValuePair<TKey, TValue> item)
{
return Remove(item.Key);
}
#endregion
internal class DuplicateKeyException : Exception
{

View File

@@ -89,7 +89,7 @@ namespace Umbraco.Core.Models
public readonly PropertyInfo PublishedSelector = ExpressionHelper.GetPropertyInfo<Content, bool>(x => x.Published);
public readonly PropertyInfo ReleaseDateSelector = ExpressionHelper.GetPropertyInfo<Content, DateTime?>(x => x.ReleaseDate);
public readonly PropertyInfo ExpireDateSelector = ExpressionHelper.GetPropertyInfo<Content, DateTime?>(x => x.ExpireDate);
public readonly PropertyInfo PublishNamesSelector = ExpressionHelper.GetPropertyInfo<Content, IReadOnlyCollection<CultureName>>(x => x.PublishNames);
public readonly PropertyInfo PublishNamesSelector = ExpressionHelper.GetPropertyInfo<Content, IReadOnlyDictionary<string, CultureName>>(x => x.PublishNames);
}
/// <summary>
@@ -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);
/// <inheritdoc />
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
/// <inheritdoc/>
[IgnoreDataMember]
public IReadOnlyKeyedCollection<string, CultureName> PublishNames => _publishInfos ?? NoNames;
public IReadOnlyDictionary<string, CultureName> PublishNames => _publishInfos ?? NoNames;
/// <inheritdoc/>
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)
{

View File

@@ -70,7 +70,7 @@ namespace Umbraco.Core.Models
public readonly PropertyInfo DefaultContentTypeIdSelector = ExpressionHelper.GetPropertyInfo<ContentBase, int>(x => x.ContentTypeId);
public readonly PropertyInfo PropertyCollectionSelector = ExpressionHelper.GetPropertyInfo<ContentBase, PropertyCollection>(x => x.Properties);
public readonly PropertyInfo WriterSelector = ExpressionHelper.GetPropertyInfo<ContentBase, int>(x => x.WriterId);
public readonly PropertyInfo CultureNamesSelector = ExpressionHelper.GetPropertyInfo<ContentBase, IReadOnlyCollection<CultureName>>(x => x.CultureNames);
public readonly PropertyInfo CultureNamesSelector = ExpressionHelper.GetPropertyInfo<ContentBase, IReadOnlyDictionary<string, CultureName>>(x => x.CultureNames);
}
protected void PropertiesChanged(object sender, NotifyCollectionChangedEventArgs e)
@@ -156,11 +156,11 @@ namespace Umbraco.Core.Models
/// <inheritdoc />
public bool IsCultureAvailable(string culture)
=> _cultureInfos != null && _cultureInfos.Contains(culture);
=> _cultureInfos != null && _cultureInfos.ContainsKey(culture);
/// <inheritdoc />
[DataMember]
public virtual IReadOnlyKeyedCollection<string, CultureName> CultureNames => _cultureInfos ?? NoNames;
public virtual IReadOnlyDictionary<string, CultureName> CultureNames => _cultureInfos ?? NoNames;
/// <inheritdoc />
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);
}
}

View File

@@ -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<ContentTypeSort> _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<PropertyType> 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
/// </summary>
[IgnoreDataMember]
//fixme should we mark this as EditorBrowsable hidden since it really isn't ever used?
internal PropertyTypeCollection PropertyTypeCollection => _propertyTypes;
internal PropertyTypeCollection PropertyTypeCollection => _noGroupPropertyTypes;
/// <summary>
/// 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)

View File

@@ -12,13 +12,14 @@ namespace Umbraco.Core.Models
/// <summary>
/// The culture names of a content's variants
/// </summary>
public class CultureNameCollection : KeyedCollection<string, CultureName>, INotifyCollectionChanged, IDeepCloneable, IReadOnlyKeyedCollection<string, CultureName>
public class CultureNameCollection : ObservableDictionary<string, CultureName>, IDeepCloneable
{
/// <summary>
/// Creates a new collection from another collection
/// </summary>
/// <param name="names"></param>
public CultureNameCollection(IEnumerable<CultureName> names) : base(StringComparer.InvariantCultureIgnoreCase)
public CultureNameCollection(IEnumerable<CultureName> names)
: base(x => x.Culture, StringComparer.InvariantCultureIgnoreCase)
{
foreach (var n in names)
Add(n);
@@ -27,21 +28,11 @@ namespace Umbraco.Core.Models
/// <summary>
/// Creates a new collection
/// </summary>
public CultureNameCollection() : base(StringComparer.InvariantCultureIgnoreCase)
public CultureNameCollection()
: base(x => x.Culture, StringComparer.InvariantCultureIgnoreCase)
{
}
/// <summary>
/// Returns all keys in the collection
/// </summary>
public IEnumerable<string> 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;
}
/// <summary>
/// Add or update the <see cref="CultureName"/>
/// </summary>
@@ -63,78 +54,20 @@ namespace Umbraco.Core.Models
});
}
/// <summary>
/// Gets the index for a specified culture
/// </summary>
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;
}
/// <summary>
/// Resets the collection to only contain the <see cref="CultureName"/> instances referenced in the <paramref name="names"/> parameter.
/// </summary>
/// <param name="names">The property groups.</param>
/// <remarks></remarks>
internal void Reset(IEnumerable<CultureName> 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));
}
}
}

View File

@@ -133,7 +133,7 @@ namespace Umbraco.Core.Models
/// <para>Because a dictionary key cannot be <c>null</c> this cannot get the invariant
/// name, which must be get via the <see cref="PublishName"/> property.</para>
/// </remarks>
IReadOnlyKeyedCollection<string, CultureName> PublishNames { get; }
IReadOnlyDictionary<string, CultureName> PublishNames { get; }
/// <summary>
/// Gets the published cultures.

View File

@@ -58,7 +58,7 @@ namespace Umbraco.Core.Models
/// <para>Because a dictionary key cannot be <c>null</c> this cannot contain the invariant
/// culture name, which must be get or set via the <see cref="TreeEntityBase.Name"/> property.</para>
/// </remarks>
IReadOnlyKeyedCollection<string, CultureName> CultureNames { get; }
IReadOnlyDictionary<string, CultureName> CultureNames { get; }
/// <summary>
/// Gets the available cultures.

View File

@@ -14,10 +14,12 @@ namespace Umbraco.Core.Models
/// </summary>
[Serializable]
[DataContract]
//TODO: Change this to ObservableDictionary so we can reduce the INotifyCollectionChanged implementation details
public class PropertyGroupCollection : KeyedCollection<string, PropertyGroup>, INotifyCollectionChanged, IDeepCloneable
{
private readonly ReaderWriterLockSlim _addLocker = new ReaderWriterLockSlim();
//fixme: this doesn't seem to be used anywhere
internal Action OnAdd;
internal PropertyGroupCollection()

View File

@@ -13,11 +13,13 @@ namespace Umbraco.Core.Models
/// </summary>
[Serializable]
[DataContract]
//TODO: Change this to ObservableDictionary so we can reduce the INotifyCollectionChanged implementation details
public class PropertyTypeCollection : KeyedCollection<string, PropertyType>, INotifyCollectionChanged, IDeepCloneable
{
[IgnoreDataMember]
private readonly ReaderWriterLockSlim _addLocker = new ReaderWriterLockSlim();
//fixme: This doesn't seem to be used
[IgnoreDataMember]
internal Action OnAdd;

View File

@@ -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<PropertyTypeDto>("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<int> 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

View File

@@ -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<string>(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<string>(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);
}
}

View File

@@ -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<string, string>();
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);

View File

@@ -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.

View File

@@ -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++;
}

View File

@@ -109,7 +109,6 @@
<Compile Include="Attempt.cs" />
<Compile Include="AttemptOfTResult.cs" />
<Compile Include="AttemptOfTResultTStatus.cs" />
<Compile Include="Collections\IReadOnlyKeyedCollection.cs" />
<Compile Include="Components\AuditEventsComponent.cs" />
<Compile Include="BindingRedirects.cs" />
<Compile Include="ByteArrayExtensions.cs" />

View File

@@ -272,7 +272,7 @@ AnotherContentFinder
public void Resolves_Actions()
{
var actions = _typeLoader.GetActions();
Assert.AreEqual(34, actions.Count());
Assert.AreEqual(33, actions.Count());
}
[Test]

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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]

View File

@@ -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)

View File

@@ -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());
}

View File

@@ -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<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Nothing);
var publishedContent = Mock.Of<IPublishedContent>();
Mock.Get(publishedContent).Setup(x => x.Id).Returns(1234);

View File

@@ -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);

View File

@@ -26,7 +26,7 @@ namespace Umbraco.Web.Models.Mapping
.ForMember(dto => dto.AdditionalData, expression => expression.Ignore());
CreateMap<IMacro, IEnumerable<MacroParameter>>()
.ConvertUsing(macro => macro.Properties.Select(Mapper.Map<MacroParameter>).ToList());
.ConvertUsing(macro => macro.Properties.Values.Select(Mapper.Map<MacroParameter>).ToList());
CreateMap<IMacroProperty, MacroParameter>()
.ForMember(x => x.View, expression => expression.Ignore())

View File

@@ -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

View File

@@ -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;
}
/// <summary>

View File

@@ -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));
}
}