This document describes the architectural decisions, patterns, and conventions for the MauiControlsExtras library. All contributors and agents should follow these guidelines when developing new controls or modifying existing ones.
MauiControlsExtras provides custom controls for .NET MAUI applications, designed to fill gaps for CRUD/LOB (Line of Business) applications. The library emphasizes:
- Consistent Styling: All controls share common styling properties through a base class hierarchy
- MVVM Support: Every user action has both an event and a corresponding command
- Theme Integration: Controls respect global themes while allowing per-instance customization
- Optional Behaviors: Interface-based patterns for validation, clipboard, selection, and undo/redo
- Internal Reuse: When a library control can fulfill a need, prefer it over standard MAUI controls (e.g., DataGridComboBoxColumn uses our ComboBox, not MAUI Picker)
src/MauiControlsExtras/
├── Base/ # Base classes and interfaces
│ ├── StyledControlBase.cs # Core styling (all controls)
│ ├── TextStyledControlBase.cs # Typography (text controls)
│ ├── ListStyledControlBase.cs # Collection styling
│ ├── HeaderedControlBase.cs # Header styling
│ ├── NavigationControlBase.cs # Navigation state colors
│ ├── AnimatedControlBase.cs # Animation support
│ ├── IClipboardSupport.cs # Copy/cut/paste interface
│ ├── ISelectable.cs # Selection interface
│ ├── IUndoRedo.cs # Undo/redo interface
│ └── Validation/
│ ├── IValidatable.cs # Validation interface
│ └── ValidationResult.cs # Validation result type
├── Theming/
│ ├── IThemeAware.cs # Theme change notifications
│ ├── ControlsTheme.cs # Theme definition class
│ └── MauiControlsExtrasTheme.cs # Static theme manager
├── Controls/ # Control implementations
│ ├── ComboBox.xaml/.cs
│ └── [other controls]
└── Converters/ # Value converters
└── MauiAssetImageConverter.cs
Controls inherit from the most specific base class that provides the properties they need:
ContentView
└── StyledControlBase (colors, borders, shadows)
├── TextStyledControlBase (+ typography)
│ └── ComboBox, NumericUpDown, TokenEntry, etc.
├── ListStyledControlBase (+ collection styling)
│ └── TreeView, DataGridView, etc.
├── HeaderedControlBase (+ header styling)
│ └── Accordion, PropertyGrid, Calendar, etc.
├── NavigationControlBase (+ navigation states)
│ └── Wizard, TabBar, etc. (step/tab navigation)
└── AnimatedControlBase (+ animation support)
└── [animated controls]
| Control Type | Base Class | Key Features |
|---|---|---|
| Text input controls | TextStyledControlBase |
Font, text color, placeholder |
| Dropdown/combo controls | TextStyledControlBase |
Font, text color, placeholder |
| List/grid controls | ListStyledControlBase |
Alternating rows, selection colors, separators |
| Accordion/expandable | HeaderedControlBase |
Header styling, border |
| Wizard/stepper (UI navigation) | NavigationControlBase |
Active/inactive/visited states |
| Animated controls | AnimatedControlBase |
Duration, easing, enable flag |
| Data navigation (BindingNavigator) | StyledControlBase |
No per-item visual states |
| Breadcrumb | StyledControlBase |
Own active/inactive colors, no visited state |
| Simple styled controls | StyledControlBase |
Just colors, borders, shadows |
All controls inherit these core properties:
// Colors
AccentColor // Primary accent (default: null, uses theme)
ForegroundColor // Text/icon color (theme-aware)
DisabledColor // Disabled state color
ErrorColor // Validation error color (#D32F2F)
SuccessColor // Success indication (#388E3C)
WarningColor // Warning indication (#F57C00)
// Borders
CornerRadius // Rounded corners (default: 4)
BorderColor // Normal border (theme-aware)
BorderThickness // Border width (default: 1)
FocusBorderColor // Focused state (defaults to AccentColor)
ErrorBorderColor // Error state (defaults to ErrorColor)
DisabledBorderColor // Disabled state
// Shadows
HasShadow // Enable shadow (default: false)
ShadowColor // Shadow color (theme-aware)
ShadowOffset // Shadow position
ShadowRadius // Shadow blur
ShadowOpacity // Shadow transparency
// Elevation
Elevation // Material-style elevation (0-24)For every nullable property, there is an Effective* computed property that falls back to theme defaults:
// Property definition
public Color? BorderColor { get; set; }
// Effective property (never null)
public Color EffectiveBorderColor =>
BorderColor ?? MauiControlsExtrasTheme.GetBorderColor();Rule: Always bind XAML to Effective* properties, not raw properties.
<!-- CORRECT -->
<Border Stroke="{Binding EffectiveBorderColor, Source={x:Reference thisControl}}" />
<!-- WRONG - may be null -->
<Border Stroke="{Binding BorderColor, Source={x:Reference thisControl}}" />When a property changes, always notify both the raw and effective properties:
private static void OnBorderColorChanged(BindableObject bindable, object oldValue, object newValue)
{
if (bindable is StyledControlBase control)
{
control.OnPropertyChanged(nameof(EffectiveBorderColor)); // Always include
control.OnBorderColorChanged((Color?)oldValue, (Color?)newValue);
}
}ControlsTheme contains all theme values organized by category:
public class ControlsTheme
{
// Semantic colors
public Color AccentColor { get; set; }
public Color ErrorColor { get; set; }
public Color SuccessColor { get; set; }
public Color WarningColor { get; set; }
// Surface colors (separate for light/dark)
public Color SurfaceColor { get; set; }
public Color SurfaceColorDark { get; set; }
// Selection colors
public Color SelectionBackgroundColor { get; set; }
public Color SelectionTextColor { get; set; }
// Typography
public double DefaultFontSize { get; set; }
public string? FontFamily { get; set; }
// Shape
public double DefaultCornerRadius { get; set; }
public double DefaultBorderThickness { get; set; }
// Animation
public int AnimationDuration { get; set; }
public Easing AnimationEasing { get; set; }
public bool EnableAnimations { get; set; }
}The library ships with predefined themes:
ControlsTheme.Default- Balanced defaultsControlsTheme.Modern- Rounded, subtle shadowsControlsTheme.Compact- Reduced spacing, smaller radiiControlsTheme.Fluent- Microsoft Fluent-inspiredControlsTheme.Material3- Google Material 3-inspiredControlsTheme.HighContrast- Accessibility-focused
// Apply a predefined theme
MauiControlsExtrasTheme.ApplyTheme(ControlsTheme.Material3);
// Create a custom theme
var custom = MauiControlsExtrasTheme.CreateCustomTheme(theme =>
{
theme.AccentColor = Colors.Purple;
theme.DefaultCornerRadius = 12;
});
MauiControlsExtrasTheme.ApplyTheme(custom);
// Modify current theme
MauiControlsExtrasTheme.ModifyCurrentTheme(theme =>
{
theme.EnableAnimations = false;
});Controls implement IThemeAware and subscribe to theme changes:
public abstract class StyledControlBase : ContentView, IThemeAware
{
protected StyledControlBase()
{
MauiControlsExtrasTheme.ThemeChanged += OnGlobalThemeChanged;
}
public virtual void OnThemeChanged(AppTheme theme)
{
// Notify all effective properties
OnPropertyChanged(nameof(EffectiveBorderColor));
OnPropertyChanged(nameof(EffectiveForegroundColor));
// ... etc
}
}The library automatically bridges MAUI's Application.RequestedThemeChanged event to the library's ThemeChanged event. This ensures controls update when users toggle Application.Current.UserAppTheme (the standard MAUI approach for Light/Dark switching).
- Initialization: The bridge is enabled lazily from
StyledControlBase.EnsureThemeSubscription()viaMauiControlsExtrasTheme.EnableMauiThemeBridge(). No manual setup is required. - Debounce: A
_lastNotifiedThemeguard prevents double-firing when bothRaiseThemeChanged()andRequestedThemeChangedfire for the same theme transition. - Idempotent:
EnableMauiThemeBridge()is safe to call multiple times — only the first call subscribes. - Null-safe: When
Application.Currentis null (unit tests, design-time), the bridge is a no-op.
Every user action must have both an event AND a command.
// Event (code-behind support)
public event EventHandler<SelectionChangedEventArgs>? SelectionChanged;
// Command (MVVM support)
public ICommand? SelectionChangedCommand { get; set; }
public object? SelectionChangedCommandParameter { get; set; }When an action occurs, invoke both the event and command:
private void OnItemSelected(object item)
{
var args = new SelectionChangedEventArgs(oldItem, item);
// Raise event
SelectionChanged?.Invoke(this, args);
// Execute command
var parameter = SelectionChangedCommandParameter ?? item;
if (SelectionChangedCommand?.CanExecute(parameter) == true)
{
SelectionChangedCommand.Execute(parameter);
}
}| Action | Command Name | Command Parameter |
|---|---|---|
| Selection change | SelectionChangedCommand |
Selected item or custom |
| Open/expand | OpenedCommand |
Control instance or null |
| Close/collapse | ClosedCommand |
Control instance or null |
| Clear/reset | ClearCommand |
None |
| Validate | ValidateCommand |
Validation result |
| Value change | ValueChangedCommand |
New value |
Controls opt-in to behaviors by implementing interfaces. Do NOT add these to base classes.
For controls that support right-click context menus with platform-specific native implementations:
public interface IContextMenuSupport
{
ContextMenuItemCollection ContextMenuItems { get; }
bool ShowDefaultContextMenu { get; set; }
event EventHandler<ContextMenuOpeningEventArgs>? ContextMenuOpening;
void ShowContextMenu(Point? position = null);
}Platform implementations:
- Windows: MenuFlyout with FontIcon support
- macOS: UIMenu via UIContextMenuInteraction
- iOS: UIAlertController (action sheet style)
- Android: PopupMenu
Implementation requirements:
- Use
ContextMenuService.Currentto show native menus - Fire
ContextMenuOpeningevent before showing menu - Populate
ContextMenuItemswith custom items - Add default items (Copy, Paste, etc.) when
ShowDefaultContextMenuis true
For controls that support input validation:
public interface IValidatable
{
bool IsValid { get; }
IReadOnlyList<string> ValidationErrors { get; }
ICommand? ValidateCommand { get; set; }
ValidationResult Validate();
}Implementation requirements:
- Add
IsRequiredproperty for required field validation - Call
Validate()on value changes or when explicitly requested - Update
IsValidandValidationErrorsafter validation - Apply
EffectiveErrorBorderColorwhen invalid
For controls that support copy/cut/paste:
public interface IClipboardSupport
{
bool CanCopy { get; }
bool CanCut { get; }
bool CanPaste { get; }
void Copy();
void Cut();
void Paste();
object? GetClipboardContent();
ICommand? CopyCommand { get; set; }
ICommand? CutCommand { get; set; }
ICommand? PasteCommand { get; set; }
}Implementation requirements:
- Update
Can*properties when selection or content changes - Support keyboard shortcuts (Ctrl+C, Ctrl+X, Ctrl+V)
- Handle platform-specific clipboard APIs
- Cut/Paste should be undoable if control implements
IUndoRedo
For controls that support content/item selection:
public interface ISelectable
{
bool HasSelection { get; }
bool IsAllSelected { get; }
bool SupportsMultipleSelection { get; }
void SelectAll();
void ClearSelection();
object? GetSelection();
void SetSelection(object? selection);
event EventHandler<SelectionChangedEventArgs>? SelectionChanged;
ICommand? SelectAllCommand { get; set; }
ICommand? ClearSelectionCommand { get; set; }
ICommand? SelectionChangedCommand { get; set; }
}Implementation requirements:
- Support keyboard shortcut Ctrl+A for SelectAll
- Raise
SelectionChangedfor all selection modifications - Update
HasSelectionandIsAllSelectedappropriately
For controls that support undo/redo:
public interface IUndoRedo
{
bool CanUndo { get; }
bool CanRedo { get; }
int UndoCount { get; }
int RedoCount { get; }
int UndoLimit { get; set; }
bool Undo();
bool Redo();
void ClearUndoHistory();
string? GetUndoDescription();
string? GetRedoDescription();
void BeginBatchOperation(string? description = null);
void EndBatchOperation();
void CancelBatchOperation();
ICommand? UndoCommand { get; set; }
ICommand? RedoCommand { get; set; }
}Implementation requirements:
- Support Ctrl+Z (undo) and Ctrl+Y/Ctrl+Shift+Z (redo)
- Clear redo stack when new changes occur after undo
- Implement batch operations for multi-step changes
- Respect
UndoLimitto prevent memory issues - Clear history when loading new data
All controls in this library MUST support keyboard navigation, mouse interactions, and proper focus management. This is NOT optional - controls without desktop platform support are considered incomplete.
The library targets all MAUI-supported platforms:
- Windows (desktop) - Full keyboard + mouse
- macOS (via Mac Catalyst) - Full keyboard + mouse
- iOS (tablet/phone) - Touch + external keyboard
- Android (tablet/phone) - Touch + external keyboard
All interactive controls MUST implement IKeyboardNavigable:
public interface IKeyboardNavigable
{
/// <summary>
/// Gets whether this control can receive keyboard focus.
/// </summary>
bool CanReceiveFocus { get; }
/// <summary>
/// Gets or sets whether keyboard navigation is enabled.
/// </summary>
bool IsKeyboardNavigationEnabled { get; set; }
/// <summary>
/// Handles a key press event. Returns true if handled.
/// </summary>
bool HandleKeyPress(KeyEventArgs e);
/// <summary>
/// Gets the keyboard shortcuts supported by this control.
/// </summary>
IReadOnlyList<KeyboardShortcut> GetKeyboardShortcuts();
}Controls MUST support these standard shortcuts where applicable:
| Shortcut | Action | Applicable Controls |
|---|---|---|
Tab |
Move focus to next control | All |
Shift+Tab |
Move focus to previous control | All |
Enter |
Activate/confirm | Buttons, ComboBox, DataGrid |
Escape |
Cancel/close | Popups, Dropdowns, Edit mode |
Space |
Toggle/select | CheckBox, Toggle, TreeView |
Ctrl+A |
Select all | Text, DataGrid, Lists |
Ctrl+C |
Copy | IClipboardSupport controls |
Ctrl+X |
Cut | IClipboardSupport controls |
Ctrl+V |
Paste | IClipboardSupport controls |
Ctrl+Z |
Undo | IUndoRedo controls |
Ctrl+Y |
Redo | IUndoRedo controls |
Delete |
Delete selection | DataGrid, TokenEntry, Lists |
F2 |
Enter edit mode | DataGrid, Editable cells |
Arrow Keys |
Navigate | All navigable controls |
Home/End |
Navigate to start/end | Lists, DataGrid, Text |
Page Up/Down |
Page navigation | Lists, DataGrid |
Arrow Up/Down- Navigate rowsArrow Left/Right- Navigate columnsTab- Move to next cellShift+Tab- Move to previous cellEnter- Commit edit and move downEscape- Cancel editF2- Enter edit modeDelete- Delete selected rows (if allowed)Ctrl+C/X/V/Z/Y- Clipboard and undo
Arrow Up/Down- Navigate itemsArrow Right- Expand node / move to first childArrow Left- Collapse node / move to parentEnter- Activate itemSpace- Toggle selection (multi-select mode)+/-or*- Expand/collapse
Arrow Up/Down- Navigate items in dropdownEnter- Select item and closeEscape- Close without selectingSpace- Toggle selection (multi-select)Type-ahead- Filter/search itemsAlt+Down- Open dropdownHome/End- Jump to first/last item
Arrow Up- Increment valueArrow Down- Decrement valuePage Up- Increment by large stepPage Down- Decrement by large stepHome- Set to minimumEnd- Set to maximum
Enter- Confirm current tokenBackspace- Delete last token (when input empty)Delete- Delete selected tokenArrow Left/Right- Navigate tokensCtrl+A- Select all tokens
Arrow Left/Right- Move selected thumbTab- Switch between thumbsHome/End- Move to min/maxPage Up/Down- Large step movement
Arrow Left/Right- Decrease/increase rating1-5(or1-N) - Set specific rating0orDelete- Clear rating
- Focus on click - Clicking a control MUST focus it
- Visual focus indicator - Focused controls MUST show clear visual feedback
- Hover states - Desktop controls SHOULD show hover feedback
Controls with actions MUST support right-click context menus:
- DataGridView - Copy, Paste, Delete, Undo/Redo
- TreeView - Expand All, Collapse All, actions
- TokenEntry - Copy, Delete token
- ComboBox - Cut, Copy, Paste, Select All, Clear
- MultiSelectComboBox - Cut, Copy, Paste, Select All, Clear
- Text controls - Cut, Copy, Paste, Select All
- DataGridView - Scroll vertically
- ComboBox dropdown - Scroll items
- NumericUpDown - Increment/decrement value
- RangeSlider - Adjust value (when focused)
- Rating - Adjust rating (when focused)
- DataGridView - Enter edit mode
- TreeView - Expand/collapse or activate
All focusable controls MUST define these visual states:
// Required focus-related properties (inherited from StyledControlBase)
FocusBorderColor // Border color when focused- Controls MUST participate in tab navigation
- Use
TabIndexfor custom ordering IsTabStop="False"only for non-interactive elements
Modal controls (popups, dialogs) MUST trap focus:
- Tab cycles within the control
- Escape closes and returns focus
public partial class MyControl : StyledControlBase, IKeyboardNavigable
{
public bool CanReceiveFocus => IsEnabled && IsVisible;
public bool IsKeyboardNavigationEnabled { get; set; } = true;
public MyControl()
{
InitializeComponent();
// Attach keyboard behavior
Behaviors.Add(new KeyboardBehavior
{
KeyPressedCommand = new Command<KeyEventArgs>(OnKeyPressed)
});
}
public bool HandleKeyPress(KeyEventArgs e)
{
if (!IsKeyboardNavigationEnabled) return false;
switch (e.Key)
{
case Keys.Enter:
Activate();
return true;
case Keys.Escape:
Cancel();
return true;
// ... handle other keys
}
// Handle shortcuts
if (e.Modifiers.HasFlag(KeyModifiers.Control))
{
switch (e.Key)
{
case Keys.C when this is IClipboardSupport cs:
cs.Copy();
return true;
case Keys.V when this is IClipboardSupport cs:
cs.Paste();
return true;
case Keys.Z when this is IUndoRedo ur:
ur.Undo();
return true;
case Keys.Y when this is IUndoRedo ur:
ur.Redo();
return true;
}
}
return false;
}
public IReadOnlyList<KeyboardShortcut> GetKeyboardShortcuts()
{
var shortcuts = new List<KeyboardShortcut>
{
new("Enter", "Activate control"),
new("Escape", "Cancel operation"),
};
if (this is IClipboardSupport)
{
shortcuts.Add(new("Ctrl+C", "Copy"));
shortcuts.Add(new("Ctrl+V", "Paste"));
}
return shortcuts;
}
}Controls MUST be tested for:
- All keyboard shortcuts - Verify each shortcut works
- Tab navigation - Verify focus moves correctly
- Focus visuals - Verify focus indicator is visible
- Mouse interactions - Verify click, right-click, hover, wheel
- Cross-platform - Test on Windows, macOS minimum
Always use x:Name="thisControl" for the root element:
<base:TextStyledControlBase
xmlns:base="clr-namespace:MauiControlsExtras.Base"
x:Class="MauiControlsExtras.Controls.MyControl"
x:Name="thisControl">Use Source={x:Reference thisControl} for bindings:
<Border Stroke="{Binding EffectiveBorderColor, Source={x:Reference thisControl}}"
StrokeThickness="{Binding EffectiveBorderThickness, Source={x:Reference thisControl}}">Use AppThemeBinding for colors not exposed via Effective properties:
<Label TextColor="{AppThemeBinding Light=#212121, Dark=#FFFFFF}" />Conditionally apply shadows based on HasShadow:
<Border.Shadow>
<Shadow Brush="{Binding EffectiveShadowColor, Source={x:Reference thisControl}}"
Offset="{Binding EffectiveShadowOffset, Source={x:Reference thisControl}}"
Radius="{Binding EffectiveShadowRadius, Source={x:Reference thisControl}}"
Opacity="{Binding EffectiveShadowOpacity, Source={x:Reference thisControl}}" />
</Border.Shadow>| Type | Convention | Example |
|---|---|---|
| Bindable property | {Name}Property |
AccentColorProperty |
| CLR property | PascalCase | AccentColor |
| Effective property | Effective{Name} |
EffectiveAccentColor |
| Command | {Action}Command |
SelectionChangedCommand |
| Command parameter | {Action}CommandParameter |
SelectionChangedCommandParameter |
| Type | Convention | Example |
|---|---|---|
| Event | {Action} (past tense preferred) |
SelectionChanged, Opened |
| Event args | {Action}EventArgs |
SelectionChangedEventArgs |
| Type | Convention | Example |
|---|---|---|
| Property changed handler | On{Property}Changed |
OnAccentColorChanged |
| Event handler | On{ElementName}{Event} |
OnCollapsedTapped |
| Virtual override hook | On{Property}Changed (protected virtual) |
Allows subclass override |
- Property defaults - Verify default values match documentation
- Effective property fallback - Verify fallback to theme when null/-1
- Command execution - Verify commands are invoked with correct parameters
- Event raising - Verify events are raised at correct times
- Validation - Verify validation logic and error messages
[Fact]
public void AccentColor_WhenNotSet_ReturnsThemeDefault()
{
var control = new MyControl();
Assert.Equal(MauiControlsExtrasTheme.Current.AccentColor, control.EffectiveAccentColor);
}
[Fact]
public void SelectionChangedCommand_WhenItemSelected_IsExecuted()
{
var executed = false;
var control = new MyControl
{
SelectionChangedCommand = new Command(() => executed = true)
};
control.SelectedItem = new object();
Assert.True(executed);
}- Change base class to appropriate styled base
- Remove properties now inherited from base (e.g.,
AccentColor) - Update XAML bindings to use
Effective*properties - Add command equivalents for all events
- Implement relevant interfaces (
IValidatable,IClipboardSupport, etc.) - Add tests for new functionality
- Property removals: Deprecated for one major version before removal
- Property renames: Both names supported for one major version
- Base class changes: Document migration path in release notes
| Version | Changes |
|---|---|
| 1.0.0 | Initial release with ComboBox |
| 2.0.0 | Added base class hierarchy, theming, MVVM commands, validation, data action interfaces |