Controls
RenderSurface
RenderSurface is the control for showing MonoGame-produced pixels inside the WPF-style UI tree. It reserves a normal layout slot, presents either a manual ImageSource or a control-managed render target, and gives you a focused bridge between declarative UI layout and custom drawing code.
Quick start
Use manual mode when another part of your application already owns the texture. Use managed mode when you want RenderSurface to allocate an offscreen target and ask you to draw into it.
Minimal control in XAML
<RenderSurface x:Name="PreviewSurface"
Width="320"
Height="180"
Stretch="Uniform" />
Present a texture you already have
namespace InkkSlinger;
public partial class MiniMapView : UserControl
{
private readonly RenderSurface? _previewSurface;
public MiniMapView()
{
InitializeComponent();
_previewSurface = this.FindName("PreviewSurface") as RenderSurface;
}
private void Publish(Texture2D texture)
{
_previewSurface?.Present(texture);
_previewSurface?.RefreshSurface();
}
}
Attach a draw handler
public partial class DiagnosticsView : UserControl
{
private readonly RenderSurface? _surface;
public DiagnosticsView()
{
InitializeComponent();
_surface = this.FindName("PreviewSurface") as RenderSurface;
if (_surface != null)
{
_surface.DrawSurface += OnDrawSurface;
}
}
private void OnDrawSurface(SpriteBatch spriteBatch, Rectangle bounds)
{
// Draw into the supplied offscreen target.
}
}
Default recommendation: start in manual mode if your gameplay or simulation code already renders into a texture. Choose managed mode only when you want the control to own the redraw target and callback lifecycle.
Choosing a mode
RenderSurface has three user-facing modes. Two are managed modes, where the control owns an offscreen target. The third is manual presentation mode, where you provide the finished surface yourself.
Manual presentation
Set Surface or call Present(...) when some other subsystem already produces the pixels. The control just presents that surface through the normal UI render path.
DrawSurface event
Subscribe to DrawSurface when you want direct managed rendering without building a custom subclass. The control creates the target and invokes your callback when a redraw is needed.
Subclass override
Override OnDrawSurface when the drawing logic belongs inside a reusable control type. This activates managed mode automatically, even without event subscribers.
- Managed mode is active whenever at least one
DrawSurfacehandler is attached or a subclass overridesOnDrawSurface. - While managed mode is active, the managed presentation surface overrides any manual
Surfacevalue. - If managed mode later becomes inactive, the previously assigned manual surface becomes visible again.
For deeper conceptual background, see What it is and The three modes.
Layout and sizing behavior
RenderSurface inherits from SurfacePresenterBase, so it measures from the current surface when it can and applies Stretch and StretchDirection to determine how the final texture is positioned inside its arranged slot.
Manual mode sizing
If the current surface has pixel dimensions, the control uses that natural size as its desired size. A 64x32 image produces a desired size of 64x32 before parent layout constraints are applied.
Managed mode sizing
Managed mode reports zero intrinsic size during measure. Give the control explicit space through Width, Height, minimum size, or a parent layout that stretches it.
- The final arranged slot can be larger or smaller than the surface's natural pixel size.
- The rendered texture is centered inside that slot after stretch scaling is computed.
- Rendering is clipped to the layout slot.
- When managed mode is arranged to a new pixel size, its offscreen resources are recreated.
Typical managed-mode layout
<Border Background="#081019"
BorderBrush="#28445F"
BorderThickness="1"
Padding="12">
<RenderSurface x:Name="GameSurface"
Width="560"
Height="392"
MinWidth="320"
MinHeight="224"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Stretch="Uniform" />
</Border>
Stretch and direction
<RenderSurface Width="320"
Height="180"
Stretch="UniformToFill"
StretchDirection="DownOnly" />
See Sizing and layout for the dedicated overview page.
Redraw and lifetime behavior
The redraw contract is small, but each method has a specific purpose. Use the method that matches how your pixels are produced.
Manual mode methods
Present(ImageSource?) and Present(Texture2D) change the current manual surface. RefreshSurface() requests another render pass without forcing a new measure pass. ClearSurface() removes the current manual surface.
Managed mode methods
InvalidateVisual() marks the managed surface dirty so it will be redrawn. RefreshSurface() also works in managed mode when you want the same lightweight redraw signal.
- Presenting the same surface reference again invalidates rendering without invalidating measure.
- Presenting a different surface size invalidates measure, arrange, and render because the natural size changed.
- Managed surfaces are cleared to transparent before each redraw.
- The draw callback receives a
SpriteBatchand aRectanglein managed-surface pixel coordinates. - Managed render targets are recreated when the graphics device changes or the arranged size changes.
Frame-driven reusable subclass
namespace InkkSlinger;
public sealed class RadarSurface : RenderSurface
{
protected override bool IsFrameUpdateActive => true;
protected override void OnFrameUpdate(GameTime gameTime)
{
_ = gameTime;
InvalidateVisual();
}
protected override void OnDrawSurface(SpriteBatch spriteBatch, Rectangle bounds)
{
// Draw the current frame here.
}
}
Important rule: OnFrameUpdate only participates when managed mode is active and IsFrameUpdateActive returns true.
Property and API reference
Surface API
Surface: manual ImageSource presented when managed mode is inactive.
Present(ImageSource?): set or replace the current manual surface.
Present(Texture2D): wrap a Texture2D in an ImageSource and present it.
RefreshSurface(): schedule another draw without changing the current source.
ClearSurface(): clear the manual surface reference.
Managed drawing API
DrawSurface: event-based draw hook for managed mode.
OnDrawSurface(SpriteBatch, Rectangle): virtual draw override for reusable derived controls.
IsFrameUpdateActive: opt-in flag for frame updates while managed mode is active.
OnFrameUpdate(GameTime): per-frame callback for subclasses that need to animate or schedule redraw work.
Inherited presentation properties
Stretch: controls scaling inside the arranged slot. Supported values are None, Fill, Uniform, and UniformToFill.
StretchDirection: constrains scaling to Both, UpOnly, or DownOnly.
Layout expectations
Width, Height, MinWidth, and MinHeight are commonly required in managed mode because the control has no intrinsic desired size there.
Opacity is applied when the resolved texture is finally drawn into the UI tree.
RenderSurface is a presenter, not a content host. It does not expose a child content model.
Patterns and examples
Manual placeholder surface
var surface = new RenderSurface();
surface.Present(ImageSource.FromPixels(160, 90));
CPU-backed texture upload
private void TryUploadLatestFrameData()
{
if (_framePixels == null || _surfaceTexture == null)
{
return;
}
RenderFrame(_framePixels, _surfaceTexture.Width, _surfaceTexture.Height);
_surfaceTexture.SetData(_framePixels);
_gameSurface?.RefreshSurface();
}
Managed event drawing
private void OnDrawGameSurface(SpriteBatch spriteBatch, Rectangle bounds)
{
spriteBatch.Draw(_pixel, bounds, new Color(8, 12, 21));
spriteBatch.Draw(_pixel, new Rectangle(bounds.X + 16, bounds.Y + 16, 96, 32), Color.CornflowerBlue);
}
Trigger a managed redraw
private void OnBoardChanged()
{
_gameSurface?.InvalidateVisual();
}
Reusable subclass
public sealed class MiniMapSurface : RenderSurface
{
protected override void OnDrawSurface(SpriteBatch spriteBatch, Rectangle bounds)
{
// Custom reusable drawing code.
}
}
The repository's demo views cover both major patterns: RenderSurfaceView uses manual presentation with a texture upload path, and RenderSurfaceGpuView uses the managed DrawSurface event.
Notes and pitfalls
Managed mode is exclusive while active. Manual and managed surfaces are not blended. The managed presentation surface wins until managed mode is turned off.
Managed mode needs real layout space. A managed RenderSurface without explicit sizing or parent-provided space can collapse because it measures as zero intrinsic size.
Use RefreshSurface() when pixels changed but layout did not. This is the lightweight redraw signal after updating an existing texture or when you want another render pass without changing the control contract.
Draw callbacks target an offscreen surface first. Your callback does not draw directly to the final UI backbuffer. It draws into the control-managed target, which is then presented like any other surface.
Expect recreation on resize and graphics-device changes. Managed targets are disposable graphics resources, so code should not assume they persist across those transitions.
For the lifecycle-focused companion page, see Rules worth knowing.