Profilo di StephenBadCorporateLogoFotoBlogElenchi Strumenti Guida
28 settembre

Making a Designer of Your Own - Mixing and Folding

For those of you who are waiting for my next article on creating a designer for XNA Framework game components, here it is! I've been working on creating a CodePlex project for the sample source code, but this turned out to be more work than I first imagined. I took a few shortcuts when I initially created my little designer, and for the code to work on other people's machines, I had to implement a bunch of additional features (no more hard-coded paths or pre-compiled assets!).

Anyway, as I continue to get that all prepared, I was forced to fix a few bugs in my designer serializer. I'd forgotten how some of my own code worked, so it took a bit of debugging before I figured out what was going on and how to fix it. Certainly there will be more bugs, so I decided to write about how serialization works -- both for my own benefit, and for others.

What The Heck is Serialization?

In the managed designer infrastructure, serialization is the process of converting objects into source code, and deserialization is the process of converting source code into objects. In general, serialization is easier than deserialization, but both are quite complicated. Fortunately, the managed designer infrastructure provides an extensible serialization implementation based on CodeDom. The key word there is "extensible," because it means we can rely on the standard behavior most of the time, and then customize things when we need to.

For convenience in the following description, let me introduce the term serialization manager as the part of the designer infrastructure that coordinates serialization and deserialization.

To deserialize an object from source code, the serialization manager invokes a parser to convert the source code from a file into its CodeDom representation. The CodeDom represents a type declaration and its initialization method. The serialization manager reflects on the base type to retrieve its DesignerSerializer attributes, which identify the specific CodeDom serializers to use. Then the serialization manager will instantiate the appropriate serializer and invoke its Deserialize method.

There are two kinds of serializers: TypeCodeDomSerializer and CodeDomSerializer. The first is for serializing/deserializing a type declaration. The second is for serializing/deserializing statements. To deserialize an object from code, you need both kinds of serializers.

This is a good time for an example.

Let's say that I have the following code in a file called MenuScreen1.cs:

   1:  public class MenuScreen1 : MenuScreen
   2:  {
   3:      private ToggleMenuItem toggleMenuItem1;
   4:   
   5:      public MenuScreen1()
   6:      {
   7:          this.InitializeComponent();
   8:      }
   9:   
  10:      private void InitializeComponent()
  11:      {
  12:          this.toggleMenuItem1 = new Ferpect.GameState.MenuItems.ToggleMenuItem();
  13:          // 
  14:          // toggleMenuItem1
  15:          // 
  16:          this.toggleMenuItem1.Label = "Sound";
  17:          this.toggleMenuItem1.ToggleValue = true;
  18:          // 
  19:          // MainMenuScreen
  20:          // 
  21:          this.MenuItems.Add(this.toggleMenuItem1);
  22:          this.Text = "Options";
  23:      }
  24:  }

When the parser reads this file and converts it to its CodeDom representation, it ignores all properties and methods in the class except the InitializeComponent method. The resulting CodeDom is a CodeTypeDeclaration with fields and one method containing a statement collection.

After parsing, the serialization manager needs to get the serializers for the base type of the class being deserialized. By inspecting the CodeTypeDeclaration, the serialization manager will determine the base class is MenuScreen (as seen on line 1). It then uses ITypeResolutionService to get a Type instance for MenuScreen, and then uses TypeDescriptor to get its DesignerSerializer attributes. If the base type doesn't have any DesignerSerializer attributes, the serialization manager continues searching up its inheritance chain until it finds a base type that identifies a TypeCodeDomSerializer.

After finding the type serializer for MenuScreen, the serialization manager will instantiate the serializer and invoke its Deserialize method, passing it the CodeTypeDeclaration for MenuScreen1. The standard TypeCodeDomSerializer will look at the base type in the CodeTypeDeclaration, MenuScreen, and instantiate it.

This very first object is returned to the serialization manager and placed on the design surface. Since it's the first component on the design surface, it becomes the root component, and its name is the name of the type being deserialized, MenuScreen1. The serialization manager then adds MenuScreen1 to its name table.

At this point, MenuScreen1 is only partly deserialized. After adding the root component to its name table, the serialization manager begins to interpret the code statements in the InitializeComponent method. The InitializeComponent method contains statements that are sorted into groups according to the object reference on the left-hand side.

That is, all the statements referencing "toggleMenuItem1" are grouped together, and so are the statements referencing "MenuScreen1". Each member variable is added to the name table, then for each one, the serialization manager retrieves the CodeDomSerializer for the member variable's type, and uses it to deserialize that member's assignment statements. The last step is to retrieve the CodeDomSerializer for MenuScreen, and use it to deserialize the remaining assignment statements for MenuScreen1.

If there is an assignment statement that references another member variable on the right-hand-side, the serialization manager is used to resolve the name. If the object hasn't been added to the name table yet, then the serialization manager will immediately deserialize the named component's statements so that it can return the instance.

In the example above, toggleMenuItem1 will be deserialized into a ToggleMenuItem instance, and that instance's Label and ToggleValue properties will be initialized to "Sound" and true, respectively.

When MenuScreen1's statements are deserialized, you can see there is a reference to toggleMenuItem1 on line 21. The serializer will resolve this reference by asking the serialization manager for an object with that name. Since toggleMenuItem1 is already deserialized at that point, it is returned, and then can be used to be added to the MenuItems collection.

When the deserialization is complete, the design surface shows a MenuScreen instance that has been initialized exactly like the code in the MenuScreen1.cs file.

Serialization is similar, but works in the opposite direction. The root component is serialized into a type declaration, then each of the other components on the design surface becomes a member variable plus some initialization statements, and finally the root component's initialization statements are generated.

The statements usually are generated into the InitializeComponent method, because that's the only method that will be parsed during deserialization. A designer isn't going to be much use if it can't round-trip from objects to code and back again.

The code file, MenuScreen1.cs may contain other code written by the user. For example, the user may add event handlers. After serializing the design surface to CodeDom, the CodeDom is turned into source code by a code generator. That source code is then strategically inserted into the source file to completely replace existing methods with the same names.

Skillful Substitution Explained

In my last post on designers, I said the following:

Every experienced cook knows how to substitute certain ingredients for others. However, it takes a certain amount of skill and know-how to still end up with a palatable result. (I'm really determined to keep this cooking analogy going...)

The reason I started to write this whole series in the first place was to explain how to re-use the existing Windows Forms designer for XNA Framework game components. The problem with trying to use the Windows Forms designer is that it only works with Windows Forms Control classes, and our XNA Framework game components are not Controls.

As I discussed in previoius posts, it is possible to implement a Windows Forms Control that will create an XNA Framework graphics device and draw an XNA Framework game component. Following my example above, let's imagine that MenuScreen is an XNA Framework game component. To be able to design this component in the Windows Forms designer, I need to deserialize MenuScreen into a Control that can draw a MenuScreen component (let's call that a MenuScreenControl).

I need several things for this. First, I need to put an attribute on MenuScreen to point to a custom type serializer. The type serializer is responsible for deserializing the CodeTypeDeclaration into an object instance. With a very small amount of code, it's possible to create a custom TypeCodeDomSerializer that simply modifies the CodeTypeDeclaration it is given so it looks like MenuScreen1 derives from MenuScreenControl, and then passes it along to the TypeCodeDomSerializer for MenuScreenControl.

The MenuScreenControl instance will use ICustomTypeDescriptor to make it look like a MenuScreen instance when viewed through reflection (design-time reflection is always done with TypeDescriptor). Since serialization uses TypeDescriptor, we don't need to modify the TypeCodeDomSerializer's serialization step -- because the ICustomTypeDescriptor implementation fools the serializer into thinking it's serializing a MenuScreen instance.

The MenuScreenControl implementation is pretty straightforward. It is just a GraphicsDeviceControl with ICustomTypeDescriptor. Since it needs to draw a MenuScreen instance, it makes sense to instantiate one of those and store it in a member variable. Then the ICustomTypeDescriptor implementation just needs to invoke TypeDescriptor on the MenuScreen instance.

Note: There is a bit of futzing required to make the properties work properly in the Properties Window, but I'll explain that another time.

The last thing required is a custom CodeDomSerializer, also identified by an attribute on MenuScreen. This one is required to acquire the CodeDomSerializer of a standard Component, and then just delegate its Serialize method to that instance. The reason for this is because the initialization statements are generated by the CodeDomSerializer, and the standard serializer for a Control will generate additional statements for Control layout that won't compile when applied to a game component.

Wrapping Up the Leftovers

At this point, you're probably thinking, "How many times can this guy write 'serializer' in one post?" I don't blame you. But if you stuck through all this, then you're probably starting to see how extensible the designer framework really is, and how finding the right extensibility points is the key to re-using code and saving yourself a truck load of work.

Commenti

Attendere...
Il commento immesso è troppo lungo. Immetti un commento più breve.
Immissione non effettuata. Riprova.
Impossibile aggiungere il commento al momento. Riprova più tardi.
Per aggiungere un commento è necessaria l'autorizzazione di un genitore. Chiedi autorizzazione
I tuoi genitori hanno disattivato i commenti.
Impossibile eliminare il commento al momento. Riprova più tardi.
Hai raggiunto il numero massimo di commenti pubblicabili giornalmente. Riprova tra 24 ore.
Impossibile lasciare commenti. La funzionalità è stata disattivata perché i sistemi hanno rilevato una possibile attività di spamming dal tuo account. Se ritieni che il tuo account è stato disattivato per errore, contatta il supporto tecnico di Windows Live.
Esegui il seguente controllo di protezione per completare la pubblicazione del commento.
I caratteri digitati nel controllo di protezione devono corrispondere ai caratteri dell'immagine o della riproduzione audio.
Stephen Styrchak ha disattivato i commenti di questa pagina.

Riferimenti

L'URL di riferimento per questo intervento è:
http://badcorporatelogo.spaces.live.com/blog/cns!43EB71B104A2D711!323.trak
Blog che fanno riferimento a questo intervento
  • Nessuno