/* ****************************************************************************
*
* Copyright (c) Microsoft Corporation.
*
* This source code is subject to terms and conditions of the Microsoft Public License. A
* copy of the license can be found in the License.html file at the root of this distribution. If
* you cannot locate the Microsoft Public License, please send an email to
* dlr@microsoft.com. By using this source code in any fashion, you are agreeing to be bound
* by the terms of the Microsoft Public License.
*
* You must not remove this notice, or any other, from this software.
*
*
* ***************************************************************************/
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Dynamic;
using System.Dynamic.Utils;
using System.Linq.Expressions;
using System.Runtime.CompilerServices;
#if SILVERLIGHT
using System.Core;
#endif
namespace System.Dynamic {
///
/// Represents an object with members that can be dynamically added and removed at runtime.
///
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix")]
public sealed class ExpandoObject : IDynamicMetaObjectProvider, IDictionary, INotifyPropertyChanged {
internal readonly object LockObject; // the readonly field is used for locking the Expando object
private ExpandoData _data; // the data currently being held by the Expando object
private int _count; // the count of available members
internal readonly static object Uninitialized = new object(); // A marker object used to identify that a value is uninitialized.
internal const int AmbiguousMatchFound = -2; // The value is used to indicate there exists ambiguous match in the Expando object
internal const int NoMatch = -1; // The value is used to indicate there is no matching member
private PropertyChangedEventHandler _propertyChanged;
///
/// Creates a new ExpandoObject with no members.
///
public ExpandoObject() {
_data = ExpandoData.Empty;
LockObject = new object();
}
#region Get/Set/Delete Helpers
///
/// Try to get the data stored for the specified class at the specified index. If the
/// class has changed a full lookup for the slot will be performed and the correct
/// value will be retrieved.
///
internal bool TryGetValue(object indexClass, int index, string name, bool ignoreCase, out object value) {
// read the data now. The data is immutable so we get a consistent view.
// If there's a concurrent writer they will replace data and it just appears
// that we won the ----
ExpandoData data = _data;
if (data.Class != indexClass || ignoreCase) {
/* Re-search for the index matching the name here if
* 1) the class has changed, we need to get the correct index and return
* the value there.
* 2) the search is case insensitive:
* a. the member specified by index may be deleted, but there might be other
* members matching the name if the binder is case insensitive.
* b. the member that exactly matches the name didn't exist before and exists now,
* need to find the exact match.
*/
index = data.Class.GetValueIndex(name, ignoreCase, this);
if (index == ExpandoObject.AmbiguousMatchFound) {
throw Error.AmbiguousMatchInExpandoObject(name);
}
}
if (index == ExpandoObject.NoMatch) {
value = null;
return false;
}
// Capture the value into a temp, so it doesn't get mutated after we check
// for Uninitialized.
object temp = data[index];
if (temp == Uninitialized) {
value = null;
return false;
}
// index is now known to be correct
value = temp;
return true;
}
///
/// Sets the data for the specified class at the specified index. If the class has
/// changed then a full look for the slot will be performed. If the new class does
/// not have the provided slot then the Expando's class will change. Only case sensitive
/// setter is supported in ExpandoObject.
///
internal void TrySetValue(object indexClass, int index, object value, string name, bool ignoreCase, bool add) {
ExpandoData data;
object oldValue;
lock (LockObject) {
data = _data;
if (data.Class != indexClass || ignoreCase) {
// The class has changed or we are doing a case-insensitive search,
// we need to get the correct index and set the value there. If we
// don't have the value then we need to promote the class - that
// should only happen when we have multiple concurrent writers.
index = data.Class.GetValueIndex(name, ignoreCase, this);
if (index == ExpandoObject.AmbiguousMatchFound) {
throw Error.AmbiguousMatchInExpandoObject(name);
}
if (index == ExpandoObject.NoMatch) {
// Before creating a new class with the new member, need to check
// if there is the exact same member but is deleted. We should reuse
// the class if there is such a member.
int exactMatch = ignoreCase ?
data.Class.GetValueIndexCaseSensitive(name) :
index;
if (exactMatch != ExpandoObject.NoMatch) {
Debug.Assert(data[exactMatch] == Uninitialized);
index = exactMatch;
} else {
ExpandoClass newClass = data.Class.FindNewClass(name);
data = PromoteClassCore(data.Class, newClass);
// After the class promotion, there must be an exact match,
// so we can do case-sensitive search here.
index = data.Class.GetValueIndexCaseSensitive(name);
Debug.Assert(index != ExpandoObject.NoMatch);
}
}
}
// Setting an uninitialized member increases the count of available members
oldValue = data[index];
if (oldValue == Uninitialized) {
_count++;
} else if (add) {
throw Error.SameKeyExistsInExpando(name);
}
data[index] = value;
}
// Notify property changed, outside of the lock.
var propertyChanged = _propertyChanged;
if (propertyChanged != null && value != oldValue) {
// Use the canonical case for the key.
propertyChanged(this, new PropertyChangedEventArgs(data.Class.Keys[index]));
}
}
///
/// Deletes the data stored for the specified class at the specified index.
///
internal bool TryDeleteValue(object indexClass, int index, string name, bool ignoreCase, object deleteValue) {
ExpandoData data;
lock (LockObject) {
data = _data;
if (data.Class != indexClass || ignoreCase) {
// the class has changed or we are doing a case-insensitive search,
// we need to get the correct index. If there is no associated index
// we simply can't have the value and we return false.
index = data.Class.GetValueIndex(name, ignoreCase, this);
if (index == ExpandoObject.AmbiguousMatchFound) {
throw Error.AmbiguousMatchInExpandoObject(name);
}
}
if (index == ExpandoObject.NoMatch) {
return false;
}
object oldValue = data[index];
if (oldValue == Uninitialized) {
return false;
}
// Make sure the value matches, if requested.
//
// It's a shame we have to call Equals with the lock held but
// there doesn't seem to be a good way around that, and
// ConcurrentDictionary in mscorlib does the same thing.
if (deleteValue != Uninitialized && !object.Equals(oldValue, deleteValue)) {
return false;
}
data[index] = Uninitialized;
// Deleting an available member decreases the count of available members
_count--;
}
// Notify property changed, outside of the lock.
var propertyChanged = _propertyChanged;
if (propertyChanged != null) {
// Use the canonical case for the key.
propertyChanged(this, new PropertyChangedEventArgs(data.Class.Keys[index]));
}
return true;
}
///
/// Returns true if the member at the specified index has been deleted,
/// otherwise false. Call this function holding the lock.
///
internal bool IsDeletedMember(int index) {
Debug.Assert(index >= 0 && index <= _data.Length);
if (index == _data.Length) {
// The member is a newly added by SetMemberBinder and not in data yet
return false;
}
return _data[index] == ExpandoObject.Uninitialized;
}
///
/// Exposes the ExpandoClass which we've associated with this
/// Expando object. Used for type checks in rules.
///
internal ExpandoClass Class {
get {
return _data.Class;
}
}
///
/// Promotes the class from the old type to the new type and returns the new
/// ExpandoData object.
///
private ExpandoData PromoteClassCore(ExpandoClass oldClass, ExpandoClass newClass) {
Debug.Assert(oldClass != newClass);
lock (LockObject) {
if (_data.Class == oldClass) {
_data = _data.UpdateClass(newClass);
}
return _data;
}
}
///
/// Internal helper to promote a class. Called from our RuntimeOps helper. This
/// version simply doesn't expose the ExpandoData object which is a private
/// data structure.
///
internal void PromoteClass(object oldClass, object newClass) {
PromoteClassCore((ExpandoClass)oldClass, (ExpandoClass)newClass);
}
#endregion
#region IDynamicMetaObjectProvider Members
DynamicMetaObject IDynamicMetaObjectProvider.GetMetaObject(Expression parameter) {
return new MetaExpando(parameter, this);
}
#endregion
#region Helper methods
private void TryAddMember(string key, object value) {
ContractUtils.RequiresNotNull(key, "key");
// Pass null to the class, which forces lookup.
TrySetValue(null, -1, value, key, false, true);
}
private bool TryGetValueForKey(string key, out object value) {
// Pass null to the class, which forces lookup.
return TryGetValue(null, -1, key, false, out value);
}
private bool ExpandoContainsKey(string key) {
return _data.Class.GetValueIndexCaseSensitive(key) >= 0;
}
// We create a non-generic type for the debug view for each different collection type
// that uses DebuggerTypeProxy, instead of defining a generic debug view type and
// using different instantiations. The reason for this is that support for generics
// with using DebuggerTypeProxy is limited. For C#, DebuggerTypeProxy supports only
// open types (from MSDN http://msdn.microsoft.com/en-us/library/d8eyd8zc.aspx).
private sealed class KeyCollectionDebugView {
private ICollection collection;
public KeyCollectionDebugView(ICollection collection) {
Debug.Assert(collection != null);
this.collection = collection;
}
[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
public string[] Items {
get {
string[] items = new string[collection.Count];
collection.CopyTo(items, 0);
return items;
}
}
}
[DebuggerTypeProxy(typeof(KeyCollectionDebugView))]
[DebuggerDisplay("Count = {Count}")]
private class KeyCollection : ICollection {
private readonly ExpandoObject _expando;
private readonly int _expandoVersion;
private readonly int _expandoCount;
private readonly ExpandoData _expandoData;
internal KeyCollection(ExpandoObject expando) {
lock (expando.LockObject) {
_expando = expando;
_expandoVersion = expando._data.Version;
_expandoCount = expando._count;
_expandoData = expando._data;
}
}
private void CheckVersion() {
if (_expando._data.Version != _expandoVersion || _expandoData != _expando._data) {
//the underlying expando object has changed
throw Error.CollectionModifiedWhileEnumerating();
}
}
#region ICollection Members
public void Add(string item) {
throw Error.CollectionReadOnly();
}
public void Clear() {
throw Error.CollectionReadOnly();
}
public bool Contains(string item) {
lock (_expando.LockObject) {
CheckVersion();
return _expando.ExpandoContainsKey(item);
}
}
public void CopyTo(string[] array, int arrayIndex) {
ContractUtils.RequiresNotNull(array, "array");
ContractUtils.RequiresArrayRange(array, arrayIndex, _expandoCount, "arrayIndex", "Count");
lock (_expando.LockObject) {
CheckVersion();
ExpandoData data = _expando._data;
for (int i = 0; i < data.Class.Keys.Length; i++) {
if (data[i] != Uninitialized) {
array[arrayIndex++] = data.Class.Keys[i];
}
}
}
}
public int Count {
get {
CheckVersion();
return _expandoCount;
}
}
public bool IsReadOnly {
get { return true; }
}
public bool Remove(string item) {
throw Error.CollectionReadOnly();
}
#endregion
#region IEnumerable Members
public IEnumerator GetEnumerator() {
for (int i = 0, n = _expandoData.Class.Keys.Length; i < n; i++) {
CheckVersion();
if (_expandoData[i] != Uninitialized) {
yield return _expandoData.Class.Keys[i];
}
}
}
#endregion
#region IEnumerable Members
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() {
return GetEnumerator();
}
#endregion
}
// We create a non-generic type for the debug view for each different collection type
// that uses DebuggerTypeProxy, instead of defining a generic debug view type and
// using different instantiations. The reason for this is that support for generics
// with using DebuggerTypeProxy is limited. For C#, DebuggerTypeProxy supports only
// open types (from MSDN http://msdn.microsoft.com/en-us/library/d8eyd8zc.aspx).
private sealed class ValueCollectionDebugView {
private ICollection