![]() |
|
Spaces home BadCorporateLogoPhotosProfileFriends | ![]() |
|
October 05 Making a Designer of Your Own - An Imperfect ExampleMonths ago, I started a little project that was supposed to be a diversion from my day job. My day job is building XNA Game Studio... And my project was an experiment to make programming with XNA Game Studio easier. I admit that I'm not very good at creating diversions for myself. Anyway, after a couple nights of playing with samples from the XNA Creators Club Online web site, I had integrated the MenuScreen class into Visual Studio's designer framework. The result was being able to double-click a code file in a game project, and have a game menu open up in a document window. I added support for dragging-and-dropping menu items from the Toolbox, and editing properties in the Properties Window. It demonstrated some exciting possibilities, but at that point was only barely functional. After posting some initial screenshots to my blog, I discovered that, although I'm terrible at distracting myself from work, I am pretty good at finding interesting things to work on. My blog traffic exploded by two orders of magnitude, and I received a number of requests for source code. I'm pretty certain that my source code isn't self-explanatory. The designer I built is a customization for an existing framework, and the vast majority of the functionality isn't in my source. Anyone who understands enough of the framework to see how my source code works doesn't need the source code in the first place. So instead of answering requests with code, I started to explain the framework I was integrating into. I'm finally at the point where further explanation will be easier to follow with a working example. For that reason, I've put my prototype project on CodePlex. The project doesn't build a ready-to-use designer. The design-time experience is runnable, but not usable. It's got tons of potential, and I'm going to continue working on it, but at this point, it's far from perfect. My CodePlex project is called ferpect. If you are interested in building designer support for your own libraries, then I encourage you to take a look at it, and join the discussion if you'd like to request specific features be added for your reference. I'm really looking forward to continuing my work on this project. Now that the fundamentals are out of the way, I think being able to review changesets and associated check-in comments will cut down on the amount of explanation required for new features and concepts. That's all for now! September 28 Making a Designer of Your Own - Mixing and FoldingFor 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 ExplainedIn my last post on designers, I said the following:
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 LeftoversAt 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. September 17 Debugging Zune Games in XGS 3.0If you've been using XNA Game Studio to develop games for Zune, you're probably familiar with the following debug output:
This message gets spit into the Output Window while debugging, about once per second, if not more. It shows up so frequently, in fact, that it makes it very hard to see any other output at all! Since it's pretty rare that you're actually interested in these thread exit messages, seeing them can get quite aggravating. That's why I'm posting this to explain how to suppress these messages from the debug output. While debugging, right-click on the Output Window, and uncheck "Thread Exit Messages". Done! Now you can use the Output Window again! Contact InfoI know I said in my last post (ages ago!) that I would provide designer source code in my next post (this one)... But I was wrong, because here is the next post and there's no source code anywhere to be found! This post is specifically to address a couple people who have tried to contact me recently. I can't reply to messages unless you provide an e-mail address, or you set your account permissions to allow anyone to send you messages. If you were expecting a reply and didn't get one, please verify that you gave me a way to answer, and ping me again. I'm happy to answer a few questions, but I do need a way to respond. Thanks! P.S. For the rest of you, I've started setting up a project on CodePlex... But in doing so I found a bunch of features I haven't implemented yet. For example, rendering the design surface when an asset can't be loaded (what should the menu designer show when there are no fonts in your project?). July 27 Making a Designer of Your Own - Soup StockIn 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 ConceptI 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 PrepThe 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.
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 CodeI 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).
Next StepsWe'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:
We want to be able to define new classes in our projects like this:
...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:
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 SubstitutionEvery 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. July 12 Managed Designer Resources: The Restaurant Supply StoreRecently, I've been pretty swamped by work. Nights, weekends, holidays... I've been in the office so long, my wife has forgotten my name! This year, crunch time is like the playoffs - I'm not shaving and my beard is growing out. It's my first time with a beard, really. Honestly, it drives me nuts. I hate having food or drinks touch my my moustache and leave a lasting flavor. So besides working all the time, I'm also constantly wiping my mouth. Bleah! Anyway, I've gotten a few messages from people asking for more about designers. Well, I still don't have time to put into writing. I did a couple weeks ago, but a hardware failure destroyed several hours of work and I didn't feel like starting again right away. Then I got too busy. So instead of writing my own article on designers, I'm going to post a number of links where you can get information from the source: MSDN. If I'd found the second link earlier, I probably wouldn't have written one of my posts since it covers the same stuff. I think I remember reading it several years ago, but I didn't find it when I looked again. Here's a topic on developing components: http://msdn.microsoft.com/en-us/library/51sc2s5c(VS.71).aspx This specific sub-topic discusses design-time attributes: http://msdn.microsoft.com/en-us/library/tk67c2t8(VS.71).aspx That page links to the docs on customizing the design-time, which is what I'm writing about: http://msdn.microsoft.com/en-us/library/37899azc(VS.71).aspx The set of topics above actually provides a great deal of information and several samples. I think that's the best set of documentation for this kind of stuff. After having posted these links, I'll try to focus on XNA Framework-specific customizations from now on. It'll probably be another week before my schedule is back to normal. After that, I'll be back to writing. June 24 More on Designers... Still to ComeI've started the next three posts for my series on customizing the designer. Unfortunately, I haven't had time to finish any of them. I've received a couple messages from people saying thanks for the info... But when is the next part coming? :-) I really appreciate that people are interested, and I do intend to continue writing. Sorry that it's taking me so long. Life has caught up with me. When I built my menu designer in the first place, it was because I was looking for a diversion after writing a big design document. Well, implementation of that design is underway, and I'm having to put in longer hours to try to keep the schedule I set for myself (I ran into a couple unexpected setbacks -- which, ironically, is generally expected). That leaves less time at the end of the day for other interests. Add to that the fact that it finally stopped raining (it rains all winter here), and suddenly I don't have an excuse anymore to sit inside instead of going out to work on my house and property. The remodel I started in January isn't finished yet, and I've got family coming to visit later this summer. So my evening hours are spent working with power tools instead of software tools. My wife doesn't care who's waiting for my next blog post - she wants the house put back together and the furniture moved back in! Thanks for your patience... I'll be posting the next article as soon as I can! June 10 Making a Designer of Your Own - House SaladLast time, I started to write about the managed designer infrastructure in Visual Studio. This infrastructure is all part of the .NET Framework's System.ComponentModel namespace, and so it naturally deals with components (things that implement System.ComponentModel.IComponent) as the base unit. The component model is meant to provide a generic framework for any kind of designer, but in Visual Studio it was implemented specifically to support Windows Forms and source code generation. It was later made more generic, with the intention of supporting any components, but there is still a lot of implementation that works only with Controls. So what does the Control designer have to do with making a designer for XNA Framework game components? Nothing, except that in order to re-use the existing infrastructure, you'll need to know a bit about Control designers. Combining all the ingredients available in the Component Model to create a visual programming experience for XNA game components takes a fair bit of background knowledge. As I said in my last post, you can't just look at my recipe for a Game State designer and know how to build your own Input designer. The bulk of the implementation isn't in my code - it's in the designer infrastructure. If you want to make your own dishes, you need to know what's in the pantry, and how to combine certain things to get the desired flavor. Last time, I showed how any class deriving from Component will support a design view in Visual Studio. This design view is based on the concept of composition. You add objects to the class by dragging them from the Toolbox onto the design surface. Doing so results in a field being declared for the new object instance, and code to instantiate and initialize it. The designer lets you select and name objects on the design surface so that you can further edit them in the Properties Window. After making these changes, the entire design surface is serialized to code so that the composite class can be compiled and then re-created at runtime. When you design a Control, the objects on the design surface are usually represented at design time by the same objects you'll use at runtime. So when you add a Button to the Form designer, a real Button instance is created on the design surface to represent the Button instance you'll get at runtime. When you change the Text property on the Button in the Properties Window, the text on the button changes on the design surface because you've actually changed the Text property of that Button instance. You can change the BackColor and ForeColor properties for more visual evidence that the Properties Window is letting you edit the actual control instance on the design surface. At first glance, you might imagine that the Properties Window is using reflection to enumerate the properties of the selected object, get their values, and allow you to set them. That makes sense and might even seem pretty obvious at first... But if you change the Visible property to false, suddenly things are not so obvious. The button is still visible on the design surface! (If you aren't sure what Visible is intended to do, you can run the project at this point to see that the button isn't visible at runtime.) Figure 1 - Something invisible is revealed when you set the Visible property to false. The reason the Button didn't disappear is because the Visible property in the Properties Window doesn't belong to the Button the way the Text, BackColor, and ForeColor properties do. Instead, it belongs to the Button's designer. :-) At this point, things are undoubtedly going to get confusing. The term "designer" is overloaded, and I need to use it to mean something other than the visually-oriented composition editor we've been discussing until now. Every component created on the design surface has a designer associated with it. The designer is another class that modifies the design-time appearance and behavior of the component. One role of the component designer is to filter or enhance an object's metadata. To put it another way, it can change the attributes on a component or its properties, and can remove, replace, or add properties. A designer can do this to change both the appearance and behavior of a control at design-time. As an example, the Button's designer replaces the Visible property with a different property that comes from the designer instance. In designer-speak, this is called shadowing. In the Properties Window, the Visible property appears to belong to the Button, but in fact it belongs to the Button's designer. When you change the Visible property value, it's the designer's property that is changed (you don't see the effect because the designer was never visible in the first place). This prevents the Button instance from becoming invisible, which would make it hard to select or reposition it on the design surface. If you look more closely at the properties of the Button instance, you'll notice there are a few listed that aren't actually properties of the Button class. To pick out just a few, in the Design category, you'll find "GenerateMember", "Locked", and "Modifiers". These control whether the instance will be declared as a field of the class or a local variable, whether the control can be moved or resized on the design surface, and the visibility modifier of the field declaration (if there is one), respectively. Changing these properties affects the code generated for the Button instance, but they don't represent actual properties of any object on the design surface. A basic designer implementation is provided by System.ComponentModel.Design.ComponentDesigner. This class makes it easy to override just the parts you want to modify. As the documentation says, you associate a designer with a component through the DesignerAttribute. Now, a word of caution when using the DesignerAttribute. Its constructor has overloads to use System.Type or System.String arguments to specify the designer's type. If you implement the designer in the same assembly as the component, you can use the System.Type constructor. However, if you implement the designer in a separate assembly, it is almost always a good idea to use the System.String constructor. The reason is that the designer is always given a reference to the component instance, and if you want the designer to be able to reference that component by its proper type, it will be dependent on the component's assembly. That precludes having a reference to the designer assembly from the component assembly, because then you have a circular dependency. So the way to avoid it is to allow you component to be ignorant of its designer, except for its name. A very good reason to have the designer in a different assembly is because the designer will often depend on a bunch of assemblies that you don't ever need at runtime, or that maybe aren't redistributable. In fact, the designer class itself is not needed at runtime! So if you keep it in a separate assembly, then you can keep your runtime libraries lightweight and your redist small. Your customers will appreciate that! I think I'll wrap it up here. Next time, I'll write more about how designers affect the behavior of controls on the design surface. At this point, it's also appropriate to start using code samples to illustrate, and I don't have anything ready right now. June 02 Making a Designer of Your Own - A Starter CourseA couple people have e-mailed me recently about my "teaser" screen shots, because they want to know how I did it. As much as I'd like to explain the menu designer I'm working on, it's complicated. Writing long blog entries just isn't as interesting as writing code (for me, at least), so in all honesty, I'd rather work on the designer than try to explain what I'm doing. The easiest way to explain it is, I'm using the Windows Forms designer framework. It's reasonably well-documented, but not all in one place. You can't read it like a book. Someone For that reason, I'm not going to share my recipe -- not yet, at least. Instead, I'll try to focus on explaining how to cook. Let's start with an appetizer, and work our way up to the entree. In Visual Studio, there is already an extensible designer infrastructure for components (types that implement System.ComponentModel.IComponent). This infrastructure has further specialization to support Components (types that derive from System.ComponentModel.Component) and Controls (types that derive from System.Windows.Forms.Control). Note that the general infrastructure is for the IComponent interface, while there is extra stuff for the Component class, which is a default implementation of the IComponent interface. There is even more extra stuff for Controls and various specialized subclasses, like Form. The extra stuff is the interesting part. Windows Forms has an editor registered in Visual Studio that will handle any code file in a code project that contains a class declaration for a class that derives from Component. The IDE determines this by asking the language service (the thing that gives you Intellisense) for the first class declaration in a code file. Then it uses another service (probably ITypeResolutionService, but I forget, and it doesn't matter enough to check), to determine whether the base class of the type declaration derives from Component. If so, then the file is treated as a "component" file. Let's take a look at an example. Figure 1 shows a regular Class Library project in VC# Express. Look what happens when I edit the declaration for Class1 to derive from Component. Figure 2 shows the subtle change. In the Solution Explorer, the icon next to the Class1.cs file is the "component file" icon. Even though the file is already open in the code editor, now when you double-click on the file in Solution Explorer, it will open the Component Designer. Figure 3 shows the Class1 class in the the Component Designer. The Component Designer provides a basic, visual code editing experience. You can drag and drop components onto the Class1 design view from the Toolbox, and edit those objects in the Properties Window. The changes you make will be serialized to code in a method called InitializeComponent in the Class1 class declaration. After some experimentation, an observant person will note several things. First, the generated code appears in InitializeComponent, but that function is never called. This is because the designer generally ignores everything else in the class except that function. The standard practice is to define a default constructor that calls InitializeComponent. Since Class1 was not created from a Component template, I'll need to add that code myself. Second, the name of Class1 in the Properties Window refers to the name of the type Class1. Every other component dropped onto the design surface has a name that refers to the name of the member variable that the component is assigned to. Third, the properties in the Properties Window aren't the properties of Class1. If you add a new property to the class definition in Class1.cs, it won't appear in the Properties Window at all. For example, let's add a string property to Class1. public class Class1 : Component { private string myProperty; public string MyProperty { get { return myProperty; } set { myProperty = value; } } } Going back to the design view, MyProperty is nowhere to be seen. If you look closely at the Properties Window, you'll see the reason why. It shows Class1 to be an instance of System.ComponentModel.Component. This is important -- the designer creates an instance of the component's base class at design time. The same goes for editing Forms or Controls. This only applies to the root component, however -- the type being designed -- and not for components that are dragged onto the design surface. Part of the reason for this is because you are changing the source code for the type as you edit it in the designer -- making it difficult to keep a compiled instance of the type-being-edited up to date with the latest changes. Now that you know the designer creates an instance of the base type instead of the actual type, you might already have guessed what I will do next. I'm going to add another class to the project in its own file, Class2.cs, and declare a new class Class2 that derives from Class1. Now, as long as I've compiled the project first, I can double-click on Class2.cs and have it open up in the designer. This happens because Class2 derives from Component. Except this time, the instance on the design surface is an instance of Class1! See how the Properties Window shows MyProperty? Figure 4 shows that you can design your own types in the Component Designer. Getting your own class into the Component Designer is the first step. Now it's time to customize! There are a lot of ways to customize the design-time experience for particular components. Most of it is driven through metadata, or custom attributes. For example, using attributes, I can change the way MyProperty appears in the Properties Window. public class Class1 : Component { private string myProperty = "elephant"; [DefaultValue("elephant")] [Category("Animals")] [DisplayName("My Property")] [Description("Set this property to the first thing that comes to mind.")] public string MyProperty { get { return myProperty; } set { myProperty = value; } } } After rebuilding the project, the Properties Window for Class2 reflects the changes. Figure 5 provides an example of how you can customize the design-time behavior of your components. The MyProperty property is now displayed as My Property (note the space), it is in the Animals category instead of Misc, and it has a description at the bottom of the Properties Window. The value of the property is "elephant", which is the default value. This is important to the designer because when a property has its default value, it is not serialized to code. Imagine having a component with two hundred properties, and all but one is set to its default value (the value it has when constructed from a default constructor). You would want the code generated for that component to only set the one property that was different from its default, not all 200 values. Otherwise, such a class would initialize 199 of its properties twice! It's important to note here that the DefaultValueAttribute custom attribute is used as a hint to the designer, and doesn't actually set the property to anything. It's purpose is to tell the designer what the default value is, so the designer can compare the property's actual value against it when it's time to generate code. The Properties Window also uses it to visually indicate which properties need to be serialized -- that is, if a property's value is anything but the default, then that value will appear in bold in the Properties Window. If you specify the wrong value in the attribute, the designer will behave incorrectly -- so be careful! There are a bunch of attributes in System.ComponentModel that are interesting. A few noteworthy attributes in terms of editing properties are:
In my menu designer, I've been getting a lot of mileage out of creating custom type converters. I created one to enumerate all types that derive from GameScreen to assign to a System.Type property, I created one to enumerate the names of all SpriteFonts in the nested Content project to assign to a string property, and I created another to enumerate the names of all Texture2D content to assign to another string property. Okay, that's as far as I'm going to get in one sitting. Our starter course is looking pretty modest -- more like cheese and crackers than shrimp cocktail -- but at least there's something on the plate! May 27 Compelling Design-Time Experiences in an XNA Game Studio-Based DesignerThe other week, I started playing around with building a designer for Game State Management. It wasn't long before I had something working -- mainly because I leveraged a lot of existing code in the .NET Framework. The screen shots I showed previously are the result -- a design surface that can interact with the Toolbox and Properties Window. This is sufficient to quickly add menu items, edit their appearance, and even generate event handlers for input events. I was pretty excited about it and that's why I posted to my blog. Unfortunately, the MenuScreen and MenuEntry classes in the Game State Management sample weren't intended to be designable. They didn't have many interesting properties or events, so the designer could only save you from writing a couple lines of code before you had to drop back to source view and type the rest of your program with a keyboard. Gross! ;-P Last week, I thought about how I often navigate menu screens in games -- selecting a menu item to activate another menu screen, then canceling back to the previous screen, and so forth. To build a navigation system without writing any code, I needed a way to specify what the next screen from within the designer. What I came up with was a property of type System.Type, which could only be set to a type that derives from GameScreen (the base class for all screens). I implemented a navigation item that, when selected, would activate a new instance of the specified GameScreen type and begin a transition to it. That was pretty easy, except for the idea of someone trying to edit the System.Type property in the Properties Window. You can customize the design-time behavior of properties by using a TypeConverter, that you can associate with the property by using a TypeConverterAttribute. My custom TypeConverter class uses ITypeDiscoveryService to enumerate all the available GameScreen types in the project and list them in a combobox for easy selection. This new ability to select a screen type in the designer makes it possible to build fancy menus with pop-ups and structured navigation without writing any code. After that, it was a snap to add another navigation item that could both specify the next screen, as well as a second screen to animate during lengthy, asynchronous content loads. More recently, I thought about how I'd like to customize the appearance of a menu. I'd like to do things like customize the menu's font, or create background screens by selecting textures. I actually would like to be able to create a whole 2D scene out of sprites on non-menu screens, or to display sprites when menu items are activated... But first things first. A compelling design-time experience for any of this would require the designer to know about the content built by a project. If the menu had a Font property, you'd want the Properties Window to let you choose from a list of SpriteFont items in your project. Then, for a really good design-time experience, you'd want the designer to actually use that SpriteFont when rendering the design surface. The way I picture that working would be to have a string property for the Font, which specifies the asset name of the SpriteFont. The TypeConverter for this property would enumerate the SpriteFont items from the project, and then the menu would load the asset by name during initialization. The hard part, unfortunately, is figuring out what SpriteFonts are available in the project at design time. Tonight, I believe I've solve this problem. This figure shows a SpriteFont property added to the MenuScreen which lists available SpriteFonts from the content project at design-time. It doesn't yet use that font for rendering the design surface, but that's the next logical step. Writing a TypeConverter to be aware of the content items was tricky and required very specific domain knowledge of Visual Studio and the managed designer infrastructure, but didn't take a lot of code. I'm still implementing everything in Visual C# Express 2005, and am able to run it and debug it without installing any packages or add-ins into the IDE. I also do not have the VS SDK installed on my laptop. To be honest, I'm a bit surprised at how much you can do without the VS SDK! Being able to enumerate content from within the designer opens the door to all kinds of interesting scenarios! I'm looking forward to thinking about it some more! (I'm always looking forward to having free time!) :-) May 20 Going Somewhere? Menu Navigation Woes.There have been about a billion views of my blog since I posted screen shots of the menu designer I started working on last week. For those of you who are keenly interested in that particular topic, I am still working on it. I mean you, Eli! ;-) Getting a design surface going took some technical trickery, but I've done it before so it was no big deal. The real difficult part is making a set of classes that are intuitively designable. What does that mean? Well, I guess I'm trying to figure out the right set of classes to support, the right properties to put on all the classes, and the right set of toolbox items to have available. I want something that is both simple enough to get people started right away, and easy enough to customize that not every game looks like it came from the same designer. I've been building this designer and its components in Visual C# 2005 Express Edition. A few of you might be surprised at that, mainly because you can't use the VS SDK to build customizations for VC# Express (except for some very special cases like XNA Game Studio, but my personal projects are no exception). The fact is, a visual designer doesn't require customizations to the IDE. There's already a powerful designer infrastructure built-in to VC# Express -- the very same one used by Windows Forms! To create custom design-time experiences, you don't need to modify anything in the IDE. All you need is the right metadata on your classes, and you can take control of that infrastructure for yourself. Anyway, tonight I am trying to make sense of the mess I made with respect to navigation items. I've created an item that you can drop onto the menu and then specify what type of screen it navigates to. Selecting this menu item when the menu is running will transition to the next screen. I also gave it the option of specifying a custom background for the next screen, and for loading it on a separate thread with a load screen. The trouble is that if you use the load screen, you can't "cancel" back to the previous screen. If you don't use it, then you can. I'm having trouble figuring out what to call the properties so the resulting behavior will be obvious. Clearly, this one navigation item is too complex. The best thing to do is probably to have more than one navigation item -- say, one that pushes new screens onto the screen manager's stack (allowing back-button navigation), and another that clears the stack to start from scratch (like a gameplay or level screen). That would definitely be easier to understand, but what do I call them? Argh! The thing I like about the navigation items is that it gives you an easy way to build a complex menu system without writing any code. This screen clipping above shows how the NextScreen property has a dropdown combobox to select the type of the next screen. The combobox will list all the game screens in your project (or referenced by it). It's neat and easy, but the resulting behavior is not yet intuitive. Coming up with good names is hard. It reminds me of a quote from Knuth:
Once again, it's time for bed. May 14 Game State Management for N00bs Like MeLast week, I finished writing a design document that I'd been working on for about a month. The design review meeting seemed to go well -- at least, I think it did. There were a lot of nodding heads around the room as I presented my design, and I like to think they were nodding in approval. The alternative is that they were nodding off to sleep, but I'm going to assume that wasn't it. :-) Anyway, after so long working on the same thing, I decided to take a look at something new and different this past weekend. I installed XNA Game Studio 2.0 and VC# 2005 Express Edition on my laptop at home and started to poke around at some samples. I downloaded Nazeeh's Game State Management project template -- something that I consider to be a terrific example of what a starter kit should really be -- and created a project from it. For those of you who are not familiar with this template, I'll briefly describe it. It creates a project for a game that has a couple menu screens, and an animated loading screen to entertain people while larger screens (aka, game levels) load asynchronously in the background. It is a slick starting point for just about any game. I ran the project and put breakpoints in a few places to see how things were being invoked to get a better understanding of the code. Then I modified a few things and found it was reasonably easy to make changes and customize the screens. For those of you who don't know me, my background is in tools. I make the programs that make it easier to develop games with the XNA Framework, but I don't work on the XNA Framework libraries, and I hardly know anything about game programming. So when I looked at the sample code and saw how easy it was to customize the game state, the first thing I thought of was, "That's pretty cool -- but I can make this easier!" The funny thing about things being easy is that they are usually easy to automate. Building menus, changing properties, handling input events... Hmm, where have I seen that before? Instead of playing around with XNA Game Studio to finally learn about game programming, I decided to build a designer for Game State Management! :-) Tonight, I have a working menu designer. I used the WinFormsGraphicsDevice sample to create a design surface that actually draws the menu screen as menu items are added to it, and updates in real-time as properties are edited. To design a menu, I just need to declare a class that derives from my MenuScreen type, then double-click on the code file, and up pops the designer! I only have |