Одна з найбільших переваг 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".
Перш ніж почати писати код, важливо зрозуміти, як структурувати кросплатформний проєкт. Існує кілька підходів, кожен з яких має свої переваги та недоліки.
Найпростіший підхід — один проєкт, який таргетує кілька платформ через 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:
Переваги:
Недоліки:
Для більш складних сценаріїв (особливо 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>
Переваги:
Недоліки:
Компромісний варіант — один проєкт з 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>
Переваги:
Недоліки:
Коли ваш додаток працює на різних платформах, іноді потрібно визначити, на якій платформі він зараз виконується, щоб адаптувати поведінку.
.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();
}
}
}
Для 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>
RuntimeInformation та dependency injection з platform-specific implementations.Елегантний спосіб обробки 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)Одна з найбільших відмінностей між macOS та іншими платформами — це Menu Bar. На macOS меню додатку розташоване у верхній частині екрану (системний Menu Bar), а не у вікні додатку. Avalonia надає NativeMenu для роботи з цією особливістю.
Menu (звичайне меню):
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);
}
}
Для кросплатформних додатків рекомендується гібридний підхід:
<!-- 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);
}
}
Цей підхід дозволяє:
Avalonia надає кросплатформний API для роботи з буфером обміну через інтерфейс IClipboard.
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();
}
}
}
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;
}
}
}
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 підтримує не тільки текст, але й інші типи даних:
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 — зображенняGetFormatsAsync() перед отриманням даних.Avalonia надає кросплатформний API для 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}";
}
}
}
Особливо корисний сценарій — перетягування файлів з файлового менеджера у додаток:
<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 підходу можна створити 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