MIT License NuGet

Home / Docs / Project layout

Project layout and assets

Assets live in Resources/Raw/ and are addressed by their path below it. A file at Resources/Raw/Assets/island.glb is loaded as "Assets/island.glb" on all six platforms, with no per-platform path handling in your code.

The shape of an application

Hello/
  Hello.csproj
  GlobalUsings.cs
  App.cs
  Panels/
    Scene.cs
  Platforms/
    Windows/     App.xaml, App.xaml.cs, Package.appxmanifest
    Linux/       Linux.cs
    Android/     MainActivity.cs, AndroidManifest.xml
    iOS/         Program.cs, Info.plist
    MacCatalyst/ Program.cs, Info.plist
  Resources/
    AppIcon/     appicon.svg
    Splash/      splash.svg
    Raw/
      Assets/    island.glb, robot.glb, NotoSansMono-VariableFont.ttf, ...

Everything outside Platforms/ is shared. The web build is a separate Blazor WebAssembly project that references the same App class, because a browser host has a different project SDK rather than a different source file — see requirements for the three-project shape.

The two item groups that make assets work

Mobile and Windows read assets through the MAUI asset system; Windows and Linux desktop builds also want them next to the executable. Both groups declare the same LogicalName/Link pattern, which is what keeps the lookup path identical everywhere.

<ItemGroup>
  <MauiAsset Include="Resources\Raw\**"
             LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
</ItemGroup>

<ItemGroup Condition="'$(IsWindows)' == 'true' or '$(IsLinux)' == 'true'">
  <Content Include="Resources\Raw\**"
           CopyToOutputDirectory="PreserveNewest"
           Link="%(RecursiveDir)%(Filename)%(Extension)" />
</ItemGroup>
If a model loads on Windows but not on Android

Check these two groups first. A missing MauiAsset entry produces exactly that symptom: the desktop build finds the file on disk through the Content copy while the packaged build has nothing to open. Loading is asynchronous and a missing asset leaves the control not Ready rather than throwing, so the failure looks like an invisible object rather than an exception.

Keeping Linux sources out of the MAUI builds

The Linux target is plain net10.0, so its entry point must not be compiled into the MAUI target frameworks. The sample projects do that with two small targets rather than with #if blocks:

<Target Name="IncludeLinuxFilesForLinuxTarget" BeforeTargets="BeforeCompile"
        Condition="'$(IsLinux)' == 'true'">
  <ItemGroup>
    <Compile Include="Platforms\Linux\**\*.cs" />
  </ItemGroup>
</Target>

<Target Name="ExcludeLinuxFilesForNonLinuxTargets" AfterTargets="BeforeCompile"
        Condition="'$(IsLinux)' != 'true'">
  <ItemGroup>
    <Compile Remove="Platforms\Linux\**\*.cs" />
  </ItemGroup>
</Target>

The IsWindows, IsLinux, IsAndroid and friends used in these conditions are ordinary MSBuild properties derived from TargetFramework at the top of the project file. Copying them along with the targets is the least error-prone way to start.

Global usings

Applications normally declare their namespaces once, which is why sample code carries almost no using lines and why the snippets on this site do not repeat them:

global using System.Numerics;

global using Season.Basic;
global using Season.Utils;
global using Season.Storage;
global using Season.Models;
global using Season.Controls;
global using Season.Panels;
global using Season.Rendering;
NamespaceWhat lives there
Season.BasicBaseApp, DeviceServices, Graphics, Colors, Platform.
Season.ControlsModel, Mesh3D, InstancedModel, Sprite2D, Sprite3D, Shape, Texts, and the MeshInstanceTransform you derive instance types from.
Season.PanelsPanel and the ready-made panels: FrameButton, Input, Picker, ObjectPicker.
Season.RenderingRenderQuality, FrameSchedule, Atmosphere, WorldSettings, SceneLighting.
Season.ModelsglTF loading and animation: GltfAsset, GLTFAnimationPlayer, ModelAnimationInfo, PickMesh.
Season.StorageStorageService and the settings persistence used by the quality screen.
Season.FontsFont. Not usually a global using, since you touch it once at startup.