UX Design: Main Views & Detail Panels
Architecture Overview
The UI is organized into three spatial regions:
- Main Canvas — MonoGame 2D rendering of the solar system
- Top Panel — Date/time controls, zoom level, speed multiplier
- Side Panel — Context-sensitive detail view based on selection
This structure balances information density with visual focus on the simulation itself.
MVVM in a Game Context
The Challenge
Traditional MVVM (Xamarin.Forms, WPF) uses data binding to synchronize View and ViewModel. MonoGame doesn't have a binding engine, so we adapt the pattern:
ViewModel — Presentation logic (formatting, filtering, computed values)
View — MonoGame drawing calls and input handling
Binding — Manual update loop (call ViewModel methods each frame)
ViewModels as Presentation Controllers
SolarApp ViewModels act as presentation controllers, not just data containers:
public class BodyDetailViewModel
{
private readonly ICelestialBodyService _bodyService;
private readonly ISelectionManager _selection;
public string DisplayName { get; set; }
public string OrbitClassification { get; set; }
public Vector3 Position { get; set; }
public double OrbitalPeriod { get; set; }
public void Update(DateTime currentTime)
{
var selected = _selection.SelectedBody;
if (selected == null) return;
DisplayName = selected.Name;
Position = selected.DeterminePosition(currentTime);
OrbitClassification = ClassifyOrbit(selected);
OrbitalPeriod = CalculatePeriod(selected);
}
private string ClassifyOrbit(CelestialBody body) { /* ... */ }
}
The View simply reads these public properties and renders them:
public class BodyDetailView : IMainPanelView
{
private BodyDetailViewModel _viewModel;
public void Update(GameTime gameTime)
{
_viewModel.Update(gameTime.TotalGameTime);
}
public void Draw(SpriteBatch spriteBatch)
{
spriteBatch.DrawString(
_font,
$"Name: {_viewModel.DisplayName}",
new Vector2(10, 10),
Color.White);
// ... more drawing
}
}
Main View System
The main canvas supports multiple view types:
IMainPanelView Interface
public interface IMainPanelView
{
void Update(GameTime gameTime);
void Draw(SpriteBatch spriteBatch);
void HandleInput(InputState input);
}
View Types
- SolarSystemMainView — Full system view, all bodies visible
- BodyDetailMainView — Zoomed view of a selected body with orbital elements overlaid
- TrajectoryPlanView — Maneuver planning visualization (future)
View Factory Pattern
Views are created by a factory to manage composition and dependency injection:
public interface IMainPanelViewFactory
{
IMainPanelView CreateSolarSystemView();
IMainPanelView CreateBodyDetailView(CelestialBody body);
}
// In Game.Initialize()
_mainView = _viewFactory.CreateSolarSystemView();
This allows:
- Decoupling view creation from the Game class
- Injecting shared services (camera, render context, selection manager)
- Easy testing by providing mock factories
Side Panel & Detail Resolution
The Problem: What to Show?
When the user clicks on a celestial body, what detail should appear in the side panel?
- A Planet shows orbital elements, rotation, moons
- A Moon shows parent reference, orbital resonance, composition
- An Asteroid shows spectral class, estimated size, discovery date
Content Resolver Pattern
Rather than a large if/switch statement, use a pluggable resolver:
public interface ISidePanelContentProvider
{
bool CanHandle(CelestialBody body);
Control CreateContent(CelestialBody body);
}
// Register implementations by type
_providers.Register<Planet>(new PlanetSidePanelProvider());
_providers.Register<Moon>(new MoonSidePanelProvider());
_providers.Register<Asteroid>(new AsteroidSidePanelProvider());
// In selection handler
var provider = _providers.Resolve(selectedBody.GetType());
var control = provider.CreateContent(selectedBody);
Benefits:
- Open/Closed Principle — Add new body types without modifying existing code
- Testability — Mock providers for UI tests
- Modularity — Each content type is self-contained
SOLID Principles Applied
Single Responsibility
Each class has one reason to change:
BodyDetailViewModel— Format body data for displayBodyDetailView— Draw formatted dataISelectionManager— Track which body is selected
Open/Closed
Classes are open for extension (new body types), closed for modification (no changing ISidePanelContentResolver).
Liskov Substitution
Any IMainPanelView can be swapped at runtime without breaking the Game loop:
// User presses 'D' to detail view
_currentView = _viewFactory.CreateBodyDetailView(_selection.SelectedBody);
// Game.Update/Draw continues to work unchanged
Interface Segregation
Classes depend on narrow interfaces, not large base classes:
// Not: public class View : GameComponent { ... }
// Instead:
public interface IUpdatable { void Update(GameTime gt); }
public interface IDrawable { void Draw(SpriteBatch sb); }
public interface IInputHandler { void HandleInput(InputState i); }
public class BodyDetailView : IUpdatable, IDrawable, IInputHandler { ... }
Dependency Inversion
High-level modules (Game, ViewModels) depend on abstractions (ISelectionManager, IBodyService), not concrete implementations:
// Bad:
public class Game
{
private JsonSettingsStore _settings = new();
}
// Good:
public class Game
{
private readonly ISettingsStore _settings;
public Game(ISettingsStore settings) => _settings = settings;
}
Settings & Persistence
SolarApp uses a layered settings architecture:
ISettingsStore Interface
public interface ISettingsStore
{
void SaveRenderSettings(RenderMainSettings settings);
RenderMainSettings LoadRenderSettings();
void SaveUxSettings(UxSettings settings);
UxSettings LoadUxSettings();
}
public class JsonSettingsStore : ISettingsStore
{
// Implementation using System.Text.Json
}
Settings Types
- RenderMainSettings — Graphics quality, anti-aliasing, detail levels
- UxSettings — Window size, sidebar width, zoom level, date
Settings are loaded at startup and persisted when changed. Each setting change triggers a UI update:
public class UxSettingsTab : ISettingsTab
{
private ISettingsStore _store;
public void OnQualityChanged(QualityLevel newLevel)
{
var settings = _store.LoadRenderSettings();
settings.QualityLevel = newLevel;
_store.SaveRenderSettings(settings);
// Notify Game to regenerate assets
_onSettingsChanged?.Invoke();
}
}
Input Handling
Input flows through a centralized InputState object:
public class InputState
{
public Vector2 MousePosition { get; set; }
public bool LeftMousePressed { get; set; }
public KeyboardState KeyboardState { get; set; }
// ...
}
// In Game.Update()
var inputState = new InputState
{
MousePosition = Mouse.GetState().Position.ToVector2(),
LeftMousePressed = Mouse.GetState().LeftButton == ButtonState.Pressed,
KeyboardState = Keyboard.GetState()
};
_mainView.HandleInput(inputState);
_topPanel.HandleInput(inputState);
_sidePanel.HandleInput(inputState);
Each view can then respond to input independently, avoiding a central input dispatcher.
Testing Strategy
The MVVM/factory pattern enables comprehensive testing:
Unit Tests
ViewModels can be tested without a Game instance:
[Test]
public void BodyDetailViewModel_WhenBodyIsSelected_DisplaysName()
{
var mockBody = Substitute.For<CelestialBody>();
mockBody.Name.Returns("Earth");
var mockSelection = Substitute.For<ISelectionManager>();
mockSelection.SelectedBody.Returns(mockBody);
var vm = new BodyDetailViewModel(mockSelection);
vm.Update(DateTime.Now);
Assert.That(vm.DisplayName, Is.EqualTo("Earth"));
}
Integration Tests
View composition can be tested with mock rendering:
[Test]
public void SolarSystemView_WithAsteroid_RendersWithoutError()
{
var view = _viewFactory.CreateSolarSystemView();
var inputState = InputState.Empty;
Assert.DoesNotThrow(() =>
{
view.Update(GameTime.Zero);
view.Draw(null); // Or a mock SpriteBatch
});
}
Performance Considerations
Real-time rendering demands efficiency:
- Lazy ViewModel Updates: Only update visible panels
- Viewport Culling: Don't render bodies outside the camera
- Pooling: Reuse temporary Vector3 allocations
- Settings as Hints: Use quality settings to reduce geometry detail
Future Directions
- Reactive Bindings: Consider a lightweight reactive binding layer for ViewModel→View updates
- Scene Graph: Formalize the spatial hierarchy of panels and overlays
- Themes: Extend settings to support UI theme switching (dark/light modes)