// Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information. using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Runtime.Serialization; using System.Xml; using WrappedPair = System.Json.NGenWrapper>; namespace System.Json { /// /// A JsonObject is an unordered collection of zero or more key/value pairs. /// /// A JsonObject is an unordered collection of zero or more key/value pairs, /// where each key is a String and each value is a , which can be a /// , a , or a JsonObject. [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix", Justification = "Object in the context of JSON already conveys the meaning of dictionary")] [DataContract] public sealed class JsonObject : JsonValue, IDictionary { [DataMember] private Dictionary values = new Dictionary(StringComparer.Ordinal); private List indexedPairs; private int instanceSaveCount; private object saveLock = new object(); /// /// Creates an instance of the class initialized with an /// collection of key/value pairs. /// /// The collection of /// used to initialize the /// key/value pairs /// If items is null. /// If any of the values in the collection /// is a with property of /// value . [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "There's no complexity using this design because nested generic type is atomic type not another collection")] public JsonObject(IEnumerable> items) { AddRange(items); } /// /// Creates an instance of the class initialized with a collection of key/value pairs. /// /// The objects used to initialize the key/value pairs. /// If any of the values in the collection /// is a with property of /// value . public JsonObject(params KeyValuePair[] items) { if (items != null) { AddRange(items); } } /// /// Gets the JSON type of this . The return value /// is always . /// public override JsonType JsonType { get { return JsonType.Object; } } /// /// Gets a collection that contains the keys in this . /// public ICollection Keys { get { return values.Keys; } } /// /// Gets a collection that contains the values in this . /// public ICollection Values { get { return values.Values; } } /// /// Returns the number of key/value pairs in this . /// public override int Count { get { return values.Count; } } /// /// Gets a value indicating whether this JSON CLR object is read-only. /// bool ICollection>.IsReadOnly { get { return ((ICollection>)values).IsReadOnly; } } /// /// Gets or sets the value associated with the specified key. /// /// The key of the value to get or set. /// The associated to the specified key. /// If key is null. /// The property is set and the value is a /// with /// property of value . public override JsonValue this[string key] { get { if (key == null) { throw new ArgumentNullException("key"); } return values[key]; } set { if (value != null && value.JsonType == JsonType.Default) { throw new ArgumentNullException("value", Properties.Resources.UseOfDefaultNotAllowed); } if (key == null) { throw new ArgumentNullException("key"); } bool replacement = values.ContainsKey(key); JsonValue oldValue = null; if (replacement) { oldValue = values[key]; RaiseItemChanging(value, JsonValueChange.Replace, key); } else { RaiseItemChanging(value, JsonValueChange.Add, key); } values[key] = value; if (replacement) { RaiseItemChanged(oldValue, JsonValueChange.Replace, key); } else { RaiseItemChanged(value, JsonValueChange.Add, key); } } } /// /// Safe string indexer for the type. /// /// The key of the element to get. /// If this instance contains the given key and the value corresponding to /// the key is not null, then it will return that value. Otherwise it will return a /// instance with /// equals to . public override JsonValue ValueOrDefault(string key) { if (key != null && ContainsKey(key) && this[key] != null) { return this[key]; } return base.ValueOrDefault(key); } /// /// Adds a specified collection of key/value pairs to this instance. /// /// The collection of key/value pairs to add. /// If items is null. /// If the value of any of the items in the collection /// is a with property of /// value . [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "There's no complexity using this design because nested generic type is atomic type not another collection")] public void AddRange(IEnumerable> items) { if (items == null) { throw new ArgumentNullException("items"); } if (ChangingListenersCount > 0) { foreach (KeyValuePair item in items) { RaiseItemChanging(item.Value, JsonValueChange.Add, item.Key); } } foreach (KeyValuePair item in items) { if (item.Value != null && item.Value.JsonType == JsonType.Default) { throw new ArgumentNullException("items", Properties.Resources.UseOfDefaultNotAllowed); } values.Add(item.Key, item.Value); RaiseItemChanged(item.Value, JsonValueChange.Add, item.Key); } } /// /// Adds the elements from an array of type to this instance. /// /// The array of key/value paris to be added to this instance. /// If the value of any of the items in the array /// is a with property of /// value . public void AddRange(params KeyValuePair[] items) { AddRange(items as IEnumerable>); } /// /// /// /// IEnumerator IEnumerable.GetEnumerator() { return ((IEnumerable)values).GetEnumerator(); } /// /// Adds a key/value pair to this instance. /// /// The key for the element added. /// The for the element added. /// If the value is a /// with property of /// value . public void Add(string key, JsonValue value) { if (value != null && value.JsonType == JsonType.Default) { throw new ArgumentNullException("value", Properties.Resources.UseOfDefaultNotAllowed); } RaiseItemChanging(value, JsonValueChange.Add, key); values.Add(key, value); RaiseItemChanged(value, JsonValueChange.Add, key); } /// /// Adds a key/value pair to this instance. /// /// The key/value pair to be added. /// If the value of the pair is a /// with /// property of value . public void Add(KeyValuePair item) { Add(item.Key, item.Value); } /// /// Checks whether a key/value pair with a specified key exists in this instance. /// /// The key to check for. /// true if this instance contains the key; otherwise, false. public override bool ContainsKey(string key) { return values.ContainsKey(key); } /// /// Removes the key/value pair with a specified key from this instance. /// /// The key of the item to remove. /// true if the element is successfully found and removed; otherwise, false. /// This method returns false if key is not found in this instance. public bool Remove(string key) { JsonValue original = null; bool containsKey = false; if (ChangingListenersCount > 0 || ChangedListenersCount > 0) { containsKey = TryGetValue(key, out original); } if (containsKey && ChangingListenersCount > 0) { RaiseItemChanging(original, JsonValueChange.Remove, key); } bool result = values.Remove(key); if (containsKey && ChangedListenersCount > 0) { RaiseItemChanged(original, JsonValueChange.Remove, key); } return result; } /// /// Attempts to get the value that corresponds to the specified key. /// /// The key of the value to retrieve. /// The primitive or structured object that has the key /// specified. If this object does not contain a key/value pair with the given key, /// this parameter is set to null. /// true if the instance of the contains an element with the /// specified key; otherwise, false. public bool TryGetValue(string key, out JsonValue value) { return values.TryGetValue(key, out value); } /// /// Removes all key/value pairs from this instance. /// public void Clear() { RaiseItemChanging(null, JsonValueChange.Clear, null); values.Clear(); RaiseItemChanged(null, JsonValueChange.Clear, null); } bool ICollection>.Contains(KeyValuePair item) { return ((ICollection>)values).Contains(item); } /// /// Copies the contents of this instance into a specified /// key/value destination array beginning at a specified index. /// /// The destination array of type /// to which the elements of this are copied. /// The zero-based index at which to begin the insertion of the /// contents from this instance. public void CopyTo(KeyValuePair[] array, int arrayIndex) { ((ICollection>)values).CopyTo(array, arrayIndex); } bool ICollection>.Remove(KeyValuePair item) { if (ChangingListenersCount > 0) { if (ContainsKey(item.Key) && EqualityComparer.Default.Equals(item.Value, values[item.Key])) { RaiseItemChanging(item.Value, JsonValueChange.Remove, item.Key); } } bool result = ((ICollection>)values).Remove(item); if (result) { RaiseItemChanged(item.Value, JsonValueChange.Remove, item.Key); } return result; } /// /// Returns an enumerator over the key/value pairs contained in this instance. /// /// An which iterates /// through the members of this instance. protected override IEnumerator> GetKeyValuePairEnumerator() { return values.GetEnumerator(); } /// /// Callback method called when a Save operation is starting for this instance. /// protected override void OnSaveStarted() { lock (saveLock) { instanceSaveCount++; if (indexedPairs == null) { indexedPairs = new List(); foreach (KeyValuePair item in values) { indexedPairs.Add(new WrappedPair(item)); } } } } /// /// Callback method called when a Save operation is finished for this instance. /// protected override void OnSaveEnded() { lock (saveLock) { instanceSaveCount--; if (instanceSaveCount == 0) { indexedPairs = null; } } } /// /// Callback method called to let an instance write the proper JXML attribute when saving this /// instance. /// /// The JXML writer used to write JSON. internal override void WriteAttributeString(XmlDictionaryWriter jsonWriter) { jsonWriter.WriteAttributeString(JXmlToJsonValueConverter.TypeAttributeName, JXmlToJsonValueConverter.ObjectAttributeValue); } /// /// Callback method called during Save operations to let the instance write the start element /// and return the next element in the collection. /// /// The JXML writer used to write JSON. /// The index within this collection. /// The next item in the collection, or null of there are no more items. internal override JsonValue WriteStartElementAndGetNext(XmlDictionaryWriter jsonWriter, int currentIndex) { KeyValuePair currentPair = indexedPairs[currentIndex]; string currentKey = currentPair.Key; if (currentKey.Length == 0) { // special case in JXML world jsonWriter.WriteStartElement(JXmlToJsonValueConverter.ItemElementName, JXmlToJsonValueConverter.ItemElementName); jsonWriter.WriteAttributeString(JXmlToJsonValueConverter.ItemElementName, String.Empty); } else { jsonWriter.WriteStartElement(currentKey); } return currentPair.Value; } [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", Justification = "Context is required by CLR for this to work.")] [OnDeserialized] private void OnDeserialized(StreamingContext context) { saveLock = new object(); } private void RaiseItemChanging(JsonValue child, JsonValueChange change, string key) { if (ChangingListenersCount > 0) { RaiseChangingEvent(this, new JsonValueChangeEventArgs(child, change, key)); } } private void RaiseItemChanged(JsonValue child, JsonValueChange change, string key) { if (ChangedListenersCount > 0) { RaiseChangedEvent(this, new JsonValueChangeEventArgs(child, change, key)); } } } }