Handling Events and Commands
Handling user interactions is a fundamental aspect of building interactive applications. Avalonia UI provides a comprehensive event system and command pattern to help you respond to user actions and implement application logic.
Event Handling
Events in Avalonia UI allow controls to notify your application when something happens, such as a button click or a text change.
Basic Event Handling
You can handle events in XAML by specifying the event name and the handler method:
<Button Content="Click Me" Click="OnButtonClick" />
Then implement the handler method in the code-behind file:
/// <summary>
/// Handles the button click event.
/// </summary>
/// <param name="sender">The object that raised the event (the button).</param>
/// <param name="e">Event data containing information about the click event.</param>
/// <remarks>
/// This method demonstrates a simple event handler that changes the button's content
/// when clicked. In a real application, you would typically perform more meaningful actions.
/// </remarks>
public void OnButtonClick(object sender, RoutedEventArgs e)
{
// Cast the sender to Button to access button-specific properties
if (sender is Button button)
{
// Update the button's content to provide visual feedback
button.Content = "Clicked!";
// You could also perform other actions here, such as:
// - Updating application state
// - Navigating to another view
// - Executing business logic
}
}
Common Control Events
Different controls expose different events. Here are some common events:
Button:
Click
: Occurs when the button is clickedPointerEntered
: Occurs when the pointer enters the buttonPointerExited
: Occurs when the pointer leaves the button
TextBox:
TextChanged
: Occurs when the text changesTextInput
: Occurs when text is inputGotFocus
: Occurs when the control receives focusLostFocus
: Occurs when the control loses focus
CheckBox:
Checked
: Occurs when the checkbox is checkedUnchecked
: Occurs when the checkbox is uncheckedIsCheckedChanged
: Occurs when the IsChecked property changes
ComboBox:
SelectionChanged
: Occurs when the selected item changesDropDownOpened
: Occurs when the dropdown opensDropDownClosed
: Occurs when the dropdown closes
ListBox:
SelectionChanged
: Occurs when the selection changesDoubleTapped
: Occurs when an item is double-tapped
Event Subscription in Code
You can also subscribe to events in code:
/// <summary>
/// Main window constructor. Initializes components and sets up event handlers.
/// </summary>
public MainWindow()
{
InitializeComponent();
// Find the button control by its name
// This approach is useful when you need to access controls defined in XAML
var button = this.FindControl<Button>("myButton");
if (button != null)
{
// Subscribe to the Click event
button.Click += OnButtonClick;
}
else
{
// Log a warning if the button wasn't found
System.Diagnostics.Debug.WriteLine("Warning: Button 'myButton' not found in the visual tree");
}
}
/// <summary>
/// Handles the button click event.
/// </summary>
/// <param name="sender">The object that raised the event.</param>
/// <param name="e">Event data containing information about the click event.</param>
private void OnButtonClick(object sender, RoutedEventArgs e)
{
// Handle the click event
if (sender is Button button)
{
// Perform actions in response to the button click
System.Diagnostics.Debug.WriteLine($"Button '{button.Name}' was clicked");
}
}
/// <summary>
/// Called when the control is unloaded from the visual tree.
/// </summary>
/// <param name="e">Event data containing information about the unload event.</param>
/// <remarks>
/// It's important to unsubscribe from events when a control is unloaded to prevent memory leaks.
/// If you don't unsubscribe, the event handler will keep a reference to this control,
/// preventing it from being garbage collected.
/// </remarks>
protected override void OnUnloaded(RoutedEventArgs e)
{
// Always call the base implementation first
base.OnUnloaded(e);
// Find the button and unsubscribe from its event
var button = this.FindControl<Button>("myButton");
if (button != null)
{
button.Click -= OnButtonClick;
}
// Clean up any other resources or event subscriptions here
}
Event Arguments
Different events provide different event argument types that contain information about the event:
/// <summary>
/// Handles the button click event.
/// </summary>
/// <param name="sender">The object that raised the event.</param>
/// <param name="e">Event data containing information about the routed event.</param>
private void OnButtonClick(object sender, RoutedEventArgs e)
{
// RoutedEventArgs contains information about the routed event
// such as the source of the event and whether it's been handled
// Get the original source of the event (might be different from sender)
var originalSource = e.Source;
// Check if the event has been handled by another handler
bool isHandled = e.Handled;
// Mark the event as handled to prevent it from bubbling further
// e.Handled = true;
}
/// <summary>
/// Handles the text changed event for a TextBox.
/// </summary>
/// <param name="sender">The TextBox that raised the event.</param>
/// <param name="e">Event data containing information about the text change.</param>
private void OnTextChanged(object sender, TextChangedEventArgs e)
{
// TextChangedEventArgs contains information about the text change
if (sender is TextBox textBox)
{
// Get the current text
string newText = textBox.Text;
// Perform validation or other processing
System.Diagnostics.Debug.WriteLine($"Text changed to: {newText}");
// You could also access the TextChangedEventArgs for more details
// about the change, though Avalonia's implementation may not provide
// as much detail as WPF's version
}
}
/// <summary>
/// Handles the selection changed event for selection controls like ListBox or ComboBox.
/// </summary>
/// <param name="sender">The control that raised the event.</param>
/// <param name="e">Event data containing information about the selection change.</param>
private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
// SelectionChangedEventArgs contains information about the selection change
// Items that were added to the selection
var addedItems = e.AddedItems;
if (addedItems.Count > 0)
{
System.Diagnostics.Debug.WriteLine($"Added {addedItems.Count} item(s) to selection");
foreach (var item in addedItems)
{
System.Diagnostics.Debug.WriteLine($" - {item}");
}
}
// Items that were removed from the selection
var removedItems = e.RemovedItems;
if (removedItems.Count > 0)
{
System.Diagnostics.Debug.WriteLine($"Removed {removedItems.Count} item(s) from selection");
foreach (var item in removedItems)
{
System.Diagnostics.Debug.WriteLine($" - {item}");
}
}
}
/// <summary>
/// Handles the pointer pressed event.
/// </summary>
/// <param name="sender">The control that raised the event.</param>
/// <param name="e">Event data containing information about the pointer press.</param>
private void OnPointerPressed(object sender, PointerPressedEventArgs e)
{
// PointerPressedEventArgs contains information about the pointer press
// Get the position of the pointer relative to a specific visual element
var position = e.GetPosition(this);
System.Diagnostics.Debug.WriteLine($"Pointer pressed at: X={position.X}, Y={position.Y}");
// Get information about the pointer device
var pointer = e.Pointer;
System.Diagnostics.Debug.WriteLine($"Pointer type: {pointer.Type}");
// Check which mouse button was pressed (if it's a mouse pointer)
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
System.Diagnostics.Debug.WriteLine("Left button pressed");
}
else if (e.GetCurrentPoint(this).Properties.IsRightButtonPressed)
{
System.Diagnostics.Debug.WriteLine("Right button pressed");
}
// Capture the pointer to receive all pointer events until released
e.Pointer.Capture(sender as IInputElement);
}
Command Pattern
The command pattern is an alternative to event handling that works well with the MVVM pattern. Commands encapsulate an action and its execution logic, allowing you to bind user interface elements to actions in your view model.
ICommand Interface
Commands implement the ICommand
interface:
/// <summary>
/// Defines a command that can be bound to user interface elements like buttons.
/// </summary>
/// <remarks>
/// The ICommand interface is part of the .NET Framework and is used to implement
/// the command pattern, which separates the object that invokes an operation
/// from the object that knows how to perform it.
/// </remarks>
public interface ICommand
{
/// <summary>
/// Occurs when changes occur that affect whether the command can execute.
/// </summary>
event EventHandler? CanExecuteChanged;
/// <summary>
/// Determines whether the command can execute in its current state.
/// </summary>
/// <param name="parameter">Data used by the command. If the command does not require data,
/// this parameter can be set to null.</param>
/// <returns>true if this command can be executed; otherwise, false.</returns>
bool CanExecute(object? parameter);
/// <summary>
/// Executes the command.
/// </summary>
/// <param name="parameter">Data used by the command. If the command does not require data,
/// this parameter can be set to null.</param>
void Execute(object? parameter);
}
Execute
: Performs the command actionCanExecute
: Determines if the command can be executedCanExecuteChanged
: Raised when the ability to execute the command changes
Implementing Commands
There are several ways to implement commands in Avalonia UI:
1. Using ReactiveCommand (ReactiveUI)
ReactiveUI's ReactiveCommand
is the recommended way to implement commands in Avalonia UI:
using System;
using System.Diagnostics;
using System.Reactive;
using ReactiveUI;
namespace MyAvaloniaApp.ViewModels;
/// <summary>
/// View model for the main window demonstrating ReactiveCommand usage.
/// </summary>
/// <remarks>
/// This class shows how to implement commands using ReactiveUI's ReactiveCommand,
/// which is the recommended approach for Avalonia applications.
/// </remarks>
public class MainViewModel : ViewModelBase
{
private string _name = string.Empty;
/// <summary>
/// Gets the command that executes the greeting action.
/// </summary>
/// <remarks>
/// ReactiveCommand<TParam, TResult> is a generic type where:
/// - TParam: The type of parameter the command accepts (Unit means no parameter)
/// - TResult: The type of result the command produces (Unit means no result)
/// </remarks>
public ReactiveCommand<Unit, Unit> GreetCommand { get; }
/// <summary>
/// Initializes a new instance of the <see cref="MainViewModel"/> class.
/// </summary>
public MainViewModel()
{
// Create an observable that indicates when the command can execute
// The command will be enabled only when Name is not null or empty
var canExecute = this.WhenAnyValue(
x => x.Name,
name => !string.IsNullOrEmpty(name)
);
// Create the command, specifying the execution logic and the can-execute condition
GreetCommand = ReactiveCommand.Create(ExecuteGreet, canExecute);
// Optionally, you can subscribe to command execution to perform additional actions
GreetCommand.Subscribe(_ => Debug.WriteLine("Command executed successfully"));
// You can also handle exceptions that occur during command execution
GreetCommand.ThrownExceptions.Subscribe(ex =>
Debug.WriteLine($"Error executing command: {ex.Message}")
);
}
/// <summary>
/// Gets or sets the user's name.
/// </summary>
public string Name
{
get => _name;
set => this.RaiseAndSetIfChanged(ref _name, value);
}
/// <summary>
/// Executes the greeting action.
/// </summary>
/// <remarks>
/// This method is called when the GreetCommand is executed.
/// It demonstrates a simple action that uses the current Name property.
/// </remarks>
private void ExecuteGreet()
{
// Command execution logic
Debug.WriteLine($"Hello, {Name}!");
// In a real application, you might:
// - Update other properties in the view model
// - Interact with services
// - Navigate to another view
// - Show a dialog
}
}
2. Using DelegateCommand
If you're not using ReactiveUI, you can implement a simple DelegateCommand
:
using System;
using System.Windows.Input;
namespace MyAvaloniaApp.Commands;
/// <summary>
/// A command implementation that delegates its actions to provided methods.
/// </summary>
/// <remarks>
/// This is a simple implementation of ICommand that can be used when you're not using ReactiveUI.
/// It allows you to specify the execute and can-execute logic as delegates.
/// </remarks>
public class DelegateCommand : ICommand
{
private readonly Action<object?> _execute;
private readonly Func<object?, bool>? _canExecute;
/// <summary>
/// Occurs when changes occur that affect whether the command can execute.
/// </summary>
public event EventHandler? CanExecuteChanged;
/// <summary>
/// Initializes a new instance of the <see cref="DelegateCommand"/> class.
/// </summary>
/// <param name="execute">The action to execute when the command is invoked.</param>
/// <param name="canExecute">A function that determines whether the command can execute.</param>
/// <exception cref="ArgumentNullException">Thrown if the execute action is null.</exception>
public DelegateCommand(Action<object?> execute, Func<object?, bool>? canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
/// <summary>
/// Determines whether the command can execute in its current state.
/// </summary>
/// <param name="parameter">Data used by the command.</param>
/// <returns>true if this command can be executed; otherwise, false.</returns>
public bool CanExecute(object? parameter)
{
return _canExecute == null || _canExecute(parameter);
}
/// <summary>
/// Executes the command.
/// </summary>
/// <param name="parameter">Data used by the command.</param>
public void Execute(object? parameter)
{
if (CanExecute(parameter))
{
_execute(parameter);
}
}
/// <summary>
/// Raises the CanExecuteChanged event.
/// </summary>
/// <remarks>
/// Call this method when the can-execute state of the command changes.
/// This will cause the UI to re-evaluate whether the command can execute.
/// </remarks>
public void RaiseCanExecuteChanged()
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
}
Usage:
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Windows.Input;
using MyAvaloniaApp.Commands;
namespace MyAvaloniaApp.ViewModels;
/// <summary>
/// Base class for view models that implements property change notification.
/// </summary>
public class ViewModelBase : INotifyPropertyChanged
{
/// <summary>
/// Occurs when a property value changes.
/// </summary>
public event PropertyChangedEventHandler? PropertyChanged;
/// <summary>
/// Raises the PropertyChanged event for the specified property.
/// </summary>
/// <param name="propertyName">The name of the property that changed.</param>
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
/// <summary>
/// Sets the field to the specified value and raises the PropertyChanged event if the value changed.
/// </summary>
/// <typeparam name="T">The type of the field.</typeparam>
/// <param name="field">Reference to the backing field to update.</param>
/// <param name="value">The new value for the field.</param>
/// <param name="propertyName">The name of the property that changed.</param>
/// <returns>True if the value was changed, false otherwise.</returns>
protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
{
if (Equals(field, value))
{
return false;
}
field = value;
OnPropertyChanged(propertyName);
return true;
}
}
/// <summary>
/// View model for the main window demonstrating DelegateCommand usage.
/// </summary>
public class MainViewModel : ViewModelBase
{
private string _name = string.Empty;
private DelegateCommand? _greetCommand;
/// <summary>
/// Gets the command that executes the greeting action.
/// </summary>
/// <remarks>
/// This property lazily initializes the command when it's first accessed.
/// </remarks>
public ICommand GreetCommand => _greetCommand ??= new DelegateCommand(
_ => ExecuteGreet(),
_ => CanExecuteGreet()
);
/// <summary>
/// Gets or sets the user's name.
/// </summary>
public string Name
{
get => _name;
set
{
// Use the SetProperty helper from the base class to update the field
// and raise PropertyChanged if the value actually changed
if (SetProperty(ref _name, value))
{
// When the Name property changes, we need to notify the command
// that its CanExecute state might have changed
_greetCommand?.RaiseCanExecuteChanged();
}
}
}
/// <summary>
/// Determines whether the greet command can execute.
/// </summary>
/// <returns>True if the command can execute; otherwise, false.</returns>
private bool CanExecuteGreet()
{
// The command can only execute if the Name is not empty
return !string.IsNullOrEmpty(Name);
}
/// <summary>
/// Executes the greeting action.
/// </summary>
private void ExecuteGreet()
{
// Command execution logic
Debug.WriteLine($"Hello, {Name}!");
// In a real application, you might perform more complex actions here
}
}
Binding to Commands
You can bind commands to controls in XAML:
<TextBox Text="{Binding Name}" Watermark="Enter your name" />
<Button Content="Greet" Command="{Binding GreetCommand}" />
Command Parameters
You can pass parameters to commands:
<Button Content="Delete"
Command="{Binding DeleteCommand}"
CommandParameter="{Binding Id}" />
In the view model:
using System;
using System.Diagnostics;
using System.Reactive;
using ReactiveUI;
namespace MyAvaloniaApp.ViewModels;
/// <summary>
/// View model demonstrating command parameters.
/// </summary>
public class ItemsViewModel : ViewModelBase
{
/// <summary>
/// Gets the command that deletes an item by its ID.
/// </summary>
/// <remarks>
/// This command accepts a string parameter (the item ID) and returns no result (Unit).
/// </remarks>
public ReactiveCommand<string, Unit> DeleteCommand { get; }
/// <summary>
/// Initializes a new instance of the <see cref="ItemsViewModel"/> class.
/// </summary>
public ItemsViewModel()
{
// Create a command that accepts a string parameter (item ID)
// The generic type parameters specify:
// - string: The type of the parameter the command accepts
// - Unit: The type of result the command produces (Unit means no result)
DeleteCommand = ReactiveCommand.Create<string>(ExecuteDelete);
// You can also add a can-execute condition that depends on the parameter
// DeleteCommand = ReactiveCommand.Create<string>(
// ExecuteDelete,
// this.WhenAnyValue(x => x.CanDeleteItems, canDelete => canDelete)
// );
// Handle exceptions that might occur during command execution
DeleteCommand.ThrownExceptions.Subscribe(ex =>
Debug.WriteLine($"Error deleting item: {ex.Message}")
);
}
/// <summary>
/// Executes the delete action for an item with the specified ID.
/// </summary>
/// <param name="id">The ID of the item to delete.</param>
private void ExecuteDelete(string id)
{
// Validate the parameter
if (string.IsNullOrEmpty(id))
{
throw new ArgumentException("Item ID cannot be null or empty", nameof(id));
}
// Delete the item with the specified ID
Debug.WriteLine($"Deleting item with ID: {id}");
// In a real application, you would:
// 1. Call a service to delete the item from a database or API
// 2. Remove the item from a collection in the view model
// 3. Update the UI to reflect the deletion
// 4. Handle any errors that might occur
}
}
Command Binding to Event Handlers
You can also bind commands to event handlers using the {CompiledBinding}
syntax:
<TextBox x:Name="nameTextBox" Text="{Binding Name}" />
<Button Content="Greet" Command="{CompiledBinding GreetCommand}" />
Routed Events
Avalonia UI uses a routed event system similar to WPF. Routed events can travel up or down the control tree, allowing parent controls to handle events from their children.
Event Routing Strategies
Routed events can use different routing strategies:
- Direct Events: These events are raised only on the element where the action occurred
- Bubbling Events: These events start at the element where the action occurred and bubble up the control tree
- Tunneling Events: These events start at the root of the control tree and tunnel down to the element where the action occurred
Most events in Avalonia UI are bubbling events.
Handling Bubbling Events
You can handle bubbling events at different levels in the control tree:
<StackPanel ButtonBase.Click="OnPanelButtonClick">
<Button Content="Button 1" />
<Button Content="Button 2" />
<Button Content="Button 3" />
</StackPanel>
private void OnPanelButtonClick(object sender, RoutedEventArgs e)
{
// This handler will be called when any button in the panel is clicked
var button = e.Source as Button;
if (button != null)
{
Debug.WriteLine($"Button clicked: {button.Content}");
}
}
Stopping Event Propagation
You can stop event propagation by marking the event as handled:
private void OnButtonClick(object sender, RoutedEventArgs e)
{
// Handle the event
Debug.WriteLine("Button clicked");
// Stop the event from bubbling up further
e.Handled = true;
}
Custom Routed Events
You can define custom routed events:
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using System;
namespace MyAvaloniaApp.Controls;
/// <summary>
/// A custom control that demonstrates how to define and raise custom routed events.
/// </summary>
/// <remarks>
/// This example shows the pattern for creating routed events in Avalonia UI.
/// Custom routed events allow your controls to participate in the event routing system,
/// enabling parent controls to handle events from their children.
/// </remarks>
public class CustomControl : Control
{
/// <summary>
/// Defines the <see cref="CustomEvent"/> routed event.
/// </summary>
/// <remarks>
/// The RoutedEvent.Register method creates a new routed event with the specified parameters:
/// - The generic type parameters specify the owner type and the event args type
/// - The name parameter specifies the name of the event
/// - The routing strategy parameter specifies how the event is routed (Bubble, Tunnel, or Direct)
/// </remarks>
public static readonly RoutedEvent<RoutedEventArgs> CustomEventProperty =
RoutedEvent.Register<CustomControl, RoutedEventArgs>(
nameof(CustomEvent),
RoutingStrategies.Bubble);
/// <summary>
/// Occurs when a custom action is performed.
/// </summary>
/// <remarks>
/// This CLR event wrapper provides a familiar .NET event syntax for the routed event.
/// It delegates to the AddHandler and RemoveHandler methods to manage event subscriptions.
/// </remarks>
public event EventHandler<RoutedEventArgs> CustomEvent
{
add => AddHandler(CustomEventProperty, value);
remove => RemoveHandler(CustomEventProperty, value);
}
/// <summary>
/// Raises the <see cref="CustomEvent"/> event.
/// </summary>
/// <remarks>
/// This protected method allows derived classes to raise the event.
/// It creates a new RoutedEventArgs instance and calls RaiseEvent to start the event routing.
/// </remarks>
protected virtual void RaiseCustomEvent()
{
// Create event args with the routed event
var args = new RoutedEventArgs(CustomEventProperty);
// Raise the event, which will start the routing process
RaiseEvent(args);
}
/// <summary>
/// Performs a custom action and raises the <see cref="CustomEvent"/> event.
/// </summary>
/// <remarks>
/// This is an example of a method that would trigger the custom event.
/// In a real control, this might be called in response to user interaction
/// or some other state change.
/// </remarks>
public void DoSomething()
{
// Perform the action
// For example, update internal state, perform calculations, etc.
// Notify listeners that the action was performed
RaiseCustomEvent();
}
/// <summary>
/// Creates a custom event with additional data.
/// </summary>
/// <param name="data">Custom data to include with the event.</param>
public void DoSomethingWithData(string data)
{
// For events that need to pass custom data, you can create a custom
// event args class that derives from RoutedEventArgs
var args = new CustomRoutedEventArgs(CustomEventProperty)
{
Data = data
};
RaiseEvent(args);
}
}
/// <summary>
/// Custom event arguments for passing additional data with a routed event.
/// </summary>
public class CustomRoutedEventArgs : RoutedEventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="CustomRoutedEventArgs"/> class.
/// </summary>
/// <param name="routedEvent">The routed event associated with these args.</param>
public CustomRoutedEventArgs(RoutedEvent routedEvent) : base(routedEvent)
{
}
/// <summary>
/// Gets or sets custom data associated with the event.
/// </summary>
public string? Data { get; set; }
}
Input Handling
Avalonia UI provides a comprehensive input system for handling keyboard, pointer, and touch input.
Keyboard Input
You can handle keyboard input using keyboard events:
<TextBox KeyDown="OnKeyDown" />
using Avalonia.Input;
using System.Diagnostics;
/// <summary>
/// Handles the key down event for a control.
/// </summary>
/// <param name="sender">The control that raised the event.</param>
/// <param name="e">Event data containing information about the key press.</param>
/// <remarks>
/// This method demonstrates how to handle keyboard input in Avalonia UI.
/// It checks for specific keys and key combinations, and marks the event as handled
/// to prevent it from bubbling up to parent controls.
/// </remarks>
private void OnKeyDown(object sender, KeyEventArgs e)
{
// Check for specific keys
if (e.Key == Key.Enter)
{
// Handle Enter key press
Debug.WriteLine("Enter key pressed");
// Mark the event as handled to prevent it from bubbling up
e.Handled = true;
}
else if (e.Key == Key.Escape)
{
// Handle Escape key press
Debug.WriteLine("Escape key pressed");
e.Handled = true;
}
// Check for key combinations with modifiers
if (e.Key == Key.S && e.KeyModifiers.HasFlag(KeyModifiers.Control))
{
// Handle Ctrl+S key combination
Debug.WriteLine("Ctrl+S pressed (Save)");
e.Handled = true;
}
else if (e.Key == Key.O && e.KeyModifiers == (KeyModifiers.Control | KeyModifiers.Shift))
{
// Handle Ctrl+Shift+O key combination
Debug.WriteLine("Ctrl+Shift+O pressed");
e.Handled = true;
}
// If not handled above, you can check for other keys or key combinations
if (!e.Handled)
{
// Log the key press for debugging
Debug.WriteLine($"Key pressed: {e.Key}, Modifiers: {e.KeyModifiers}");
// You can also check the physical key if needed
Debug.WriteLine($"Physical key: {e.PhysicalKey}");
}
}
Common keyboard events:
KeyDown
: Occurs when a key is pressedKeyUp
: Occurs when a key is released
Keyboard Focus
You can manage keyboard focus:
using Avalonia.Input;
using System.Diagnostics;
/// <summary>
/// Demonstrates keyboard focus management in Avalonia UI.
/// </summary>
/// <remarks>
/// This method shows various ways to manage keyboard focus,
/// which is important for keyboard navigation and accessibility.
/// </remarks>
private void ManageFocus()
{
// Set focus to a control
// This makes the control the active element for keyboard input
myTextBox.Focus();
Debug.WriteLine("Focus set to text box");
// Check if a control has focus
bool hasFocus = myTextBox.IsFocused;
Debug.WriteLine($"Text box has focus: {hasFocus}");
// Get the currently focused element in the application
var focusedElement = FocusManager.Instance?.Current;
if (focusedElement != null)
{
Debug.WriteLine($"Currently focused element: {focusedElement}");
}
// Move focus to the next control in the tab order
// This is useful for implementing custom keyboard navigation
myTextBox.MoveFocus(NavigationDirection.Next);
Debug.WriteLine("Focus moved to next control");
// Move focus to the previous control in the tab order
myTextBox.MoveFocus(NavigationDirection.Previous);
Debug.WriteLine("Focus moved to previous control");
// Move focus in other directions (useful for grid navigation)
myTextBox.MoveFocus(NavigationDirection.Up);
myTextBox.MoveFocus(NavigationDirection.Down);
myTextBox.MoveFocus(NavigationDirection.Left);
myTextBox.MoveFocus(NavigationDirection.Right);
// You can also handle focus events
myTextBox.GotFocus += (sender, e) =>
{
Debug.WriteLine("Text box received focus");
};
myTextBox.LostFocus += (sender, e) =>
{
Debug.WriteLine("Text box lost focus");
};
// Set keyboard navigation mode for a container
// This affects how keyboard navigation works within the container
myPanel.KeyboardNavigation.TabNavigation = KeyboardNavigationMode.Cycle;
}
Pointer Input
Pointer events handle mouse, touch, and pen input:
<Rectangle Fill="Blue"
PointerPressed="OnPointerPressed"
PointerMoved="OnPointerMoved"
PointerReleased="OnPointerReleased" />
using Avalonia;
using Avalonia.Input;
using Avalonia.Media;
using Avalonia.VisualTree;
using System;
using System.Diagnostics;
/// <summary>
/// Handles the pointer pressed event.
/// </summary>
/// <param name="sender">The control that raised the event.</param>
/// <param name="e">Event data containing information about the pointer press.</param>
/// <remarks>
/// This method demonstrates how to handle pointer input in Avalonia UI.
/// It captures the pointer to track drag operations and gets position information.
/// </remarks>
private void OnPointerPressed(object sender, PointerPressedEventArgs e)
{
// Get the position of the pointer relative to the sender control
var position = e.GetPosition(sender as IVisual);
Debug.WriteLine($"Pointer pressed at: X={position.X:F2}, Y={position.Y:F2}");
// Get information about the pointer device
var pointerType = e.Pointer.Type;
Debug.WriteLine($"Pointer type: {pointerType}");
// Get the current point with additional information
var currentPoint = e.GetCurrentPoint(sender as IVisual);
// Check which button was pressed (for mouse input)
if (currentPoint.Properties.IsLeftButtonPressed)
{
Debug.WriteLine("Left button pressed");
// Store the start position for drag operations
_dragStartPoint = position;
_isDragging = true;
}
else if (currentPoint.Properties.IsRightButtonPressed)
{
Debug.WriteLine("Right button pressed");
// Show context menu or perform right-click action
}
else if (currentPoint.Properties.IsMiddleButtonPressed)
{
Debug.WriteLine("Middle button pressed");
}
// Capture the pointer to receive all pointer events until released
// This is essential for drag operations to work correctly
e.Pointer.Capture(sender as IInputElement);
// Mark the event as handled to prevent it from bubbling up
e.Handled = true;
}
/// <summary>
/// Handles the pointer moved event.
/// </summary>
/// <param name="sender">The control that raised the event.</param>
/// <param name="e">Event data containing information about the pointer movement.</param>
/// <remarks>
/// This method is called when the pointer moves over the control or
/// when the pointer moves anywhere if it was captured by this control.
/// </remarks>
private void OnPointerMoved(object sender, PointerEventArgs e)
{
// Only process the event if this control has captured the pointer
if (e.Pointer.Captured == sender)
{
// Get the current position
var position = e.GetPosition(sender as IVisual);
// If we're dragging, calculate the distance moved
if (_isDragging)
{
var deltaX = position.X - _dragStartPoint.X;
var deltaY = position.Y - _dragStartPoint.Y;
Debug.WriteLine($"Dragging: Delta X={deltaX:F2}, Delta Y={deltaY:F2}");
// In a real application, you might:
// - Move an element
// - Resize an element
// - Draw on a canvas
// - Scroll content
}
else
{
// Just tracking pointer movement
Debug.WriteLine($"Pointer moved to: X={position.X:F2}, Y={position.Y:F2}");
}
}
}
/// <summary>
/// Handles the pointer released event.
/// </summary>
/// <param name="sender">The control that raised the event.</param>
/// <param name="e">Event data containing information about the pointer release.</param>
/// <remarks>
/// This method is called when the pointer button is released.
/// It's important to release the pointer capture here to end any drag operations.
/// </remarks>
private void OnPointerReleased(object sender, PointerReleasedEventArgs e)
{
// Get the position where the pointer was released
var position = e.GetPosition(sender as IVisual);
Debug.WriteLine($"Pointer released at: X={position.X:F2}, Y={position.Y:F2}");
// Check which button was released
var properties = e.GetCurrentPoint(sender as IVisual).Properties;
if (e.InitialPressMouseButton == MouseButton.Left)
{
Debug.WriteLine("Left button released");
// Complete any drag operation
if (_isDragging)
{
var totalDragX = position.X - _dragStartPoint.X;
var totalDragY = position.Y - _dragStartPoint.Y;
Debug.WriteLine($"Drag completed: Total X={totalDragX:F2}, Total Y={totalDragY:F2}");
_isDragging = false;
}
}
else if (e.InitialPressMouseButton == MouseButton.Right)
{
Debug.WriteLine("Right button released");
}
// Release the pointer capture
// This is crucial to allow other controls to receive pointer events
e.Pointer.Capture(null);
// Mark the event as handled
e.Handled = true;
}
// Fields to track drag operations
private Point _dragStartPoint;
private bool _isDragging;
Common pointer events:
PointerPressed
: Occurs when a pointer is pressedPointerMoved
: Occurs when a pointer movesPointerReleased
: Occurs when a pointer is releasedPointerEntered
: Occurs when a pointer enters an elementPointerExited
: Occurs when a pointer leaves an element
Gesture Recognition
Avalonia UI supports common gestures:
<Border Background="LightBlue"
Tapped="OnTapped"
DoubleTapped="OnDoubleTapped" />
private void OnTapped(object sender, TappedEventArgs e)
{
Debug.WriteLine("Tapped");
}
private void OnDoubleTapped(object sender, TappedEventArgs e)
{
Debug.WriteLine("Double tapped");
}
Common gesture events:
Tapped
: Occurs when an element is tapped (clicked or touched)DoubleTapped
: Occurs when an element is double-tappedRightTapped
: Occurs when an element is right-tapped (right-clicked)
Example: Interactive Form with Validation
Let's put everything together in an example of an interactive form with validation:
View Model
using System;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Reactive;
using System.Text.RegularExpressions;
using ReactiveUI;
namespace MyAvaloniaApp.ViewModels;
/// <summary>
/// View model for the registration form that demonstrates validation and commands.
/// </summary>
/// <remarks>
/// This class shows how to implement form validation and commands in a view model.
/// It validates user input in real-time and enables/disables the register command
/// based on the validation state.
/// </remarks>
public class RegistrationViewModel : ViewModelBase
{
// Private backing fields for properties
private string _username = string.Empty;
private string _email = string.Empty;
private string _password = string.Empty;
private string _confirmPassword = string.Empty;
private ObservableCollection<string> _validationErrors;
private bool _isRegistering = false;
private bool _registrationSuccess = false;
private string _statusMessage = string.Empty;
/// <summary>
/// Initializes a new instance of the <see cref="RegistrationViewModel"/> class.
/// </summary>
public RegistrationViewModel()
{
// Initialize the validation errors collection
_validationErrors = new ObservableCollection<string>();
// Create an observable that determines when the register command can execute
// The command can execute when all fields are filled, there are no validation errors,
// and we're not currently in the process of registering
var canRegister = this.WhenAnyValue(
x => x.Username,
x => x.Email,
x => x.Password,
x => x.ConfirmPassword,
x => x.ValidationErrors.Count,
x => x.IsRegistering,
(username, email, password, confirmPassword, errorCount, isRegistering) =>
!string.IsNullOrEmpty(username) &&
!string.IsNullOrEmpty(email) &&
!string.IsNullOrEmpty(password) &&
!string.IsNullOrEmpty(confirmPassword) &&
errorCount == 0 &&
!isRegistering);
// Create the register command with the execution logic and can-execute condition
RegisterCommand = ReactiveCommand.CreateFromTask(
async () =>
{
try
{
// Set the registering flag to true to disable the command during registration
IsRegistering = true;
StatusMessage = "Registering...";
// Simulate an async operation (e.g., API call)
await System.Threading.Tasks.Task.Delay(1500);
// Registration logic
Debug.WriteLine($"Registering user: {Username}, {Email}");
// In a real application, you would call a service to register the user
// and handle success/failure
// Simulate successful registration
RegistrationSuccess = true;
StatusMessage = "Registration successful!";
}
catch (Exception ex)
{
// Handle registration failure
RegistrationSuccess = false;
StatusMessage = $"Registration failed: {ex.Message}";
Debug.WriteLine($"Registration error: {ex}");
}
finally
{
// Reset the registering flag
IsRegistering = false;
}
},
canRegister);
// Create the clear command
ClearCommand = ReactiveCommand.Create(ExecuteClear);
// Handle errors that might occur during command execution
RegisterCommand.ThrownExceptions.Subscribe(ex =>
{
StatusMessage = $"Error: {ex.Message}";
Debug.WriteLine($"Command error: {ex}");
});
// Perform initial validation
ValidateAll();
}
/// <summary>
/// Gets or sets the username.
/// </summary>
public string Username
{
get => _username;
set
{
this.RaiseAndSetIfChanged(ref _username, value);
ValidateUsername();
}
}
/// <summary>
/// Gets or sets the email address.
/// </summary>
public string Email
{
get => _email;
set
{
this.RaiseAndSetIfChanged(ref _email, value);
ValidateEmail();
}
}
/// <summary>
/// Gets or sets the password.
/// </summary>
public string Password
{
get => _password;
set
{
this.RaiseAndSetIfChanged(ref _password, value);
ValidatePassword();
// Also validate confirm password since it depends on the password
ValidateConfirmPassword();
}
}
/// <summary>
/// Gets or sets the password confirmation.
/// </summary>
public string ConfirmPassword
{
get => _confirmPassword;
set
{
this.RaiseAndSetIfChanged(ref _confirmPassword, value);
ValidateConfirmPassword();
}
}
/// <summary>
/// Gets or sets the collection of validation errors.
/// </summary>
public ObservableCollection<string> ValidationErrors
{
get => _validationErrors;
set => this.RaiseAndSetIfChanged(ref _validationErrors, value);
}
/// <summary>
/// Gets or sets a value indicating whether registration is in progress.
/// </summary>
public bool IsRegistering
{
get => _isRegistering;
set => this.RaiseAndSetIfChanged(ref _isRegistering, value);
}
/// <summary>
/// Gets or sets a value indicating whether registration was successful.
/// </summary>
public bool RegistrationSuccess
{
get => _registrationSuccess;
set => this.RaiseAndSetIfChanged(ref _registrationSuccess, value);
}
/// <summary>
/// Gets or sets the status message.
/// </summary>
public string StatusMessage
{
get => _statusMessage;
set => this.RaiseAndSetIfChanged(ref _statusMessage, value);
}
/// <summary>
/// Gets the command that registers the user.
/// </summary>
public ReactiveCommand<Unit, Unit> RegisterCommand { get; }
/// <summary>
/// Gets the command that clears the form.
/// </summary>
public ReactiveCommand<Unit, Unit> ClearCommand { get; }
/// <summary>
/// Validates all form fields.
/// </summary>
private void ValidateAll()
{
ValidateUsername();
ValidateEmail();
ValidatePassword();
ValidateConfirmPassword();
}
/// <summary>
/// Validates the username field.
/// </summary>
private void ValidateUsername()
{
RemoveError("Username");
if (string.IsNullOrWhiteSpace(Username))
{
AddError("Username is required");
}
else if (Username.Length < 3)
{
AddError("Username must be at least 3 characters");
}
else if (Username.Length > 50)
{
AddError("Username cannot exceed 50 characters");
}
else if (!Regex.IsMatch(Username, @"^[a-zA-Z0-9_-]+$"))
{
AddError("Username can only contain letters, numbers, underscores, and hyphens");
}
}
/// <summary>
/// Validates the email field.
/// </summary>
private void ValidateEmail()
{
RemoveError("Email");
if (string.IsNullOrWhiteSpace(Email))
{
AddError("Email is required");
}
else if (!IsValidEmail(Email))
{
AddError("Email is not valid");
}
}
/// <summary>
/// Validates the password field.
/// </summary>
private void ValidatePassword()
{
RemoveError("Password");
if (string.IsNullOrWhiteSpace(Password))
{
AddError("Password is required");
}
else if (Password.Length < 6)
{
AddError("Password must be at least 6 characters");
}
else if (Password.Length > 100)
{
AddError("Password is too long");
}
else if (!HasPasswordComplexity(Password))
{
AddError("Password must contain at least one uppercase letter, one lowercase letter, and one number");
}
}
/// <summary>
/// Validates the confirm password field.
/// </summary>
private void ValidateConfirmPassword()
{
RemoveError("ConfirmPassword");
if (string.IsNullOrWhiteSpace(ConfirmPassword))
{
AddError("Confirm Password is required");
}
else if (Password != ConfirmPassword)
{
AddError("Passwords do not match");
}
}
/// <summary>
/// Adds a validation error to the collection.
/// </summary>
/// <param name="error">The error message to add.</param>
private void AddError(string error)
{
if (!ValidationErrors.Contains(error))
{
ValidationErrors.Add(error);
}
}
/// <summary>
/// Removes all errors related to a specific property.
/// </summary>
/// <param name="propertyName">The name of the property to remove errors for.</param>
private void RemoveError(string propertyName)
{
var errors = ValidationErrors.Where(e => e.StartsWith(propertyName)).ToList();
foreach (var error in errors)
{
ValidationErrors.Remove(error);
}
}
/// <summary>
/// Checks if an email address is valid.
/// </summary>
/// <param name="email">The email address to validate.</param>
/// <returns>True if the email is valid; otherwise, false.</returns>
private bool IsValidEmail(string email)
{
// This is a simple regex for email validation
// In a real application, you might want to use a more comprehensive validation
return Regex.IsMatch(email, @"^[^@\s]+@[^@\s]+\.[^@\s]+$");
}
/// <summary>
/// Checks if a password meets complexity requirements.
/// </summary>
/// <param name="password">The password to check.</param>
/// <returns>True if the password meets complexity requirements; otherwise, false.</returns>
private bool HasPasswordComplexity(string password)
{
return Regex.IsMatch(password, @"[A-Z]") && // At least one uppercase letter
Regex.IsMatch(password, @"[a-z]") && // At least one lowercase letter
Regex.IsMatch(password, @"[0-9]"); // At least one number
}
/// <summary>
/// Executes the clear form action.
/// </summary>
private void ExecuteClear()
{
// Clear all form fields
Username = string.Empty;
Email = string.Empty;
Password = string.Empty;
ConfirmPassword = string.Empty;
// Clear validation errors and status
ValidationErrors.Clear();
StatusMessage = string.Empty;
RegistrationSuccess = false;
Debug.WriteLine("Form cleared");
}
}
View
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:MyAvaloniaApp.ViewModels"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="600" d:DesignHeight="700"
x:Class="MyAvaloniaApp.Views.RegistrationView">
<!-- Design-time data context -->
<Design.DataContext>
<vm:RegistrationViewModel />
</Design.DataContext>
<!-- Styles for the form -->
<UserControl.Styles>
<!-- Style for form labels -->
<Style Selector="TextBlock.label">
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Margin" Value="0,0,0,4" />
</Style>
<!-- Style for form fields -->
<Style Selector="TextBox">
<Setter Property="Margin" Value="0,0,0,12" />
<Setter Property="Padding" Value="8,6" />
<Setter Property="CornerRadius" Value="4" />
</Style>
<!-- Style for validation error messages -->
<Style Selector="TextBlock.error">
<Setter Property="Foreground" Value="#D32F2F" />
<Setter Property="FontSize" Value="12" />
<Setter Property="Margin" Value="0,2,0,2" />
<Setter Property="TextWrapping" Value="Wrap" />
</Style>
<!-- Style for the primary action button -->
<Style Selector="Button.primary">
<Setter Property="Background" Value="#2196F3" />
<Setter Property="Foreground" Value="White" />
<Setter Property="Padding" Value="16,8" />
<Setter Property="CornerRadius" Value="4" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="MinWidth" Value="100" />
</Style>
<!-- Style for the secondary action button -->
<Style Selector="Button.secondary">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="#BDBDBD" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Padding" Value="16,8" />
<Setter Property="CornerRadius" Value="4" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="MinWidth" Value="100" />
</Style>
<!-- Style for the success message -->
<Style Selector="Border.success">
<Setter Property="Background" Value="#E8F5E9" />
<Setter Property="BorderBrush" Value="#4CAF50" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="4" />
<Setter Property="Padding" Value="12" />
<Setter Property="Margin" Value="0,16,0,16" />
</Style>
</UserControl.Styles>
<!-- Main layout -->
<Grid RowDefinitions="Auto,*,Auto" Margin="24">
<!-- Header -->
<StackPanel Grid.Row="0" Margin="0,0,0,24">
<TextBlock Text="Registration Form" FontSize="28" FontWeight="Bold" />
<TextBlock Text="Create a new account to get started" Foreground="#757575" Margin="0,8,0,0" />
</StackPanel>
<!-- Form content -->
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
<StackPanel>
<!-- Success message (visible only after successful registration) -->
<Border Classes="success" IsVisible="{Binding RegistrationSuccess}">
<StackPanel Orientation="Horizontal" Spacing="8">
<PathIcon Data="M9,20.42L2.79,14.21L5.62,11.38L9,14.77L18.88,4.88L21.71,7.71L9,20.42Z" Width="16" Height="16" Foreground="#4CAF50" />
<TextBlock Text="Registration successful! You can now log in with your credentials."
Foreground="#2E7D32" VerticalAlignment="Center" TextWrapping="Wrap" />
</StackPanel>
</Border>
<!-- Username field -->
<StackPanel>
<TextBlock Text="Username" Classes="label" />
<TextBox Text="{Binding Username}" Watermark="Enter your username"
KeyDown="OnUsernameKeyDown" />
</StackPanel>
<!-- Email field -->
<StackPanel>
<TextBlock Text="Email Address" Classes="label" />
<TextBox Text="{Binding Email}" Watermark="Enter your email address"
KeyDown="OnEmailKeyDown" />
</StackPanel>
<!-- Password field -->
<StackPanel>
<TextBlock Text="Password" Classes="label" />
<TextBox Text="{Binding Password}" PasswordChar="•" Watermark="Enter your password"
KeyDown="OnPasswordKeyDown" />
</StackPanel>
<!-- Confirm Password field -->
<StackPanel>
<TextBlock Text="Confirm Password" Classes="label" />
<TextBox Text="{Binding ConfirmPassword}" PasswordChar="•" Watermark="Confirm your password"
KeyDown="OnConfirmPasswordKeyDown" />
</StackPanel>
<!-- Password requirements hint -->
<TextBlock Text="Password must be at least 6 characters and include uppercase, lowercase, and numbers"
Foreground="#757575" FontSize="12" Margin="0,0,0,16" TextWrapping="Wrap" />
<!-- Validation Errors -->
<Border Background="#FFEBEE" BorderBrush="#FFCDD2" BorderThickness="1" CornerRadius="4" Padding="12"
IsVisible="{Binding ValidationErrors.Count}" Margin="0,8,0,16">
<StackPanel Spacing="4">
<TextBlock Text="Please fix the following errors:" Foreground="#D32F2F" FontWeight="SemiBold" Margin="0,0,0,4" />
<ItemsControl Items="{Binding ValidationErrors}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Spacing="8" Margin="0,2">
<PathIcon Data="M12,2C17.53,2 22,6.47 22,12C22,17.53 17.53,22 12,22C6.47,22 2,17.53 2,12C2,6.47 6.47,2 12,2M15.59,7L12,10.59L8.41,7L7,8.41L10.59,12L7,15.59L8.41,17L12,13.41L15.59,17L17,15.59L13.41,12L17,8.41L15.59,7Z"
Width="14" Height="14" Foreground="#D32F2F" />
<TextBlock Text="{Binding}" Classes="error" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Border>
<!-- Status message -->
<TextBlock Text="{Binding StatusMessage}" IsVisible="{Binding StatusMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
Margin="0,0,0,16" TextWrapping="Wrap" />
</StackPanel>
</ScrollViewer>
<!-- Action buttons -->
<Grid Grid.Row="2" ColumnDefinitions="*,Auto,Auto" Margin="0,16,0,0">
<!-- Loading indicator -->
<ProgressBar IsIndeterminate="True" IsVisible="{Binding IsRegistering}"
VerticalAlignment="Center" HorizontalAlignment="Left" Width="120" Height="4" />
<!-- Buttons -->
<Button Grid.Column="1" Content="Clear Form" Command="{Binding ClearCommand}"
Classes="secondary" Margin="0,0,12,0" />
<Button Grid.Column="2" Content="Register" Command="{Binding RegisterCommand}"
Classes="primary" IsEnabled="{Binding !IsRegistering}" />
</Grid>
</Grid>
</UserControl>
Adding Event Handlers
We can enhance the form with event handlers for additional functionality:
<TextBox x:Name="usernameTextBox" Text="{Binding Username}" Watermark="Enter your username" KeyDown="OnUsernameKeyDown" />
using Avalonia.Input;
using Avalonia.Interactivity;
using System.Diagnostics;
namespace MyAvaloniaApp.Views;
/// <summary>
/// Handles the key down event for the username text box.
/// </summary>
/// <param name="sender">The text box that raised the event.</param>
/// <param name="e">Event data containing information about the key press.</param>
/// <remarks>
/// This method demonstrates keyboard navigation between form fields.
/// When the user presses Enter in the username field, focus moves to the email field.
/// </remarks>
private void OnUsernameKeyDown(object sender, KeyEventArgs e)
{
// Check if the Enter key was pressed
if (e.Key == Key.Enter)
{
// Find the email text box
var emailTextBox = this.FindControl<TextBox>("emailTextBox");
if (emailTextBox != null)
{
// Move focus to the email field
emailTextBox.Focus();
// Position the cursor at the end of the text
emailTextBox.CaretIndex = emailTextBox.Text?.Length ?? 0;
// Log the focus change for debugging
Debug.WriteLine("Focus moved from username to email field");
// Mark the event as handled to prevent it from bubbling up
e.Handled = true;
}
else
{
Debug.WriteLine("Warning: Email text box not found");
}
}
else if (e.Key == Key.Tab)
{
// Tab key is handled automatically by Avalonia for focus navigation
Debug.WriteLine("Tab key pressed in username field");
}
}
Adding Input Validation
We can add real-time validation feedback:
<TextBox x:Name="emailTextBox" Text="{Binding Email}" Watermark="Enter your email address"
PropertyChanged="OnEmailPropertyChanged" />
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
using System;
using System.Diagnostics;
using System.Text.RegularExpressions;
namespace MyAvaloniaApp.Views;
/// <summary>
/// Handles property changes for the email text box to provide real-time validation feedback.
/// </summary>
/// <param name="sender">The text box that raised the event.</param>
/// <param name="e">Event data containing information about the property change.</param>
/// <remarks>
/// This method demonstrates how to provide immediate visual feedback for validation errors
/// by modifying the appearance of the control based on its content.
/// </remarks>
private void OnEmailPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
{
// Only process changes to the Text property
if (e.Property == TextBox.TextProperty)
{
try
{
// Cast the sender to TextBox
var textBox = sender as TextBox;
if (textBox == null)
{
Debug.WriteLine("Warning: Sender is not a TextBox");
return;
}
// Get the current email text
var email = textBox.Text ?? string.Empty;
// Validate the email
if (string.IsNullOrWhiteSpace(email))
{
// Email is empty
ApplyErrorStyle(textBox, "Email address is required");
}
else if (!IsValidEmail(email))
{
// Email format is invalid
ApplyErrorStyle(textBox, "Please enter a valid email address");
}
else
{
// Email is valid
RemoveErrorStyle(textBox);
}
// Log the validation result for debugging
Debug.WriteLine($"Email validation: '{email}' - {(textBox.Classes.Contains("error") ? "Invalid" : "Valid")}");
}
catch (Exception ex)
{
// Log any errors that occur during validation
Debug.WriteLine($"Error in email validation: {ex.Message}");
}
}
}
/// <summary>
/// Applies error styling to a text box.
/// </summary>
/// <param name="textBox">The text box to style.</param>
/// <param name="errorMessage">The error message to display.</param>
private void ApplyErrorStyle(TextBox textBox, string errorMessage)
{
// Add the error class if it's not already present
if (!textBox.Classes.Contains("error"))
{
textBox.Classes.Add("error");
}
// Set the tooltip to show the error message
textBox.ToolTip = errorMessage;
// You could also add a visual indicator like an icon
// or change other properties for better visibility
}
/// <summary>
/// Removes error styling from a text box.
/// </summary>
/// <param name="textBox">The text box to reset.</param>
private void RemoveErrorStyle(TextBox textBox)
{
// Remove the error class
textBox.Classes.Remove("error");
// Clear the tooltip
textBox.ToolTip = null;
}
/// <summary>
/// Checks if an email address is valid.
/// </summary>
/// <param name="email">The email address to validate.</param>
/// <returns>True if the email is valid; otherwise, false.</returns>
private bool IsValidEmail(string email)
{
// This is a simple regex for email validation
// In a real application, you might want to use a more comprehensive validation
return Regex.IsMatch(email, @"^[^@\s]+@[^@\s]+\.[^@\s]+$");
}
Add styles for the error class in the UserControl.Styles section:
<!-- Style for text boxes with validation errors -->
<Style Selector="TextBox.error">
<Setter Property="BorderBrush" Value="#D32F2F" />
<Setter Property="BorderThickness" Value="2" />
<Setter Property="Background" Value="#FFEBEE" />
</Style>
<!-- Style for the placeholder text in error text boxes -->
<Style Selector="TextBox.error /template/ TextBlock#PART_Watermark">
<Setter Property="Foreground" Value="#D32F2F" />
<Setter Property="Opacity" Value="0.7" />
</Style>
<!-- Style for the text in error text boxes -->
<Style Selector="TextBox.error /template/ TextPresenter#PART_TextPresenter">
<Setter Property="Foreground" Value="#D32F2F" />
</Style>
This example demonstrates:
- Data binding with validation
- Command binding for actions
- Event handling for enhanced user experience
- Real-time input validation
- Styling based on validation state
In the next section, we'll explore building cross-platform applications with Avalonia UI.