Profilo di StephenBadCorporateLogoFotoBlogElenchi Strumenti Guida
27 luglio

Making a Designer of Your Own - Soup Stock

In my last couple posts, I've been explaining concepts needed to customize the managed designer infrastructure in Visual Studio. I've been forced to put this project aside for quite a while on account of being too busy with other things... But this weekend I really need to think about something other than work for a while. Despite Shawn thinking I'm nuts for having the same hobby as my work, the important thing for me is that I work on different problems. Thinking about my own programming problems doesn't involve quite the stress as thinking about programming problems for work.

Anyway, I've been using the analogy of learning to cook as the theme of my blog posts. In this post, I'm going to describe a set of base classes that we can re-use for nearly any type of designable component. In cooking terms, it'll be like creating a soup stock. It forms the basis of many dishes, and each one can be uniquely characterized by adding different elements!

The Concept

I want to create a visual designer for an XNA Framework drawable component, so the first thing I need is a way to render it in a Visual Studio editor. When I first thought about this, two things came to mind. First, there is already a designer for Windows Forms Controls. Second, the Creators Club Online web site has a sample showing how to render XNA Framework graphics classes in a Windows Form Control.

At this point, it almost seems like there's nothing left to do! On the other hand, I don't really want a Windows Forms Control in my game because Windows Forms is not supported on either Xbox 360 or Zune. I would prefer to keep the code in my game libraries platform-neutral, so I can use the same components no matter what platform I'm targeting. Hmm...

I really want a designable XNA Framework game component that doesn't derive from Control, but creates a Control in the design surface when opened in design view. Furthermore, I want to edit the component's properties instead of the Control's properties. Sounds difficult -- and it is -- but applying the right ingredients to a powerful stock will result in exactly the secret sauce I'm after.

For now, let's focus on building the right stock. Later, I'll explain the switcheroo.

The Prep

The sample (here) provides source code for GraphicsDeviceControl, a Windows Forms Control that renders XNA Framework content. It's trivial to derive from this existing class and override the Draw method to draw whatever XNA Framework content that I want. That means it's trivial to make a designable Control that uses the XNA Framework to render itself.

To get started, we need to prepare a couple things. First, I'd like a base class for my designable components. The XNA Framework's DrawableGameComponent isn't suitable as a base for my designable components because it requires a Game class instance to be provided to its constructor. At design-time, we're not going to have a Game instance, so we wouldn't be able to instantiate and use it. We also would like the base class to derive from Component so that Visual Studio recognizes it as designable (at some point, we're going to need this, even if I haven't explained how it will work yet).

To start, we need two classes: the first will derive from GraphicsDeviceControl, and the second will be a modified version of DrawableGameComponent.

Class Description
ViewControl<TComponent> Derives from GraphicsDeviceControl, the control from the sample I mentioned earlier. It instantiates TComponent, which must implement IComponent, IDrawable, and IGameComponent. This control's Draw method will clear the device and invoke Draw on the TComponent instance. ViewControl<> is intended to provide the design-time view for TComponent, so it will also include logic to draw at a fixed timestep so that it doesn't monopolize the IDE's CPU usage.
ViewComponent A base implementation of IComponent, IDrawable, and IGameComponent. It is very similar to DrawableGameComponent from the XNA Framework, but differs in that it doesn't depend on a Game class being passed to its constructor.

To create a specific designable component, I'll need to introduce more classes. However, each specific designable component can re-use these same base classes. We'll look at examples another time.

The Code

I put the code files below on my skydrive, with links below.

Some of the code files are copied or derived from samples that were made available under the Microsoft Permissive License (Ms-PL). In accordance with the license, my derivative source can only be made available under the same license. Be sure to read it before looking at any of the code files!

For your convenience, I'll note the source of the original code file and a summary of the changes I've made (at least those I remember).

File Description
GraphicsDeviceService.cs
  • Originally from the WinForms sample.
  • There were two Interlocked references in this class that worried me. Use of Interlocked implies it should be thread-safe - but if that's the case, the sample code contains several bugs! I fixed the race conditions in AddRef/Release just in case, but there are more I didn't fix. [Edit: Oops! It turns out there wasn't a race condition there at all! The class is not intended to be thread-safe in general, except for the particular case of handling a finalizer thread, which it was doing correctly.]
GraphicsDeviceControl.cs
  • Originally from the WinForms sample.
  • Removed the "if (!DesignMode)" condition from OnCreateControl. The whole purpose of this control is to render our graphics at design-time! :-)
  • Switched to using the .NET Framework's ServiceContainer rather than the one defined in the sample (which does the same thing).
ViewComponent.cs This is the base class I want to use for my designable objects. It derives from Component to tie into the designer infrastructure. It also implements IDrawable, IGameComponent, and IUpdateable. These interfaces are implemented so that the specific components you create from this base class can be plugged into any XNA Framework game in a standard way.

There is an obvious problem with IGameComponent in that its Initialize method doesn't take any arguments. It's nearly useless as an interface without providing an IServiceProvider instance, because for every object implementing it, you need to define some custom mechanism outside of the interface to connect the object to its environment. To work around this problem, ViewComponent uses a ContentManager property named Content. This property must be set before Initialize is called so that the object can query its environment through the ContentManager's ServiceProvider property.

The rest of the events, properties, and methods are implemented as simply as they can be. Attributes are used to provide standard design-time metadata.
ViewControl.cs This is the control that will draw our components at design-time. It has to serve about the same purpose as the Game class in a game program, which is to provide a graphics device and other services, and to invoke the XNA Framework interface methods at the appropriate time.

My view control is quite simple at the moment (it will evolve in the next post). It uses a Stopwatch instance to invalidate the control every 16 milliseconds (max). The Draw method is then overridden to clear the graphics device and draw the ViewComponent specified as the generic type argument.

Although I said that the ViewControl is supposed to replace the Game, you might have noticed that this class doesn't actually invoke Initialize or Update on the ViewComponent. That's partly because the decision to invoke Update will depend on the specific ViewComponent instance, so it should be implemented in the specific class that derives from ViewControl<T>. Another reason is because we haven't looked at how to create a ContentManager that can be used at design-time. Invoking Initialize without a ContentManager will not succeed if the ViewComponent needs to load any content.

Next Steps

We've gathered up the primary ingredients, but it's pretty clear we can't just throw them in a bowl and set the mixer on high. We have a ViewControl class that is supposed to draw the ViewComponent, but we don't have a way to get the user experience we need. What we want is to be able to add types in a project that derive from a specific ViewComponent, and have that show up in the designer when you double-click the file.

So, for example, let's say we have this specific ViewComponent:

public class MenuScreen : ViewComponent

{

}

We want to be able to define new classes in our projects like this:

public class MenuScreen1 : MenuScreen

{

}

...And then have MenuScreen appear in the designer when the code file is opened in design view.

As I described in a previous post, the designer infrastructure will instantiate the base class of the designable component. We already know that MenuScreen is a ViewComponent, so it relies on its host to provide a graphics device and to tell it when to draw.

To be able to draw the MenuScreen, we need another class:

public class MenuScreenControl : ViewControl<MenuScreen>

{

}

We don't want this control to show up anywhere in the project where MenuScreen1 is defined, but we do want it to be instantiated in the designer in place of MenuScreen. The magic trick to this is customizing MenuScreen's designer serializer.

For detailed information on designer serialization, refer to Designer Serialization Overview.

When you try to open a code file in design view, the designer infrastructure looks at the base type of the class to see if it inherits from Component. If it does, it then looks to see if the class has a designer serializer. If the type doesn't specify one, the serializer is inherited from its base class. By default, every designable component will use the Component's designer serializer. The job of the serializer is to instantiate the component from its serialized state (or to deserialize it).

The designer serialization is based on CodeDom, so the serializer is passed a CodeDom definition of the base class from the code file, and is asked to deserialize it. Naturally, the default serializer simply instantiates the type specified in the CodeTypeDeclaration passed to it.

By providing a custom designer serializer, we can customize the deserialization step. You might have already guessed that by doing this, we can instantiate a MenuScreenControl in place of the MenuScreen component specified in the CodeTypeDeclaration!

Skillful Substitution

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...)

In our designer, it's not enough to just instantiate a MenuScreenControl in place of a MenuScreen. When looking at the design surface, it'll be exactly what a user might expect, but as soon as the user tries working with it, the illusion will fall apart! The Properties Window is going to reveal that the class is a MenuScreenControl instance, and it won't have the MenuScreen's properties. It's like we're trying to pass off tofurkey for the real thing at Thanksgiving dinner! Nobody who's ever had real turkey before is going to be fooled!

More design-time tricks are needed, and this time, ICustomTypeDescriptor is our secret ingredient. By implementing ICustomTypeDescriptor on our MenuScreenControl, we can make it appear to be an instance of MenuScreen. We can give it the same type name, the same properties, the same attributes. The best part is that if we do it right, we'll even fool the MenuScreenControl's serializer into generating code for a MenuScreen. That means no more serializer customization!

To pull off the ICustomTypeDescriptor implementation, a few helper classes are needed. They aren't very complicated, but it'll be easier to just show them off with a sample.

Next time, I'll provide a working sample and explain how to set it up properly. Proper set up is essential to avoid common pitfalls in designer debugging. After that, I'll start to introduce some neat feature ideas specifically for XNA Framework projects.

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!314.trak
Blog che fanno riferimento a questo intervento
  • Nessuno