// 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 Values => dictionary.Values; #endregion #region INotifyPropertyChanged implementation /// /// Occurs when a property is added or replaced, or when the collection is cleared. /// public event PropertyChangedEventHandler PropertyChanged; #endregion #region IScriptableObject implementation [SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes", Justification = "This member is not expected to be re-implemented in derived classes.")] void IScriptableObject.OnExposedToScriptCode(ScriptEngine engine) { if ((engine != null) && engineSet.TryAdd(engine)) { foreach (var scriptableObject in Values.OfType()) { scriptableObject.OnExposedToScriptCode(engine); } } } #endregion } }