187 lines
7.0 KiB
Markdown
187 lines
7.0 KiB
Markdown
# Avalonia-Portierung — Leitfaden
|
|
|
|
Für alle, die weitere Ansichten von WinForms nach Avalonia übertragen.
|
|
Stand: 2026-08-07.
|
|
|
|
---
|
|
|
|
## 1. Auftrag
|
|
|
|
Drei Bereiche des Hauptfensters sind noch Platzhalter. In dieser Reihenfolge portieren —
|
|
sie steigen im Umfang, und jede baut auf dem Muster der vorigen auf:
|
|
|
|
| # | Bereich | WinForms-Vorlage | Daten aus |
|
|
|---|---|---|---|
|
|
| 1 | **Info** | `frm_main.Designer.cs`, Suchwort `tabPage_Info` | `AppHost.AppVersion`, `AppHost.BuildSummary`, `host.Instance` |
|
|
| 2 | **Sicherung** | `UI/BackupPanel.cs` + `UI/BackupPanel.Designer.cs` | `host.InstancePath`, `host.Settings`, `Core.Backup.BackupService` |
|
|
| 3 | **Aufgaben** (Jobs/Services/Verlauf) | `frm_main.cs`, Abschnitt `WORKER TAB` ab Zeile 932 | `host.Instance.Agents`, `App.Services.JobHistoryService` |
|
|
|
|
**Nicht anfassen:** Chat und Einstellungen. Beide sind Entwurfsarbeit, nicht Übersetzung,
|
|
und werden gesondert gemacht.
|
|
|
|
Die WinForms-Dateien liegen noch im Repository, sind aber **nicht mehr Teil des Builds**
|
|
(siehe Kommentar in `ClawdDotNet.slnx`). Sie sind Vorlage zum Lesen — nicht zum Kompilieren,
|
|
nicht zum Reparieren.
|
|
|
|
---
|
|
|
|
## 2. Drei Regeln, die nicht verletzt werden dürfen
|
|
|
|
### 2.1 Der Schichtschnitt
|
|
|
|
```
|
|
src/ClawdDotNet.App ← Fachlogik. KEIN Verweis auf Avalonia. Niemals.
|
|
src/ClawdDotNet.Desktop ← Oberfläche. Darf App und Core verwenden.
|
|
```
|
|
|
|
`ClawdDotNet.App` muss ohne Fenster laufen — darauf setzt der geplante systemd-Dienst auf.
|
|
Sobald dort ein `using Avalonia…` steht, ist der Schnitt kaputt und fällt erst Wochen
|
|
später auf.
|
|
|
|
**Faustregel:** Alles, was Dateien liest, rechnet oder mit der Engine spricht, gehört nach
|
|
`App`. Alles, was etwas anzeigt, nach `Desktop`.
|
|
|
|
### 2.2 Fäden
|
|
|
|
Ereignisse aus `AgentEngine`, `TaskScanner`, `OpenRouterStatusService` und
|
|
`BackupScheduler` kommen auf **Hintergrundfäden**. Eine `ObservableCollection` von dort aus
|
|
zu ändern wirft entweder oder beschädigt still die Anzeige.
|
|
|
|
```csharp
|
|
// Aus einem Ereignis der Fachschicht heraus:
|
|
Dispatcher.UIThread.Post(() => Lines.Add(neu));
|
|
|
|
// Wenn ein Rückgabewert gebraucht wird:
|
|
await Dispatcher.UIThread.InvokeAsync(() => …);
|
|
```
|
|
|
|
Ein `DispatcherTimer` läuft dagegen bereits auf dem Oberflächenfaden — dort ist kein
|
|
Wechsel nötig (siehe `LogPageViewModel`).
|
|
|
|
### 2.3 Avalonia **12**, nicht 11
|
|
|
|
Praktisch alle Anleitungen im Netz sind für Avalonia 11 und lassen sich hier nicht
|
|
übernehmen. Bekannte Unterschiede:
|
|
|
|
- `BindingPlugins` ist nicht mehr öffentlich. Das übliche
|
|
`DisableAvaloniaDataAnnotationValidation()` aus den 11er-Vorlagen **entfällt ersatzlos** —
|
|
nicht nachbauen.
|
|
- `ShutdownMode` voll qualifizieren: `Avalonia.Controls.ShutdownMode`.
|
|
|
|
Diese Fehler brechen den Build. Das ist gut — sie fallen sofort auf.
|
|
|
|
---
|
|
|
|
## 3. Das Muster
|
|
|
|
Der Logs-Bereich ist als vollständiges Beispiel gebaut. Drei Dateien, drei Aufgaben:
|
|
|
|
**`src/ClawdDotNet.App/Services/LogTail.cs`** — die Fachlogik. Liest Dateien, kennt keine
|
|
Oberfläche, wäre ohne Fenster lauffähig.
|
|
|
|
**`src/ClawdDotNet.Desktop/ViewModels/LogPageViewModel.cs`** — das Ansichtsmodell. Erbt von
|
|
`PageViewModel`, hält Zustand und Befehle. Kennt keine Steuerelemente.
|
|
|
|
**`src/ClawdDotNet.Desktop/Views/LogPageView.axaml`** — die Ansicht. Nur Aufbau und
|
|
Bindungen.
|
|
|
|
### Ein neuer Bereich in vier Schritten
|
|
|
|
**1.** Ansichtsmodell anlegen, von `PageViewModel` erbend:
|
|
|
|
```csharp
|
|
public sealed partial class InfoPageViewModel : PageViewModel
|
|
{
|
|
public InfoPageViewModel(AppHost? host) : base("Info") { … }
|
|
}
|
|
```
|
|
|
|
`AppHost?` ist **nullbar** — der Entwurfsmodus des Editors erzeugt das Ansichtsmodell ohne
|
|
laufenden Aufbau. Bei `null` einfach nichts starten und Beispielwerte zeigen.
|
|
|
|
**2.** Ansicht anlegen: `Views/InfoPageView.axaml` + `.axaml.cs`. Der Name muss der
|
|
Konvention folgen — `ViewLocator` sucht `…ViewModels.FooViewModel` → `…Views.FooView`.
|
|
Passt der Name nicht, steht der gesuchte Typ im Fenster statt der Ansicht.
|
|
|
|
**3.** In `MainWindowViewModel` den Platzhalter ersetzen:
|
|
|
|
```csharp
|
|
new PlaceholderPageViewModel("Info", "…") // vorher
|
|
new InfoPageViewModel(host) // nachher
|
|
```
|
|
|
|
**4.** `x:DataType` in der AXAML setzen. Ohne das greifen die kompilierten Bindungen nicht
|
|
und Tippfehler in Bindungspfaden fallen erst zur Laufzeit auf.
|
|
|
|
### Werkzeugkasten
|
|
|
|
- Zustand: `[ObservableProperty] private string _text = "";` → erzeugt `Text` samt
|
|
Benachrichtigung.
|
|
- Befehle: `[RelayCommand] private void Speichern() { … }` → bindbar als
|
|
`SpeichernCommand`.
|
|
- Formatierung gehört in `Styles/Shell.axaml`, nicht an einzelne Steuerelemente.
|
|
Vorhandene Klassen: `heading`, `caption`, `toolbar`, `card`, `statusbar`.
|
|
- Ordner öffnen: `Core.Storage.SystemShell.OpenFolder(pfad)`. **Kein** `explorer.exe`.
|
|
- Dateinamen erzeugen: `Core.Storage.PortableFileName.Sanitize(name)`.
|
|
|
|
---
|
|
|
|
## 4. Prüfliste für die leisen Fehler
|
|
|
|
Diese Klasse bricht weder den Build noch die Tests. Vor jeder Abgabe durchgehen:
|
|
|
|
- [ ] **Fenster-Schließen behandelt?** Wartet der Code auf eine Antwort aus einem Fenster
|
|
(`TaskCompletionSource`), muss `window.Closed` als Abbruch gelten. Sonst hängt der
|
|
Ablauf lautlos für immer.
|
|
- [ ] **Sammlungen nur vom Oberflächenfaden geändert?** Siehe 2.2.
|
|
- [ ] **Wächst etwas unbegrenzt?** Listen, die im Betrieb volllaufen, brauchen eine
|
|
Obergrenze (`LogPageViewModel.MaxLines = 2000` als Vorbild).
|
|
- [ ] **Timer beendet?** `DispatcherTimer` in einem Ansichtsmodell läuft weiter, auch wenn
|
|
der Bereich nicht sichtbar ist. Bei teuren Abfragen anhalten.
|
|
- [ ] **Farben aus dem Thema?** Keine festen Farbwerte — die Anwendung läuft hell und
|
|
dunkel. `{DynamicResource …}` verwenden.
|
|
- [ ] **Keine relativen Pfade.** `./Backups` und Ähnliches hängt vom Arbeitsverzeichnis ab
|
|
und zeigt unter Linux ins Leere. `AppPaths.DataDirectory` verwenden.
|
|
- [ ] **Kein `MessageBox`, kein `System.Windows.Forms`, kein `System.Drawing`.**
|
|
|
|
---
|
|
|
|
## 5. Abnahme
|
|
|
|
```bash
|
|
dotnet build ClawdDotNet.slnx
|
|
```
|
|
|
|
```bash
|
|
dotnet test tests/ClawdDotNet.Core.Tests/ClawdDotNet.Core.Tests.csproj
|
|
```
|
|
|
|
Beide müssen fehlerfrei sein — 567 Tests, keine neuen Fehlschläge.
|
|
|
|
**Und dann tatsächlich starten.** Die Oberfläche hat keine Testabdeckung; die Fehler aus
|
|
Abschnitt 4 fallen ausschließlich beim Laufen auf.
|
|
|
|
```bash
|
|
dotnet run --project src/ClawdDotNet.Desktop
|
|
```
|
|
|
|
Hinweis: Ein Starttest hinterlässt unter Windows einen Prozess, der die `.exe` sperrt und
|
|
den nächsten Build mit `MSB3021` scheitern lässt. Aufräumen mit:
|
|
|
|
```bash
|
|
powershell -Command "Get-Process ClawdDotNet -EA SilentlyContinue | Stop-Process -Force"
|
|
```
|
|
|
|
---
|
|
|
|
## 6. Wenn etwas unklar ist
|
|
|
|
Lieber nachfragen als raten. Zwei Dinge sind besonders leicht falsch zu machen:
|
|
|
|
- **Was gehört in welche Schicht?** Im Zweifel nach `App` — von dort kann die Oberfläche
|
|
es holen, umgekehrt nicht.
|
|
- **Wie kommen Daten aus der Engine in die Ansicht?** `AppHost` gibt `Engine`, `Scanner`,
|
|
`Staging`, `Status` und `Usage` heraus; alle sind **nullbar**, wenn kein
|
|
OpenRouter-Schlüssel hinterlegt ist. Diesen Fall mitdenken — die Anwendung läuft dann
|
|
bewusst ohne Agenten.
|