3

I have a recursive class (tree hierarchy), that derives from a list, that has the children, and itself, populated from deserialization in JSON.NET.

The TLDR version is that I want to populate a variable in the children, from the parent, on each level of this class where it exists, without using $ref variables from JSON.NET (saves space when storing to a cookie).

For those that followed my question from yesterday, this may look like a duplicate, but it is not. It is the same code, but the old question revolved around setters in JSON not being fired (resolved), and the answer brought about this question (more aptly worded).

The initial call would be:

_Items = Cookies.Instance.GetJObject<CartItems>(COOKIE, jsonSetting);

Which calls:

public T GetJObject<T>(string cookieName, JsonSerializerSettings jset = null)
    {
        string cookieContent = Get(cookieName);
        return JsonConvert.DeserializeObject<T>(cookieContent, jset);
    }

The custom converter class is as follows:

public class JsonCartConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return typeof(CartItem).IsAssignableFrom(objectType);
    }

    public override bool CanWrite
    {
        get
        {
            return false;
        }
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        JObject obj = JObject.Load(reader);

        var type = obj["t"] != null ? (CARTITEMTYPE)obj["t"].Value<int>() : CARTITEMTYPE.GENERIC;
        var item = CartItems.NewByType(type);
        serializer.Populate(obj.CreateReader(), item);
        return item;
    }


    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {

    }
}

The resulting JSON is as follows:

[{"t":1,"i":1,"n":4,"r":false,"c":[{"t":5,"i":2,"n":4,"r":false,"c":[]}]},{"t":1,"i":3,"n":4,"r":false,"c":[{"t":5,"i":4,"n":4,"r":false,"c":[{"t":4,"i":6,"n":14,"r":false,"c":[]}]},{"t":1,"i":5,"n":15,"r":false,"c":[]}]}]

And what I am trying to do, would be similar to this:

public class CartItems : List<CartItem>{

public new CartItem Add(CartItem item)
    {
        item.Parent = this;
        base.Add(item);
        return item;
    }

So the problem being that we have established that deserialization doesn't call the standard Add/Insert methods from a List to populate a list. We know it is creating the list through reflection, but how? Can I intercept the assignment of the child class to the parent class, upon insertion, to assign it a variable in the child class from the parent class (i.e. child.Parent = this)?

I tried poking around in JSON.NET source to find the method in a collection it is using to populate (because even with reflection, it has to be adding them by invoking a method call, right?). Unless... It is doing something like this:

CartItems items = new CartItems() { new GenericItem() { }, new GenericItem() { } };

Edit: CartItems is the class derived from list. It is populated with multiple instances of CartItem.

dudeinco
  • 143
  • 1
  • 11

1 Answers1

1

Your problem is that you are trying to subclass List<T> to maintain parent/child relationships as items are added and removed from the list, however List<T> is not designed to be subclassed in this manner as none of the relevant methods are virtual. Instead you are hiding the Add() method via public new CartItem Add(CartItem item), however it turns out that Json.NET is not calling this replacement method (and there is no reason to assume it would).

Instead, you should use Collection<T> which is expressly designed for this purpose. From the docs:

The Collection<T> class provides protected methods that can be used to customize its behavior when adding and removing items, clearing the collection, or setting the value of an existing item.

Thus your object model should look something like the following:

public class CartItems : Collection<CartItem>
{
    public CartItems() : base() { }

    public CartItems(CartItem parent) : this()
    {
        this.Parent = parent;
    }

    public CartItem Parent { get; private set; }

    protected override void RemoveItem(int index)
    {
        CartItem oldItem = null;
        if (index >= 0 && index < Count)
        {
            oldItem = this[index];
        }

        base.RemoveItem(index);
    }

    protected override void InsertItem(int index, CartItem item)
    {
        base.InsertItem(index, item);
        if (item != null)
            item.Parent = this;
    }

    protected override void SetItem(int index, CartItem item)
    {
        CartItem oldItem = null;
        if (index >= 0 && index < Count)
        {
            oldItem = this[index];
        }

        base.SetItem(index, item);

        if (oldItem != null)
            oldItem.Parent = null;

        if (item != null)
            item.Parent = this;
    }

    protected override void ClearItems()
    {
        foreach (var item in this)
            if (item != null)
                item.Parent = null;
        base.ClearItems();
    }
}

public class CartItem
{
    public CartItem()
    {
        this.Children = new CartItems(this);
    }

    public int t { get; set; }
    public int i { get; set; }
    public int n { get; set; }
    public bool r { get; set; }

    [JsonProperty("c")]
    public CartItems Children { get; private set; }

    [JsonIgnore]
    public CartItems Parent { get; set; }
}

Notice that both CartItem and CartItems have a Parent property. CartItems.Parent is assigned in its constructor. CartItem.Parent is assigned in by the overridden InsertItem, SetItem and RemoveItem methods.

Sample fiddle.

See also Collection<T> versus List<T> what should you use on your interfaces?.

dbc
  • 80,875
  • 15
  • 141
  • 235
  • This is truly a thing of beauty, and I have been beating my head against the wall trying to figure this one out for the longest time. If I could upvote it 100x, I would. I don't know why I didn't come up with this, or if I ever would have. I have been attacking it from all the wrong angles. – dudeinco Mar 10 '17 at 18:30
  • @dudeinco - you could also have `CartItems` inherit from `ObservableCollection` if you needed to, and the overrides will compile and work as-is. – dbc Mar 10 '17 at 18:32
  • Initially, I was actually thinking about that, and may do so, but I had missed the mark on implementation... Thanks again. – dudeinco Mar 10 '17 at 19:24
  • Quick question for clarification: What is the purpose of pulling the old item on SetItem? If it is replaced, shouldn't the parent reference be removed automatically when the old item is destroyed? Same question for ClearItems... – dudeinco Mar 10 '17 at 20:48
  • 1
    I think I get it - you are working under the assumption that the child could be an independent object, and not solely attached to the parent (only referenced by the parent). Got it. – dudeinco Mar 10 '17 at 20:57