8
\$\begingroup\$

I've finally gotten to the bottom of a problem and am wondering what my best recourse is. In short, the problem is that XNA's ReflectiveReader reflects into generic type parameters, even if no instance of that generic type is stored in the object being serialized.

An example best demonstrates this. Consider the following model classes:

namespace Model
{
    using System.Collections.Generic;
    using Microsoft.Xna.Framework.Graphics;

    public abstract class Entity
    {
    }

    public sealed class TestEntity : Entity
    {
        public Texture2D Texture
        {
            get;
            set;
        }
    }

    public abstract class EntityData
    {
    }

    public abstract class EntityData<TData, TEntity> : EntityData
        where TData : EntityData
        where TEntity : Entity
    {
    }

    public sealed class TestEntityData : EntityData<TestEntityData, TestEntity>
    {
    }

    public sealed class LevelData
    {
        public List<EntityData> Entities
        {
            get;
            set;
        }
    }
}

Now suppose I want to define an instance of LevelData inside an XML file to be later loaded with the ContentManager (Test.xml):

<?xml version="1.0" encoding="utf-8"?>
<XnaContent xmlns:Model="Model">
  <Asset Type="Model:LevelData">
    <Entities>
      <Item Type="Model:TestEntityData">
      </Item>
    </Entities>
  </Asset>
</XnaContent>

Now consider this simple load logic:

Content.Load<LevelData>("Test");
Content.Load<Texture2D>("Texture");

The first line succeeds, but the second throws an exception:

Microsoft.Xna.Framework.Content.ContentLoadException was unhandled
  Message=Error loading "Texture". ContentTypeReader Microsoft.Xna.Framework.Content.Texture2DReader, Microsoft.Xna.Framework.Graphics, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553 conflicts with existing handler Microsoft.Xna.Framework.Content.ReflectiveReader`1[[Microsoft.Xna.Framework.Graphics.Texture2D, Microsoft.Xna.Framework.Graphics, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553]], Microsoft.Xna.Framework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553 for type Microsoft.Xna.Framework.Graphics.Texture2D.
  Source=Microsoft.Xna.Framework
  StackTrace:
       at Microsoft.Xna.Framework.Content.ContentTypeReaderManager.AddTypeReader(String readerTypeName, ContentReader contentReader, ContentTypeReader reader)
       at Microsoft.Xna.Framework.Content.ContentTypeReaderManager.GetTypeReader(String readerTypeName, ContentReader contentReader, List`1& newTypeReaders)
       at Microsoft.Xna.Framework.Content.ContentTypeReaderManager.ReadTypeManifest(Int32 typeCount, ContentReader contentReader)
       at Microsoft.Xna.Framework.Content.ContentReader.ReadHeader()
       at Microsoft.Xna.Framework.Content.ContentReader.ReadAsset[T]()
       at Microsoft.Xna.Framework.Content.ContentManager.ReadAsset[T](String assetName, Action`1 recordDisposableObject)
       at Microsoft.Xna.Framework.Content.ContentManager.Load[T](String assetName)
       at XnaContentManagerRepro.Game1.LoadContent() in D:\Temp\XnaContentManagerRepro\XnaContentManagerRepro\XnaContentManagerRepro\Game1.cs:line 53
       at Microsoft.Xna.Framework.Game.Initialize()
       at XnaContentManagerRepro.Game1.Initialize() in D:\Temp\XnaContentManagerRepro\XnaContentManagerRepro\XnaContentManagerRepro\Game1.cs:line 39
       at Microsoft.Xna.Framework.Game.RunGame(Boolean useBlockingRun)
       at Microsoft.Xna.Framework.Game.Run()
       at XnaContentManagerRepro.Program.Main(String[] args) in D:\Temp\XnaContentManagerRepro\XnaContentManagerRepro\XnaContentManagerRepro\Program.cs:line 15
  InnerException: 

If I set a breakpoint on the line that loads the texture and then examine the ContentTypeReaderManager.nameToReader member, I see this:

enter image description here

As you can see, a ReflectiveReader is indeed being mapped for the Texture2D type. This stems from my TestEntity class (see the the entries above the one highlighted in the image above). But if you examine my model classes, nothing hanging off LevelData has an instance of TestEntity or even Entity in it!

If I change the TestEntityData class to this:

public sealed class TestEntityData : EntityData<TestEntityData, Entity>
{
}

The exception no longer occurs. That's because TestEntity is never considered, so neither is Texture2D. Thus, the ReflectiveReader is looking at - and following - the generic type parameters in my model classes! I can only assume this is a bug - it makes no sense at all to me why this would be necessary.

My model classes have these generic type parameters for a good reason - they make my model code much simpler. Am I stuck here? Is my only option to refactor my models to never have a generic type parameter of my entity types? I considered using ContentSerializerIgnoreAttribute, but that only works against properties and fields, which makes sense considering they're the only things that should influence serialization.

Anyone have any advice?

\$\endgroup\$
10
  • \$\begingroup\$ I'm not familiar with XNA, but if you remove Texture2D from consideration, how can Load<Texture2D> succeed without raising an exception? Your question is fairly clear but it's not clear how your example relates to it. I would say however that serialisation does have to look at generic types, because otherwise it can't be guaranteed to be able to reconstruct whatever it reads from the stream. \$\endgroup\$
    – Kylotan
    Commented May 23, 2012 at 15:46
  • \$\begingroup\$ Calling Load<Texture2D> works if the reflective reader hasn't got in there first and claimed that it is reponsible for loading textures. If, for example, I skip the call to load my test level then the texture successfully loads using XNA's TextureReader or whatever it's called. I dispute that the generic parameters have any bearing on serialization. Serialization concerns itself only with the state of an object, and the object in question has no entity in it. The generic parameter is only used in methods on the object, not in data. \$\endgroup\$
    – me--
    Commented May 23, 2012 at 16:18
  • \$\begingroup\$ @user13414, serialisation needs to know exactly what kind of object it is in order to recreate it at the other end - there will be constructors to call, for example. And the type of the object includes the specific argument passed as a generic parameter, at least in languages like C# and C++ (maybe not in Java, which implements generics somewhat differently). \$\endgroup\$
    – Kylotan
    Commented May 23, 2012 at 19:16
  • \$\begingroup\$ @Kylotan: the base class is generic, not the subclass (which is the object being serialized). It is a closed generic type, not an open one. \$\endgroup\$
    – me--
    Commented May 25, 2012 at 17:00
  • 2
    \$\begingroup\$ The docs I linked to state that .NET reflection stores information about the generic types regarding their type parameters, and this can be obtained via Type.GetGenericArguments, whether a closed generic type or an open generic type. Maybe the docs are wrong and you are right, but the docs explain why Texture2D is covered by the Reflection system and hence show up in your serialisation code. Maybe you could ask on MSDN since it doesn't seem anybody here has a better idea. \$\endgroup\$
    – Kylotan
    Commented May 28, 2012 at 11:08

1 Answer 1

4
\$\begingroup\$

While it's true that in general, serialization doesn't necessarily have to concern itself with the types of the objects in question and only record representations of their state... not all serialization implementations do that. Most of the built-in .NET serialization methods do record information about types participating in serialization. There are advantages to that choice (allowing for more robust validation) as well as disadvantages (larger serialized object size), but it's not wrong per se and you just have to live with it.

XNA's content pipeline, for your types, traverses the serializable property (and field) graph and creates readers for them. You can see this behavior if you examine the initialization for ReflectiveReader<T> (the Initialize method, not the constructor). It does this via reflection, not based on the actual data in the XML (again, this is verifiable by looking at the reflected code). So it doesn't matter if there is a reference to the texture in your data or not, if there's a Texture2D property in the type's property graph, it will try to create a reader for it as part of the initialization of the content pipeline.

You are not supposed to use direct references to Texture2D objects in your custom content. You may find this thread (or this one, to a lesser degree). The alleged solution to the issue is to use external references to Texture2DContent instead.

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .