|
| 1 | +# Uno Platform MVUX Sample Apps - AI Coding Guide |
| 2 | + |
| 3 | +## Project Context |
| 4 | + |
| 5 | +This is a **German-localized** learning repository for **Uno Platform 6.3.28+** showcasing MVUX (Model-View-Update-eXtended) patterns, navigation, and Uno.Extensions. All apps use `.NET 9.0` with the `Uno.Sdk` (see `src/global.json`). Project context defaults to **Uno Platform, not MAUI**. |
| 6 | + |
| 7 | +## Architecture & Patterns |
| 8 | + |
| 9 | +### MVUX Models Convention |
| 10 | +- **All models are `partial record` types** named `*Model` (e.g., `DashboardModel`, `MainModel`). These are not the ViewModels themselves. |
| 11 | +- The bindable **ViewModel is auto-generated** from each `*Model` by the MVUX source generators at build time. You should never edit or depend on the generated files directly. |
| 12 | +- Models use constructor injection for services (DI via Uno.Extensions.Hosting) |
| 13 | +- **No `INotifyPropertyChanged`** - MVUX generates reactive bindings automatically |
| 14 | +- Expose state via `IFeed<T>`, `IListFeed<T>`, `IState<T>`, or `IListState<T>` properties |
| 15 | +- Models are **stateless** - focus on presentation logic, not state management |
| 16 | + |
| 17 | +Generated ViewModels and analyzer notes: |
| 18 | +- During test builds or when stepping through in the debugger, you may see messages about a missing `BindableAttribute` on models; these are expected with MVUX source generation and can be ignored. |
| 19 | +- Do not add attributes or change patterns to “fix” these messages; the source generator handles the bindable surface. Never modify generated code under `obj/`. |
| 20 | + |
| 21 | +Example pattern: |
| 22 | +```csharp |
| 23 | +public partial record DashboardModel |
| 24 | +{ |
| 25 | + public IListFeed<GalleryImage> GalleryImages => ListFeed.Async(_service.GetDataAsync); |
| 26 | + public IState<string> SelectedItem => State<string>.Value(this, () => "defaultValue") |
| 27 | + .ForEach(SelectionChanged); |
| 28 | +} |
| 29 | +``` |
| 30 | + |
| 31 | +#### Feed vs State |
| 32 | +- **Feeds (`IFeed<T>`, `IListFeed<T>`)**: Read-only async data streams from services |
| 33 | + - Stateless, reactive sequences similar to `IObservable<T>` |
| 34 | + - Use for data you won't edit (e.g., server responses) |
| 35 | + - Example: `IListFeed<Person> People => ListFeed.Async(_service.GetPeopleAsync);` |
| 36 | + |
| 37 | +- **States (`IState<T>`, `IListState<T>`)**: Stateful feeds with update capabilities |
| 38 | + - Replay current value + allow modifications |
| 39 | + - Use for editable data with two-way binding |
| 40 | + - Example: `IState<int> Counter => State.Value(this, () => 0);` |
| 41 | + - Update via: `await CounterState.UpdateAsync(v => v + 1, ct);` |
| 42 | + |
| 43 | +### Navigation Architecture |
| 44 | +- **Routes defined in `App.xaml.cs`** via `RegisterRoutes()` using `ViewRegistry` and `RouteRegistry` |
| 45 | +- Navigation uses **`INavigator` service** (dependency-injected), **not `Frame.Navigate()`** |
| 46 | +- Region-based navigation: `Frame`, `ContentControl`, `NavigationView`, `ContentDialog`, `Flyout`, `Popup` |
| 47 | +- ViewMap associates Views with ViewModels: `new ViewMap<PageType, ModelType>()` |
| 48 | +- DataViewMap for data-driven routes: `new DataViewMap<Page, Model, DataType>()` |
| 49 | +- Nested routes: `new RouteMap("path", View: ..., Nested: [...], IsDefault: true, DependsOn: "parent")` |
| 50 | +- Use `IRouteNotifier` in models to observe route changes |
| 51 | + |
| 52 | +Navigation patterns: |
| 53 | +```csharp |
| 54 | +// In App.xaml.cs RegisterRoutes |
| 55 | +views.Register( |
| 56 | + new ViewMap<MainPage, MainModel>(), |
| 57 | + new DataViewMap<DetailsPage, DetailsModel, Widget>() |
| 58 | +); |
| 59 | + |
| 60 | +routes.Register( |
| 61 | + new RouteMap("", View: views.FindByViewModel<ShellModel>(), |
| 62 | + Nested: [ |
| 63 | + new ("Main", View: views.FindByViewModel<MainModel>(), IsDefault: true), |
| 64 | + new ("Details", View: views.FindByViewModel<DetailsModel>(), DependsOn: "Main") |
| 65 | + ] |
| 66 | + ) |
| 67 | +); |
| 68 | + |
| 69 | +// In Model - inject INavigator |
| 70 | +public partial record MainModel(INavigator Navigator) |
| 71 | +{ |
| 72 | + public async Task NavigateToDetails(Widget widget, CancellationToken ct) |
| 73 | + => await Navigator.NavigateDataAsync(this, widget, cancellation: ct); |
| 74 | +} |
| 75 | +``` |
| 76 | + |
| 77 | +### Project Structure |
| 78 | + |
| 79 | +> **Default structure:** Place all Views and Models in the `/Presentation` folder. Only if the app grows larger, add further subfolders (as seen in MvuxGallery) within `/Presentation` to keep the structure organized and concise. |
| 80 | +
|
| 81 | +``` |
| 82 | +src/ |
| 83 | +├── DevTKSS.Uno.Samples.MvuxGallery/ # Main gallery app |
| 84 | +│ ├── Presentation/ |
| 85 | +│ │ ├── ViewModels/*Model.cs # MVUX partial records |
| 86 | +│ │ ├── Views/*Page.xaml # Pages (not Views/) |
| 87 | +│ │ ├── Shell.xaml # Main navigation shell |
| 88 | +│ ├── Models/ # Domain models & services |
| 89 | +│ ├── appsettings.json # Config sections (AppConfig, Localization) |
| 90 | +│ ├── appsettings.sampledata.json # Sample data for code examples |
| 91 | +├── DevTKSS.Extensions.Uno.Storage/ # Custom storage extensions |
| 92 | +├── global.json # Uno.Sdk version (6.3.28) |
| 93 | +├── Directory.Packages.props # Central package management |
| 94 | +``` |
| 95 | + |
| 96 | +## Key Conventions |
| 97 | + |
| 98 | +### UnoFeatures in .csproj |
| 99 | +Apps declare capabilities via `<UnoFeatures>`: Material, MVUX, Navigation, Hosting, Configuration, Localization, Serialization, Storage, ThemeService. **Do not manually add implicit package references** - they're managed by Uno.Sdk. |
| 100 | + |
| 101 | +### Configuration Pattern |
| 102 | +- Load sections from `appsettings.json` using `.EmbeddedSource<App>().Section<TConfig>()` |
| 103 | +- Keyed services for multiple code sample collections: `.AddKeyedSingletonCodeService("SampleName")` |
| 104 | +- Inject via `[FromKeyedServices("key")]` attribute |
| 105 | + |
| 106 | +### XAML Binding |
| 107 | + |
| 108 | +### Critical Troubleshooting: Never Remove `this.InitializeComponent()` |
| 109 | +### Code Editing Rule: Never Remove or Replace Required Lines with Placeholders |
| 110 | + |
| 111 | +> **🚨 DO NOT use `// ...existing code...` or any similar placeholder to remove or replace required framework or initialization lines (especially `this.InitializeComponent()`).** |
| 112 | +
|
| 113 | +- When editing, always preserve all required initialization and framework lines; do not replace them with comments, ellipses, or placeholders. |
| 114 | +- If you are unsure about a line's necessity, **leave it unchanged** and investigate the real cause of the error. |
| 115 | +- Never remove or replace `this.InitializeComponent()` or similar required lines, even if other inserted code would otherwise be correct. |
| 116 | + |
| 117 | +> **🚨 ABSOLUTE RULE: Never remove `this.InitializeComponent()` from `App.xaml.cs` or any `*Page.xaml.cs` file.** |
| 118 | +
|
| 119 | +- If you see errors or build failures related to `this.InitializeComponent()`, **do NOT delete or comment out this line**. Removing it will break the app and prevent any XAML from loading. |
| 120 | +- Instead, always check that: |
| 121 | + - The namespaces in your `*Page.xaml`, `*Page.xaml.cs`, and corresponding `*Model.cs` files are in sync and correct. |
| 122 | + - All Views and Models are properly registered in `App.xaml.cs` using the navigation/DI system. |
| 123 | + - There are no typos or mismatches in file/class names or XAML root element names. |
| 124 | +- If you are troubleshooting DI/HostBuilder/service registration issues, **never fix by removing or altering `this.InitializeComponent()`**. The problem is almost always a registration, namespace, or XAML mismatch elsewhere. |
| 125 | + |
| 126 | +> **If Copilot suggests removing or altering `this.InitializeComponent()`, this is always incorrect.** |
| 127 | +- **Always use `{Binding}` (not `{x:Bind}`) when binding anything exposed by a `*Model`** (Feeds and States). The MVUX ViewModel is generated and provided at runtime as the `DataContext`; using `{x:Bind}` here commonly leads to NullReferenceExceptions. |
| 128 | +- `FeedView` wraps async data: `<mvux:FeedView Source="{Binding GalleryImages}">` with `ValueTemplate` |
| 129 | +- Access parent model in templates: `{Binding Parent.PropertyName}` |
| 130 | +- Refresh commands: `{utu:AncestorBinding AncestorType=mvux:FeedView, Path=Refresh}` |
| 131 | + |
| 132 | + |
| 133 | +#### Views and code-behind (no ViewModel ctor/fields, no Page constructor arguments) |
| 134 | +- **Page constructors must have NO arguments** when the project uses `<UnoFeatures>...Navigation</UnoFeatures>` and navigation is registered in `App.xaml.cs` via Uno.Extensions. The navigation and DI system will only instantiate Pages using the default parameterless constructor. If you add any arguments (e.g., `MainPage(MainViewModel vm)` or `MainPage(IService svc)`), navigation will fail and the Page will not be created. |
| 135 | +- Do not inject or expect the MVUX-generated `*ViewModel` in a Page constructor, and do not rely on `DataContextChanged` to grab it early. The `INavigator` sets the `DataContext` after the view initializes; trying to access it early (or via TwoWay `{x:Bind}` with backing fields) will cause `NullReferenceException` and crash. |
| 136 | +- Avoid creating backing properties/fields in code-behind that expect the ViewModel to exist during `InitializeComponent`. Prefer pure XAML `{Binding}` to MVUX feeds/states exposed by the corresponding `*Model`. |
| 137 | + |
| 138 | +#### Selection with IListState (ListView/GridView) |
| 139 | +- When binding a `ListView` or `GridView` to an `IListState<T>` that uses the `.Selection(...)` operator, do **not** attach `Command`, `ItemClickCommand`, or `SelectionChanged` handlers on the control at the same time. Doing so prevents the MVUX selection pipeline from invoking the `.Selection(...)` operator. |
| 140 | +- Correct pattern: |
| 141 | + - Model: |
| 142 | + ```csharp |
| 143 | + public partial record PeopleModel(IPeopleService Service) |
| 144 | + { |
| 145 | + public IListFeed<Person> People => ListFeed.Async(Service.GetPeopleAsync) |
| 146 | + .Selection(SelectedPerson); |
| 147 | + public IState<Person?> SelectedPerson => State.Value(this, () => default(Person?)); |
| 148 | + } |
| 149 | + ``` |
| 150 | + - XAML: |
| 151 | + ```xml |
| 152 | + <mvux:FeedView Source="{Binding People}"> |
| 153 | + <DataTemplate> |
| 154 | + <ListView ItemsSource="{Binding Data}" SelectionMode="Single"/> |
| 155 | + </DataTemplate> |
| 156 | + </mvux:FeedView> |
| 157 | + ``` |
| 158 | + - Note: Avoid setting `ItemClick`, `IsItemClickEnabled`, `ItemClickCommand`, or `SelectionChanged` command bindings on the list control when using `.Selection(...)` on the bound `IListFeed/IListState`. |
| 159 | + |
| 160 | +### Localization |
| 161 | +- Supported cultures in `appsettings.json`: `LocalizationConfiguration.Cultures` |
| 162 | +- Inject `IStringLocalizer` for translated strings |
| 163 | +- Documentation exists in `docs/articles/en/` and `docs/articles/de/` |
| 164 | + |
| 165 | +## Build & Development |
| 166 | + |
| 167 | +### Commands |
| 168 | +```powershell |
| 169 | +# Build with solution filters for specific apps |
| 170 | +dotnet build src/DevTKSS.Uno.SampleApps-GalleryOnly.slnf |
| 171 | +dotnet build src/DevTKSS.Uno.SampleApps-Tutorials.slnf |
| 172 | + |
| 173 | +# Documentation (DocFX) |
| 174 | +./docs/Build-Docs.ps1 # Build docs to _site |
| 175 | +./docs/Clean-ApiDocs.ps1 # Clean generated API docs |
| 176 | +``` |
| 177 | + |
| 178 | +### VS Code and Visual Studio notes |
| 179 | +- In VS Code, keep `.vscode/tasks.json` in sync with solution changes (added/removed projects), or build tasks may fail. If projects change and tasks aren’t updated, update the tasks to point to the correct `.sln`/`.slnf` or project. |
| 180 | +- In Visual Studio 2022+, verify `src/[ProjectName]/Properties/launchSettings.json` when adding/removing targets or tweaking profiles so F5/run profiles match current TFMs. |
| 181 | + |
| 182 | +### Known Issues |
| 183 | +1. **Windows target disabled** in MvuxGallery Issue [#15](https://github.com/DevTKSS/DevTKSS.Uno.SampleApps/issues/15): ResourcesDictionary import bug prevents building |
| 184 | +2. **Theme changes** not reactive for ThemeResource styles Issue [#13](https://github.com/DevTKSS/DevTKSS.Uno.SampleApps/issues/13) |
| 185 | +3. **DocFX source links** fail for `[!INCLUDE]` markup - uses workaround includes instead of redirects |
| 186 | + |
| 187 | +#### Windows target and ResourceDictionaries |
| 188 | +- Current limitation: the MvuxGallery app cannot build with the Windows target when using external `Styles/*.xaml` ResourceDictionary files (see repository issue about this limitation). If you need the Windows target and centralized DataTemplates, define them directly inside `App.xaml` instead of separate dictionary files. |
| 189 | +
|
| 190 | +### Warnings Suppressed |
| 191 | +- `NU1507`: Multiple package sources with CPM |
| 192 | +- `NETSDK1201`: RID won't create self-contained app |
| 193 | +- `PRI257`: Default language (en) vs resources (en-us) |
| 194 | +
|
| 195 | +## Sample App Specifics |
| 196 | +
|
| 197 | +### MvuxGallery Features |
| 198 | +- **FeedView + GridView/ListView** patterns with ItemOverlayTemplate |
| 199 | +- Centralized DataTemplates in `Styles/GalleryTemplates.xaml` |
| 200 | +- Code sample viewer using `IStorage.ReadPackageFileAsync()` from Assets |
| 201 | +- TabBar navigation, NavigationView structure |
| 202 | +- Custom extensions: `DevTKSS.Extensions.Uno.Storage` for line-range file reading |
| 203 | +
|
| 204 | +### XamlNavigationApp |
| 205 | +- Tutorial-focused app for XAML markup navigation |
| 206 | +- Demonstrates MVUX + Navigation combined patterns |
| 207 | +- Bilingual README files: `ReadMe.en.md`, `ReadMe.de.md` |
| 208 | +
|
| 209 | +## Contributing Context |
| 210 | +- Primary language: German (documentation available in EN/DE) |
| 211 | +- Video tutorials on YouTube (German with English subtitles) |
| 212 | +- Apache License 2.0 |
| 213 | +- Use GitHub Discussions for questions, Issues for bugs |
| 214 | +
|
| 215 | +## Uno Platform Context |
| 216 | +
|
| 217 | +### Important Notes |
| 218 | +- This uses **Uno.Sdk** (not WinAppSDK/WinUI directly) |
| 219 | +- **Not MAUI** - uses .NET mobile bindings directly |
| 220 | +- Targets: iOS/iPadOS, Android, macOS, Windows, Linux, WebAssembly |
| 221 | +- Skia (canvas) and Native (native elements) renderers available |
| 222 | +- Free C# and XAML Hot Reload support |
| 223 | +
|
| 224 | +### MVUX Specifics |
| 225 | +- **FeedView control** wraps async data with loading/error states |
| 226 | + - `Source="{Binding Feed}"` binds to IFeed/IState |
| 227 | + - `ValueTemplate` for successful data display |
| 228 | + - `ErrorTemplate` and `ProgressTemplate` for states |
| 229 | + - `{Binding Data}` accesses feed value in template |
| 230 | + - `{Binding Refresh}` command triggers feed refresh |
| 231 | +- Feeds are **awaitable**: `var data = await this.MyFeed;` |
| 232 | +- BindableViewModel auto-generated with naming pattern `*Model` → `*ViewModel` |
| 233 | +- Use `[ReactiveBindable]` attribute to customize code generation |
| 234 | +
|
| 235 | +### XAML Best Practices |
| 236 | +- Prefer `{Binding}` over `{x:Bind}` for MVUX feeds (runtime-reactive) |
| 237 | +- Use `{utu:AncestorBinding}` from Uno.Toolkit for parent access |
| 238 | +- Centralize DataTemplates in ResourceDictionaries (see `Styles/GalleryTemplates.xaml`) |
| 239 | +- FeedView `State` property auto-set as DataContext for templates |
0 commit comments