| MAUI-CommunityToolkit-11 :👈 | 👉:MAUI-Styles-and-Resources |
The “Headless” Pattern in C# (and .NET MAUI) |
The “Headless” pattern in C# (and .NET MAUI) refers to building components that contain only logic and state management, without any UI rendering. This allows you to separate business logic from presentation, making components reusable across different UI frameworks or platforms. In MAUI, headless components are often paired with XAML or markup-based views to provide the visual layer.
Definition: A headless component encapsulates logic, state, and behavior but does not dictate how the UI looks.
Purpose: Promotes separation of concerns — the logic can be reused across different UI implementations.
Analogy: Similar to “headless CMS” where the backend manages content but doesn’t enforce how it’s displayed.
Reusability: Same logic can be used in multiple views.
Testability: Easier to unit test since UI is decoupled.
Flexibility: Different UI layers (desktop, mobile, web) can consume the same logic.
Let’s say we want a headless counter component:
public class CounterComponent
{
public int Count { get; private set; }
public event Action<int> OnCountChanged;
public void Increment()
{
Count++;
OnCountChanged?.Invoke(Count);
}
public void Decrement()
{
Count--;
OnCountChanged?.Invoke(Count);
}
}
This class has no UI code.
It manages state (Count) and exposes events (OnCountChanged).
Any UI framework (MAUI, WPF, Blazor) can subscribe to the event and render accordingly.
Here’s how you can integrate the headless component into a MAUI page:
public partial class CounterPage : ContentPage
{
private readonly CounterComponent _counter;
public CounterPage()
{
InitializeComponent();
_counter = new CounterComponent();
_counter.OnCountChanged += UpdateUI;
}
private void IncrementClicked(object sender, EventArgs e)
{
_counter.Increment();
}
private void DecrementClicked(object sender, EventArgs e)
{
_counter.Decrement();
}
private void UpdateUI(int newCount)
{
CounterLabel.Text = $"Count: {newCount}";
}
}
And the XAML UI:
<VerticalStackLayout Padding="20">
<Label x:Name="CounterLabel" Text="Count: 0" FontSize="24"/>
<Button Text="Increment" Clicked="IncrementClicked"/>
<Button Text="Decrement" Clicked="DecrementClicked"/>
</VerticalStackLayout>
The logic lives in CounterComponent.
The UI (XAML) simply reacts to state changes.
This makes the counter reusable in other apps or platforms without rewriting logic.
Headless pattern = logic-only components.
In C#/.NET MAUI, it’s implemented by separating state/behavior classes from UI markup.
Great for scalable, testable, and reusable applications.
By separating logic from UI, you can unit test the behavior of your component without worrying about the rendering layer. In MAUI, this means you don’t need to spin up a full UI or emulator to validate your business rules.
No UI dependencies: Tests run faster because they don’t rely on MAUI’s rendering engine.
Pure logic testing: You can focus on state changes, events, and outputs.
Predictable results: UI frameworks often introduce async rendering or platform-specific quirks — headless avoids that.
public class CounterComponent
{
public int Count { get; private set; }
public event Action<int> OnCountChanged;
public void Increment()
{
Count++;
OnCountChanged?.Invoke(Count);
}
public void Decrement()
{
Count--;
OnCountChanged?.Invoke(Count);
}
}
using Xunit;
public class CounterComponentTests
{
[Fact]
public void Increment_ShouldIncreaseCount()
{
var counter = new CounterComponent();
int observedCount = 0;
counter.OnCountChanged += c => observedCount = c;
counter.Increment();
Assert.Equal(1, counter.Count);
Assert.Equal(1, observedCount);
}
[Fact]
public void Decrement_ShouldDecreaseCount()
{
var counter = new CounterComponent();
counter.Increment(); // start at 1
int observedCount = 0;
counter.OnCountChanged += c => observedCount = c;
counter.Decrement();
Assert.Equal(0, counter.Count);
Assert.Equal(0, observedCount);
}
}
<VerticalStackLayout Padding="20">
<Label x:Name="CounterLabel" Text="Count: 0" FontSize="24"/>
<Button Text="Increment" Clicked="IncrementClicked"/>
<Button Text="Decrement" Clicked="DecrementClicked"/>
</VerticalStackLayout>
public partial class CounterPage : ContentPage
{
private readonly CounterComponent _counter;
public CounterPage()
{
InitializeComponent();
_counter = new CounterComponent();
_counter.OnCountChanged += count => CounterLabel.Text = $"Count: {count}";
}
private void IncrementClicked(object sender, EventArgs e) => _counter.Increment();
private void DecrementClicked(object sender, EventArgs e) => _counter.Decrement();
}
The unit tests validate the headless logic (CounterComponent) without touching MAUI’s UI. The UI layer simply subscribes to events and displays results. This makes your app more robust, testable, and maintainable.
let’s scale the headless pattern into a real-world MAUI scenario like a form validator. This shows how headless logic makes unit testing straightforward, while the UI layer simply consumes the results.
public class LoginValidator
{
public bool Validate(string username, string password, out string errorMessage)
{
if (string.IsNullOrWhiteSpace(username))
{
errorMessage = "Username is required.";
return false;
}
if (string.IsNullOrWhiteSpace(password))
{
errorMessage = "Password is required.";
return false;
}
if (password.Length < 6)
{
errorMessage = "Password must be at least 6 characters.";
return false;
}
errorMessage = string.Empty;
return true;
}
}
Logic only: No UI code, just validation rules.
Reusable: Can be plugged into MAUI, Blazor, WPF, or even a console app.
using Xunit;
public class LoginValidatorTests
{
[Fact]
public void EmptyUsername_ShouldFail()
{
var validator = new LoginValidator();
var result = validator.Validate("", "password123", out var error);
Assert.False(result);
Assert.Equal("Username is required.", error);
}
[Fact]
public void ShortPassword_ShouldFail()
{
var validator = new LoginValidator();
var result = validator.Validate("user", "123", out var error);
Assert.False(result);
Assert.Equal("Password must be at least 6 characters.", error);
}
[Fact]
public void ValidCredentials_ShouldPass()
{
var validator = new LoginValidator();
var result = validator.Validate("user", "password123", out var error);
Assert.True(result);
Assert.Equal(string.Empty, error);
}
}
Tests run without MAUI UI.
You can validate rules quickly and reliably.
XAML UI:
<VerticalStackLayout Padding="20">
<Entry x:Name="UsernameEntry" Placeholder="Enter username"/>
<Entry x:Name="PasswordEntry" Placeholder="Enter password" IsPassword="True"/>
<Button Text="Login" Clicked="OnLoginClicked"/>
<Label x:Name="ErrorLabel" TextColor="Red"/>
</VerticalStackLayout>
Code-behind:
public partial class LoginPage : ContentPage
{
private readonly LoginValidator _validator = new LoginValidator();
public LoginPage()
{
InitializeComponent();
}
private void OnLoginClicked(object sender, EventArgs e)
{
var isValid = _validator.Validate(
UsernameEntry.Text,
PasswordEntry.Text,
out var errorMessage);
ErrorLabel.Text = isValid ? "Login successful!" : errorMessage;
}
}
Unit tests validate the headless logic (LoginValidator) independently.
MAUI UI just consumes results and displays feedback.
This makes your app robust, testable, and maintainable, especially for complex workflows like authentication, shopping carts, or form submissions.
| MAUI-CommunityToolkit-11 :👈 | 👉:MAUI-Styles-and-Resources |