Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 166 additions & 0 deletions AVALONIA_MIGRATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
# Avalonia UI Migration Documentation

## Overview

This document describes the WPF to Avalonia UI migration for the Algoloop application. The migration creates a cross-platform UI using Avalonia UI 11.2.2 targeting .NET 8.0.

## Project Structure

### New Project: Algoloop.Avalonia

Located in: `Algoloop.Avalonia/`

**Key Files:**
- `Program.cs` - Application entry point
- `App.axaml` / `App.axaml.cs` - Application configuration with Fluent theme
- `Views/MainWindow.axaml` / `Views/MainWindow.axaml.cs` - Main application window
- `ViewModels/` - Stub ViewModels for initial implementation
- `Model/` - Business logic models
- `Resources/` - UI resources (icons, images)

## How to Run

### Prerequisites
- .NET 8.0 SDK or later
- For Windows: No additional requirements
- For Linux: X11 display server
- For macOS: XQuartz (X11 for macOS)

### Build and Run

```bash
# Navigate to repository root
cd /path/to/Algoloop

# Restore dependencies
dotnet restore Algoloop.Avalonia/Algoloop.Avalonia.csproj

# Build the project
dotnet build Algoloop.Avalonia/Algoloop.Avalonia.csproj

# Run the application
dotnet run --project Algoloop.Avalonia/Algoloop.Avalonia.csproj
```

### Running from Visual Studio
1. Open `Algoloop.sln` in Visual Studio 2022 or later
2. Set `Algoloop.Avalonia` as the startup project
3. Press F5 to run

## Current Implementation Status

### ✅ Completed
- [x] Created Algoloop.Avalonia project targeting .NET 8.0
- [x] Added Avalonia NuGet packages (11.2.2)
- [x] Implemented App.axaml with Fluent theme
- [x] Created MainWindow with basic UI layout
- [x] Added Program.cs entry point
- [x] Integrated ViewModelLocator and dependency injection
- [x] Configured logging infrastructure
- [x] Added resources (icons, images)
- [x] Application builds successfully
- [x] Application starts (requires display server)

### ⚠️ Known Limitations

1. **ViewModels are Stubs**: Current ViewModels are minimal implementations. Full business logic from WPF ViewModels needs to be migrated.

2. **View Implementations**: Tab content views (Markets, Strategies, Research, Logs) show placeholder text. These need to be implemented with proper Avalonia controls.

3. **WPF-Specific Dependencies**: The following WPF-specific features need Avalonia equivalents:
- DevExpress ThemedWindow → Replaced with standard Avalonia Window
- WebView2 (for Research tab) → Needs cross-platform alternative
- Extended WPF Toolkit controls → Need Avalonia replacements
- OxyPlot.Wpf → Should use OxyPlot.Avalonia
- StockSharp charting → Needs evaluation for Avalonia compatibility

4. **Missing Features**:
- Settings dialog
- About dialog
- Theme switching
- Detailed tab implementations
- Data binding to real business logic
- Chart implementations

5. **Platform-Specific Considerations**:
- Window placement/state persistence uses WPF APIs - needs Avalonia solution
- Some keyboard shortcuts may behave differently
- File dialogs use platform-native implementations

## Migration Notes

### Replaced Components

| WPF Component | Avalonia Replacement | Status |
|---------------|---------------------|--------|
| dx:ThemedWindow | Window | ✅ Complete |
| System.Windows.Controls.Menu | Avalonia.Controls.Menu | ✅ Complete |
| System.Windows.Controls.TabControl | Avalonia.Controls.TabControl | ✅ Complete |
| StatusBar | Border + TextBlock | ✅ Complete |
| WebView2 | Placeholder TextBlock | ⚠️ Needs replacement |

### Code Changes Made

1. **Removed WPF Dependencies**:
- Changed from `UseWPF` to Avalonia SDK
- Removed Windows-specific targeting
- Updated to .NET 8.0

2. **XAML Migration**:
- Changed namespace from `http://schemas.microsoft.com/winfx/2006/xaml/presentation` to `https://github.com/avaloniaui`
- Updated property names (e.g., `DockPanel.Dock` works the same)
- Disabled compiled bindings for dynamic resource access

3. **Code-Behind Changes**:
- Changed `System.Windows.Window` to `Avalonia.Controls.Window`
- Updated event handler signatures
- Cross-platform URL opening logic

## Next Steps

To complete the migration:

1. **Implement Full ViewModels**:
- Port business logic from WPF ViewModels
- Remove WPF-specific dependencies (Window, DataGridColumn, etc.)
- Implement proper commands and data binding

2. **Implement View Content**:
- Create MarketsView.axaml
- Create StrategiesView.axaml
- Create LogView.axaml
- Replace WebView2 in Research tab

3. **Add Dialogs**:
- Settings dialog
- About dialog
- Other modal dialogs

4. **Charting**:
- Integrate OxyPlot.Avalonia
- Port chart implementations

5. **Testing**:
- Test on Windows, Linux, and macOS
- Verify data binding and commands
- Performance testing

## References

- [Avalonia UI Documentation](https://docs.avaloniaui.net/)
- [Avalonia Samples](https://github.com/AvaloniaUI/Avalonia.Samples)
- [WPF to Avalonia Migration Guide](https://docs.avaloniaui.net/docs/guides/platforms/wpf-migration)
- [Avalonia Community](https://github.com/AvaloniaUI/Avalonia/discussions)

## Support

For issues related to the Avalonia migration, please:
1. Check existing GitHub issues
2. Review Avalonia documentation
3. Create a new issue with details about the problem

---

**Last Updated**: October 2025
**Avalonia Version**: 11.2.2
**Target Framework**: .NET 8.0
41 changes: 41 additions & 0 deletions Algoloop.Avalonia/Algoloop.Avalonia.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Platforms>x64</Platforms>
<PlatformTarget>x64</PlatformTarget>
<Nullable>enable</Nullable>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationManifest>app.manifest</ApplicationManifest>
<ApplicationIcon>Resources\AlgoloopIcon.ico</ApplicationIcon>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
</PropertyGroup>

<ItemGroup>
<AvaloniaResource Include="Assets\**" />
<AvaloniaResource Include="Resources\**" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Avalonia" Version="11.2.2" />
<PackageReference Include="Avalonia.Desktop" Version="11.2.2" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.2" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.2.2" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Common\QuantConnect.csproj" />
<ProjectReference Include="..\Algorithm\QuantConnect.Algorithm.csproj" />
<ProjectReference Include="..\AlgorithmFactory\QuantConnect.AlgorithmFactory.csproj" />
<ProjectReference Include="..\Logging\QuantConnect.Logging.csproj" />
<ProjectReference Include="..\Configuration\QuantConnect.Configuration.csproj" />
</ItemGroup>

<ItemGroup>
<Folder Include="Assets\" />
<Folder Include="Resources\" />
<Folder Include="Views\" />
</ItemGroup>
</Project>
14 changes: 14 additions & 0 deletions Algoloop.Avalonia/App.axaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Algoloop.Avalonia.App"
xmlns:local="using:Algoloop.Avalonia"
xmlns:vm="using:Algoloop.Wpf.ViewModels"
RequestedThemeVariant="Default">
<Application.Styles>
<FluentTheme />
</Application.Styles>

<Application.Resources>
<vm:ViewModelLocator x:Key="Locator" />
</Application.Resources>
</Application>
124 changes: 124 additions & 0 deletions Algoloop.Avalonia/App.axaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* Copyright 2018 Capnode AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

using Algoloop.Avalonia.Views;
using Algoloop.Wpf.Model;
using Algoloop.Wpf.ViewModels;
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using QuantConnect.Configuration;
using QuantConnect.Logging;
using System;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Threading.Tasks;

namespace Algoloop.Avalonia
{
public partial class App : Application
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);

// Set working directory to exe directory
string unc = Assembly.GetExecutingAssembly().Location;
string folder = Path.GetDirectoryName(unc) ?? string.Empty;
Directory.SetCurrentDirectory(folder);
}

public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
// Set Log handler
string logfile = Path.Combine(MainService.GetProgramDataFolder(), AboutModel.Product + ".log");
File.Delete(logfile);
Log.DebuggingEnabled = Config.GetBool("debug-mode", false);
Log.DebuggingLevel = Config.GetInt("debug-level", 1);
Log.LogHandler = new LogItemHandler(logfile);

// Exception Handling Wiring
AppDomain.CurrentDomain.UnhandledException += UnhandledExceptionHandler;
TaskScheduler.UnobservedTaskException += UnobservedTaskExceptionHandler;

desktop.MainWindow = new MainWindow();

desktop.ShutdownRequested += OnShutdownRequested;
}

base.OnFrameworkInitializationCompleted();
}

private void OnShutdownRequested(object? sender, ShutdownRequestedEventArgs e)
{
ViewModelLocator.ResearchViewModel?.StopJupyter();
ViewModelLocator.MainViewModel?.SaveConfig();
Log.Trace($"Exit \"{AboutModel.Product}\"");
}

public static void LogError(Exception ex, string? message = null)
{
if (string.IsNullOrEmpty(message))
{
Log.Error($"{ex.GetType()}: {ex.Message}", true);
}
else
{
Log.Error($"{message} {ex.GetType()}: {ex.Message}", true);
}

if (ex.InnerException != null)
{
LogError(ex.InnerException, message);
}

if (ex is ReflectionTypeLoadException rex)
{
foreach (Exception exception in rex.LoaderExceptions ?? Array.Empty<Exception>())
{
LogError(exception, message);
}
}
}

private void UnobservedTaskExceptionHandler(object? sender, UnobservedTaskExceptionEventArgs e)
{
e?.SetObserved(); // Prevents the Program from terminating.

if (e.Exception != null && e.Exception is Exception tuex)
{
LogError(tuex, nameof(UnobservedTaskExceptionHandler));
}
else if (sender is Exception ex)
{
LogError(ex, nameof(UnobservedTaskExceptionHandler));
}
}

private void UnhandledExceptionHandler(object sender, UnhandledExceptionEventArgs e)
{
if (e.ExceptionObject != null && e.ExceptionObject is Exception uex)
{
LogError(uex, nameof(UnhandledExceptionHandler));
}
else if (sender is Exception ex)
{
Log.Error(ex, nameof(UnhandledExceptionHandler));
}
}
}
}
9 changes: 9 additions & 0 deletions Algoloop.Avalonia/Model/AboutModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Algoloop.Wpf.Model
{
public static class AboutModel
{
public static string Product => "Algoloop";
public static string Title => "Algoloop";
public static string Version => "1.0.0-avalonia";
}
}
29 changes: 29 additions & 0 deletions Algoloop.Avalonia/Model/MainService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System;
using System.IO;
using System.Reflection;

namespace Algoloop.Wpf.Model
{
public static class MainService
{
public static string GetProgramFolder()
{
string unc = Assembly.GetExecutingAssembly().Location;
return Path.GetDirectoryName(unc) ?? string.Empty;
}

public static string GetProgramDataFolder()
{
string programDataFolder = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
AboutModel.Product);

if (!Directory.Exists(programDataFolder))
{
Directory.CreateDirectory(programDataFolder);
}

return programDataFolder;
}
}
}
Loading