Intercept list population to assign values in deserialization

心已入冬 提交于 2019-12-31 01:57:08

问题


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.


回答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?.



来源:https://stackoverflow.com/questions/42723879/intercept-list-population-to-assign-values-in-deserialization

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!