2016-05-18 23:34:56 +02:00
using System ;
using System.Collections.Generic ;
using System.Collections.ObjectModel ;
using System.Linq ;
2019-07-19 10:22:44 +02:00
using System.Runtime.Serialization ;
2016-05-18 23:34:56 +02:00
namespace Umbraco.Core.Collections
{
/// <summary>
2017-07-20 11:21:28 +02:00
/// An ObservableDictionary
2016-05-18 23:34:56 +02:00
/// </summary>
/// <remarks>
/// Assumes that the key will not change and is unique for each element in the collection.
/// Collection is not thread-safe, so calls should be made single-threaded.
/// </remarks>
/// <typeparam name="TValue">The type of elements contained in the BindableCollection</typeparam>
/// <typeparam name="TKey">The type of the indexing key</typeparam>
2018-10-19 15:41:28 +11:00
public class ObservableDictionary < TKey , TValue > : ObservableCollection < TValue > , IReadOnlyDictionary < TKey , TValue > , IDictionary < TKey , TValue >
2016-05-18 23:34:56 +02:00
{
2018-10-19 15:41:28 +11:00
protected Dictionary < TKey , int > Indecies { get ; }
protected Func < TValue , TKey > KeySelector { get ; }
2016-05-18 23:34:56 +02:00
/// <summary>
/// Create new ObservableDictionary
/// </summary>
/// <param name="keySelector">Selector function to create key from value</param>
2018-10-23 15:04:41 +02:00
/// <param name="equalityComparer">The equality comparer to use when comparing keys, or null to use the default comparer.</param>
2018-10-19 15:41:28 +11:00
public ObservableDictionary ( Func < TValue , TKey > keySelector , IEqualityComparer < TKey > equalityComparer = null )
2016-05-18 23:34:56 +02:00
{
2018-10-23 15:04:41 +02:00
KeySelector = keySelector ? ? throw new ArgumentException ( "keySelector" ) ;
2018-10-19 15:41:28 +11:00
Indecies = new Dictionary < TKey , int > ( equalityComparer ) ;
2016-05-18 23:34:56 +02:00
}
#region Protected Methods
2018-10-23 15:04:41 +02:00
2016-05-18 23:34:56 +02:00
protected override void InsertItem ( int index , TValue item )
{
var key = KeySelector ( item ) ;
if ( Indecies . ContainsKey ( key ) )
throw new DuplicateKeyException ( key . ToString ( ) ) ;
2018-10-23 15:04:41 +02:00
if ( index ! = Count )
2016-05-18 23:34:56 +02:00
{
foreach ( var k in Indecies . Keys . Where ( k = > Indecies [ k ] > = index ) . ToList ( ) )
{
Indecies [ k ] + + ;
}
}
base . InsertItem ( index , item ) ;
Indecies [ key ] = index ;
}
protected override void ClearItems ( )
{
base . ClearItems ( ) ;
Indecies . Clear ( ) ;
}
protected override void RemoveItem ( int index )
{
var item = this [ index ] ;
var key = KeySelector ( item ) ;
base . RemoveItem ( index ) ;
Indecies . Remove ( key ) ;
foreach ( var k in Indecies . Keys . Where ( k = > Indecies [ k ] > index ) . ToList ( ) )
{
Indecies [ k ] - - ;
}
}
2018-10-23 15:04:41 +02:00
2016-05-18 23:34:56 +02:00
#endregion
2018-10-19 15:41:28 +11:00
public bool ContainsKey ( TKey key )
2016-05-18 23:34:56 +02:00
{
return Indecies . ContainsKey ( key ) ;
}
/// <summary>
/// Gets or sets the element with the specified key. If setting a new value, new value must have same key.
/// </summary>
/// <param name="key">Key of element to replace</param>
/// <returns></returns>
2018-10-19 15:41:28 +11:00
public TValue this [ TKey key ]
2016-05-18 23:34:56 +02:00
{
2018-10-23 15:04:41 +02:00
get = > this [ Indecies [ key ] ] ;
2016-05-18 23:34:56 +02:00
set
{
//confirm key matches
if ( ! KeySelector ( value ) . Equals ( key ) )
throw new InvalidOperationException ( "Key of new value does not match" ) ;
if ( ! Indecies . ContainsKey ( key ) )
{
2018-10-23 15:04:41 +02:00
Add ( value ) ;
2016-05-18 23:34:56 +02:00
}
else
{
this [ Indecies [ key ] ] = value ;
}
}
}
/// <summary>
/// Replaces element at given key with new value. New value must have same key.
/// </summary>
/// <param name="key">Key of element to replace</param>
/// <param name="value">New value</param>
2017-07-20 11:21:28 +02:00
///
2016-05-18 23:34:56 +02:00
/// <exception cref="InvalidOperationException"></exception>
/// <returns>False if key not found</returns>
2018-10-19 15:41:28 +11:00
public bool Replace ( TKey key , TValue value )
2016-05-18 23:34:56 +02:00
{
if ( ! Indecies . ContainsKey ( key ) ) return false ;
2018-10-23 15:04:41 +02:00
2016-05-18 23:34:56 +02:00
//confirm key matches
if ( ! KeySelector ( value ) . Equals ( key ) )
throw new InvalidOperationException ( "Key of new value does not match" ) ;
this [ Indecies [ key ] ] = value ;
return true ;
}
2019-01-21 09:10:08 +01:00
public void ReplaceAll ( IEnumerable < TValue > values )
{
if ( values = = null ) throw new ArgumentNullException ( nameof ( values ) ) ;
Clear ( ) ;
foreach ( var value in values )
{
Add ( value ) ;
}
}
2018-10-19 15:41:28 +11:00
public bool Remove ( TKey key )
2016-05-18 23:34:56 +02:00
{
if ( ! Indecies . ContainsKey ( key ) ) return false ;
2018-10-23 15:04:41 +02:00
RemoveAt ( Indecies [ key ] ) ;
2016-05-18 23:34:56 +02:00
return true ;
}
/// <summary>
/// Allows us to change the key of an item
/// </summary>
/// <param name="currentKey"></param>
/// <param name="newKey"></param>
2018-10-19 15:41:28 +11:00
public void ChangeKey ( TKey currentKey , TKey newKey )
2016-05-18 23:34:56 +02:00
{
if ( ! Indecies . ContainsKey ( currentKey ) )
{
throw new InvalidOperationException ( "No item with the key " + currentKey + "was found in the collection" ) ;
}
2018-10-23 15:04:41 +02:00
2016-05-18 23:34:56 +02:00
if ( ContainsKey ( newKey ) )
{
throw new DuplicateKeyException ( newKey . ToString ( ) ) ;
}
var currentIndex = Indecies [ currentKey ] ;
Indecies . Remove ( currentKey ) ;
Indecies . Add ( newKey , currentIndex ) ;
}
2018-10-19 15:41:28 +11:00
#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 ( ) ;
2018-10-23 15:04:41 +02:00
bool ICollection < KeyValuePair < TKey , TValue > > . IsReadOnly = > false ;
2018-10-19 15:41:28 +11:00
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
2019-07-19 10:22:44 +02:00
/// <summary>
/// The exception that is thrown when a duplicate key inserted.
/// </summary>
/// <seealso cref="System.Collections.ObjectModel.ObservableCollection{TValue}" />
/// <seealso cref="System.Collections.Generic.IReadOnlyDictionary{TKey, TValue}" />
/// <seealso cref="System.Collections.Generic.IDictionary{TKey, TValue}" />
2019-10-04 01:00:09 +02:00
[Obsolete("Throw an ArgumentException when trying to add a duplicate key instead.")]
2019-07-19 10:22:44 +02:00
[Serializable]
2016-05-18 23:34:56 +02:00
internal class DuplicateKeyException : Exception
{
2019-07-19 10:22:44 +02:00
/// <summary>
/// Gets the key.
/// </summary>
/// <value>
/// The key.
/// </value>
public string Key { get ; }
/// <summary>
/// Initializes a new instance of the <see cref="DuplicateKeyException" /> class.
/// </summary>
public DuplicateKeyException ( )
{ }
/// <summary>
/// Initializes a new instance of the <see cref="DuplicateKeyException" /> class.
/// </summary>
/// <param name="key">The key.</param>
2016-05-18 23:34:56 +02:00
public DuplicateKeyException ( string key )
2019-07-19 10:22:44 +02:00
: this ( key , null )
{ }
/// <summary>
/// Initializes a new instance of the <see cref="DuplicateKeyException" /> class.
/// </summary>
/// <param name="key">The key.</param>
/// <param name="innerException">The exception that is the cause of the current exception, or a null reference (<see langword="Nothing" /> in Visual Basic) if no inner exception is specified.</param>
public DuplicateKeyException ( string key , Exception innerException )
: base ( "Attempted to insert duplicate key \"" + key + "\" in collection." , innerException )
2016-05-18 23:34:56 +02:00
{
Key = key ;
}
2019-07-19 10:22:44 +02:00
/// <summary>
/// Initializes a new instance of the <see cref="DuplicateKeyException" /> class.
/// </summary>
/// <param name="info">The <see cref="T:System.Runtime.Serialization.SerializationInfo" /> that holds the serialized object data about the exception being thrown.</param>
/// <param name="context">The <see cref="T:System.Runtime.Serialization.StreamingContext" /> that contains contextual information about the source or destination.</param>
protected DuplicateKeyException ( SerializationInfo info , StreamingContext context )
: base ( info , context )
{
Key = info . GetString ( nameof ( Key ) ) ;
}
/// <summary>
/// When overridden in a derived class, sets the <see cref="T:System.Runtime.Serialization.SerializationInfo" /> with information about the exception.
/// </summary>
/// <param name="info">The <see cref="T:System.Runtime.Serialization.SerializationInfo" /> that holds the serialized object data about the exception being thrown.</param>
/// <param name="context">The <see cref="T:System.Runtime.Serialization.StreamingContext" /> that contains contextual information about the source or destination.</param>
/// <exception cref="ArgumentNullException">info</exception>
public override void GetObjectData ( SerializationInfo info , StreamingContext context )
{
if ( info = = null )
{
throw new ArgumentNullException ( nameof ( info ) ) ;
}
info . AddValue ( nameof ( Key ) , Key ) ;
base . GetObjectData ( info , context ) ;
}
2018-10-23 15:04:41 +02:00
}
2016-05-18 23:34:56 +02:00
}
}