Кросплатформна розробка з Avalonia
Кросплатформна розробка з Avalonia
Одна з найбільших переваг Avalonia — це справжня кросплатформність. На відміну від WPF, який працює тільки на Windows, Avalonia дозволяє створювати додатки, які працюють на Windows, Linux, macOS, Android, iOS, та навіть у браузері через WebAssembly. При цьому ви пишете код один раз, а він працює скрізь — це не просто маркетинговий слоган, а реальність, яку ми детально розглянемо у цій статті.
Проте кросплатформність — це не тільки "написав один раз, працює скрізь". Кожна платформа має свої особливості, конвенції та очікування користувачів. Користувачі macOS очікують бачити меню у Menu Bar, користувачі Windows звикли до системного трею, а користувачі Linux цінують відкритість та налаштовуваність. Успішний кросплатформний додаток — це не просто додаток, який "працює" на всіх платформах, а додаток, який "відчувається нативним" на кожній платформі.
У цій статті ми розглянемо, як структурувати кросплатформний проєкт, як писати platform-specific код коли це необхідно, як використовувати native API кожної платформи (NativeMenu для macOS, TrayIcon для Windows/Linux, platform-specific dialogs), як працювати з файловою системою кросплатформно, як реалізувати drag-n-drop та clipboard, та як тестувати додаток на різних платформах. Ми також обговоримо архітектурні рішення, які дозволяють балансувати між "write once, run anywhere" та "native look and feel".
- Cross-platform — кросплатформність, здатність програми працювати на різних операційних системах
- Runtime Identifier (RID) — ідентифікатор платформи для .NET (win-x64, linux-x64, osx-arm64)
- Conditional Compilation — умовна компіляція, включення/виключення коду залежно від платформи
- Native API — нативний API операційної системи
- Menu Bar — системне меню у верхній частині екрану (macOS)
- System Tray — системний трей, область для іконок фонових додатків (Windows/Linux)
- WebAssembly (WASM) — технологія для запуску коду у браузері
- Shared Project — спільний проєкт, код якого використовується кількома платформо-специфічними проєктами
Архітектура кросплатформного проєкту
Перш ніж почати писати код, важливо зрозуміти, як структурувати кросплатформний проєкт. Існує кілька підходів, кожен з яких має свої переваги та недоліки.
Підхід 1: Single Project (найпростіший)
Найпростіший підхід — один проєкт, який таргетує кілька платформ через TargetFrameworks:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.0.0" />
<PackageReference Include="Avalonia.Desktop" Version="11.0.0" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.0" />
</ItemGroup>
</Project>
Цей проєкт працює на Windows, Linux та macOS без додаткових налаштувань. Avalonia автоматично визначає платформу та використовує відповідний backend:
- Windows: Win32 API
- Linux: X11 або Wayland
- macOS: Cocoa (AppKit)
Переваги:
- Простота — один проєкт, один csproj
- Легко підтримувати
- Підходить для більшості desktop-додатків
Недоліки:
- Складно додати platform-specific код
- Не підходить для Mobile (Android/iOS) та WASM
Підхід 2: Shared Project + Platform-Specific Projects
Для більш складних сценаріїв (особливо Mobile та WASM) використовується структура з shared project:
MySolution/
├── MyApp.Shared/ # Спільний код (Views, ViewModels, Services)
│ ├── Views/
│ ├── ViewModels/
│ ├── Services/
│ └── MyApp.Shared.csproj
├── MyApp.Desktop/ # Desktop (Windows, Linux, macOS)
│ ├── Program.cs
│ └── MyApp.Desktop.csproj
├── MyApp.Android/ # Android
│ ├── MainActivity.cs
│ └── MyApp.Android.csproj
├── MyApp.iOS/ # iOS
│ ├── AppDelegate.cs
│ └── MyApp.iOS.csproj
└── MyApp.Browser/ # WebAssembly
├── Program.cs
└── MyApp.Browser.csproj
MyApp.Shared.csproj (спільний код):
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.0.0" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.0" />
</ItemGroup>
</Project>
MyApp.Desktop.csproj (desktop platforms):
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalonia.Desktop" Version="11.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MyApp.Shared\MyApp.Shared.csproj" />
</ItemGroup>
</Project>
MyApp.Android.csproj (Android):
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-android</TargetFramework>
<OutputType>Exe</OutputType>
<Nullable>enable</Nullable>
<SupportedOSPlatformVersion>21</SupportedOSPlatformVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalonia.Android" Version="11.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MyApp.Shared\MyApp.Shared.csproj" />
</ItemGroup>
</Project>
Переваги:
- Чітке розділення спільного та platform-specific коду
- Підтримка всіх платформ (Desktop, Mobile, WASM)
- Гнучкість у налаштуванні кожної платформи
Недоліки:
- Більш складна структура
- Потрібно підтримувати кілька csproj файлів
Підхід 3: Multi-targeting (компроміс)
Компромісний варіант — один проєкт з multi-targeting:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFrameworks>net8.0;net8.0-android;net8.0-ios</TargetFrameworks>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.0.0" />
</ItemGroup>
<!-- Desktop-specific -->
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
<PackageReference Include="Avalonia.Desktop" Version="11.0.0" />
</ItemGroup>
<!-- Android-specific -->
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0-android'">
<PackageReference Include="Avalonia.Android" Version="11.0.0" />
</ItemGroup>
<!-- iOS-specific -->
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0-ios'">
<PackageReference Include="Avalonia.iOS" Version="11.0.0" />
</ItemGroup>
</Project>
Переваги:
- Один проєкт для всіх платформ
- Можливість використовувати conditional compilation
Недоліки:
- Складний csproj файл
- Важко налаштовувати platform-specific build options
- Single Project: Якщо ви робите тільки desktop-додаток (Windows, Linux, macOS) без platform-specific коду
- Shared Project: Якщо потрібна підтримка Mobile або WASM, або багато platform-specific коду
- Multi-targeting: Якщо хочете один проєкт, але з підтримкою Mobile (рідко використовується)
Platform Detection — визначення платформи
Коли ваш додаток працює на різних платформах, іноді потрібно визначити, на якій платформі він зараз виконується, щоб адаптувати поведінку.
RuntimeInformation — визначення ОС у runtime
.NET надає клас RuntimeInformation для визначення поточної операційної системи:
using System.Runtime.InteropServices;
public class PlatformService
{
public static bool IsWindows =>
RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
public static bool IsLinux =>
RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
public static bool IsMacOS =>
RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
public static string GetPlatformName()
{
if (IsWindows) return "Windows";
if (IsLinux) return "Linux";
if (IsMacOS) return "macOS";
return "Unknown";
}
public static string GetArchitecture()
{
return RuntimeInformation.ProcessArchitecture.ToString();
// X64, Arm64, X86, Arm
}
}
Використання у коді:
public class MainViewModel : ViewModelBase
{
public string WelcomeMessage =>
$"Welcome to MyApp on {PlatformService.GetPlatformName()}!";
public void OpenSettings()
{
if (PlatformService.IsMacOS)
{
// macOS: Settings у Menu Bar
ShowMacOSPreferences();
}
else
{
// Windows/Linux: Settings у вікні
ShowSettingsWindow();
}
}
}
Conditional Compilation — умовна компіляція
Для platform-specific коду на етапі компіляції використовуйте preprocessor directives:
public class FileService
{
public string GetDefaultDownloadsPath()
{
#if WINDOWS
return Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"Downloads");
#elif LINUX
return Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"Downloads");
#elif OSX
return Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"Downloads");
#else
return Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
#endif
}
}
Щоб використовувати ці символи, додайте їх у csproj:
<PropertyGroup Condition="$([MSBuild]::IsOSPlatform('Windows'))">
<DefineConstants>$(DefineConstants);WINDOWS</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="$([MSBuild]::IsOSPlatform('Linux'))">
<DefineConstants>$(DefineConstants);LINUX</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="$([MSBuild]::IsOSPlatform('OSX'))">
<DefineConstants>$(DefineConstants);OSX</DefineConstants>
</PropertyGroup>
- Код принципово різний для різних платформ
- Потрібно виключити залежності для певних платформ
- Потрібна максимальна продуктивність (без runtime checks)
RuntimeInformation та dependency injection з platform-specific implementations.Platform-specific DI registrations
Елегантний спосіб обробки platform-specific коду — через Dependency Injection:
// Інтерфейс
public interface IPlatformService
{
string GetPlatformName();
void ShowNotification(string title, string message);
void OpenUrl(string url);
}
// Windows implementation
public class WindowsPlatformService : IPlatformService
{
public string GetPlatformName() => "Windows";
public void ShowNotification(string title, string message)
{
// Windows Toast Notification
var toastXml = ToastNotificationManager.GetTemplateContent(
ToastTemplateType.ToastText02);
// ...
}
public void OpenUrl(string url)
{
Process.Start(new ProcessStartInfo
{
FileName = url,
UseShellExecute = true
});
}
}
// macOS implementation
public class MacOSPlatformService : IPlatformService
{
public string GetPlatformName() => "macOS";
public void ShowNotification(string title, string message)
{
// macOS Notification Center
// Використання NSUserNotification через P/Invoke або Xamarin.Mac
}
public void OpenUrl(string url)
{
Process.Start("open", url);
}
}
// Linux implementation
public class LinuxPlatformService : IPlatformService
{
public string GetPlatformName() => "Linux";
public void ShowNotification(string title, string message)
{
// libnotify через notify-send
Process.Start("notify-send", $"\"{title}\" \"{message}\"");
}
public void OpenUrl(string url)
{
Process.Start("xdg-open", url);
}
}
// Реєстрація у DI
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddPlatformServices(
this IServiceCollection services)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
services.AddSingleton<IPlatformService, WindowsPlatformService>();
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
services.AddSingleton<IPlatformService, MacOSPlatformService>();
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
services.AddSingleton<IPlatformService, LinuxPlatformService>();
}
return services;
}
}
// Використання у ViewModel
public class MainViewModel : ViewModelBase
{
private readonly IPlatformService _platformService;
public MainViewModel(IPlatformService platformService)
{
_platformService = platformService;
}
public void ShowWelcome()
{
_platformService.ShowNotification(
"Welcome",
$"Running on {_platformService.GetPlatformName()}");
}
}
Цей підхід має кілька переваг:
- Чистий код без
#ifдиректив - Легко тестувати (можна замокати
IPlatformService) - Легко додати нову платформу (просто нова implementation)
- Дотримання SOLID принципів
NativeMenu — системне меню macOS
Одна з найбільших відмінностей між macOS та іншими платформами — це Menu Bar. На macOS меню додатку розташоване у верхній частині екрану (системний Menu Bar), а не у вікні додатку. Avalonia надає NativeMenu для роботи з цією особливістю.
Різниця між Menu та NativeMenu
Menu (звичайне меню):
- Розташоване у вікні додатку
- Працює на всіх платформах однаково
- Виглядає як частина вікна
NativeMenu (системне меню):
- Розташоване у системному Menu Bar (macOS)
- На Windows/Linux ігнорується або відображається як звичайне меню
- Виглядає як нативне меню ОС
Створення NativeMenu
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
public class App : Application
{
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new MainWindow();
// Створюємо NativeMenu тільки для macOS
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
SetupMacOSMenu(desktop.MainWindow);
}
}
base.OnFrameworkInitializationCompleted();
}
private void SetupMacOSMenu(Window mainWindow)
{
var menu = new NativeMenu();
// Application Menu (перше меню, з назвою додатку)
var appMenu = new NativeMenuItem("MyApp");
var appSubMenu = new NativeMenu();
appSubMenu.Add(new NativeMenuItem
{
Header = "About MyApp",
Command = ReactiveCommand.Create(() => ShowAboutDialog())
});
appSubMenu.Add(new NativeMenuItemSeparator());
appSubMenu.Add(new NativeMenuItem
{
Header = "Preferences...",
Gesture = KeyGesture.Parse("Cmd+,"),
Command = ReactiveCommand.Create(() => ShowPreferences())
});
appSubMenu.Add(new NativeMenuItemSeparator());
appSubMenu.Add(new NativeMenuItem
{
Header = "Quit MyApp",
Gesture = KeyGesture.Parse("Cmd+Q"),
Command = ReactiveCommand.Create(() =>
((IClassicDesktopStyleApplicationLifetime)Application.Current!
.ApplicationLifetime!).Shutdown())
});
appMenu.Menu = appSubMenu;
menu.Add(appMenu);
// File Menu
var fileMenu = new NativeMenuItem("File");
var fileSubMenu = new NativeMenu();
fileSubMenu.Add(new NativeMenuItem
{
Header = "New",
Gesture = KeyGesture.Parse("Cmd+N"),
Command = ReactiveCommand.Create(() => CreateNewDocument())
});
fileSubMenu.Add(new NativeMenuItem
{
Header = "Open...",
Gesture = KeyGesture.Parse("Cmd+O"),
Command = ReactiveCommand.Create(() => OpenDocument())
});
fileSubMenu.Add(new NativeMenuItemSeparator());
fileSubMenu.Add(new NativeMenuItem
{
Header = "Save",
Gesture = KeyGesture.Parse("Cmd+S"),
Command = ReactiveCommand.Create(() => SaveDocument())
});
fileMenu.Menu = fileSubMenu;
menu.Add(fileMenu);
// Edit Menu
var editMenu = new NativeMenuItem("Edit");
var editSubMenu = new NativeMenu();
editSubMenu.Add(new NativeMenuItem
{
Header = "Cut",
Gesture = KeyGesture.Parse("Cmd+X")
});
editSubMenu.Add(new NativeMenuItem
{
Header = "Copy",
Gesture = KeyGesture.Parse("Cmd+C")
});
editSubMenu.Add(new NativeMenuItem
{
Header = "Paste",
Gesture = KeyGesture.Parse("Cmd+V")
});
editMenu.Menu = editSubMenu;
menu.Add(editMenu);
// Встановлюємо меню для вікна
NativeMenu.SetMenu(mainWindow, menu);
}
}
Гібридний підхід: Menu + NativeMenu
Для кросплатформних додатків рекомендується гібридний підхід:
<!-- MainWindow.axaml -->
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="MyApp.Views.MainWindow">
<!-- Звичайне меню для Windows/Linux -->
<Window.Styles>
<Style Selector="Window">
<Setter Property="IsVisible" Value="True" />
</Style>
<!-- Приховуємо звичайне меню на macOS -->
<Style Selector="Window[IsOSX=True] > DockPanel > Menu">
<Setter Property="IsVisible" Value="False" />
</Style>
</Window.Styles>
<DockPanel>
<!-- Звичайне меню (Windows/Linux) -->
<Menu DockPanel.Dock="Top">
<MenuItem Header="_File">
<MenuItem Header="_New" InputGesture="Ctrl+N" />
<MenuItem Header="_Open" InputGesture="Ctrl+O" />
<Separator />
<MenuItem Header="_Save" InputGesture="Ctrl+S" />
<MenuItem Header="_Exit" />
</MenuItem>
<MenuItem Header="_Edit">
<MenuItem Header="_Cut" InputGesture="Ctrl+X" />
<MenuItem Header="_Copy" InputGesture="Ctrl+C" />
<MenuItem Header="_Paste" InputGesture="Ctrl+V" />
</MenuItem>
</Menu>
<!-- Основний контент -->
<ContentControl Content="{Binding CurrentView}" />
</DockPanel>
</Window>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
// Встановлюємо attached property для стилів
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
this.SetValue(IsOSXProperty, true);
}
}
public static readonly StyledProperty<bool> IsOSXProperty =
AvaloniaProperty.Register<MainWindow, bool>(nameof(IsOSX));
public bool IsOSX
{
get => GetValue(IsOSXProperty);
set => SetValue(IsOSXProperty, value);
}
}
Цей підхід дозволяє:
- На macOS використовувати NativeMenu у Menu Bar
- На Windows/Linux використовувати звичайне Menu у вікні
- Зберігати один XAML файл для всіх платформ
- Перше меню завжди має назву додатку (не "File")
- "About", "Preferences" та "Quit" у Application Menu
- Використовуйте стандартні keyboard shortcuts (Cmd+Q для Quit, Cmd+, для Preferences)
- Додайте стандартні меню: File, Edit, View, Window, Help
- Використовуйте ellipsis (...) для команд, які відкривають діалоги
Clipboard — кросплатформний буфер обміну
Avalonia надає кросплатформний API для роботи з буфером обміну через інтерфейс IClipboard.
Базові операції з Clipboard
using Avalonia.Input.Platform;
public class ClipboardService
{
private readonly IClipboard? _clipboard;
public ClipboardService(Window window)
{
// Отримуємо clipboard з TopLevel (Window)
_clipboard = window.Clipboard;
}
// Копіювання тексту
public async Task SetTextAsync(string text)
{
if (_clipboard != null)
{
await _clipboard.SetTextAsync(text);
}
}
// Отримання тексту
public async Task<string?> GetTextAsync()
{
if (_clipboard != null)
{
return await _clipboard.GetTextAsync();
}
return null;
}
// Очищення clipboard
public async Task ClearAsync()
{
if (_clipboard != null)
{
await _clipboard.ClearAsync();
}
}
}
Використання у ViewModel
public class TextEditorViewModel : ViewModelBase
{
private readonly IClipboard _clipboard;
private string _selectedText = string.Empty;
public TextEditorViewModel(IClipboard clipboard)
{
_clipboard = clipboard;
CopyCommand = ReactiveCommand.CreateFromTask(CopyAsync);
CutCommand = ReactiveCommand.CreateFromTask(CutAsync);
PasteCommand = ReactiveCommand.CreateFromTask(PasteAsync);
}
public string SelectedText
{
get => _selectedText;
set => this.RaiseAndSetIfChanged(ref _selectedText, value);
}
public ICommand CopyCommand { get; }
public ICommand CutCommand { get; }
public ICommand PasteCommand { get; }
private async Task CopyAsync()
{
if (!string.IsNullOrEmpty(SelectedText))
{
await _clipboard.SetTextAsync(SelectedText);
}
}
private async Task CutAsync()
{
if (!string.IsNullOrEmpty(SelectedText))
{
await _clipboard.SetTextAsync(SelectedText);
// Видаляємо виділений текст
SelectedText = string.Empty;
}
}
private async Task PasteAsync()
{
var text = await _clipboard.GetTextAsync();
if (!string.IsNullOrEmpty(text))
{
SelectedText = text;
}
}
}
Реєстрація Clipboard у DI
public partial class App : Application
{
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
var services = new ServiceCollection();
// Реєструємо Clipboard як singleton
services.AddSingleton<IClipboard>(provider =>
{
// Clipboard доступний тільки після створення Window
var window = desktop.MainWindow
?? throw new InvalidOperationException("MainWindow not created");
return window.Clipboard
?? throw new InvalidOperationException("Clipboard not available");
});
services.AddTransient<TextEditorViewModel>();
var serviceProvider = services.BuildServiceProvider();
desktop.MainWindow = new MainWindow
{
DataContext = serviceProvider.GetRequiredService<TextEditorViewModel>()
};
}
base.OnFrameworkInitializationCompleted();
}
}
Складні типи даних у Clipboard
Clipboard підтримує не тільки текст, але й інші типи даних:
public class AdvancedClipboardService
{
private readonly IClipboard _clipboard;
public AdvancedClipboardService(IClipboard clipboard)
{
_clipboard = clipboard;
}
// Копіювання файлів
public async Task SetFilesAsync(IEnumerable<string> filePaths)
{
var dataObject = new DataObject();
dataObject.Set(DataFormats.Files, filePaths.ToArray());
await _clipboard.SetDataObjectAsync(dataObject);
}
// Отримання файлів
public async Task<IEnumerable<string>?> GetFilesAsync()
{
var formats = await _clipboard.GetFormatsAsync();
if (formats.Contains(DataFormats.Files))
{
var dataObject = await _clipboard.GetDataAsync(DataFormats.Files);
return dataObject as IEnumerable<string>;
}
return null;
}
// Копіювання зображення
public async Task SetImageAsync(Bitmap bitmap)
{
var dataObject = new DataObject();
dataObject.Set(DataFormats.Bitmap, bitmap);
await _clipboard.SetDataObjectAsync(dataObject);
}
// Перевірка наявності даних певного типу
public async Task<bool> ContainsTextAsync()
{
var formats = await _clipboard.GetFormatsAsync();
return formats.Contains(DataFormats.Text);
}
public async Task<bool> ContainsFilesAsync()
{
var formats = await _clipboard.GetFormatsAsync();
return formats.Contains(DataFormats.Files);
}
}
DataFormats.Text— простий текстDataFormats.Files— список файлівDataFormats.Bitmap— зображення- Custom formats — власні формати (для drag-n-drop між вікнами вашого додатку)
GetFormatsAsync() перед отриманням даних.Drag-and-Drop — кросплатформне перетягування
Avalonia надає кросплатформний API для drag-and-drop операцій, який працює однаково на всіх платформах.
Базовий Drag-and-Drop
<!-- Джерело drag (можна перетягувати) -->
<Border Background="LightBlue"
Width="100" Height="100"
DragDrop.AllowDrop="False"
PointerPressed="OnDragStart">
<TextBlock Text="Drag me!"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Border>
<!-- Ціль drop (можна скидати) -->
<Border Background="LightGreen"
Width="200" Height="200"
Margin="20"
DragDrop.AllowDrop="True"
DragOver="OnDragOver"
Drop="OnDrop">
<TextBlock x:Name="DropTargetText"
Text="Drop here"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Border>
public partial class DragDropWindow : Window
{
public DragDropWindow()
{
InitializeComponent();
}
private async void OnDragStart(object? sender, PointerPressedEventArgs e)
{
if (sender is Border border)
{
var dataObject = new DataObject();
dataObject.Set(DataFormats.Text, "Hello from drag!");
var result = await DragDrop.DoDragDrop(e, dataObject, DragDropEffects.Copy);
if (result == DragDropEffects.Copy)
{
// Drag успішний
}
}
}
private void OnDragOver(object? sender, DragEventArgs e)
{
// Перевіряємо, чи можемо прийняти дані
if (e.Data.Contains(DataFormats.Text))
{
e.DragEffects = DragDropEffects.Copy;
}
else
{
e.DragEffects = DragDropEffects.None;
}
}
private void OnDrop(object? sender, DragEventArgs e)
{
if (e.Data.Contains(DataFormats.Text))
{
var text = e.Data.Get(DataFormats.Text) as string;
DropTargetText.Text = $"Dropped: {text}";
}
}
}
Drag-and-Drop файлів
Особливо корисний сценарій — перетягування файлів з файлового менеджера у додаток:
<Border Background="LightGray"
Width="400" Height="300"
DragDrop.AllowDrop="True"
DragOver="OnFileDragOver"
Drop="OnFileDrop">
<StackPanel VerticalAlignment="Center">
<TextBlock Text="Drop files here"
HorizontalAlignment="Center"
FontSize="20" />
<ItemsControl x:Name="FilesList" Margin="20">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Border>
private void OnFileDragOver(object? sender, DragEventArgs e)
{
// Перевіряємо, чи це файли
if (e.Data.Contains(DataFormats.Files))
{
e.DragEffects = DragDropEffects.Copy;
}
else
{
e.DragEffects = DragDropEffects.None;
}
}
private void OnFileDrop(object? sender, DragEventArgs e)
{
if (e.Data.Contains(DataFormats.Files))
{
var files = e.Data.GetFiles();
if (files != null)
{
var filePaths = files
.Select(f => f.Path.LocalPath)
.ToList();
FilesList.ItemsSource = filePaths;
// Обробка файлів
foreach (var filePath in filePaths)
{
ProcessFile(filePath);
}
}
}
}
private void ProcessFile(string filePath)
{
// Ваша логіка обробки файлу
Console.WriteLine($"Processing: {filePath}");
}
MVVM-friendly Drag-and-Drop
Для MVVM підходу можна створити Behavior:
using Avalonia.Xaml.Interactivity;
public class DropFileBehavior : Behavior<Control>
{
public static readonly StyledProperty<ICommand?> CommandProperty =
AvaloniaProperty.Register<DropFileBehavior, ICommand?>(nameof(Command));
public ICommand? Command
{
get => GetValue(CommandProperty);
set => SetValue(CommandProperty, value);
}
protected override void OnAttached()
{
base.OnAttached();
if (AssociatedObject != null)
{
DragDrop.SetAllowDrop(AssociatedObject, true);
AssociatedObject.AddHandler(DragDrop.DropEvent, OnDrop);
AssociatedObject.AddHandler(DragDrop.DragOverEvent, OnDragOver);
}
}
protected override void OnDetaching()
{
if (AssociatedObject != null)
{
AssociatedObject.RemoveHandler(DragDrop.DropEvent, OnDrop);
AssociatedObject.RemoveHandler(DragDrop.DragOverEvent, OnDragOver);
}
base.OnDetaching();
}
private void OnDragOver(object? sender, DragEventArgs e)
{
if (e.Data.Contains(DataFormats.Files))
{
e.DragEffects = DragDropEffects.Copy;
}
else
{
e.DragEffects = DragDropEffects.None;
}
}
private void OnDrop(object? sender, DragEventArgs e)
{
if (e.Data.Contains(DataFormats.Files) && Command?.CanExecute(null) == true)
{
var files = e.Data.GetFiles();
if (files != null)
{
var filePaths = files.Select(f => f.Path.LocalPath).ToArray();
Command.Execute(filePaths);
}
}
}
}
Використання у XAML:
<Border Background="LightGray" Width="400" Height="300">
<Interaction.Behaviors>
<behaviors:DropFileBehavior Command="{Binding DropFilesCommand}" />
</Interaction.Behaviors>
<TextBlock Text="Drop files here" />
</Border>
ViewModel:
public class FileProcessorViewModel : ViewModelBase
{
public FileProcessorViewModel()
{
DropFilesCommand = ReactiveCommand.Create<string[]>(OnFilesDropped);
}
public ICommand DropFilesCommand { get; }
private void OnFilesDropped(string[] filePaths)
{
foreach (var filePath in filePaths)
{
// Обробка файлу
Console.WriteLine($"Dropped: {filePath}");
}
}
}
Avalonia Headless Testing — тестування UI без вікон
Революційний підхід до тестування користувацького інтерфейсу: рендеринг UI в пам'яті, симуляція взаємодій та візуальна регресія без реальних вікон та GPU
Пакування та розгортання Avalonia додатків
Підготовка Avalonia-додатку для розповсюдження: dotnet publish, self-contained vs framework-dependent, trimming, platform-specific packaging, auto-updates та CI/CD