// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.ClearScript.Util;
namespace Microsoft.ClearScript
{
///
/// Represents a scriptable collection of named properties.
///
///
/// If an object that implements this interface is added to a script engine (see
/// AddHostObject), script code
/// will be able to access the properties stored in the collection as if they were members of
/// the object itself, using the script language's native syntax for member access. No
/// other members of the object will be accessible. This interface also allows objects to
/// implement dynamic properties for script languages that support them.
///
public interface IPropertyBag : IDictionary
{
}
///
/// Provides a default implementation.
///
public class PropertyBag : IPropertyBag, INotifyPropertyChanged, IScriptableObject
{
#region data
private readonly Dictionary dictionary;
private readonly ICollection> collection;
private readonly bool isReadOnly;
private readonly ConcurrentWeakSet engineSet = new ConcurrentWeakSet();
#endregion
#region constructors
///
/// Initializes a new writable with the default property name comparer.
///
public PropertyBag()
: this(false)
{
}
///
/// Initializes a new with the default property name comparer.
///
/// True to make the read-only, false to make it writable.
///
/// The host can modify a read-only by calling
/// SetPropertyNoCheck,
/// RemovePropertyNoCheck, or
/// ClearNoCheck.
///
public PropertyBag(bool isReadOnly)
: this(isReadOnly, null)
{
}
///
/// Initializes a new writable .
///
/// The comparer to use for property names, or null to use the default string comparer.
public PropertyBag(IEqualityComparer comparer)
: this(false, comparer)
{
}
///
/// Initializes a new .
///
/// True to make the read-only, false to make it writable.
/// The comparer to use for property names, or null to use the default string comparer.
public PropertyBag(bool isReadOnly, IEqualityComparer comparer)
{
dictionary = new Dictionary(comparer);
collection = dictionary;
this.isReadOnly = isReadOnly;
}
#endregion
#region public members
///
/// Gets the property name comparer for the .
///
public IEqualityComparer Comparer => dictionary.Comparer;
///
/// Sets a property value without checking whether the is read-only.
///
/// The name of the property to set.
/// The property value.
///
/// This operation is never exposed to script code.
///
public void SetPropertyNoCheck(string name, object value)
{
dictionary[name] = value;
NotifyPropertyChanged(name);
NotifyExposedToScriptCode(value);
}
///
/// Removes a property without checking whether the is read-only.
///
/// The name of the property to remove.
/// True if the property was found and removed, false otherwise.
///
/// This operation is never exposed to script code.
///
public bool RemovePropertyNoCheck(string name)
{
if (dictionary.Remove(name))
{
NotifyPropertyChanged(name);
return true;
}
return false;
}
///
/// Removes all properties without checking whether the is read-only.
///
///
/// This operation is never exposed to script code.
///
public void ClearNoCheck()
{
dictionary.Clear();
NotifyPropertyChanged(null);
}
#endregion
#region internal members
internal int EngineCount => engineSet.Count;
private void CheckReadOnly()
{
if (isReadOnly)
{
throw new UnauthorizedAccessException("The object is read-only");
}
}
private void AddPropertyNoCheck(string name, object value)
{
dictionary.Add(name, value);
NotifyPropertyChanged(name);
NotifyExposedToScriptCode(value);
}
private void NotifyPropertyChanged(string name)
{
var handler = PropertyChanged;
handler?.Invoke(this, new PropertyChangedEventArgs(name));
}
private void NotifyExposedToScriptCode(object value)
{
if (value is IScriptableObject scriptableObject)
{
engineSet.ForEach(scriptableObject.OnExposedToScriptCode);
}
}
#endregion
#region IEnumerable implementation
[SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes", Justification = "This member requires explicit implementation to resolve ambiguity.")]
IEnumerator IEnumerable.GetEnumerator()
{
return dictionary.GetEnumerator();
}
#endregion
#region IEnumerable> implementation
[SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes", Justification = "This member requires explicit implementation to resolve ambiguity.")]
IEnumerator> IEnumerable>.GetEnumerator()
{
return dictionary.GetEnumerator();
}
#endregion
#region ICollection> implementation
[SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes", Justification = "This member is not expected to be re-implemented in derived classes.")]
void ICollection>.Add(KeyValuePair item)
{
CheckReadOnly();
SetPropertyNoCheck(item.Key, item.Value);
}
[SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes", Justification = "This member is not expected to be re-implemented in derived classes.")]
void ICollection>.Clear()
{
CheckReadOnly();
ClearNoCheck();
}
[SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes", Justification = "This member is not expected to be re-implemented in derived classes.")]
bool ICollection>.Contains(KeyValuePair item)
{
return collection.Contains(item);
}
[SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes", Justification = "This member is not expected to be re-implemented in derived classes.")]
void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex)
{
collection.CopyTo(array, arrayIndex);
}
[SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes", Justification = "This member is not expected to be re-implemented in derived classes.")]
bool ICollection>.Remove(KeyValuePair item)
{
CheckReadOnly();
if (collection.Remove(item))
{
NotifyPropertyChanged(item.Key);
return true;
}
return false;
}
[SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes", Justification = "This member is not expected to be re-implemented in derived classes.")]
int ICollection>.Count => collection.Count;
[SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes", Justification = "This member is not expected to be re-implemented in derived classes.")]
bool ICollection>.IsReadOnly => isReadOnly;
#endregion
#region IDictionary implementation
///
/// Determines whether the contains a property with the specified name.
///
/// The name of the property to locate.
/// True if the contains a property with the specified name, false otherwise.
public bool ContainsKey(string key)
{
return dictionary.ContainsKey(key);
}
///
/// Adds a property to the .
///
/// The name of the property to add.
/// The property value.
public void Add(string key, object value)
{
CheckReadOnly();
AddPropertyNoCheck(key, value);
}
///
/// Removes a property from the .
///
/// The name of the property to remove.
/// True if the property was successfully found and removed, false otherwise.
public bool Remove(string key)
{
CheckReadOnly();
return RemovePropertyNoCheck(key);
}
///
/// Looks up a property value in the .
///
/// The name of the property to locate.
/// The property value if the property was found, null otherwise.
/// True if the property was found, false otherwise.
public bool TryGetValue(string key, out object value)
{
return dictionary.TryGetValue(key, out value);
}
///
/// Gets or sets a property value in the .
///
/// The name of the property to get or set.
/// The property value.
public object this[string key]
{
get => dictionary[key];
set
{
CheckReadOnly();
SetPropertyNoCheck(key, value);
}
}
///
/// Gets a collection of property names from the .
///
public ICollection Keys => dictionary.Keys;
///
/// Gets a collection of property values from the .
///
public ICollection