Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Building Cross-Platform Desktop Applications with Avalonia UI

Tech 2

Avalonia is a cross-platform UI framework for .NET, enabling developers to create desktop applications that run on Windows, Linux, and macOS. It draws inspiration from WPF but extends its capabilities across multiple operating systems, making it suitable for environments requiring compatibility with diverse platforms.

Framework Comparison

When evaluating UI frameworks, consider these aspects:

Cross-Platform Consistency

  • Swing: Varies in appearance and performance across platforms.
  • JavaFX: Improved over Swing but with limited Linux support.
  • Avalonia: Delivers uniform experience across Windows, Linux, and macOS.

Performence Characteristics

  • Swing: Older technology with performance limitations for complex UIs.
  • JavaFX: Better performance than Swing but may lag in demanding scenarios.
  • Avalonia: Leverages .NET Core for efficient rendering and responsiveness.

Development Efficiency

  • Swing: Requires extensive boilerplate code.
  • JavaFX: Uses FXML with a steeper learning curve.
  • Avalonia: Employs XAML for declarative UI design, familiar to WPF developers.

Community and Support

  • Swing: Mature with abundant resources but minimal new development.
  • JavaFX: Moderately active community with limited corporate backing.
  • Avalonia: Growing community with continuous enhancements.

Core Concepts

XAML Markup

XAML provides a declarative syntax for defining user interfaces. Example:

<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Application Window">
    <StackPanel>
        <TextBlock Text="Welcome Message" 
                   HorizontalAlignment="Center"/>
        <Button Content="Action Button" 
                Margin="0,10,0,0"/>
    </StackPanel>
</Window>

Data Binding

Connect UI elements too data sources with automatic updates:

<TextBox Text="{Binding UserInput}"/>

Styling System

Define visual styles for controls:

<Style Selector="Button">
    <Setter Property="Background" Value="#2c3e50"/>
    <Setter Property="Foreground" Value="White"/>
    <Setter Property="Padding" Value="8"/>
</Style>

Development Setup

Install Prerequisites

  1. Download .NET SDK from the official Microsoft website.
  2. Install an IDE: Visual Studio or JetBrains Rider.

Initialize Project

dotnet new --install Avalonia.Templates
dotnet new avalonia.app -n SampleApplication
cd SampleApplication
dotnet run

Project Structure

SampleApplication/
├── Program.cs                 # Application entry point
├── App.axaml                  # Application-level resources
├── App.axaml.cs
├── MainWindow.axaml           # Primary window definition
├── MainWindow.axaml.cs
├── ViewModels/                # ViewModel classes
├── Models/                    # Data models
├── Views/                     # Additional UI components
└── Assets/                    # Resource files

Essential Components

Common Controls

<!-- Button with event handler -->
<Button Content="Execute Action" 
        Click="OnActionTriggered"/>

<!-- Data-bound text input -->
<TextBox Text="{Binding InputValue}"/>

<!-- List display with template -->
<ListBox Items="{Binding ItemCollection}">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <TextBlock Text="{Binding DisplayName}"/>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

Layout Containers

<!-- Vertical stack -->
<StackPanel Orientation="Vertical">
    <Label Content="First Item"/>
    <Label Content="Second Item"/>
</StackPanel>

<!-- Grid layout -->
<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto"/>
        <ColumnDefinition Width="*"/>
    </Grid.ColumnDefinitions>
    <Label Grid.Column="0" Content="Field:"/>
    <TextBox Grid.Column="1" Text="{Binding FieldValue}"/>
</Grid>

Event Handling

XAML declaration:

<Button Content="Interact" Click="HandleInteraction"/>

Code implementation:

private void HandleInteraction(object sender, RoutedEventArgs args)
{
    // Event logic here
}

MVVM Implementation

Data Model

public class TaskItem
{
    public string Description { get; set; }
    public bool IsFinished { get; set; }
}

ViewModel with Reactive Extensions

using System.Collections.ObjectModel;
using ReactiveUI;

public class TaskViewModel : ReactiveObject
{
    private ObservableCollection<TaskItem> _tasks;
    public ObservableCollection<TaskItem> Tasks
    {
        get => _tasks;
        set => this.RaiseAndSetIfChanged(ref _tasks, value);
    }

    private string _newTaskDescription;
    public string NewTaskDescription
    {
        get => _newTaskDescription;
        set => this.RaiseAndSetIfChanged(ref _newTaskDescription, value);
    }

    public ReactiveCommand<Unit, Unit> AddTaskCommand { get; }

    public TaskViewModel()
    {
        Tasks = new ObservableCollection<TaskItem>();
        AddTaskCommand = ReactiveCommand.Create(AddTask);
    }

    private void AddTask()
    {
        if (!string.IsNullOrWhiteSpace(NewTaskDescription))
        {
            Tasks.Add(new TaskItem { Description = NewTaskDescription });
            NewTaskDescription = string.Empty;
        }
    }
}

View Definition

<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:vm="using:Application.ViewModels"
        x:Class="Application.Views.MainWindow"
        Title="Task Manager">

    <Design.DataContext>
        <vm:TaskViewModel/>
    </Design.DataContext>

    <DockPanel>
        <StackPanel DockPanel.Dock="Top" Orientation="Horizontal">
            <TextBox Text="{Binding NewTaskDescription}" Width="300"/>
            <Button Content="Add Task" Command="{Binding AddTaskCommand}"/>
        </StackPanel>
        <ListBox Items="{Binding Tasks}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <CheckBox Content="{Binding Description}" 
                              IsChecked="{Binding IsFinished}"/>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </DockPanel>
</Window>

Advanced Features

Custom Control Example

public class ProgressIndicator : Control
{
    public static readonly StyledProperty<double> PercentageProperty =
        AvaloniaProperty.Register<ProgressIndicator, double>(nameof(Percentage));

    public double Percentage
    {
        get => GetValue(PercentageProperty);
        set => SetValue(PercentageProperty, value);
    }

    public ProgressIndicator()
    {
        UpdateVisualState(Percentage);
    }

    protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
    {
        base.OnPropertyChanged(change);
        if (change.Property == PercentageProperty)
        {
            UpdateVisualState(change.NewValue.GetValueOrDefault<double>());
        }
    }

    private void UpdateVisualState(double value)
    {
        PseudoClasses.Set(":low", value < 25);
        PseudoClasses.Set(":medium", value >= 25 && value < 75);
        PseudoClasses.Set(":high", value >= 75);
    }
}

Animation Implementation

<Border Background="LightGray" Width="100" Height="100">
    <Border.Styles>
        <Style Selector="Border:pointerover">
            <Setter Property="RenderTransform">
                <Setter.Value>
                    <RotateTransform Angle="15"/>
                </Setter.Value>
            </Setter>
        </Style>
    </Border.Styles>
    <Border.Transitions>
        <Transitions>
            <TransformOperationsTransition Property="RenderTransform" 
                                           Duration="0:0:0.3"/>
        </Transitions>
    </Border.Transitions>
</Border>

Reactive Search with Debouncing

public class SearchViewModel : ReactiveObject
{
    private string _queryText;
    public string QueryText
    {
        get => _queryText;
        set => this.RaiseAndSetIfChanged(ref _queryText, value);
    }

    public ObservableCollection<string> Results { get; } = 
        new ObservableCollection<string>();

    public SearchViewModel()
    {
        this.WhenAnyValue(x => x.QueryText)
            .Throttle(TimeSpan.FromMilliseconds(300))
            .Where(term => !string.IsNullOrWhiteSpace(term))
            .SelectMany(ExecuteSearch)
            .ObserveOn(RxApp.MainThreadScheduler)
            .Subscribe(items =>
            {
                Results.Clear();
                foreach (var item in items) Results.Add(item);
            });
    }

    private async Task<IEnumerable<string>> ExecuteSearch(string term)
    {
        await Task.Delay(800);
        return new[] 
        { 
            $"Match 1: {term}", 
            $"Match 2: {term}", 
            $"Match 3: {term}" 
        };
    }
}

Performance Optimization

Virtualized List

<ListBox Items="{Binding LargeData}"
         VirtualizationMode="Recycling">
    <!-- Item template -->
</ListBox>

Asynchronous Data Loading

public async Task LoadContentAsync()
{
    var dataset = await _service.FetchDataAsync();
    DisplayItems = new ObservableCollection<DataItem>(dataset);
}

Compiled Bindings

Enable in XAML:

xmlns:comp="using:Avalonia.Data.CompiledBindings"
comp:DataType="{x:Type viewmodels:PrimaryViewModel}"

Usage:

<TextBlock Text="{CompiledBinding ItemTitle}"/>

Testing Strategies

ViewModel Unit Test

[Fact]
public void Command_Adds_New_Item_To_Collection()
{
    var vm = new TaskViewModel();
    vm.NewTaskDescription = "Test Item";
    vm.AddTaskCommand.Execute(null);
    Assert.Contains(vm.Tasks, item => item.Description == "Test Item");
}

Headless UI Test

[AvaloniaFact]
public void Input_Field_Updates_ViewModel_Property()
{
    using var app = AppBuilder.Configure<App>()
        .UseHeadless()
        .StartWithClassicDesktopLifetime(Array.Empty<string>());

    var window = new MainWindow();
    var vm = new TaskViewModel();
    window.DataContext = vm;

    var input = window.FindControl<TextBox>("TaskInput");
    input.Text = "Sample Task";
    Assert.Equal("Sample Task", vm.NewTaskDescription);
}

Deployment

Platform-Specific Publishing

# Windows
 dotnet publish -c Release -r win-x64 --self-contained

# macOS
 dotnet publish -c Release -r osx-x64 --self-contained

# Linux
 dotnet publish -c Release -r linux-x64 --self-contained

Installer Creation

  • Windows: WiX Toolset or Inno Setup
  • macOS: create-dmg utility
  • Linux: AppImage or Flatpak formats

Language Comparison

Property Syntax

C# (Avalonia):

public string UserName { get; set; }

Java equivalent:

private String userName;
public String getUserName() { return userName; }
public void setUserName(String value) { userName = value; }

Asynchronous Pattern

C# async/await:

async Task<string> LoadDataAsync()
{
    return await _source.GetStringAsync();
}

Collection Initialization

var items = new List<string> { "First", "Second" };
var mapping = new Dictionary<int, string> { [1] = "One", [2] = "Two" };

Ecosystem Components

  • Material.Avalonia: Material Design implementation
  • Avalonia.FuncUI: Functional UI approach using F#
  • AvaloniaEdit: Advanced text editor component
  • Avalonia.Diagnostics: Runtime inspection tools
  • ReactiveUI: Reactive programming integration

Development Guidelines

  1. Adopt MVVM architecture for separasion of concerns
  2. Utilize XAML's declarative capabilities fully
  3. Implement data binding instead of manual UI updates
  4. Employ ReactiveUI for complex state management
  5. Maintain cross-platform compatibility in core logic
  6. Apply styles and themes for consistent appearance
  7. Optimize with virtualization and async patterns
  8. Write comprehensive unit tests
  9. Engage with the active community
  10. Continuously explore framework advancements

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.