Fun With Compiled Content

EDIT:  I realized it was probably a much smarter idea to just zip up the code along with the designer code and upload it somewhere.  So here it is.

Wouldn’t it be neat to be able to have a dialog you could pop up that would show all the pre-compiled content of a certain Type, with it all listed in a nice tree showing the directory structure?  Of course it would!  Only a crazy person would think otherwise.  Well the good news is I already did this, so feel free to plunder the code for your own use.

public partial class ContentBrowser : Form

{
    private static Dictionary<string, TreeNode> contentTrees = new Dictionary<string, TreeNode>();
    private static string contentDirectory;
    private static ContentManager contentManager;

    private static string[] contentTypes = {    "Texture",
                                                "Texture2D",
                                                "Texture3D",
                                                "TextureCube",
                                                "SpriteFont",
                                                "Model",
                                                "Effect"    };

    /// <summary>
    /// Traverses the specified content directory for all loadable content, and stores
    /// it as static data for use when an instance of the Form is created.
    /// </summary>
    /// <param name="services">IServiceProvider implementation, contains IGraphicsDeviceService</param>
    /// <param name="contentDirectory">The content directory to traverse</param>
    /// <param name="ownerWindow">Owner window for the status dialog</param>
    public static void Initialize(ServiceContainer services, string contentDirectory, IWin32Window ownerWindow)
    {
        ContentBrowser.contentDirectory = contentDirectory;

        // Make a content manager
        contentManager = new ContentManager(services, contentDirectory);

        // Make a small progress dialog so the user knows something is going on
        Form notificationDialog = new Form();
        notificationDialog.FormBorderStyle = FormBorderStyle.FixedDialog;
        notificationDialog.Size = new Size(350, 150);
        notificationDialog.Text = "JSMapEditor";
        notificationDialog.StartPosition = FormStartPosition.CenterScreen;
        notificationDialog.ShowInTaskbar = false;
        notificationDialog.ShowIcon = false;
        notificationDialog.ControlBox = false;

        Label statusLabel = new Label();
        statusLabel.Size = new Size(200, 50);
        statusLabel.Location = new System.Drawing.Point(100, 50);
        statusLabel.Text = "Loading Content";
        notificationDialog.Controls.Add(statusLabel);           
        notificationDialog.Show(ownerWindow);

        // Do the content loading/enumeration on a worker thread so we
        // can keep pumping messages on this thread
        Stopwatch timer = new Stopwatch();
        timer.Start();
        int count = 0;
        long time = 0;
        long lastTime = 0;
        long loadTime = 0;
        Thread workerThread = new Thread(EnumerateContent);
        workerThread.Start();

        while (!workerThread.Join(0))
        {
            Application.DoEvents();

            time = timer.ElapsedMilliseconds;
            loadTime += time - lastTime;
            lastTime = time;

            if (loadTime > 300)
            {
                statusLabel.Text = "Loading Content";
                for (int i = 1; i <= count % 4; i++)
                    statusLabel.Text += ".";
                count++;
                loadTime -= 300;
            }
        }

        notificationDialog.Hide();

        // Dispose of the content
        contentManager.Dispose();      contentManager = null;      GC.Collect();
    }

    /// <summary>
    /// Enumerates all loadable content for types in contentTypes, and stores
    /// the resulting tree in contentTrees
    /// </summary>
    private static void EnumerateContent()
    {
        // Recursively build the content tree
        foreach (string contentType in contentTypes)
        {
            TreeNode rootNode = new TreeNode("Content\\");
            BuildContentTree(rootNode, contentManager.RootDirectory, contentType);
            contentTrees.Add(contentType, rootNode);
        }
    }

    /// <summary>
    /// Builds the tree by looking for acceptable content.  Recursively calls itself
    /// to traverse subdirectories
    /// <param name="parentNode">The TreeNode representing the current directory</param>
    /// <param name="directory">The current direcotry to traverse</param>
    /// <param name="contentType">The name of the content Type to look for</param>
    /// </summary>
    private static void BuildContentTree(TreeNode parentNode, string directory, string contentType)
    {
        // Find all the subdirectories, and recursively search them
        string[] subdirectories = Directory.GetDirectories(directory);
        foreach (string subdirectory in subdirectories)
        {
            string relativePath = subdirectory.Substring(directory.Length + 1);
            TreeNode directoryNode = new TreeNode(relativePath + "\\");
            BuildContentTree(directoryNode, subdirectory, contentType);
            if (directoryNode.Nodes.Count > 0)
                parentNode.Nodes.Add(directoryNode);
        }

        // Check out all the .xnb files, see if we can load them as the target type
        string[] contentFiles = Directory.GetFiles(directory, "*.xnb");
        foreach (string contentFile in contentFiles)
        {
            string loadName = Path.GetDirectoryName(contentFile) + "\\"
                            + Path.GetFileNameWithoutExtension(contentFile);
            if (TryLoadContent(loadName, contentType))
            {
                TreeNode contentNode = new TreeNode(Path.GetFileNameWithoutExtension(contentFile));
                contentNode.Tag = loadName;
                parentNode.Nodes.Add(contentNode);
            }
        }
    }

    /// <summary>
    /// Checks whether the filename is valid by attempting to
    /// load it with the ContentManager.
    /// </summary>
    /// <param name="contentFile">The filename to check</param>
    /// <param name="contentType">The name of the content Type to check the content against</param>
    /// <returns>True if successful</returns>
    private static bool TryLoadContent(string contentFile, string contentType)
    {
        try
        {
            object content = contentManager.Load<object>(contentFile);
            if (content.GetType().Name == contentType)
                return true;
            else
                return false;
        }
        catch (ContentLoadException)
        {
            return false;
        }
    }

    private string selectedContentFile;
    public string SelectedContentFile
    {
        get { return selectedContentFile; }
    }

    /// <summary>
    /// Creates an instance of ContentBrowser
    /// </summary>
    /// <param name="value">The default content filename</param>
    /// <param name="contentType">The type of content to browse</param>
    public ContentBrowser(String value, Type contentType)
    {
        InitializeComponent();

        contentTree.Nodes.Add(contentTrees[contentType.Name]);
        contentTree.ExpandAll();

        selectedContentFile = value;
    }

    /// <summary>
    /// Called when the Form is closed
    /// </summary>
    /// <param name="e"></param>
    protected override void OnClosed(EventArgs e)
    {
        contentTree.Nodes.Clear();
        base.OnClosed(e);
    }

    /// <summary>
    /// Event handler for the TreeView's mouse clicks
    /// </summary>
    /// <param name="sender">contentTree</param>
    /// <param name="e">Event args</param>
    private void contentTree_NodeMouseClick(object sender, TreeNodeMouseClickEventArgs e)
    {
        // Tag != null means it's a content node
        if (e.Node.Tag != null)
        {
            selectedContentFile = (string)e.Node.Tag;
            selectedContentFile = selectedContentFile.Substring(contentDirectory.Length + 1);
        }
    }
}

Okay so a few notes on usage…it uses the string array “contentTypes” to know which types to check for.  You should fill this out with whatever Type’s you’re loading from the ContentManager.  The static Initialize method should either be called when your app starts up, and you’ll need to do it before you can actually create an instance of ContentBrowser.  It shows a little loading dialog while it’s working, so you have something to show the user while it’s happening.  You could take that out, if you wanted.

I also made a UITypeEditor so that you can have this dialog to set a property in a PropertyGrid:

/// <summary>
/// Used to allow the user to browse for content in the PropertyGrid
/// </summary>
/// <typeparam name="T">The Type of content to display in the ContentBrowser</typeparam>
public class ContentEditor<T> : UITypeEditor
{
    public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context)
    {           
         return UITypeEditorEditStyle.Modal;
    }

    public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value)
    {
        IWindowsFormsEditorService editorService = null;
        if (provider != null)
        {
            editorService = provider.GetService(typeof(IWindowsFormsEditorService)) as IWindowsFormsEditorService;
        }

        if (editorService != null)
        {
            // Pop up our dialog
            ContentBrowser browser = new ContentBrowser((string)value, typeof(T));
            if (editorService.ShowDialog(browser) == DialogResult.OK)
                value = browser.SelectedContentFile;
        }

        return value;
    }
}