2
votes

I have a custom collection which I would like to serialise with JSON.NET:

I need it to serialise the child collections within this custom collection.

On deserialisation I need to hook up PropertyChanged event for items within the collection.

If I pass my collection as is, Json sees IEnumerable and serialises the items in the collection ok, but ignores the other collections within.

If I attribute the collection with [JsonObject] it will serialise all the internal collections but not the internal _list;

if I add [JsonProperty] to the internal _list it will serialise all collections.

But since it sets the _list as a property during deserialization The Add Method of my custom collection is not called and as a therefore the propertyChanged events of the items within _list never get hooked up.

I tried hiding the internal _list and wrapping it with a public getter setter, I thought if during deserialization it used the public setter to set the internal _list I could attach to the item events there, but that does not work either.

Is there anything I can do to during deserialization to get the notifyproperty changed events of the items in the internal _list hooked up?

Edit: I tried a converter:

public class TrackableCollectionConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(TrackableCollectionCollection<ITrackableEntity>);
    }

    public override object ReadJson(
        JsonReader reader, Type objectType,
        object existingValue, JsonSerializer serializer)
    {
        // N.B. null handling is missing
        var surrogate = serializer.Deserialize<TrackableCollectionCollection<ITrackableEntity>>(reader);


        var trackableCollection = new TrackableCollectionCollection<ITrackableEntity>();
        foreach (var el in surrogate)
            trackableCollection.Add(el);

        foreach (var el in surrogate.NewItems)
            trackableCollection.NewItems.Add(el);

        foreach (var el in surrogate.ModifiedItems)
            trackableCollection.ModifiedItems.Add(el);

        foreach (var el in surrogate.DeletedItems)
            trackableCollection.DeletedItems.Add(el);

        return trackableCollection;
    }

    public override void WriteJson(JsonWriter writer, object value,
                                   JsonSerializer serializer)
    {
        serializer.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
        serializer.Serialize(writer, value);
    }

}

Gives error:

{"Message":"An error has occurred.","ExceptionMessage":"The 'ObjectContent`1' type failed to serialize the response body for content type 'application/json; charset=utf-8'.","ExceptionType":"System.InvalidOperationException","StackTrace":null,"InnerException":{"Message":"An error has occurred.","ExceptionMessage":"Token PropertyName in state Property would result in an invalid JSON object. Path '[0]'.","ExceptionType":"Newtonsoft.Json.JsonWriterException","StackTrace":" at Newtonsoft.Json.JsonWriter.AutoComplete(JsonToken tokenBeingWritten)\r\n at Newtonsoft.Json.JsonWriter.InternalWritePropertyName(String name)\r\n at Newtonsoft.Json.JsonTextWriter.WritePropertyName(String name, Boolean escape)\r\n at Newtonsoft.Json.Serialization.JsonProperty.WritePropertyName(JsonWriter writer)\r\n at Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.SerializeObject(JsonWriter writer, Object value, JsonObjectContract contract, JsonProperty member, JsonContainerContract collectionContract, JsonProperty containerProperty)\r\n at Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.SerializeValue(JsonWriter writer, Object value, JsonContract valueContract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerProperty)\r\n at Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.SerializeList(JsonWriter writer, IEnumerable values, JsonArrayContract contract, JsonProperty member, JsonContainerContract collectionContract, JsonProperty containerProperty)\r\n at Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.SerializeValue(JsonWriter writer, Object value, JsonContract valueContract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerProperty)\r\n at Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.Serialize(JsonWriter jsonWriter, Object value, Type objectType)\r\n at Newtonsoft.Json.JsonSerializer.SerializeInternal(JsonWriter jsonWriter, Object value, Type objectType)\r\n at Newtonsoft.Json.JsonSerializer.Serialize(JsonWriter jsonWriter, Object value)\r\n at System.Net.Http.Formatting.BaseJsonMediaTypeFormatter.WriteToStream(Type type, Object value, Stream writeStream, Encoding effectiveEncoding)\r\n at System.Net.Http.Formatting.JsonMediaTypeFormatter.WriteToStream(Type type, Object value, Stream writeStream, Encoding effectiveEncoding)\r\n at System.Net.Http.Formatting.BaseJsonMediaTypeFormatter.WriteToStream(Type type, Object value, Stream writeStream, HttpContent content)\r\n at System.Net.Http.Formatting.BaseJsonMediaTypeFormatter.WriteToStreamAsync(Type type, Object value, Stream writeStream, HttpContent content, TransportContext transportContext, CancellationToken cancellationToken)\r\n--- End of stack trace from previous location where exception was thrown ---\r\n at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)\r\n at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)\r\n at System.Runtime.CompilerServices.TaskAwaiter.GetResult()\r\n at System.Web.Http.WebHost.HttpControllerHandler.d__1b.MoveNext()"}}

here is the collection as I have it so far.

[Serializable]
[JsonObject]
[JsonConverter(typeof(TrackableCollectionConverter))]
public class TrackableCollectionCollection<T> : IList<T> where T : ITrackableEntity
{
    [JsonIgnore]
    IList<T> _list = new List<T>();

    [JsonProperty]
    public IList<T> List
    {
        get { return _list; }
        set 
        { 
            _list = value; 

            foreach(var item in _list)
                item.PropertyChanged += item_PropertyChanged;
        }
    } 

    [DataMember]
    public IList<T> NewItems
    {
        get { return _newItems; }
    }
    IList<T> _newItems = new List<T>();

    [DataMember]
    public IList<T> ModifiedItems
    {
        get { return _modifiedChildren; }
    }
    IList<T> _modifiedChildren = new List<T>();

    [DataMember]
    public IList<T> DeletedItems
    {
        get { return _deletedItems; }
    }
    IList<T> _deletedItems = new List<T>();

    #region Implementation of IEnumerable

    public IEnumerator<T> GetEnumerator()
    {
        return _list.GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

    #endregion

    #region Implementation of ICollection<T>

    public void Add(T item)
    {
        if (item.Id.Equals(default(Guid)))
            _newItems.Add(item);
        else
        {
            // I thought about doing this but that would screw the EF object generation.
            // throw new NotSupportedException("");
        }

        item.PropertyChanged += item_PropertyChanged;

        _list.Add(item);
    }


    public void Clear()
    {
        NewItems.Clear();
        ModifiedItems.Clear();

        foreach(var item in _list)
        {
            item.PropertyChanged -= item_PropertyChanged;
            DeletedItems.Add(item);
        }

        _list.Clear();
    }

    public bool Contains(T item)
    {
        return _list.Contains(item);
    }

    public void CopyTo(T[] array, int arrayIndex)
    {
        _list.CopyTo(array, arrayIndex);
    }

    public bool Remove(T item)
    {
        if (NewItems.Contains(item))
            NewItems.Remove(item);

        if (ModifiedItems.Contains(item))
            ModifiedItems.Remove(item);

        if (!DeletedItems.Contains(item))
            DeletedItems.Add(item);

        return _list.Remove(item);
    }

    public int Count
    {
        get { return _list.Count; }
    }

    public bool IsReadOnly
    {
        get { return _list.IsReadOnly; }
    }

    #endregion

    #region Implementation of IList<T>

    public int IndexOf(T item)
    {
        return _list.IndexOf(item);
    }

    public void Insert(int index, T item)
    {
        if (item.Id.Equals(default(Guid)))
            _newItems.Add(item);
        else
        {
            // I thought about doing this but that would screw the EF object generation.
            // throw new NotSupportedException("");
        }

        item.PropertyChanged += item_PropertyChanged;

        _list.Insert(index, item);
    }

    public void RemoveAt(int index)
    {
        var item = this[index];

        if (NewItems.Contains(item))
            NewItems.Remove(item);

        if (ModifiedItems.Contains(item))
            ModifiedItems.Remove(item);

        if (!DeletedItems.Contains(item))
            DeletedItems.Add(item);

        _list.RemoveAt(index);
    }

    public T this[int index]
    {
        get { return _list[index]; }
        set { _list[index] = value; }
    }
    #endregion
    void item_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
    {
        if (((T)sender).Id.Equals(default(Guid)))
            return; // The Item is already in the newItems collection

        if (ModifiedItems.Contains((T)sender))
            return;

        ModifiedItems.Add((T)sender);

    }
}
2
Sounds like you might to need to make a custom JsonConverter for this class.Brian Rogers
Yeah tried that got a new error, and cant figure out how to resolve it. see edit.John

2 Answers

1
votes

You could serialize your custom container as a JsonObject, as you are doing now, and serialize the embedded list as a proxy ObservableCollection<T>. You can then listen in for additions and removals to the proxy and handle them accordingly. Note -- no custom JsonConverter required. Since I don't have your definition for ITrackableEntity, here's a quick prototype wrapper IList<T> for a List<T>:

[Serializable]
[JsonObject]
public class ListContainer<T> : IList<T> 
{
    [JsonIgnore]
    readonly List<T> _list = new List<T>();

    [JsonProperty("List")]
    private IList<T> SerializableList
    {
        get
        {
            var proxy = new ObservableCollection<T>(_list);
            proxy.CollectionChanged += new System.Collections.Specialized.NotifyCollectionChangedEventHandler(proxy_CollectionChanged);
            return proxy;
        }
        set
        {
            _list.Clear();
            _list.AddRange(value);
        }
    }

    void proxy_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
    {
        if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add)
        {
            foreach (var item in e.NewItems.Cast<T>())
                Add(item);
        }
        else if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Remove)
        {
            foreach (var item in e.NewItems.Cast<T>())
                Remove(item);
        }
        else
        {
            Debug.Assert(false);
            throw new NotImplementedException();
        }
    }

    [JsonIgnore]
    public int Count
    {
        get { return _list.Count; }
    }

    [JsonIgnore]
    public bool IsReadOnly
    {
        get { return ((IList<T>)_list).IsReadOnly; }
    }

    // Everything beyond here is boilerplate.

    #region IList<T> Members

    public int IndexOf(T item)
    {
        return _list.IndexOf(item);
    }

    public void Insert(int index, T item)
    {
        _list.Insert(index, item);
    }

    public void RemoveAt(int index)
    {
        _list.RemoveAt(index);
    }

    public T this[int index]
    {
        get
        {
            return _list[index];
        }
        set
        {
            _list[index] = value;
        }
    }

    #endregion

    #region ICollection<T> Members

    public void Add(T item)
    {
        _list.Add(item);
    }

    public void Clear()
    {
        _list.Clear();
    }

    public bool Contains(T item)
    {
        return _list.Contains(item);
    }

    public void CopyTo(T[] array, int arrayIndex)
    {
        _list.CopyTo(array, arrayIndex);
    }

    public bool Remove(T item)
    {
        return _list.Remove(item);
    }

    #endregion

    #region IEnumerable<T> Members

    public IEnumerator<T> GetEnumerator()
    {
        return _list.GetEnumerator();
    }

    #endregion

    #region IEnumerable Members

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

    #endregion
}

And then, to test:

    public static void TestListContainerJson()
    {
        var list = new ListContainer<int>();
        list.Add(101);
        list.Add(102);
        list.Add(103);

        var json = JsonConvert.SerializeObject(list);
        var newList = JsonConvert.DeserializeObject<ListContainer<int>>(json);
        Debug.Assert(list.SequenceEqual(newList)); // No assert.
    }

Update

It turns out that Json.NET follows the same pattern as XmlSerializer: if you serialize the proxy list as an array, the setter will be called with the fully populated array after being read, and you can add them as required:

[Serializable]
[JsonObject]
public class ListContainer<T> : IList<T>
{
    [JsonIgnore]
    readonly List<T> _list = new List<T>();

    [JsonProperty("List")]
    private T [] SerializableList
    {
        get
        {
            return _list.ToArray();
        }
        set
        {
            Clear();
            foreach (var item in value)
                Add(item);
        }
    }

    [JsonIgnore]
    public int Count
    {
        get { return _list.Count; }
    }

    [JsonIgnore]
    public bool IsReadOnly
    {
        get { return ((IList<T>)_list).IsReadOnly; }
    }

    // Everything beyond here is boilerplate.
}

This is much cleaner than my first solution.

Also, I suspect that your NewItems and ModifiedItems list contain references to items in the main _list. By default Json.NET will effectively clone these during serialization & deserialization. To avoid this, look into the PreserveReferencesHandling functionality. More here.

0
votes

I solved my issue.

First:[JsonConverter(typeof(TrackableCollectionConverter))] should not be on the class definition. other than that TrackableCollection remains untouched.

I have modified my converter thus:

public class TrackableCollectionConverter<TEntity, TDeserialiseType> : JsonConverter where TEntity: ITrackableEntity
{
    public override bool CanConvert(Type objectType)
    {
        return true;
        //return objectType == typeof(TrackableCollectionCollection<ITrackableEntity>);
    }

    public override object ReadJson(
        JsonReader reader, Type objectType,
        object existingValue, JsonSerializer serializer)
    {
        // N.B. null handling is missing
        var surrogate = serializer.Deserialize<TDeserialiseType>(reader) as TrackableCollectionCollection<TEntity>;



        var trackablecollection = new TrackableCollectionCollection<TEntity>();
        foreach (var el in surrogate)
            trackablecollection.Add(el);

        foreach (var el in surrogate.NewItems)
            trackablecollection.NewItems.Add(el);

        foreach (var el in surrogate.ModifiedItems)
            trackablecollection.ModifiedItems.Add(el);

        foreach (var el in surrogate.DeletedItems)
            trackablecollection.DeletedItems.Add(el);

        return trackablecollection;
    }

    public override void WriteJson(JsonWriter writer, object value,
                                   JsonSerializer serializer)
    {
        serializer.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
        serializer.Serialize(writer, value);
    }

}

And finally put the CustomConverter attribute in the correct place. on the Entity Property. In my case I have an Entity called Parent:

[JsonObject(IsReference = true)]
[DataContract(IsReference = true)]
public class Parent : TrackableEntityBase
{
    [DataMember]
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public Guid ParentId
    {
        get { return base.Id ; }
        set
        {
            if (base.Id.Equals(default(Guid)))
                base.Id = value;

            if (base.Id.Equals(value))
                return;

            throw new InvalidOperationException("Primary Keys cannot be changed once set.");
        }
    }

    [DataMember]
    public String Name 
    {
        get { return _name; }
        set
        {
            if (!String.IsNullOrWhiteSpace(_name) && _name.Equals(value, StringComparison.Ordinal))
            {
                return;
            }

            _name = value;
            OnPropertyChanged("Name");
        }
    }
    String _name;

    [DataMember]
    [JsonConverter(typeof(TrackableCollectionConverter<Child, TrackableCollectionCollection<Child>>))]
    public virtual TrackableCollectionCollection<Child> Children { get; set; }
}

This works well producing json like this:

{"$id":"1","ParentId":"6d884973-5060-e411-8265-cffad877042b","Name":"Parent1","Children":{"List":[{"$id":"2","ChildId":"5bd66353-3f61-e411-8265-cffad877042b","ParentId":"6d884973-5060-e411-8265-cffad877042b","Name":"Billy","Parent":{"$ref":"1"},"Id":"5bd66353-3f61-e411-8265-cffad877042b","IsModified":true}],"NewItems":[],"ModifiedItems":[{"$ref":"2"}],"DeletedItems":[],"Count":1,"IsReadOnly":false},"Id":"6d884973-5060-e411-8265-cffad877042b","IsModified":true}