File Loading
XnaFiddle has no disk or traditional content pipeline. All assets live in memory and are loaded through InMemoryContentManager, a custom ContentManager subclass that serves files from a static Dictionary<string, byte[]>.
Four entry paths, one destination
| Source | Mechanism | Ends up in | SourceUrl set? |
|---|---|---|---|
| Example assets | Embedded resources in the assembly, loaded by ExampleGallery.LoadAssets() |
RegisterContentFile() |
Yes — points to wwwroot/examples/{ExampleName}/{file} |
| URL-fetched assets | FetchAndAddAssetUrl(url) via &assets= share param or URL input |
RegisterContentFile() |
Yes — the fetched URL |
| User drag-and-drop | JS fileDropInterop -> OnFileDropped JSInvokable |
RegisterContentFile() |
No — cannot be re-fetched |
| Gist import | LoadFromGistId: each gist file whose ext is in SupportedAssetExtensions is base64-decoded by DecodeGistAssetAsync (fetches raw_url when truncated) and registered via RegisterGistAsset |
RegisterContentFile() |
No (issue #82) |
All four paths converge on RegisterContentFile(fileName, bytes) in Index.razor.cs, which does two things:
InMemoryContentManager.AddFile(fileName, bytes)— stores data under the original filename AND the extension-stripped name (soContent.Load<Texture2D>("KniIcon")works without knowing the extension)contentFileCache.register(fileName, base64)via JS interop — registers the file in the JS-side XHR cache soTitleContainer.OpenStream()can resolve it (see below)
Supported formats
Controlled by SupportedAssetExtensions in Index.razor.cs:
.png— loaded asTexture2DviaTexture2D.FromStream().wav— loaded asSoundEffectviaSoundEffect.FromStream().fnt— BMFont text format; stored as raw bytes. The UI parsespagelines to show which texture files the font references (so users know what companion.pngfiles to also drop).ttf— TrueType font; stored as raw bytes for FontStashSharp.ember— stored as raw bytes.tmx.tsx.world.ldtk.ogmo.json.txt.xml— text/data formats (tilemap & level-editor files plus generic data); stored as raw bytes and read by user code viaTitleContainer.OpenStream(noLoad<T>branch)
InMemoryContentManager.Load<T>() has explicit branches for Texture2D and SoundEffect. Any other type falls through to base.Load<T>(), which will fail (no disk content pipeline exists).
Asset thumbnail previews (decision record)
The asset list (Index.razor, the _assetsOpen block) shows a 128×128 hover thumbnail for image assets, folded into one popup with the info the native title used to show. ImagePreviewUri(fileName) in Index.razor.cs returns a data:image/png;base64,… string built from the bytes already in InMemoryContentManager.Files, or null for non-image assets. For image assets the chip renders a .xf-asset-popup (<img> + a text line) shown on hover via the panel's <style> block, and the filename's native title is set to null so the two don't compete; non-image assets keep the plain native title (no popup).
Positioning / stacking. The popup opens below the chip (top:100%) — the asset bar sits near the top of the window, so an earlier "above" attempt was off-screen. To paint over the Monaco editor wrapper (a position:relative, later-in-DOM sibling inside #editorPanel that would otherwise occlude a downward popup), the asset-bar container carries position:relative; z-index:30. #editorPanel is overflow:hidden and full-height, so the short popup stays within its bounds and isn't clipped.
Decision — inline base64 data URI, not a JS URL.createObjectURL blob URL. Chosen because it works for drag-dropped assets too (they have no SourceUrl) with zero JS interop and no object-URL lifecycle/cleanup. Trade-off: the base64 string lives in the DOM and is recomputed on each render of the panel — negligible for the small example PNGs, heavier for a large (up to 10 MB) user-dropped image.
Gated to .png (the only image format; .wav/.ttf/.fnt/.ember get no preview). To back out: delete ImagePreviewUri, the <style> block, and the previewUri/.xf-asset-preview lines in the chip — nothing else depends on them. If large drops become a problem: switch to a blob object URL (create on RegisterContentFile, revoke on RemoveAsset/UnregisterContentFile) or memoize the data URI per asset, instead of recomputing inline.
TitleContainer XHR intercept
TitleContainer.OpenStream(path) is the standard XNA/MonoGame/KNI way to load raw files. In KNI's Blazor platform, it performs a synchronous XHR GET to the relative URL path. Since XnaFiddle's content files exist only in memory (not on a web server), a JS-side XHR monkey-patch intercepts these requests.
JS side (wwwroot/index.html, contentFileCache IIFE):
contentFileCache.register(path, base64)decodes base64 to a binary string and stores it keyed by pathcontentFileCache.unregister(path)removes a cached entry;clear()removes allXMLHttpRequest.prototype.openis patched to capture the method and URL on each instanceXMLHttpRequest.prototype.sendis patched: if the request is a GET and the URL matches a registered path, it sets_cfcIntercepted = trueand stores the cached data instead of hitting the network- Property getters for
status,responseText, andreadyStateare overridden to return cached values when_cfcInterceptedis set; non-intercepted XHRs fall through to the original native getters
C# side (Index.razor.cs):
RegisterContentFile(fileName, data)calls bothInMemoryContentManager.AddFile()andcontentFileCache.register()viaIJSInProcessRuntimeUnregisterContentFile(fileName)does the reverse withRemoveFile()andunregister()
Path gotcha — Content.RootDirectory matches export:
Content.RootDirectoryis"Content"in both the fiddle and exported projects (set on theInMemoryContentManagerright after the user'sGame1constructor runs, inIndex.razor.cs).RegisterContentFile/UnregisterContentFileregister/unregister the JS XHR cache entry under"Content/" + fileName, soTitleContainer.OpenStream("DroidSans.ttf")(bare filename, no prefix) does not resolve — it must beTitleContainer.OpenStream(Path.Combine(Content.RootDirectory, "DroidSans.ttf")), same as export requiresInMemoryContentManager.AddFile/RemoveFilestill key on the barefileName(unaffected) —Content.Load<T>()resolution goes throughNormalizeAssetPath, which strips any leading"Content/"before the dictionary lookup, soContent.Load<Texture2D>("DroidSans")keeps working with or without a prefix- Export-compatible user code should use
Path.Combine(Content.RootDirectory, "file.ext")forTitleContainer.OpenStreamcalls — this now fails the same way locally as it would after export if the prefix is missing
Embedded example assets
Naming convention: Examples/{ExampleName}.{AssetFile} in the filesystem becomes embedded resource XnaFiddle.Examples.{ExampleName}.{AssetFile}.
Example: Examples/TextureLoading.KniIcon.png is the asset KniIcon.png for the TextureLoading example.
The .csproj has two wildcard EmbeddedResource includes — one for *.cs (example code) and one for everything else (assets). No manual .csproj edits are needed when adding a new asset file.
ExampleGallery.LoadAssets(name) finds all embedded resources that share the example's prefix but are not the .cs file, strips the prefix, and returns them as ExampleAsset[] (filename + byte array).
Static copies for sharing
Every example asset is also served as a static file under wwwroot/examples/{ExampleName}/{AssetFile}. This duplicate is what makes share links work: LoadExampleAssets() sets AssetInfo.SourceUrl to {Navigation.BaseUri}examples/{ExampleName}/{file}, so GetAssetUrlsFragment() includes those URLs in the &assets= share fragment.
When adding a new example asset: place the file in Examples/ (embedded resource, picked up by wildcard) — the CopyExampleStaticAssets MSBuild target (BeforeBuild in XnaFiddle.BlazorGL.csproj) copies it to wwwroot/examples/{ExampleName}/{AssetFile} automatically on the next build. No manual copy step.
Drag-and-drop flow
- JS
fileDropInteroplistens onwindowfordragenter/dragover/dragleave/dropin the capture phase, so the whole UI is a drop target (issue #28). Handlers are gated one.dataTransfer.typescontaining'Files'; for a real file drag theypreventDefault()+stopPropagation()so the event is intercepted before Monaco sees it (no text-insertion caret, no Monaco file handling). Non-file drags early-return untouched, leaving Monaco's internal text drag-drop intact. The dashed affordance is a top-levelpointer-events:noneoverlay<div>positioned over whichever panel (#editorPanel/#canvasHolder) the pointer is over — anoutlineon the panel itself gets occluded by Monaco's stacking context. Routing is not location-based. - All dropped files are passed through to C# — no JS-side MIME or extension filtering
- File is read as base64 via
FileReader, sent to C#OnFileDropped(fileName, base64) - C# validates extension against
SupportedAssetExtensions, enforces 10 MB limit - Calls
RegisterContentFile()(InMemoryContentManager + JS XHR cache) and updates the UI asset list
Keyboard event suppression
A capturing-phase IIFE in monaco-interop.js intercepts keydown/keyup on window and calls stopPropagation() when focus is inside a .monaco-editor element. This prevents KNI (which listens in the bubbling phase) from receiving keyboard input while the user types in the editor. F5 is exempted from keydown suppression so the compile-and-run shortcut always works.
Build version detection
MSBuild generates BuildInfo.g.cs (C# const) and wwwroot/js/build-version.js (window._buildVersion) with the same UTC timestamp. On startup, C# compares BuildInfo.BuildTime against the JS value fetched from the browser. If they differ, the app sets _staleAssets = true and shows a warning banner with a Refresh button, indicating the browser is serving cached static assets from an older build.
Exported project support
Multi-platform exports produce a solution with a common project ({Name}Common) holding Game1.cs and RawContentManager.cs, plus per-platform projects ({Name}.DesktopGL, {Name}.BlazorGL, etc.) each with their own entry point. Content files live in a shared Content/ folder at the solution root.
RawContentManager (generated by ProjectExporter.GenerateRawContentManager) replaces InMemoryContentManager in exports. It uses TitleContainer.OpenStream(Path.Combine(RootDirectory, assetName + ext)) for non-desktop platforms (Android, Blazor) and File.OpenRead for desktop. Supports Texture2D (.png, .jpg, .jpeg, .bmp) and SoundEffect (.wav).
BlazorGL content linking: BlazorGL serves content from wwwroot/Content/. In multi-platform exports, a post-build MSBuild target (CopySharedContent) copies files from the shared Content/ folder into wwwroot/Content/. Other platforms reference Content/ directly (or as Android assets).
Static persistence
InMemoryContentManager._files is static — assets survive across recompilations. The loaded-asset cache (_loaded) is per-instance and cleared on Unload(). Each compile run creates a fresh InMemoryContentManager instance, but the underlying file store persists.
_files also holds alias keys (bare filename, no-extension) for Load<T> resolution; the public Files property returns a separate _primaryFiles dict (exact registered names only, no aliases) so callers like export don't double-ship an asset under its alias.
Adding a new file format
- Add the extension to
SupportedAssetExtensionsinIndex.razor.cs - Add a type check branch in
InMemoryContentManager.Load<T>()(alongside the existingTexture2DandSoundEffectbranches) - Update the
GenerateRawContentManagertemplate inProjectExporter.csif exported projects should also support the format - For embedded example assets: place the file in
Examples/with the{ExampleName}.{filename}naming convention; the.csprojwildcards will pick it up automatically
Key files
| File | Role |
|---|---|
XnaFiddle.BlazorGL/InMemoryContentManager.cs |
Static file store + ContentManager that loads Texture2D and SoundEffect from bytes |
XnaFiddle.BlazorGL/ExampleGallery.cs |
Reads embedded resources; LoadAssets() extracts non-code files for an example |
XnaFiddle.BlazorGL/Pages/Index.razor.cs |
OnFileDropped JSInvokable, SupportedAssetExtensions, LoadExampleAssets(), stale-assets check |
XnaFiddle.Core/ProjectExporter.cs |
GenerateRawContentManager template with Texture2D + SoundEffect branches |
XnaFiddle.BlazorGL/wwwroot/index.html |
XHR intercept for TitleContainer.OpenStream, canvas tick loop, splitter layout, other JS interop |
XnaFiddle.BlazorGL/wwwroot/js/monaco-interop.js |
fileDropInterop (drag-and-drop), keyboard event suppression for Monaco |
XnaFiddle.BlazorGL/wwwroot/js/build-version.js |
MSBuild-generated; sets window._buildVersion for stale-asset detection |
XnaFiddle.BlazorGL/XnaFiddle.BlazorGL.csproj |
EmbeddedResource wildcards for Examples/, GenerateBuildInfo target |
XnaFiddle.BlazorGL/Examples/SoundPlayback.cs |
Example showing Content.Load<SoundEffect>() usage |