Scaffold AvParser: Avalonia 12 shell with adaptive layout
Greenfield skeleton for a parser desktop app. The domain is deliberately a placeholder — IParser<TIn,TOut> plus two sample parsers — so the shell is runnable and verifiable end to end before real logic lands. Layers run one way: Core (no Avalonia, no IO) <- Infrastructure <- UI <- Desktop. UI is a class library rather than the exe so headless tests build real views without dragging in Program.cs, Serilog or the container. Adaptive layout is built from what Avalonia actually offers, since it has no AdaptiveTrigger or media queries: ResponsiveLayout observes Visual.Bounds and projects a breakpoint onto both an attached property and :compact/:medium/ :expanded pseudoclasses, with 24px hysteresis so dragging a window edge cannot make the layout flap. Pane state lives in the view model because a style setter loses to a local value permanently; styles own only the visual variance. Stack notes worth remembering: Avalonia.ReactiveUI is deprecated in favour of ReactiveUI.Avalonia, and ReactiveUI 24 runs on the Primitives engine (RxVoid, ISequencer, Signal<T>) and no longer self-initialises. Avalonia.Headless.XUnit 12.x requires xUnit v3. InvariantGlobalization must stay false or Semi.Avalonia throws in its static constructor. 102 tests across three projects, including headless guards for the two failures that are otherwise completely silent: a stylesheet whose selectors match nothing, and a light palette too low-contrast for cards to read. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
# CLAUDE.md
|
||||
|
||||
Конвенции этого репозитория и грабли, на которые здесь уже наступили. Читать до правок.
|
||||
|
||||
## Команды
|
||||
|
||||
```bash
|
||||
dotnet build AvParser.slnx -c Release
|
||||
```
|
||||
```bash
|
||||
dotnet test AvParser.slnx -c Release
|
||||
```
|
||||
```bash
|
||||
dotnet csharpier check .
|
||||
```
|
||||
```bash
|
||||
dotnet run --project src/AvParser.Desktop
|
||||
```
|
||||
|
||||
## Слои
|
||||
|
||||
`Core ← Infrastructure ← UI ← Desktop`, строго в одну сторону.
|
||||
|
||||
- **`AvParser.Core` не ссылается на Avalonia.** Это единственное ограничение, которое здесь
|
||||
по-настоящему несущее: домен должен запускаться из CLI, worker-сервиса или бенчмарка. Как
|
||||
только Avalonia станет доступна из домена, кто-нибудь потянется к `Dispatcher.UIThread` или
|
||||
`IStorageProvider` внутри парсера.
|
||||
- **`AvParser.UI` — библиотека, а не exe.** Headless-тесты собирают настоящие View, не
|
||||
подтягивая `Program.cs`, Serilog и контейнер.
|
||||
- **`AvParser.Desktop` — тонкий composition root.** Логика туда не переезжает.
|
||||
|
||||
## Добавить парсер
|
||||
|
||||
1. Реализовать `ITextParser` в `Core/Parsing/`.
|
||||
2. Одна строка в `CoreServiceCollectionExtensions.AddAvParserCore()`.
|
||||
|
||||
Всё. `IParserCatalog`, страница Parse и выпадающий список подхватят его сами.
|
||||
|
||||
## Добавить страницу
|
||||
|
||||
1. Наследник `PageViewModel` в `UI/ViewModels/XxxViewModel.cs` (`Title`, `IconKey`).
|
||||
2. `UI/Views/XxxView.axaml` — имя обязано соответствовать конвенции `ViewLocator`:
|
||||
`...ViewModels.XxxViewModel` → `...Views.XxxView`.
|
||||
3. Регистрация в `AddAvParserUI()`: конкретным типом **и** как `PageViewModel` — порядок этих
|
||||
регистраций и есть порядок пунктов в рельсе навигации.
|
||||
|
||||
## ReactiveUI 24 (дистрибутив Primitives)
|
||||
|
||||
Это не классический ReactiveUI. `System.Reactive` не используется:
|
||||
|
||||
| Классика | Здесь |
|
||||
|---|---|
|
||||
| `Unit` | `RxVoid` |
|
||||
| `IScheduler` | `ISequencer` (`ReactiveUI.Primitives.Concurrency`) |
|
||||
| `Subject<T>` / `BehaviorSubject<T>` | `Signal<T>` / `BehaviorSignal<T>` |
|
||||
| `RxApp.MainThreadScheduler` | `RxSchedulers.MainThreadScheduler` |
|
||||
| `TestScheduler` | `VirtualClock`, `ImmediateSequencer.Instance` |
|
||||
|
||||
Привычные имена операторов (`Select`, `Where`, `Throttle`, `DistinctUntilChanged`,
|
||||
`CombineLatest`) **работают** — Primitives отдаёт оба набора. `using ReactiveUI.Primitives;`
|
||||
нужен ради `Subscribe(Action<T>)`.
|
||||
|
||||
**ReactiveUI 24 не инициализируется сама.** Первый `WhenAnyValue` бросит
|
||||
`InvalidOperationException`, пока не отработал builder. В приложении это делает
|
||||
`AppBuilder.UseReactiveUI(...)`; в проекте VM-тестов — module initializer
|
||||
`ReactiveUiBootstrap`. Новый тестовый проект без Avalonia обязан сделать то же самое.
|
||||
|
||||
## Конвенции ViewModel
|
||||
|
||||
- **Каждая VM принимает `ISequencer? mainThread = null`** и использует его в `outputScheduler:`
|
||||
и `ToProperty(..., scheduler)`. Именно это делает тесты синхронными: они передают
|
||||
`ImmediateSequencer.Instance`. Без этого пришлось бы гонять диспетчер.
|
||||
- У VM с необязательным `ISequencer` регистрация в DI — явная фабрика, а не по типу: иначе
|
||||
выбор конструктора контейнером зависит от порядка регистраций.
|
||||
- `[Reactive]` из `ReactiveUI.SourceGenerators` на partial-свойствах; класс — `partial`.
|
||||
|
||||
## Грабли, уже оплаченные
|
||||
|
||||
- **Селектор типа в Avalonia матчит точный тип.** `UserControl.shell` не матчит `ShellView`
|
||||
(наследник `ReactiveUserControl<T>`) и молча не делает ничего. Использовать
|
||||
`:is(UserControl).shell`. Голый `.shell` тоже матчит, но тогда XAML-компилятор не может
|
||||
вывести тип для `Setter` и падает с AVLN2200.
|
||||
- **Style-сеттер навсегда проигрывает локальному значению.** Не стилизовать `IsPaneOpen` и
|
||||
`DisplayMode` — они биндятся во ViewModel.
|
||||
- **`IPseudoClasses.Set` требует ведущего `:`**.
|
||||
- **`InvariantGlobalization` обязан быть `false`**: Semi.Avalonia строит `CultureInfo` в
|
||||
статическом конструкторе и падает целиком.
|
||||
- **Превьюер рефлексирует безпараметровый статический `BuildAvaloniaApp()`.** Необязательный
|
||||
параметр ломает его вызов, вторая перегрузка — `AmbiguousMatchException`.
|
||||
- **Compiled bindings включены по умолчанию** (Avalonia 12): `x:DataType` нужен на каждом
|
||||
`UserControl` и каждом `DataTemplate`.
|
||||
- **`Avalonia.Headless.XUnit` 12.x — это xUnit v3**, а не v2. Тестовые проекты — `Exe`.
|
||||
- **Headless: ручной `Measure`/`Arrange` внутри окна бесполезен** — следующий проход layout
|
||||
окна вернёт свой размер. Задавать ширину самому `Window`. Но и без окна нельзя: у
|
||||
открепленного контрола не строится визуальное дерево.
|
||||
- **csharpier — единственный владелец форматирования** (включая `.axaml` и `.csproj`).
|
||||
`IDE0055` понижен до suggestion: два форматтера с `TreatWarningsAsErrors` дерутся насмерть.
|
||||
|
||||
## Качество
|
||||
|
||||
- Центральные версии пакетов — `Directory.Packages.props`. **Никаких `Version=` в csproj**
|
||||
(иначе NU1008 на restore).
|
||||
- `TreatWarningsAsErrors` включён; NuGet-advisory (`NU19xx`) выведены из ошибок, чтобы
|
||||
свежая CVE не роняла сборку кода, который никто не трогал.
|
||||
- Тестовые послабления анализаторов — в `tests/Directory.Build.props`, не в самих тестах.
|
||||
|
||||
## Что осталось абстрактным
|
||||
|
||||
Домен — заглушка. `IParser<TInput, TOutput>` + `DelimitedTextParser` + `KeyValueTextParser`
|
||||
существуют, чтобы каркас проверялся end-to-end. Когда появится настоящая доменная логика,
|
||||
демо-парсеры удаляются вместе с их тестами и `SampleFor`/`LargeSampleFor` в `ParseViewModel`.
|
||||
Reference in New Issue
Block a user