936 lines
28 KiB
C#
936 lines
28 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.ComponentModel;
|
|
using System.Diagnostics;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using JD.Collections;
|
|
using JD.Collections.Extensions;
|
|
using JD.Kernel.Classes;
|
|
using JD.Kernel.Interfaces;
|
|
using JD.Kernel.StaticHelpers;
|
|
using JD.Reflection;
|
|
using JD.Serializers.Xml;
|
|
|
|
namespace JD.PropertyService
|
|
{
|
|
/// <summary>
|
|
/// A collection of properties and sub-properties.
|
|
/// </summary>
|
|
[Serializable]
|
|
public class Properties : IProperties, ICloneable<Properties>
|
|
{
|
|
#region Constants
|
|
|
|
private const string PropertiesElement = "Properties";
|
|
private const string PropertyName = "Property";
|
|
private const string NameAttribute = "name";
|
|
private const string ValueAttribute = "value";
|
|
private const string TypeAttribute = "type";
|
|
|
|
#endregion
|
|
|
|
#region Variables
|
|
|
|
private readonly PropertyCollection m_properties = new PropertyCollection();
|
|
private readonly IProperties m_parent;
|
|
private int m_isUpdatingCount;
|
|
private readonly TypeConverterCache _typeConverter = new TypeConverterCache();
|
|
|
|
#endregion
|
|
|
|
#region Constructor
|
|
|
|
/// <summary>
|
|
/// Default creation constructor.
|
|
/// </summary>
|
|
public Properties()
|
|
: this(null, null)
|
|
{
|
|
}
|
|
|
|
private Properties(Properties other, IProperties parent)
|
|
{
|
|
m_parent = parent;
|
|
|
|
if (other != null)
|
|
{
|
|
other.CopyTo(this);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Properties
|
|
|
|
public bool IsUpdating
|
|
{
|
|
get { return m_isUpdatingCount > 0; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Whether these properties are read-only.
|
|
/// </summary>
|
|
public virtual bool IsReadOnly
|
|
{
|
|
get { return false; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Number of values.
|
|
/// </summary>
|
|
public int Count
|
|
{
|
|
get
|
|
{
|
|
return m_properties.Count;
|
|
}
|
|
}
|
|
|
|
public IEnumerable<string> Keys
|
|
{
|
|
get { return m_properties.Select(v => v.Key); }
|
|
}
|
|
|
|
public IEnumerable<object> Values
|
|
{
|
|
get { return m_properties.Select(v => v.Value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Values.
|
|
/// </summary>
|
|
public IEnumerable<string> ValueKeys
|
|
{
|
|
get { return m_properties.Select(v => v.Key); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parent of these properties, if any.
|
|
/// </summary>
|
|
IReadOnlyProperties IReadOnlyProperties.Parent
|
|
{
|
|
get { return Parent; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Root of these properties.
|
|
/// </summary>
|
|
IReadOnlyProperties IReadOnlyProperties.Root
|
|
{
|
|
get { return Root; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the value of the property with the given <paramref name="key"/>.
|
|
/// If no property exists with that key, <see cref="string.Empty"/> is returned.
|
|
/// </summary>
|
|
/// <param name="key">Key of the property to get/set.</param>
|
|
/// <returns>The value of the property.</returns>
|
|
/// <exception cref="ArgumentNullException">Thrown if <paramref name="key"/> is null.</exception>
|
|
public string this[string key]
|
|
{
|
|
get
|
|
{
|
|
return Get(key, string.Empty);
|
|
}
|
|
set
|
|
{
|
|
Set(key, value);
|
|
}
|
|
}
|
|
|
|
object IReadOnlyDictionary<string, object>.this[string key]
|
|
{
|
|
get { return this[key]; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parent of these properties, if any.
|
|
/// </summary>
|
|
public IProperties Parent
|
|
{
|
|
get
|
|
{
|
|
return m_parent;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Root of these properties, if any.
|
|
/// </summary>
|
|
public IProperties Root
|
|
{
|
|
get
|
|
{
|
|
return m_parent == null ? this : m_parent.Root;
|
|
}
|
|
}
|
|
|
|
public IEnumerable<string> SubKeys
|
|
{
|
|
get
|
|
{
|
|
return m_properties.Where(pair => pair.Value is Properties).Select(pair => pair.Key);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Methods
|
|
|
|
#region Get / Set
|
|
|
|
/// <summary>
|
|
/// Gets the value of the property with the given <paramref name="key"/>.
|
|
/// If no property exists with that key, <paramref name="defaultValue"/> is added and returned.
|
|
/// </summary>
|
|
/// <param name="key">Key of the property to get.</param>
|
|
/// <param name="defaultValue">Default value to use is the property is not found.</param>
|
|
/// <param name="options">Options to use to retrieve the property.</param>
|
|
/// <returns>The value of the property.</returns>
|
|
/// <exception cref="ArgumentNullException">Thrown if <paramref name="key"/> is null.</exception>
|
|
public T Get<T>(string key, T defaultValue, PropertyOptions options)
|
|
{
|
|
Throw.IfNull(key, "key");
|
|
|
|
if (!m_properties.ContainsKey(key))
|
|
{
|
|
if (typeof(T) == typeof(string) && !ReferenceEquals(defaultValue, null) && options != PropertyOptions.DontExpandValue)
|
|
{
|
|
return (T)(object)StringParserService.StringParserService.Parse(defaultValue.ToString());
|
|
}
|
|
|
|
return defaultValue;
|
|
}
|
|
|
|
return GetImpl(key, defaultValue, options, true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the value of the property with the given <paramref name="key"/>.
|
|
/// If no property exists with that key, <paramref name="defaultValue"/> is added and returned.
|
|
/// </summary>
|
|
/// <param name="key">Key of the property to get.</param>
|
|
/// <param name="defaultValue">Default value to use is the property is not found.</param>
|
|
/// <returns>The value of the property.</returns>
|
|
/// <exception cref="ArgumentNullException">Thrown if <paramref name="key"/> is null.</exception>
|
|
public T Get<T>(string key, T defaultValue)
|
|
{
|
|
return Get(key, defaultValue, PropertyOptions.None);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the value of the property with the given <paramref name="key"/>.
|
|
/// If no property exists with that key, an exception is thrown.
|
|
/// </summary>
|
|
/// <param name="key">Key of the property to get.</param>
|
|
/// <returns>The value of the property.</returns>
|
|
/// <exception cref="ArgumentNullException">Thrown if <paramref name="key"/> is null.</exception>
|
|
/// <exception cref="KeyNotFoundException">Thrown if the <paramref name="key"/> doesn't exist in these properties.</exception>
|
|
public T Get<T>(string key)
|
|
{
|
|
Throw.IfNull(key, "key");
|
|
|
|
if (!m_properties.ContainsKey(key))
|
|
{
|
|
throw new KeyNotFoundException();
|
|
}
|
|
|
|
return GetImpl(key, default(T), PropertyOptions.None, false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets sub-properties with the given <paramref name="key"/>.
|
|
/// </summary>
|
|
/// <param name="key">Key to get the properties for.</param>
|
|
/// <returns>The sub-properties for the given <paramref name="key"/> or null if not found.</returns>
|
|
/// <exception cref="ArgumentNullException">Thrown if <paramref name="key"/> is empty.</exception>
|
|
IReadOnlyProperties IReadOnlyProperties.GetSubKey(string key)
|
|
{
|
|
return GetSubKey(key);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates sub-properties with the given <paramref name="key"/> or opens existing one.
|
|
/// </summary>
|
|
/// <param name="key">Key to get the properties for.</param>
|
|
/// <param name="mode"></param>
|
|
/// <returns>The sub-properties for the given <paramref name="key"/> or null if could not create a key (in a read-only instance for example).</returns>
|
|
/// <exception cref="ArgumentNullException">Thrown if <paramref name="key"/> is empty.</exception>
|
|
IReadOnlyProperties IReadOnlyProperties.CreateSubKey(string key, AccessContext mode)
|
|
{
|
|
return CreateSubKey(key, mode);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Try gets the value of the property with the given <paramref name="key"/>.
|
|
/// </summary>
|
|
/// <typeparam name="T"></typeparam>
|
|
/// <param name="key"></param>
|
|
/// <param name="value"></param>
|
|
/// <returns></returns>
|
|
public bool TryGet<T>(string key, out T value)
|
|
{
|
|
Throw.IfNull(key, "key");
|
|
|
|
ValueHolder result;
|
|
if (m_properties.TryGetValue(key, out result))
|
|
{
|
|
value = (T)result.Value;
|
|
return true;
|
|
}
|
|
|
|
value = default(T);
|
|
return false;
|
|
}
|
|
|
|
private T GetImpl<T>(string key, T defaultValue, PropertyOptions options, bool safeCastInstance)
|
|
{
|
|
ValueHolder valueHolder = m_properties[key];
|
|
|
|
Debug.Assert(valueHolder != null);
|
|
|
|
object o = valueHolder.Value;
|
|
if (o is string && typeof (T) != typeof (object))
|
|
{
|
|
if (typeof (T) != typeof (string))
|
|
{
|
|
TypeConverter c = _typeConverter.GetConverter(typeof (T));
|
|
try
|
|
{
|
|
o = c.ConvertFromInvariantString(o.ToString());
|
|
}
|
|
catch (Exception)
|
|
{
|
|
if (safeCastInstance)
|
|
{
|
|
o = defaultValue;
|
|
}
|
|
else
|
|
{
|
|
throw new InvalidCastException();
|
|
}
|
|
}
|
|
|
|
Remove(key, false);
|
|
Set(key, o, false);
|
|
}
|
|
else if (options != PropertyOptions.DontExpandValue)
|
|
{
|
|
o = StringParserService.StringParserService.Parse(o.ToString());
|
|
}
|
|
}
|
|
|
|
if (typeof(T) != typeof(object) && !typeof(T).IsAssignableFrom(o.GetType()))
|
|
{
|
|
return defaultValue;
|
|
}
|
|
|
|
return (T)o;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the value of the property with the given <paramref name="key"/>.
|
|
/// </summary>
|
|
/// <param name="key">Key of the property to set.</param>
|
|
/// <param name="value">Value of the property.</param>
|
|
/// <exception cref="ArgumentNullException">Thrown if <paramref name="key"/> or <paramref name="value"/> is null.</exception>
|
|
/// <exception cref="NotSupportedException">Thrown when <see cref="IsReadOnly"/> is true.</exception>
|
|
public void Set<T>(string key, T value)
|
|
{
|
|
Set(key, value, true);
|
|
}
|
|
|
|
private void Set<T>(string key, T value, bool triggerEvent)
|
|
{
|
|
Throw.IfNull(key, "key");
|
|
Throw.IfNull(value, "value");
|
|
|
|
T oldValue = default(T);
|
|
if (m_properties.ContainsKey(key))
|
|
{
|
|
oldValue = GetImpl(key, oldValue, PropertyOptions.None, true);
|
|
|
|
if (Equals(oldValue, value))
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (triggerEvent && !OnPropertyChanging())
|
|
{
|
|
return;
|
|
}
|
|
|
|
m_properties.Replace(new ValueHolder(key, value));
|
|
|
|
if (triggerEvent)
|
|
{
|
|
OnPropertyChanged(new PropertyChangedEventArgs(this, key, oldValue, value));
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Hierarchy
|
|
|
|
/// <summary>
|
|
/// Creates sub-properties with the given <paramref name="key"/> or opens existing one.
|
|
/// </summary>
|
|
/// <param name="key">Key to get the properties for.</param>
|
|
/// <param name="mode"></param>
|
|
/// <returns>The sub-properties for the given <paramref name="key"/> or null if not found.</returns>
|
|
/// <exception cref="ArgumentNullException">Thrown if <paramref name="key"/> is empty.</exception>
|
|
public IProperties CreateSubKey(string key, AccessContext mode)
|
|
{
|
|
return GetSubKey(key, mode, true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets sub-properties with the given <paramref name="key"/>.
|
|
/// </summary>
|
|
/// <param name="key">Key to get the properties for.</param>
|
|
/// <returns>The sub-properties for the given <paramref name="key"/> or null if not found.</returns>
|
|
/// <exception cref="ArgumentNullException">Thrown if <paramref name="key"/> is empty.</exception>
|
|
/// <exception cref="ArgumentException">Thrown if <paramref name="key"/> contains spaces.</exception>
|
|
public IProperties GetSubKey(string key)
|
|
{
|
|
return GetSubKey(key, AccessContext.Read, false);
|
|
}
|
|
|
|
private IProperties GetSubKey(string key, AccessContext mode, bool createIfNotFound)
|
|
{
|
|
Throw.IfEmpty(key, "key");
|
|
|
|
string[] tokens = key.Split(new [] { '\\', '/' }, StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
if (tokens.Length == 0)
|
|
{
|
|
throw new ArgumentException("Invalid key.");
|
|
}
|
|
|
|
Properties propertiesToReturn = null;
|
|
Properties currentProperties = this;
|
|
foreach (string subKey in tokens)
|
|
{
|
|
currentProperties = propertiesToReturn = currentProperties.GetSubKeyImpl(subKey, mode, createIfNotFound);
|
|
|
|
if (propertiesToReturn == null)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
return propertiesToReturn;
|
|
}
|
|
|
|
private Properties GetSubKeyImpl(string subKey, AccessContext mode, bool createIfNotFound)
|
|
{
|
|
Properties propertiesToReturn;
|
|
if (!TryGet(subKey, out propertiesToReturn) && createIfNotFound)
|
|
{
|
|
propertiesToReturn = new Properties(null, this);
|
|
Set(subKey, propertiesToReturn, mode == AccessContext.Write);
|
|
}
|
|
return propertiesToReturn;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempts to remove the sub-key with the given <paramref name="key"/>.
|
|
/// </summary>
|
|
/// <param name="key">Name of the sub-key to remove.</param>
|
|
/// <param name="mode">PropertyChanged event is called when removed if the mode is in write.</param>
|
|
/// <returns>True if the sub-key existed and was removed. False if there is no sub-key with the name.</returns>
|
|
public bool RemoveSubKey(string key, AccessContext mode)
|
|
{
|
|
if (key == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return Remove(key, mode == AccessContext.Write);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Other
|
|
|
|
/// <summary>
|
|
/// Returns a read-only version of these properties.
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
public IReadOnlyProperties AsReadOnly()
|
|
{
|
|
return new ReadOnlyProperties(this);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Whether the properties contain the specified value.
|
|
/// </summary>
|
|
/// <param name="key">Name of the property to check for.</param>
|
|
/// <returns>True if the value exists. False otherwise.</returns>
|
|
public bool Contains(string key)
|
|
{
|
|
return m_properties.ContainsKey(key);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempts to remove the property with the given <paramref name="key"/>.
|
|
/// </summary>
|
|
/// <param name="key">Name of the property to remove.</param>
|
|
/// <returns>True if the property existed and was removed. False if there is no property with the name.</returns>
|
|
public bool Remove(string key)
|
|
{
|
|
return Remove(key, true);
|
|
}
|
|
|
|
private bool Remove(string key, bool triggerChange)
|
|
{
|
|
if (key == null || !m_properties.ContainsKey(key))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (triggerChange && !OnPropertyChanging())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
object oldValue = m_properties[key];
|
|
|
|
m_properties.Remove(key);
|
|
|
|
if (triggerChange)
|
|
{
|
|
OnPropertyChanged(new PropertyChangedEventArgs(this, key, oldValue, null));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clears all the properties and sub-keys. (Use with caution).
|
|
/// </summary>
|
|
public void Clear()
|
|
{
|
|
if (!OnPropertyChanging())
|
|
{
|
|
return;
|
|
}
|
|
|
|
int count = m_properties.Count;
|
|
m_properties.Clear();
|
|
|
|
if (count != 0)
|
|
{
|
|
OnPropertyChanged(new PropertyChangedEventArgs(this, null, null, null));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overriden.
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
public override string ToString()
|
|
{
|
|
StringBuilder builder = new StringBuilder("[Properties ");
|
|
|
|
if (IsReadOnly)
|
|
{
|
|
builder.Append("(ReadOnly)");
|
|
}
|
|
|
|
builder.Append(": ");
|
|
|
|
foreach (string value in ValueKeys)
|
|
{
|
|
builder.Append(value);
|
|
builder.Append('=');
|
|
builder.Append(Get<object>(value).ToString());
|
|
builder.Append(", ");
|
|
}
|
|
|
|
builder.Remove(builder.Length - 2, 2);
|
|
builder.Append("]");
|
|
|
|
return builder.ToString();
|
|
}
|
|
|
|
public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
|
|
{
|
|
return m_properties.Select(v => new KeyValuePair<string, object>(v.Key, v.Value)).GetEnumerator();
|
|
}
|
|
|
|
IEnumerator IEnumerable.GetEnumerator()
|
|
{
|
|
return GetEnumerator();
|
|
}
|
|
|
|
/// <summary>
|
|
/// True if the content equals the other (does not implement equals to be sure it is still performant in a hash).
|
|
/// </summary>
|
|
/// <param name="other"></param>
|
|
/// <returns></returns>
|
|
public bool IsEqualTo(IReadOnlyProperties other)
|
|
{
|
|
Properties properties = other as Properties;
|
|
if (properties == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return m_properties.IsEquivalentTo(properties.m_properties, new ValueComparer());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds the values to the target.
|
|
/// </summary>
|
|
public void CopyTo(IProperties other)
|
|
{
|
|
Throw.IfNull(other, "other");
|
|
|
|
foreach (var value in m_properties.ThreadSafeEnumerate)
|
|
{
|
|
other.Set(value.Key, CloneValue(value.Value, other));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Try to save the file.
|
|
/// </summary>
|
|
/// <returns>Returns true if executed.</returns>
|
|
public virtual bool Save()
|
|
{
|
|
return false;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Xml Serialization
|
|
|
|
public void WriteToXml(XmlSerializerWriter serializer)
|
|
{
|
|
WriteToXml(serializer, true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes to xml
|
|
/// </summary>
|
|
/// <param name="serializer"></param>
|
|
/// <param name="encapsulate">Whether to encapsulate writing into an element.</param>
|
|
public virtual void WriteToXml(XmlSerializerWriter serializer, bool encapsulate)
|
|
{
|
|
using (encapsulate ? serializer.StartElement(PropertiesElement) : null)
|
|
{
|
|
foreach (var pair in m_properties)
|
|
{
|
|
using (serializer.StartElement(PropertyName))
|
|
{
|
|
WritePropertyToXml(serializer, pair.Key, pair.Value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void WritePropertyToXml(XmlSerializerWriter serializer, string propertyName, object value)
|
|
{
|
|
serializer.Serialize(NameAttribute, propertyName);
|
|
if (value is Properties)
|
|
{
|
|
((Properties)value).WriteToXml(serializer, false);
|
|
}
|
|
else
|
|
{
|
|
serializer.Serialize(TypeAttribute, TypeToString(value.GetType()));
|
|
serializer.Serialize(ValueAttribute, value);
|
|
}
|
|
}
|
|
|
|
public virtual void ReadFromXml(XmlSerializerReader serializer)
|
|
{
|
|
using (OpenEnclosingIfAny(serializer))
|
|
{
|
|
foreach (string elementName in serializer)
|
|
{
|
|
if (elementName == PropertyName && serializer.AttributeExists(NameAttribute))
|
|
{
|
|
ReadPropertyFromXml(serializer);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static IDisposable OpenEnclosingIfAny(XmlSerializerReader serializer)
|
|
{
|
|
if (serializer.NextElementIs(PropertiesElement))
|
|
{
|
|
return serializer.StartElement();
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private void ReadPropertyFromXml(XmlSerializerReader serializer)
|
|
{
|
|
string propertyName = serializer.Serialize(NameAttribute);
|
|
object value;
|
|
|
|
if (serializer.AttributeExists(ValueAttribute))
|
|
{
|
|
string type = serializer.Serialize(TypeAttribute);
|
|
value = serializer.Serialize(ValueAttribute, ConvertType(type));
|
|
}
|
|
else
|
|
{
|
|
Properties properties;
|
|
if (!serializer.AttributeExists(TypeAttribute))
|
|
{
|
|
properties = new Properties(null, this);
|
|
}
|
|
else
|
|
{
|
|
Guid guid = new Guid(serializer.Serialize(TypeAttribute));
|
|
throw new InvalidProgramException("A type must be registered on guid " + guid);
|
|
}
|
|
|
|
properties.ReadFromXml(serializer);
|
|
value = properties;
|
|
}
|
|
Set(propertyName, value);
|
|
}
|
|
|
|
|
|
private static string TypeToString(Type type)
|
|
{
|
|
if (type == typeof(bool))
|
|
{
|
|
return "Bool";
|
|
}
|
|
if (type == typeof(float))
|
|
{
|
|
return "Float";
|
|
}
|
|
if (type == typeof(int))
|
|
{
|
|
return "Int";
|
|
}
|
|
if (type == typeof(string))
|
|
{
|
|
return "String";
|
|
}
|
|
return type.FullName;
|
|
}
|
|
|
|
private static Type ConvertType(string typeName)
|
|
{
|
|
// cute name support.
|
|
switch (typeName)
|
|
{
|
|
case "Bool":
|
|
return typeof(bool);
|
|
case "Float":
|
|
return typeof(float);
|
|
case "Int":
|
|
return typeof(int);
|
|
case "String":
|
|
case "complex":
|
|
case "":
|
|
return typeof(string);
|
|
}
|
|
|
|
Type type = Type.GetType(typeName);
|
|
if (type == null)
|
|
{
|
|
return typeof(string);
|
|
}
|
|
return type;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Clone
|
|
|
|
/// <summary>
|
|
/// Clones the current properties.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This cloning is shallow.
|
|
/// </remarks>
|
|
/// <returns></returns>
|
|
public virtual Properties Clone()
|
|
{
|
|
return new Properties(this, null);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clone
|
|
/// </summary>
|
|
public Properties Clone(IProperties container)
|
|
{
|
|
return new Properties(this, container);
|
|
}
|
|
|
|
object ICloneable.Clone()
|
|
{
|
|
return Clone();
|
|
}
|
|
|
|
private static object CloneValue(object value, IProperties container)
|
|
{
|
|
Properties propertiesToClone = value as Properties;
|
|
if (propertiesToClone != null)
|
|
{
|
|
return propertiesToClone.Clone(container);
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Bulk Update
|
|
|
|
/// <summary>
|
|
/// Starts an update that does not trigger the changed event
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
public IDisposable BeginScopedUpdate()
|
|
{
|
|
m_isUpdatingCount++;
|
|
|
|
return new DisposableHelper(EndUpdate);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ends a scoped update
|
|
/// </summary>
|
|
public void EndUpdate()
|
|
{
|
|
Throw.InvalidOperationIf(!IsUpdating, "No update active!");
|
|
|
|
m_isUpdatingCount--;
|
|
if (!IsUpdating)
|
|
{
|
|
OnPropertyChanged(new PropertyChangedEventArgs(this, null, null, null));
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Events
|
|
|
|
/// <summary>
|
|
/// Raised before a property changes.
|
|
/// </summary>
|
|
/// <returns>False if cancelled (if the property cannot be changed)</returns>
|
|
protected virtual bool OnPropertyChanging()
|
|
{
|
|
if (IsUpdating) // Don't raise event PropertyChanged when updating.
|
|
{
|
|
return true;
|
|
}
|
|
|
|
CancelEventArgs e = new CancelEventArgs();
|
|
PropertyChanging?.Invoke(this, e);
|
|
return !e.Cancel;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Raised before a property changes.
|
|
/// </summary>
|
|
[field: NonSerialized]
|
|
public event EventHandler<CancelEventArgs> PropertyChanging;
|
|
|
|
/// <summary>
|
|
/// Triggered when the value of a property changes.
|
|
/// </summary>
|
|
[field: NonSerialized]
|
|
public event EventHandler<PropertyChangedEventArgs> PropertyChanged;
|
|
|
|
/// <summary>
|
|
/// Overridden. Don't raise event PropertyChanged when updating.
|
|
/// </summary>
|
|
protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
|
|
{
|
|
Properties parent = m_parent as Properties;
|
|
if (parent != null)
|
|
{
|
|
parent.OnPropertyChanged(e);
|
|
}
|
|
|
|
if (!IsUpdating)
|
|
{
|
|
PropertyChanged?.Invoke(this, e);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#endregion
|
|
|
|
#region Nested
|
|
|
|
private class ValueComparer : IEqualityComparer<ValueHolder>
|
|
{
|
|
bool IEqualityComparer<ValueHolder>.Equals(ValueHolder x, ValueHolder y)
|
|
{
|
|
if (!string.Equals(x.Key, y.Key, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (x.Value is Properties)
|
|
{
|
|
return (y.Value is Properties) && (((Properties)x.Value).IsEqualTo((Properties)y.Value));
|
|
}
|
|
|
|
return Equals(x.Value, y.Value);
|
|
}
|
|
|
|
public int GetHashCode(ValueHolder obj)
|
|
{
|
|
if (obj.Value is Properties)
|
|
{
|
|
return ((Properties)obj.Value).Count;
|
|
}
|
|
return obj.Value.GetHashCode();
|
|
}
|
|
}
|
|
|
|
bool IReadOnlyDictionary<string, object>.TryGetValue(string key, out object value)
|
|
{
|
|
return TryGet(key, out value);
|
|
}
|
|
|
|
public bool ContainsKey(string key)
|
|
{
|
|
return m_properties.ContainsKey(key);
|
|
}
|
|
|
|
private class ValueHolder
|
|
{
|
|
public readonly string Key;
|
|
public readonly object Value;
|
|
|
|
public ValueHolder(string key, object value)
|
|
{
|
|
Key = key;
|
|
Value = value;
|
|
}
|
|
}
|
|
|
|
[Serializable]
|
|
private class PropertyCollection : KeyedCollection<string, ValueHolder>
|
|
{
|
|
public PropertyCollection()
|
|
: base(StringComparer.OrdinalIgnoreCase)
|
|
{}
|
|
|
|
protected override string GetKeyForItem(ValueHolder value)
|
|
{
|
|
return value.Key;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|