WinForms Content Loading Sample

This sample shows you how to import arbitrary 3D models at run time by running the Content Pipeline importers and processors dynamically on the fly as opposed to building all the content ahead of time in the usual way.

Sample Overview

The XNA Framework Content Pipeline is responsible for converting content files such as 3D models, textures, and sounds into a format that your game can easily load and use. Normally, these content files are added to your Visual Studio project. The Content Pipeline build process runs inside Visual Studio as you build your code. The compiled .xnb files produced by the content build are then distributed alongside the game executable. This has the advantage of doing the content processing ahead of time, which can be slow. Finishing the processing first enables the game to load the compiled content files as quickly as possible. Also, it means that Visual Studio exposes any errors with the content when you build the project. This is better than discovering errors at run time when you come to use the content later.

There is only one problem with this approach: what happens if you don't know ahead of time what content you are going to need? Perhaps you want to let your users add new models to your game, or maybe you are building a level editor that needs to load arbitrary files chosen through a run-time file selector.

The solution is to invoke the Content Pipeline build process on the fly from your own code. The Content Pipeline is implemented as an MSBuild task. You can use the MSBuild API to construct a temporary content project, add whatever files you like to it, build it into a temporary directory, and then load the resulting .xnb files through the ContentManager. You can repeat this as many times as you like to import and load arbitrary content files.

This sample builds on the classes described previously in the WinForms Graphics Device Sample. It implements a 3D model viewer control that uses a file selector to let the user choose any .fbx or .x format model. Then it imports that file, loads it, and displays it on the screen.

Although the Content Pipeline build process is incremental, meaning it can avoid doing conversion work if a piece of content was already processed during a previous build, this sample deletes its temporary directory when it exits. This forces subsequent runs to rebuild the content from scratch. Depending on your scenario, you might want to replace this directory management system with a simpler fixed path. You could sacrifice some disk space to improve incremental rebuild performance.

Note that this sample runs only on Windows. WinForms, MSBuild, and the Content Pipeline are not available on Xbox 360. If you want your game to run on Xbox 360, you must build all your content ahead of time as part of the Visual Studio project. Note also that the XNA Framework redistributable installer does not include the Content Pipeline. This sample will run only on computers that have the full XNA Game Studio installed.

Minimum Shader Profile

Vertex Shader Model 1.1
Pixel Shader Model 1.1

How the Sample Works

This sample reuses the GraphicsDeviceControl.cs, GraphicsDeviceService.cs, and ServiceContainer.cs files that were previously presented in the WinForms Graphics Device Sample. If you are unfamiliar with this code, see that sample for more information.

The main form of the application contains a single ModelViewerControl, which derives from GraphicsDeviceControl. This control can be assigned any Model object. It will display it to the user, gradually spinning. When the model is changed, the MeasureModel method is used to identify the size and center of the new object. This ensures the model will always be visible, centered on the screen, and drawn at a sensible size. This applies no matter what scale to which it was built or the location of the origin of the model data.

The MainFormconstructor creates two important worker classes: a ContentBuilder that will be used to dynamically import and process our content files, and a ContentManager that will be used to load the content after it has been built. After displaying a file selector to let the user select a 3D model file, the Form1.LoadModel method performs these steps:

  1. Calls ContentManager.Unload to unload any previous model data.
  2. Calls ContentBuilder.Add to specify what new file to build, and which processor to use. This sample always just specifies the built-in ModelProcessor, but a more advanced application might want to use some other custom processor.
  3. Calls ContentBuilder.Build to build the selected content.
  4. Checks whether any build errors occurred, and displays a message box if the build failed.
  5. Calls ContentManager.Load to read in the model just built, and sets the ModelViewerControl to display this new object.

The ContentBuilder class is more straightforward. It declares a list of what pipeline assemblies should be used to build the content (this sample just specifies the four standard importers that come built-in to XNA Game Studio). It declares three helper classes used to perform the build: an MSBuild Engine object, an MSBuild Project object, and a custom ErrorLogger class that implements the MSBuild ILogger interface. The CreateBuildProject method uses the MSBuild object model to create a simple project for building XNA Framework content. This adds a reference to the standard ContentPipeline.targets file at the end of this project. The reference declares the MSBuild targets necessary to carry out the content build. The Add and Clear methods change what content files are included in the project. Finally, the Build method performs the build operation, and returns a list of error messages if anything went wrong.

Temporary Directory Management

The ContentBuilder class builds content into a temporary directory, from which the ContentLoader can load it. This poses a question: what directory should you use? You want a location that is not in use already. Ideally, you should delete these temporary files when the program exits.

ContentBuilder.CreateTempDirectory chooses a temporary directory location by calling Path.GetTempPath to look up a suitable location. Then it makes a nested directory named GetType().FullName, which will return "WinFormsContentLoading.ContentBuilder". It is unlikely that any other program would be using a directory with that name!

But what if the user runs more than one copy of your program at the same time? Both instances would try to use the same temporary directory, which would lead to a conflict. The solution is to include Process.GetCurrentProcess().Id as part of the directory name. This means each process will choose a different directory name.

But what if a single program creates more than one instance of the ContentBuilder class? This sample never does that. Other programs might, however, and it seems like something a robust implementation ought to support. There would be conflicts if both instances of ContentBuilder tried to use the same temporary directory. The solution is to include a unique integer value, called a "salt," as part of the directory name. Each time we create a new ContentBuilder, we increment the salt. The first instance gets 1, the next 2, then 3, and so forth. Each instance will chooses a different directory name.

What about cleaning up the temporary directory when you are finished? This is handled by ContentBuilder.DeleteTempDirectory, which is called by Dispose. DeleteTempDirectory does the job in almost all situations. Still, it has one flaw: what if the program crashes or is killed off in the debugger before it has a chance to run this shutdown code? Hopefully, your finished program will never crash. Unfortunately, this is likely to happen a lot while you are developing and testing. It would be a nuisance if each crash left an extra temporary directory cluttering up your hard drive. The solution is the PurgeStaleTempDirectories method. This method is called on startup. As a rule, it is not possible to perform cleanup after you crashed, but the next time the program is run, you can check to see if there are any stale directories that were left behind by previous runs of the same program, and delete them as necessary.

Depending on your application, you might prefer to always use the same temporary directory name, and never delete it. This will leave files lying around on your hard drive. The content build process is incremental. If your program tries to load the same content files that were already built during a previous run, you will not need to carry out any actual processing work. This can speed up loading times for programs such as level editors that are likely to want to load the same files each time they start up.