diff --git a/.commitlint.config.json b/.commitlint.config.json new file mode 100644 index 0000000..1d8135c --- /dev/null +++ b/.commitlint.config.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://www.schemastore.org/commitlintrc.json", + "extends": ["@commitlint/config-conventional"], + "rules": { + "body-leading-blank": [1, "always"], + "body-max-line-length": [2, "always", 100], + "footer-leading-blank": [1, "always"], + "footer-max-line-length": [2, "always", 100], + "header-max-length": [2, "always", 50], + "scope-case": [2, "always", "lower-case"], + "subject-case": [2, "never", ["sentence-case", "start-case", "pascal-case", "upper-case"]], + "subject-empty": [2, "never"], + "subject-full-stop": [2, "never", "."], + "type-case": [2, "always", "lower-case"], + "type-empty": [2, "never"], + "type-enum": [ + 2, + "always", + ["feat", "fix", "docs", "perf", "refactor", "build", "ci", "revert", "style", "test", "chore"] + ] + } +} \ No newline at end of file diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..520dc16 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,6 @@ +# A CODEOWNERS file uses a pattern that follows the same rules used in gitignore files. +# The pattern is followed by one or more GitHub usernames or team names using the +# standard @username or @org/team-name format. You can also refer to a user by an +# email address that has been added to their GitHub account, for example user@example.com + +* @DevTKSS diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..5419891 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,372 @@ +# Uno Platform MVUX Sample Apps - AI Coding Guide + +## Project Context + +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**. + +## Architecture & Patterns + +### MVUX Models Convention +- **All models are `partial record` types** named `*Model` (e.g., `DashboardModel`, `MainModel`). These are not the ViewModels themselves. +- 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. +- Models use constructor injection for services (DI via Uno.Extensions.Hosting) +- **No `INotifyPropertyChanged`** - MVUX generates reactive bindings automatically +- Expose state via `IFeed`, `IListFeed`, `IState`, or `IListState` properties +- Models are **stateless** - focus on presentation logic, not state management + +Generated ViewModels and analyzer notes: +- 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. +- Do not add attributes or change patterns to “fix” these messages; the source generator handles the bindable surface. Never modify generated code under `obj/`. + +Example pattern: +```csharp +public partial record DashboardModel +{ + public IListFeed GalleryImages => ListFeed.Async(_service.GetDataAsync); + public IState SelectedItem => State.Value(this, () => "defaultValue") + .ForEach(SelectionChanged); +} +``` + +#### Feed vs State +- **Feeds (`IFeed`, `IListFeed`)**: Read-only async data streams from services + - Stateless, reactive sequences similar to `IObservable` + - Use for data you won't edit (e.g., server responses) + - Example: `IListFeed People => ListFeed.Async(_service.GetPeopleAsync);` + +- **States (`IState`, `IListState`)**: Stateful feeds with update capabilities + - Replay current value + allow modifications + - Use for editable data with two-way binding + - Example: `IState Counter => State.Value(this, () => 0);` + - Update via: `await CounterState.UpdateAsync(v => v + 1, ct);` + +### Navigation Architecture +- **Routes defined in `App.xaml.cs`** via `RegisterRoutes()` using `ViewRegistry` and `RouteRegistry` +- Navigation uses **`INavigator` service** (dependency-injected), **not `Frame.Navigate()`** +- Region-based navigation: `Frame`, `ContentControl`, `NavigationView`, `ContentDialog`, `Flyout`, `Popup` +- ViewMap associates Views with ViewModels: `new ViewMap()` +- DataViewMap for data-driven routes: `new DataViewMap()` +- Nested routes: `new RouteMap("path", View: ..., Nested: [...], IsDefault: true, DependsOn: "parent")` +- Use `IRouteNotifier` in models to observe route changes + +Navigation patterns: +```csharp +// In App.xaml.cs RegisterRoutes +views.Register( + new ViewMap(), + new DataViewMap() +); + +routes.Register( + new RouteMap("", View: views.FindByViewModel(), + Nested: [ + new ("Main", View: views.FindByViewModel(), IsDefault: true), + new ("Details", View: views.FindByViewModel(), DependsOn: "Main") + ] + ) +); + +// In Model - inject INavigator +public partial record MainModel(INavigator Navigator) +{ + public async Task NavigateToDetails(Widget widget, CancellationToken ct) + => await Navigator.NavigateDataAsync(this, widget, cancellation: ct); +} +``` + +### Project Structure + +> **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. + +``` +src/ +├── DevTKSS.Uno.Samples.MvuxGallery/ # Main gallery app +│ ├── Presentation/ +│ │ ├── ViewModels/*Model.cs # MVUX partial records +│ │ ├── Views/*Page.xaml # Pages (not Views/) +│ │ ├── Shell.xaml # Main navigation shell +│ ├── Models/ # Domain models & services +│ ├── appsettings.json # Config sections (AppConfig, Localization) +│ ├── appsettings.sampledata.json # Sample data for code examples +├── DevTKSS.Extensions.Uno.Storage/ # Custom storage extensions +├── global.json # Uno.Sdk version (6.3.28) +├── Directory.Packages.props # Central package management +``` + +## Key Conventions + +### UnoFeatures in .csproj +Apps declare capabilities via ``: Material, MVUX, Navigation, Hosting, Configuration, Localization, Serialization, Storage, ThemeService. **Do not manually add implicit package references** - they're managed by Uno.Sdk. + +### Configuration Pattern +- Load sections from `appsettings.json` using `.EmbeddedSource().Section()` +- Keyed services for multiple code sample collections: `.AddKeyedSingletonCodeService("SampleName")` +- Inject via `[FromKeyedServices("key")]` attribute + +### XAML Binding + +### Critical Troubleshooting: Never Remove `this.InitializeComponent()` +### Code Editing Rule: Never Remove or Replace Required Lines with Placeholders + +> **🚨 DO NOT use `// ...existing code...` or any similar placeholder to remove or replace required framework or initialization lines (especially `this.InitializeComponent()`).** + +- When editing, always preserve all required initialization and framework lines; do not replace them with comments, ellipses, or placeholders. +- If you are unsure about a line's necessity, **leave it unchanged** and investigate the real cause of the error. +- Never remove or replace `this.InitializeComponent()` or similar required lines, even if other inserted code would otherwise be correct. + +> **🚨 ABSOLUTE RULE: Never remove `this.InitializeComponent()` from `App.xaml.cs` or any `*Page.xaml.cs` file.** + +- 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. +- Instead, always check that: + - The namespaces in your `*Page.xaml`, `*Page.xaml.cs`, and corresponding `*Model.cs` files are in sync and correct. + - All Views and Models are properly registered in `App.xaml.cs` using the navigation/DI system. + - There are no typos or mismatches in file/class names or XAML root element names. +- 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. + +> **If Copilot suggests removing or altering `this.InitializeComponent()`, this is always incorrect.** +- **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. +- `FeedView` wraps async data: `` with `ValueTemplate` +- Access parent model in templates: `{Binding Parent.PropertyName}` +- Refresh commands: `{utu:AncestorBinding AncestorType=mvux:FeedView, Path=Refresh}` + + +#### Views and code-behind (no ViewModel ctor/fields, no Page constructor arguments) +- **Page constructors must have NO arguments** when the project uses `...Navigation` 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. +- 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. +- 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`. + +#### Selection with IListState (ListView/GridView) +- When binding a `ListView` or `GridView` to an `IListState` 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. +- Correct pattern: + - Model: + ```csharp + public partial record PeopleModel(IPeopleService Service) + { + public IListFeed People => ListFeed.Async(Service.GetPeopleAsync) + .Selection(SelectedPerson); + public IState SelectedPerson => State.Value(this, () => default(Person?)); + } + ``` + - XAML: + ```xml + + + + + + ``` + - Note: Avoid setting `ItemClick`, `IsItemClickEnabled`, `ItemClickCommand`, or `SelectionChanged` command bindings on the list control when using `.Selection(...)` on the bound `IListFeed/IListState`. + +### Localization +- Supported cultures in `appsettings.json`: `LocalizationConfiguration.Cultures` +- Inject `IStringLocalizer` for translated strings +- Documentation exists in `docs/articles/en/` and `docs/articles/de/` +- **German documentation style**: Use informal "Du" form (duzen) instead of formal "Sie" form. Address readers directly and personally (e.g., "du kannst", "dein Model", "wenn du"). German docs should feel like peer-to-peer communication, not formal instruction. + +## Documentation Guidelines + +### DocFX Markdown Best Practices + +#### Code Snippets and Regions +- Use `` and `` in XAML files for DocFX code snippet references +- Reference snippets in markdown: `[!code-xaml[](../../../../src/ProjectName/File.xaml#RegionName)]` +- Use relative paths from the markdown file location (e.g., `../../../../src/...`) or tilde notation (`~/src/...`) +- Never use `#region-Name` or `` syntax - these are incorrect +- Highlight specific lines: `[!code-xaml[](path#RegionName?highlight=15,18,22)]` where line numbers are relative to the region + +#### Images and Attachments +- Store images in `docs/articles/.attachments/` folder +- Reference images using relative paths from the markdown file: `![](./.attachments/ImageName.png)` +- **Always verify image paths are correct** relative to the markdown file location, not from `docfx.json` +- DocFX resolves image paths relative to the markdown file itself, not from a central config + +#### Formatting Rules +- **Never use emoji in documentation** (✅, ❌, etc.) - DocFX may not render them correctly +- Use plain markdown bullets, numbered lists, or bold text instead +- **Never add inline comments in code samples** - they may not render properly in DocFX +- Always place code explanations in separate text sections below code blocks +- **Tab heading indentation**: When using DocFX tabs (`#### [Tab Name](#tab/tabid)`), ensure the tab heading level is **one level deeper** than its parent section heading + - Example: If the parent section is `### Section Name`, tab headings should be `#### [Tab Name](#tab/tabid)` + - Example: If the parent section is `## Section Name`, tab headings should be `### [Tab Name](#tab/tabid)` +- **Markdown linting**: Pay attention to proper markdown formatting + - Avoid extra blank lines between sections (use single blank line) + - Ensure proper spacing around lists (blank line before and after list blocks) + - No trailing whitespace at end of lines + - Files should end with a single newline character + - **MD028 - No blank lines between alert boxes**: When using consecutive alert boxes (e.g., `> [!WARNING]`, `> [!NOTE]`), do NOT add blank lines between them + - Correct: Alert boxes directly after each other without blank lines + - Incorrect: Blank line separating consecutive alert boxes + - Example: + ```markdown + > [!WARNING] + > First warning message + > [!NOTE] + > Following note without blank line between + ``` +- Example: + ```markdown + ```csharp + public IState Name => State.Value(this, () => "default"); + ``` + + This state holds the user's name with a default value. + ``` + +#### Alert Boxes (Callouts) +Use alert boxes strategically to highlight important information without creating "rainbow docs": + +- **When to use alert boxes:** + - `> [!WARNING]` - Critical pitfalls that will cause errors or crashes (e.g., ListView ItemClickCommand conflicts) + - `> [!TIP]` - Decision-making guidance or useful features (e.g., "When to use Value vs Async", FeedParameter benefits) + - `> [!NOTE]` - Important design rationale or context (e.g., why button-triggered vs ForEach callbacks) + - `> [!IMPORTANT]` - Essential requirements or prerequisites + +- **When NOT to use alert boxes:** + - For general explanations (use regular text) + - For every bullet list (reserve for truly important items) + - More than 3-4 alert boxes per tutorial page (avoid "rainbow docs") + +- **Best practices:** + - Limit to 3-4 strategically placed alert boxes per document + - Use WARNING for errors/crashes, TIP for choices/features, NOTE for rationale + - Convert existing bold text lists to alert boxes only if they represent critical decisions or warnings + - Keep the content inside concise and focused + +#### Tutorial Structure Pattern +When creating tutorial documentation, follow this consistent structure: + +1. **Overview Section** + - Brief description of what will be built + - Bullet list of key features/learning goals + - Explanation of why this pattern/approach is needed + +2. **Prerequisites Section** + - List required prior knowledge or tutorials that should be completed first + - Link to previous tutorials in the learning path using xref links (e.g., "Complete [Tutorial Name](xref:uid-of-tutorial) first") + - For "getting started" tutorials at the beginning of a new chapter: link to general app setup guides + - Use language-appropriate links: English docs (`/en/`) link to English guides, German docs (`/de/`) link to German guides + - Prefer `xref:` links for internal documentation references instead of relative paths + - Example: "Before starting this tutorial, ensure you have completed [How to: Basic MVUX Setup](xref:howto-basic-mvux-setup)" + +**Common Getting Started Docs to Link:** + +- **Root-level basics** (in `docs/articles/en/` or `docs/articles/de/`): + - `HowTo-Setup-DevelopmentEnvironment-*.md` (UID: `DevTKSS.Uno.Setup.DevelopmentEnvironment.en` or `.de`) - For first-time setup prerequisites + - `HowTo-CreateApp-*.md` (UID: `DevTKSS.Uno.Setup.HowTo-CreateNewUnoApp.en` or `.de`) - For app creation fundamentals + - `HowTo-Adding-New-Pages-*.md` (UID: `DevTKSS.Uno.Setup.HowTo-AddingNewPages.en` or `.de`) - For basic page creation + - `HowTo-Adding-New-VM-Class-Record-*.md` (UID: `DevTKSS.Uno.Setup.HowTo-AddingNew-VM-Class-Record.en` or `.de`) - For MVUX Model creation basics + - `HowTo-Using-DI-in-ctor-*.md` (UID: `DevTKSS.Uno.Setup.Using-DI-in-ctor.en` or `.de`) - For dependency injection fundamentals + - `Introduction-*.md` (UID: `DevTKSS.Uno.SampleApps.Intro.en` or `.de`) - For general project introduction + +- **Topic-specific getting started** (in subdirectories like `Navigation/`, `Mvux-StateManagement/`): + - `Navigation/Extensions-Navigation-*.md` - For navigation system fundamentals + - `Navigation/HowTo-RegisterRoutes-*.md` - For route registration basics + - `Navigation/HowTo-UpgradeExistingApp-*.md` - For adding navigation to existing apps + - Link to these when starting a tutorial within that specific topic area + - Check the `uid:` field in each markdown file's front matter for the correct xref link + +3. **Visual Reference** (if available) + - Screenshot or diagram showing the end result + - Place after prerequisites, before implementation details + +4. **Model/Backend Setup** + - Show the data layer first (Model, services, states) + - Use tabbed sections for alternative approaches (e.g., `.Async()` vs `.Value()`) + - Explain key elements with bullet points below code samples + +5. **View/UI Implementation** + - Show XAML/UI code after the model is defined + - Highlight key binding lines in code snippets + - Add warning callouts for common pitfalls + - Explain bindings in bullet points + +6. **Command/Logic Implementation** + - Show methods that handle user interactions + - Explain the "why" behind design decisions + - Use bullet points to highlight key API usage + +7. **Advanced Topics** (optional) + - Attributes, optimization techniques, alternatives + - Show code variations with explanations + +8. **Summary Section** + - Numbered list of what was demonstrated (no emojis) + - Key takeaway or pattern reinforcement + +This flow follows: **Prerequisites → See what we're building → Build the foundation → Connect the UI → Add behavior → Master advanced techniques** + +## Build & Development + +### Commands +```powershell +# Build with solution filters for specific apps +dotnet build src/DevTKSS.Uno.SampleApps-GalleryOnly.slnf +dotnet build src/DevTKSS.Uno.SampleApps-Tutorials.slnf + +# Documentation (DocFX) +./docs/Build-Docs.ps1 # Build docs to _site +./docs/Clean-ApiDocs.ps1 # Clean generated API docs +``` + +### VS Code and Visual Studio notes +- 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. +- In Visual Studio 2022+, verify `src/[ProjectName]/Properties/launchSettings.json` when adding/removing targets or tweaking profiles so F5/run profiles match current TFMs. + +### Known Issues +1. **Windows target disabled** in MvuxGallery Issue [#15](https://github.com/DevTKSS/DevTKSS.Uno.SampleApps/issues/15): ResourcesDictionary import bug prevents building +2. **Theme changes** not reactive for ThemeResource styles Issue [#13](https://github.com/DevTKSS/DevTKSS.Uno.SampleApps/issues/13) +3. **DocFX source links** fail for `[!INCLUDE]` markup - uses workaround includes instead of redirects + +#### Windows target and ResourceDictionaries +- 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. + +### Warnings Suppressed +- `NU1507`: Multiple package sources with CPM +- `NETSDK1201`: RID won't create self-contained app +- `PRI257`: Default language (en) vs resources (en-us) + +## Sample App Specifics + +### MvuxGallery Features +- **FeedView + GridView/ListView** patterns with ItemOverlayTemplate +- Centralized DataTemplates in `Styles/GalleryTemplates.xaml` +- Code sample viewer using `IStorage.ReadPackageFileAsync()` from Assets +- TabBar navigation, NavigationView structure +- Custom extensions: `DevTKSS.Extensions.Uno.Storage` for line-range file reading + +### XamlNavigationApp +- Tutorial-focused app for XAML markup navigation +- Demonstrates MVUX + Navigation combined patterns +- Bilingual README files: `ReadMe.en.md`, `ReadMe.de.md` + +## Contributing Context +- Primary language: German (documentation available in EN/DE) +- Video tutorials on YouTube (German with English subtitles) +- Apache License 2.0 +- Use GitHub Discussions for questions, Issues for bugs + +## Uno Platform Context + +### Important Notes +- This uses **Uno.Sdk** (not WinAppSDK/WinUI directly) +- **Not MAUI** - uses .NET mobile bindings directly +- Targets: iOS/iPadOS, Android, macOS, Windows, Linux, WebAssembly +- Skia (canvas) and Native (native elements) renderers available +- Free C# and XAML Hot Reload support + +### MVUX Specifics +- **FeedView control** wraps async data with loading/error states + - `Source="{Binding Feed}"` binds to IFeed/IState + - `ValueTemplate` for successful data display + - `ErrorTemplate` and `ProgressTemplate` for states + - `{Binding Data}` accesses feed value in template + - `{Binding Refresh}` command triggers feed refresh +- Feeds are **awaitable**: `var data = await this.MyFeed;` +- BindableViewModel auto-generated with naming pattern `*Model` → `*ViewModel` +- Use `[ReactiveBindable]` attribute to customize code generation + +### XAML Best Practices +- Prefer `{Binding}` over `{x:Bind}` for MVUX feeds (runtime-reactive) +- Use `{utu:AncestorBinding}` from Uno.Toolkit for parent access +- Centralize DataTemplates in ResourceDictionaries (see `Styles/GalleryTemplates.xaml`) +- FeedView `State` property auto-set as DataContext for templates diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..dd97797 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,31 @@ +version: 2 +updates: + - package-ecosystem: "nuget" + directory: "/" + schedule: + interval: weekly + target-branch: master + open-pull-requests-limit: 10 + labels: ["dependencies", "nuget"] + commit-message: + prefix: "chore" + include: scope + pull-request-branch-name: + separator: "-" + groups: + minor-and-patch: + patterns: + - "*" + update-types: + - minor + - major + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: weekly + target-branch: master + labels: ["dependencies", "github-actions"] + commit-message: + prefix: "chore" + include: scope \ No newline at end of file diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 0000000..f1c3676 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,40 @@ +# Documentation +documentation: + - changed-files: + - any-glob-to-any-file: 'docs/**/*' + - any-glob-to-any-file: '**/*.md' + +# Source Code +source: + - changed-files: + - any-glob-to-any-file: 'src/**/*.cs' + - any-glob-to-any-file: 'src/**/*.xaml' + +# Extensions +extensions: + - changed-files: + - any-glob-to-any-file: 'src/DevTKSS.Extensions.Uno/**/*' + - any-glob-to-any-file: 'src/DevTKSS.Extensions.Uno.Storage/**/*' + +# Sample Apps +samples: + - changed-files: + - any-glob-to-any-file: 'src/DevTKSS.Uno.MvuxListApp/**/*' + - any-glob-to-any-file: 'src/DevTKSS.Uno.Samples.MvuxGallery/**/*' + - any-glob-to-any-file: 'src/DevTKSS.Uno.XamlNavigationApp-1/**/*' + +# CI/CD +ci-cd: + - changed-files: + - any-glob-to-any-file: '.github/**/*' + +# Dependencies +dependencies: + - changed-files: + - any-glob-to-any-file: 'src/Directory.Packages.props' + - any-glob-to-any-file: 'src/global.json' + +# Tests +tests: + - changed-files: + - any-glob-to-any-file: 'src/Tests/**/*' \ No newline at end of file diff --git a/.github/labels.yaml b/.github/labels.yaml new file mode 100644 index 0000000..fca94ee --- /dev/null +++ b/.github/labels.yaml @@ -0,0 +1,88 @@ +# GitHub Labels Configuration für DevTKSS.Uno.SampleApps +# Bestehende Standard-Labels bleiben unverändert +# Neue kategorisierte Labels werden hinzugefügt + +# === Bestehende Standard-Labels (NICHT überschreiben) === +# - bug: "d73a4a" - Something isn't working +# - documentation: "0075ca" - Improvements or additions to documentation +# - duplicate: "cfd3d7" - This issue or pull request already exists +# - enhancement: "a2eeef" - New feature or request +# - good first issue: "7057ff" - Good for newcomers +# - help wanted: "008672" - Extra attention is needed +# - invalid: "e4e669" - This doesn't seem right +# - question: "d876e3" - Further information is requested +# - wontfix: "ffffff" - This will not be worked on + +# === NEUE LABELS === + +# Kind - Art des Changes +- name: "kind/bug" + color: "d73a4a" + description: "Etwas funktioniert nicht" + +- name: "kind/feature" + color: "a2eeef" + description: "Neue Funktionalität" + +- name: "kind/dependency" + color: "0366d6" + description: "Dependency Updates (NuGet, SDK)" + +- name: "kind/refactor" + color: "fbca04" + description: "Code-Umstrukturierung ohne funktionale Änderungen" + +# Area - Projektbereich +- name: "area/extensions" + color: "5319e7" + description: "DevTKSS.Extensions.Uno Projekte" + +- name: "area/samples" + color: "d4c5f9" + description: "Sample Applications" + +- name: "area/mvux" + color: "b60205" + description: "MVUX-bezogene Änderungen" + +- name: "area/navigation" + color: "c2e0c6" + description: "Navigation-bezogene Änderungen" + +- name: "area/ci-cd" + color: "ededed" + description: "CI/CD Workflows und GitHub Actions" + +- name: "area/tests" + color: "d93f0b" + description: "Test Code und Testing Infrastructure" + +- name: "area/docs" + color: "0075ca" + description: "Dokumentation (docs/, README)" + +# Status +- name: "status/needs-review" + color: "fbca04" + description: "Wartet auf Code Review" + +- name: "status/blocked" + color: "d73a4a" + description: "Blockiert durch andere Issues/PRs" + +- name: "status/ready-to-merge" + color: "0e8a16" + description: "Bereit zum Merge" + +# Do Not Merge +- name: "do-not-merge/work-in-progress" + color: "ee0701" + description: "PR ist noch in Arbeit (WIP)" + +- name: "do-not-merge/needs-tests" + color: "ee0701" + description: "Tests fehlen noch" + +- name: "do-not-merge/breaking-change" + color: "b60205" + description: "Breaking Change - benötigt besondere Aufmerksamkeit" \ No newline at end of file diff --git a/.github/workflows/build-deploy-docs.yml b/.github/workflows/build-deploy-docs.yml index 5c5487a..3e82577 100644 --- a/.github/workflows/build-deploy-docs.yml +++ b/.github/workflows/build-deploy-docs.yml @@ -17,14 +17,12 @@ permissions: concurrency: group: pages - cancel-in-progress: false + cancel-in-progress: true jobs: - publish-docs: - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - runs-on: windows-latest + build-docs: + if: ${{ github.ref == 'refs/heads/master' }} + runs-on: ubuntu-latest steps: - name: Checkout @@ -54,6 +52,15 @@ jobs: with: path: 'docs/_site' + deploy-docs: + needs: build-docs + if: ${{ needs.build-docs.result == 'success' && github.ref == 'refs/heads/master' }} + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + + steps: - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 diff --git a/.github/workflows/conventional-commits.yml b/.github/workflows/conventional-commits.yml new file mode 100644 index 0000000..f749a9a --- /dev/null +++ b/.github/workflows/conventional-commits.yml @@ -0,0 +1,20 @@ +name: Conventional Commits + +on: + pull_request: + branches: + - main + - release/* + +env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + +jobs: + commitsar: + name: Validate for conventional commits + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - name: Run commitsar + uses: aevea/commitsar@v0.20.2 diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml new file mode 100644 index 0000000..2c98826 --- /dev/null +++ b/.github/workflows/labeler.yml @@ -0,0 +1,12 @@ +name: "Pull Request Labeler" +on: + pull_request: + +jobs: + triage: + runs-on: ubuntu-latest + steps: + - uses: actions/labeler@v5 + if: github.repository == 'DevTKSS/DevTKSS.Uno.SampleApps' + with: + repo-token: "${{ secrets.GITHUB_TOKEN }}" \ No newline at end of file diff --git a/.github/workflows/sync-labels.yml b/.github/workflows/sync-labels.yml new file mode 100644 index 0000000..13df1d7 --- /dev/null +++ b/.github/workflows/sync-labels.yml @@ -0,0 +1,33 @@ +name: Sync Labels + +on: + workflow_dispatch: + push: + branches: + - main + - feature/* + - dev/* + paths: + - .github/labels.yaml + +permissions: + contents: read + issues: write + pull-requests: write + repository-projects: read + +jobs: + sync: + name: Apply labels from .github/labels.yaml + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Apply labels (create/update only) + uses: crazy-max/ghaction-github-labeler@v5 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + yaml-file: .github/labels.yaml + skip-delete: true + dry-run: false diff --git a/src/.run/Readme.md b/.run/Readme.md similarity index 100% rename from src/.run/Readme.md rename to .run/Readme.md diff --git a/.run/UnoHotDesignApp1.run.xml b/.run/UnoHotDesignApp1.run.xml new file mode 100644 index 0000000..1af77ac --- /dev/null +++ b/.run/UnoHotDesignApp1.run.xml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + diff --git a/.vscode/extensions.json b/.vscode/extensions.json index a63ad40..f1c4066 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,5 +1,7 @@ { "recommendations": [ - "unoplatform.vscode" + "unoplatform.vscode", + "ms-dotnettools.csdevkit", + "DavidAnson.vscode-markdownlint" ], } diff --git a/.vscode/launch.json b/.vscode/launch.json index 209ee0f..7949a98 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,6 +4,13 @@ // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/main/debugger-launchjson.md "version": "0.2.0", "configurations": [ + { + "name": "Uno Platform MvuxGallery Desktop Debug", + "type": "Uno", + "request": "launch", + // any Uno* task will do, this is simply to satisfy vscode requirement when a launch.json is present + "preLaunchTask": "build-desktop-MvuxGallery" + }, { // Use IntelliSense to find out which attributes exist for C# debugging // Use hover for the description of the existing attributes @@ -13,13 +20,13 @@ "request": "launch", "preLaunchTask": "build-desktop-MvuxGallery", // If you have changed target frameworks, make sure to update the program path. - "program": "${workspaceFolder}/DevTKSS.Uno.SampleApps.MvuxGallery/bin/Debug/net9.0-desktop/DevTKSS.Uno.SampleApps.MvuxGallery.dll", + "program": "${workspaceFolder}/src/DevTKSS.Uno.Samples.MvuxGallery/bin/Debug/net9.0-desktop/DevTKSS.Uno.Samples.MvuxGallery.dll", "args": [], "launchSettingsProfile": "DevTKSS MvuxGallery (Desktop)", "env": { "DOTNET_MODIFIABLE_ASSEMBLIES": "debug" }, - "cwd": "${workspaceFolder}/DevTKSS.Uno.SampleApps.MvuxGallery", + "cwd": "${workspaceFolder}/src/DevTKSS.Uno.Samples.MvuxGallery", // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console "console": "internalConsole", "stopAtEntry": false @@ -30,13 +37,33 @@ "request": "launch", "preLaunchTask": "build-desktop-XamlNavigationApp", // If you have changed target frameworks, make sure to update the program path. - "program": "${workspaceFolder}/DevTKSS.Uno.XamlNavigationApp/bin/Debug/net9.0-desktop/DevTKSS.Uno.XamlNavigationApp.dll", + "program": "${workspaceFolder}/src/DevTKSS.Uno.XamlNavigationApp/bin/Debug/net9.0-desktop/DevTKSS.Uno.XamlNavigationApp.dll", "args": [], "launchSettingsProfile": "DevTKSS.Uno.XamlNavigationApp (Desktop)", "env": { "DOTNET_MODIFIABLE_ASSEMBLIES": "debug" }, - "cwd": "${workspaceFolder}/DevTKSS.Uno.XamlNavigationApp", + "cwd": "${workspaceFolder}/src/DevTKSS.Uno.XamlNavigationApp", + // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console + "console": "internalConsole", + "stopAtEntry": false + }, + { + // Use IntelliSense to find out which attributes exist for C# debugging + // Use hover for the description of the existing attributes + // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md + "name": "Uno Platform Desktop Debug (MvuxListApp)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build-desktop-MvuxListApp", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/src/DevTKSS.Uno.MvuxListApp/bin/Debug/net9.0-desktop/DevTKSS.Uno.MvuxListApp.dll", + "args": [], + "launchSettingsProfile": "DevTKSS.Uno.MvuxListApp (Desktop)", + "env": { + "DOTNET_MODIFIABLE_ASSEMBLIES": "debug" + }, + "cwd": "${workspaceFolder}/src/DevTKSS.Uno.MvuxListApp", // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console "console": "internalConsole", "stopAtEntry": false @@ -47,13 +74,13 @@ "request": "launch", "preLaunchTask": "build-desktop-SimpleMemberSelectionApp", // If you have changed target frameworks, make sure to update the program path. - "program": "${workspaceFolder}/DevTKSS.Uno.SimpleMemberSelectionApp-1/bin/Debug/net9.0-desktop/DevTKSS.Uno.SimpleMemberSelectionApp.dll", + "program": "${workspaceFolder}/src/DevTKSS.Uno.SimpleMemberSelectionApp/bin/Debug/net9.0-desktop/DevTKSS.Uno.SimpleMemberSelectionApp.dll", "args": [], "launchSettingsProfile": "DevTKSS.Uno.SimpleMemberSelectionApp (Desktop)", "env": { "DOTNET_MODIFIABLE_ASSEMBLIES": "debug" }, - "cwd": "${workspaceFolder}/DevTKSS.Uno.SimpleMemberSelectionApp", + "cwd": "${workspaceFolder}/src/DevTKSS.Uno.SimpleMemberSelectionApp", // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console "console": "internalConsole", "stopAtEntry": false diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 9230005..4fac57f 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -7,7 +7,7 @@ "type": "process", "args": [ "build", - "${workspaceFolder}/DevTKSS.Uno.SampleApps.MvuxGallery/DevTKSS.Uno.SampleApps.MvuxGallery.csproj", + "${workspaceFolder}/src/DevTKSS.Uno.Samples.MvuxGallery/DevTKSS.Uno.Samples.MvuxGallery.csproj", "/property:GenerateFullPaths=true", "/property:TargetFramework=net9.0-desktop", "/consoleloggerparameters:NoSummary" @@ -20,7 +20,7 @@ "type": "process", "args": [ "publish", - "${workspaceFolder}/DevTKSS.Uno.SampleApps.MvuxGallery/DevTKSS.Uno.SampleApps.MvuxGallery.csproj", + "${workspaceFolder}/src/DevTKSS.Uno.Samples.MvuxGallery/DevTKSS.Uno.Samples.MvuxGallery.csproj", "/property:GenerateFullPaths=true", "/property:TargetFramework=net9.0-desktop", "/consoleloggerparameters:NoSummary" @@ -33,7 +33,7 @@ "type": "process", "args": [ "build", - "${workspaceFolder}/DevTKSS.Uno.XamlNavigationApp/DevTKSS.Uno.XamlNavigationApp.csproj", + "${workspaceFolder}/src/DevTKSS.Uno.XamlNavigationApp/DevTKSS.Uno.XamlNavigationApp.csproj", "/property:GenerateFullPaths=true", "/property:TargetFramework=net9.0-desktop", "/consoleloggerparameters:NoSummary" @@ -46,20 +46,47 @@ "type": "process", "args": [ "publish", - "${workspaceFolder}/DevTKSS.Uno.XamlNavigationApp/DevTKSS.Uno.XamlNavigationApp.csproj", + "${workspaceFolder}/src/DevTKSS.Uno.XamlNavigationApp/DevTKSS.Uno.XamlNavigationApp.csproj", "/property:GenerateFullPaths=true", "/property:TargetFramework=net9.0-desktop", "/consoleloggerparameters:NoSummary" ], "problemMatcher": "$msCompile" }, - { + + { + "label": "build-desktop-MvuxListApp", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/src/DevTKSS.Uno.MvuxListApp/DevTKSS.Uno.MvuxListApp.csproj", + "/property:GenerateFullPaths=true", + "/property:TargetFramework=net9.0-desktop", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish-desktop-MvuxListApp", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/src/DevTKSS.Uno.MvuxListApp/DevTKSS.Uno.MvuxListApp.csproj", + "/property:GenerateFullPaths=true", + "/property:TargetFramework=net9.0-desktop", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { "label": "build-desktop-SimpleMemberSelectionApp", "command": "dotnet", "type": "process", "args": [ "build", - "${workspaceFolder}/DevTKSS.Uno.SimpleMemberSelectionApp/DevTKSS.Uno.SimpleMemberSelectionApp.csproj", + "${workspaceFolder}/src/DevTKSS.Uno.SimpleMemberSelectionApp/DevTKSS.Uno.SimpleMemberSelectionApp.csproj", "/property:GenerateFullPaths=true", "/property:TargetFramework=net9.0-desktop", "/consoleloggerparameters:NoSummary" @@ -72,7 +99,7 @@ "type": "process", "args": [ "publish", - "${workspaceFolder}/DevTKSS.Uno.SimpleMemberSelectionApp/DevTKSS.Uno.SimpleMemberSelectionApp.csproj", + "${workspaceFolder}/src/DevTKSS.Uno.SimpleMemberSelectionApp/DevTKSS.Uno.SimpleMemberSelectionApp.csproj", "/property:GenerateFullPaths=true", "/property:TargetFramework=net9.0-desktop", "/consoleloggerparameters:NoSummary" diff --git a/DevTKSS.Uno.SampleApps.slnx b/DevTKSS.Uno.SampleApps.slnx index 3179580..6206468 100644 --- a/DevTKSS.Uno.SampleApps.slnx +++ b/DevTKSS.Uno.SampleApps.slnx @@ -26,9 +26,13 @@ + + + + - + diff --git a/README.md b/README.md index 126a849..a764036 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ The [Mvux Gallery](https://github.com/DevTKSS/DevTKSS.Uno.SampleApps/tree/master #### Featured Controls -- **FeedView** combined with GridView and ListView +- **FeedView** combined with **GridView** and **ListView** - **DataTemplate** centralized resource definitions - **Card**, **Grid**, **NavigationView** - **ItemOverlayTemplate** (replicated from WinUI 3 Gallery) @@ -99,8 +99,15 @@ A complete tutorial application demonstrating navigation patterns with MVUX and A basic application demonstrating selection and display of member names in a `ListView` bound to a `ListState` in the Model using MVUX. +**Tutorial Documentation available:** + +- **MVUX State Management Tutorials** - Learn how to use `ListState` and `ListFeed` alongside with `ListView` and `Button.Command`-Binding (🇩🇪 [German](https://devtkss.github.io/DevTKSS.Uno.SampleApps/articles/de/Mvux-StateManagement/Overview-de.html) | 🇬🇧 [English](https://devtkss.github.io/DevTKSS.Uno.SampleApps/articles/en/Mvux-StateManagement/HowTo-Binding-ListState-and-ListFeed-de.md)) + +**Video Tutorials available:** + - **[Video Tutorial - How To: Binden von ListState und ImmutableList zu FeedView & ListView im UI | Uno Community Tutorial](https://youtu.be/wOsSlv1YFic)** - Step-by-step guide (🇩🇪 German) -- **[Source Code](https://github.com/DevTKSS/DevTKSS.Uno.SampleApps/tree/master/src/DevTKSS.Uno.SimpleMemberSelectionApp/)** +- **[Video Tutorial Series](https://youtube.com/playlist?list=PLEL6kb4Bivm_g81iKBl-f0eYPNr5h2dFX)** - Complete walkthrough (🇩🇪 German with English subtitles) +- **[Source Code](https://github.com/DevTKSS/DevTKSS.Uno.SampleApps/tree/master/src/DevTKSS.Uno.XamlNavigationApp-1/)** - Browse the implementation --- diff --git a/docs/articles/.attachments/Binding-ListState-FeedView.png b/docs/articles/.attachments/Binding-ListState-FeedView.png new file mode 100644 index 0000000..b80fc54 Binary files /dev/null and b/docs/articles/.attachments/Binding-ListState-FeedView.png differ diff --git a/docs/articles/.attachments/DevTKSS.Uno.XamlNavigationApp.png b/docs/articles/.attachments/DevTKSS.Uno.XamlNavigationApp.png index b80fc54..8e9c909 100644 Binary files a/docs/articles/.attachments/DevTKSS.Uno.XamlNavigationApp.png and b/docs/articles/.attachments/DevTKSS.Uno.XamlNavigationApp.png differ diff --git a/docs/articles/.attachments/MvuxListApp-ListState-UpdateAllAsync.gif b/docs/articles/.attachments/MvuxListApp-ListState-UpdateAllAsync.gif new file mode 100644 index 0000000..46aaba4 Binary files /dev/null and b/docs/articles/.attachments/MvuxListApp-ListState-UpdateAllAsync.gif differ diff --git a/docs/articles/de/Mvux-StateManagement/HowTo-Binding-ListState-Selection-de.md b/docs/articles/de/Mvux-StateManagement/HowTo-Binding-ListState-Selection-de.md new file mode 100644 index 0000000..0746e8d --- /dev/null +++ b/docs/articles/de/Mvux-StateManagement/HowTo-Binding-ListState-Selection-de.md @@ -0,0 +1,143 @@ +--- +uid: DevTKSS.Uno.MvuxStateManagement.ListState.Selection.de +--- +# Anleitung: Binding von ListState mit Selection + +## Überblick + +In diesem Beispiel zeigen wir dir, wie du ein `ListState` aus deinem Model an eine `ListView` bindest und wie du die Auswahl eines Elements verfolgst. Wir erstellen eine einfache Mitgliederlisten-Anzeige, bei der du: + +- Eine Liste von Mitgliedern in einer `ListView` anzeigen kannst +- Ein Mitglied aus der `ListView` auswählen kannst +- Das ausgewählte Mitglied oben auf der Seite angezeigt bekommst + +Dieses Beispiel zeigt die Grundlagen des `.Selection(...)`-Operators, der sowohl mit `IListState` als auch mit `IListFeed` funktioniert. + +## Voraussetzungen + +Bevor du mit diesem Tutorial beginnst, stelle sicher, dass du: + +- [Anleitung: Erstellen einer Uno Platform App](xref:DevTKSS.Uno.Setup.HowTo-CreateNewUnoApp.de) abgeschlossen hast +- [Anleitung: Hinzufügen neuer Pages](xref:DevTKSS.Uno.Setup.HowTo-AddingNewPages.de) abgeschlossen hast +- [Anleitung: Hinzufügen neuer MVUX Model-Klassen](xref:DevTKSS.Uno.Setup.HowTo-AddingNew-VM-Class-Record.de) abgeschlossen hast +- Grundlegendes Verständnis von Dependency Injection aus [Anleitung: Verwendung von DI im Constructor](xref:DevTKSS.Uno.Setup.Using-DI-in-ctor.de) hast + +## Visuelle Referenz + +![Mitgliederlisten UI mit Selection](../../.attachments/Binding-ListState-FeedView.png) + +## Das Model Setup + +Zunächst definieren wir die States, die für die Anzeige und Auswahl benötigt werden. + +### Initialisierung von ListState + +Es gibt zwei gängige Möglichkeiten, den `ListState` zu initialisieren: + +#### [Verwendung von `ListState.Async(...)`](#tab/Async) + +Mit der `ListState.Async(...)`-Methode kannst du eine asynchrone Methode bereitstellen, die einmal aufgerufen wird, um die anfängliche Liste der Mitglieder zu erhalten. Dies ist nützlich, wenn du Daten asynchron aus einer API oder Datenbank laden musst. + +```csharp +private readonly IImmutableList _listMembers = ImmutableList.Create( + [ + "Hans", + "Lisa", + "Anke", + "Tom" + ]); + +private async ValueTask> GetMembersAsync(CancellationToken ct) + => _listMembers; + +public IListState Members => ListState.Async(this, GetMembersAsync) + .Selection(SelectedMember); + +public IState SelectedMember => State.Value(this, () => string.Empty); +``` + +Die Schlüsselelemente in diesem Code: + +- `_listMembers` - Eine statische unveränderliche Liste, die unsere Mitgliedernamen enthält +- `GetMembersAsync(...)` - Asynchrone Methode, die die Liste zurückgibt (obwohl es sich um statische Daten handelt) +- `Members` - `IListState` initialisiert über `Async(...)` mit `.Selection(...)`-Operator +- `SelectedMember` - `IState`, das das aktuell ausgewählte Mitglied verfolgt + +**Hinweis:** Obwohl dieser Ansatz funktioniert, erfordert er erheblichen Boilerplate-Code (Feld + asynchrone Methode + ListState-Property), selbst für statische Daten, die eigentlich kein asynchrones Laden benötigen. + +#### [Verwendung von `ListState.Value(...)`](#tab/Value) + +Mit der `ListState.Value(...)`-Methode kannst du eine statische Liste von Mitgliedern direkt in einer einzigen Zeile bereitstellen. Dieser Ansatz reduziert den Boilerplate drastisch und eignet sich perfekt für Demonstrationszwecke oder beim Umgang mit statischen Daten. + +```csharp +public IListState Members => ListState.Value(this, + () => ImmutableList.Create( + [ + "Hans", + "Lisa", + "Anke", + "Tom" + ]) + ).Selection(SelectedMember); + +public IState SelectedMember => State.Value(this, () => string.Empty); +``` + +**Vorteile gegenüber dem Async-Ansatz:** + +- **90% weniger Code** - Kein separates Feld oder asynchrone Methode erforderlich +- **Mehrzeilige Definition** - Klarer, lesbarer Property-Ausdruck mit ordnungsgemäßer Formatierung +- **Sofortige Klarheit** - Du kannst die Daten direkt dort sehen, wo sie definiert sind +- **Gleiche Funktionalität** - Erhält trotzdem den `.Selection(...)`-Operator und alle ListState-Features +- **Ideal für statische Daten** - Kein unnötiger asynchroner Overhead für bereits verfügbare Daten + +> [!TIP] +> **Wann Value vs Async verwenden:** +> +> - **Verwende `.Value(...)`**, wenn deine Daten statisch sind, aus Konstanten stammen oder synchron berechnet werden +> - **Verwende `.Async(...)`**, wenn du tatsächlich Daten aus einer API, Datenbank abrufen oder asynchrone Operationen durchführen musst + +*** + +## Die View (XAML) + +Nachdem wir nun unser Model mit den erforderlichen States eingerichtet haben, erstellen wir die UI. Unsere UI besteht aus einem `TextBlock`, der das ausgewählte Mitglied anzeigt, und einer `ListView` zur Anzeige aller Mitglieder: + +```xaml + + + + + + + +``` + +> [!WARNING] +> Wenn du das `ListView`-Control verwendest, stelle sicher, dass du die `ItemClickCommand`-Eigenschaft der `ListView` **nicht** gleichzeitig mit dem `.Selection(...)`-Operator des `ListState` setzt, da dies das Auswahlverhalten beeinträchtigt und den State, den du zur Widerspiegelung der aktuellen Auswahl verwendest, nicht wie erwartet aktualisiert. Du musst dich für eine der beiden Optionen entscheiden. + +Beachte die wichtigsten Bindings: + +- `ItemsSource="{Binding Path=Members}"` - bindet an unser `IListState` +- `Text="{Binding Path=SelectedMember, Mode=OneWay}"` - zeigt das ausgewählte Mitglied an + +## Zusammenfassung + +Dieses Beispiel demonstriert: + +1. Binding von `IListState` an eine `ListView` mit `.Selection(...)`-Operator (funktioniert auch mit `IListFeed`) +2. Verwendung eines separaten `IState` zur Verfolgung der Auswahl +3. Anzeige des ausgewählten Elements in der UI +4. Zwei Initialisierungsmethoden: `.Async(...)` für echte asynchrone Daten vs `.Value(...)` für statische Daten + +- [Link zum Source Code](../../../../src/DevTKSS.Uno.XamlNavigationApp-1/Presentation/DashboardModel.cs) + +Im nächsten Tutorial lernst du, wie du die ausgewählten Elemente bearbeiten und aktualisieren kannst. + +- [Nächstes Tutorial: Aktualisierung von ListState Items](xref:DevTKSS.Uno.MvuxStateManagement.ListState.UpdateItems.de) diff --git a/docs/articles/de/Mvux-StateManagement/HowTo-Binding-ListState-and-ListFeed-de.md b/docs/articles/de/Mvux-StateManagement/HowTo-Binding-ListState-and-ListFeed-de.md new file mode 100644 index 0000000..2dd82b9 --- /dev/null +++ b/docs/articles/de/Mvux-StateManagement/HowTo-Binding-ListState-and-ListFeed-de.md @@ -0,0 +1,55 @@ +--- +uid: DevTKSS.Uno.Mvux-StateManagement.Overview.de +--- +# Übersicht: Mvux State Management + +## Einführung + +In dieser Tutorial-Serie lernst du, wie du `ListState` und `ListFeed` in deinen Uno Platform MVUX-Apps verwendest. Diese Komponenten ermöglichen dir die reaktive Verwaltung von Listen-Daten mit automatischer UI-Aktualisierung. + +## Was ist der Unterschied zwischen ListFeed und ListState? + +- **`IListFeed`** - Schreibgeschützte read-only Daten-Sammlungen (z.B. Server-Antworten) + - Unterstützt `RequestRefreshAsync` oder `RefreshAsync` und den `.Selection(...)` Operator + - Kein Support für `ForEach`-Callbacks oder direkte Item-Updates via bspw. `UpdateAllAsync(...)` + +- **`IListState`** - read-write Daten-Sammlungen + - Ermöglicht direkte Aktualisierung und Key-matching Updates von Elementen mit `UpdateAllAsync(...)` oder `UpdateItemAsync(...)` + - Unterstützt `ForEach`-Callbacks für die Verarbeitung von Elementen + - Unterstützt `AddAsync`/`RemoveAsync`-Operationen + - Ebenfalls kompatibel mit dem `.Selection(...)` Operator + +## Tutorial-Serie + +Diese Serie besteht aus zwei aufeinander aufbauenden Tutorials: + +### 1. [Binding von ListState mit Selection](xref:DevTKSS.Uno.MvuxStateManagement.ListState-Selection.de) + +In diesem ersten Tutorial lernst du die Grundlagen: + +- Wie du ein `ListState` an eine `ListView` bindest +- Wie du den `.Selection(...)` Operator verwendest +- Wie du das ausgewählte Element in der UI anzeigst +- Unterschiede zwischen `.Async(...)` und `.Value(...)` Initialisierung + +### 2. [Aktualisierung von ListState Items](xref:DevTKSS.Uno.MvuxStateManagement.Update-ListStateItems.de) + +Im zweiten Tutorial erweitern wir die Funktionalität: + +- Wie du Elemente in einem `ListState` bearbeitest +- Verwendung von `UpdateAllAsync(...)` mit Filterkriterien +- Einsatz von `[FeedParameter]` für saubereres State-Handling +- Warum `IListState` für Aktualisierungen erforderlich ist + +## Voraussetzungen + +Bevor du mit diesen Tutorials beginnst, stelle sicher, dass du: + +- [Anleitung: Erstellen einer Uno Platform App](xref:DevTKSS.Uno.Setup.HowTo-CreateNewUnoApp.de) abgeschlossen hast +- [Anleitung: Hinzufügen neuer Pages](xref:DevTKSS.Uno.Setup.HowTo-AddingNewPages.de) abgeschlossen hast +- [Anleitung: Hinzufügen neuer MVUX Model-Klassen](xref:DevTKSS.Uno.Setup.HowTo-AddingNew-VM-Class-Record.de) abgeschlossen hast +- Grundlegendes Verständnis von Dependency Injection aus [Anleitung: Verwendung von DI im Constructor](xref:DevTKSS.Uno.Setup.Using-DI-in-ctor.de) hast + +## Los geht's + +Beginne mit dem ersten Tutorial: [Binding von ListState mit Selection](xref:DevTKSS.Uno.MvuxStateManagement.ListState-Selection.de) diff --git a/docs/articles/de/Mvux-StateManagement/HowTo-Update-ListState-Items-de.md b/docs/articles/de/Mvux-StateManagement/HowTo-Update-ListState-Items-de.md new file mode 100644 index 0000000..c003499 --- /dev/null +++ b/docs/articles/de/Mvux-StateManagement/HowTo-Update-ListState-Items-de.md @@ -0,0 +1,160 @@ +--- +uid: DevTKSS.Uno.MvuxStateManagement.ListState.UpdateItems.de +--- +# Anleitung: Aktualisierung von ListState Items + +## Überblick + +In diesem Tutorial erweitern wir das vorherige Beispiel und fügen die Möglichkeit hinzu, Elemente in einem `ListState` zu bearbeiten. Du wirst lernen: + +- Wie du einen zusätzlichen State für Benutzereingaben erstellst +- Wie du `UpdateAllAsync(...)` verwendest, um Elemente zu aktualisieren +- Wie du `[FeedParameter]` für saubereres State-Handling nutzt +- Warum wir `IListState` anstelle von `IListFeed` für Aktualisierungen benötigen + +Dieses Szenario zeigt, warum wir `ListState` anstelle von `ListFeed` benötigen: Während `ListFeed` nur `RequestRefresh` oder `Refresh` Aktionen unterstützt (die einen neuen API-/Service-Aufruf erfordern), ermöglicht `ListState` die direkte Aktualisierung von Elementen in der Liste mithilfe von Filterkriterien. + +## Voraussetzungen + +Bevor du mit diesem Tutorial beginnst, stelle sicher, dass du: + +- [Anleitung: Binding von ListState mit Selection](xref:DevTKSS.Uno.MvuxStateManagement.ListState-Selection.de) erfolgreich abgeschlossen hast + +## Visuelle Referenz + +![Mitgliederlisten-Editor mit Update-Funktion](../../.attachments/MvuxListApp-ListState-UpdateAllAsync.gif) + +## Erweiterung des Models + +Wir erweitern unser bestehendes Model um einen zusätzlichen State für die Bearbeitung und eine Methode zum Aktualisieren: + +### Zusätzlicher State für die Bearbeitung + +Wir benötigen einen State, um den geänderten Mitgliedernamen zu halten, den du eingibst: + +```csharp +public IState ModifiedMemberName => State.Empty(this); +``` + +Dieser State ist bidirektional an die `TextBox` gebunden und erfasst deine Eingabe. + +### Vollständiges Model + +So sieht dein vollständiges Model aus: + +```csharp +public partial record MainModel +{ + public IListState Members => ListState.Value(this, + () => ImmutableList.Create( + [ + "Hans", + "Lisa", + "Anke", + "Tom" + ]) + ).Selection(SelectedMember); + + public IState SelectedMember => State.Value(this, () => string.Empty); + + public IState ModifiedMemberName => State.Empty(this); +} +``` + +## Erweiterte View (XAML) + +Jetzt fügen wir die Bearbeitungselemente zur UI hinzu: + +[!code-xaml[](../../../../src/DevTKSS.Uno.MvuxListApp/Presentation/MainPage.xaml#MembersView?highlight=18,23,27,30)] + +Die neuen Bindings: + +- `Text="{Binding Path=ModifiedMemberName, Mode=TwoWay}"` - bidirektionales Binding für die Bearbeitung +- `Command="{Binding Path=RenameMemberAsync}"` - löst die Umbenennungsoperation aus + +## Implementierung des Rename-Befehls + +> [!NOTE] +> Wir verwenden einen schaltflächengesteuerten Befehl anstelle eines `.ForEach(...)`-Callbacks, um dir die explizite Kontrolle darüber zu geben, wann die Umbenennung erfolgt. Dies verhindert unbeabsichtigte Änderungen, wenn du: +> +> - Das falsche Mitglied ausgewählt hast +> - Noch die korrekte Schreibweise nachschlägst +> - Deine Meinung über die Umbenennung änderst + +Hier ist die Befehlsimplementierung: + +```csharp +public async ValueTask RenameMemberAsync( + [FeedParameter(nameof(ModifiedMemberName))] string? modName, + [FeedParameter(nameof(SelectedMember))] string? replaceMember, + CancellationToken ct) +{ + if (string.IsNullOrWhiteSpace(modName)) + return; + + await Members.UpdateAllAsync( + match: item => item == replaceMember, + updater: _ => modName, + ct: ct + ); + + await Members.TrySelectAsync(modName, ct); +} +``` + +Wichtige Punkte: + +- **`UpdateAllAsync(...)`** - Aktualisiert Elemente im `ListState`, die den Filterkriterien entsprechen +- **`match: item => item == replaceMember`** - Findet das aktuell ausgewählte Mitglied +- **`updater: _ => modName`** - Ersetzt es durch den neuen Namen +- **`TrySelectAsync(...)`** - Wählt das Mitglied erneut anhand seines neuen Namens aus + +## Verwendung des FeedParameter-Attributs + +Beachte die `[FeedParameter]`-Attribute auf den Methodenparametern. Diese leistungsstarke Funktion wartet automatisch auf State-Werte und bindet sie an deine Methodenparameter, wodurch manuelle `await`-Aufrufe eliminiert werden: + +```csharp +[FeedParameter(nameof(ModifiedMemberName))] string? modName, +[FeedParameter(nameof(SelectedMember))] string? replaceMember +``` + +> [!TIP] +> **Vorteile:** +> +> - Kein manuelles `await` der States innerhalb der Methode erforderlich +> - Parameter können andere Namen als die ursprünglichen States haben (verbessert die Lesbarkeit) +> - Sauberere, fokussiertere Methodenimplementierung + +**Alternative:** Verwende `[ImplicitFeedParameter]` auf Klassenebene, um alle Parameter automatisch zu binden, indem Namen exakt mit deinen States übereinstimmen: + +```csharp +[ImplicitFeedParameter] +public partial record MainModel +{ + ... + + public async ValueTask RenameMemberAsync( + string? ModifiedMemberName, + string? SelectedMember, + CancellationToken ct) + { ... } +} +``` + +Mit `[ImplicitFeedParameter]` auf der Klasse werden alle Methodenparameter automatisch gebunden, indem ihre Namen exakt mit deinen State-Property-Namen übereinstimmen. Das bedeutet: + +- Der Parameter `ModifiedMemberName` bindet automatisch an den `ModifiedMemberName`-State +- Der Parameter `SelectedMember` bindet automatisch an den `SelectedMember`-State +- Keine individuellen `[FeedParameter]`-Attribute für jeden Parameter erforderlich +- Parameternamen müssen exakt mit State-Namen übereinstimmen (Groß-/Kleinschreibung beachten) + +## Zusammenfassung + +Dieses Beispiel demonstriert: + +1. Verwendung von bidirektionalem Binding für Benutzereingaben über `IState` +2. Aktualisierung von Listenelementen mit `UpdateAllAsync(...)` - nur verfügbar bei `IListState` (nicht `IListFeed`) +3. Befehlsbasierte Aktualisierungen für explizite Benutzerkontrolle +4. Nutzung von `[FeedParameter]` für saubereres asynchrones State-Handling + +Dieses Muster gewährleistet Datenkonsistenz und gibt dir die volle Kontrolle darüber, wann Änderungen in den Daten erfolgen und wann auch nicht. diff --git a/docs/articles/de/Mvux-StateManagement/toc.yml b/docs/articles/de/Mvux-StateManagement/toc.yml new file mode 100644 index 0000000..0fe4147 --- /dev/null +++ b/docs/articles/de/Mvux-StateManagement/toc.yml @@ -0,0 +1,10 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/dotnet/docfx/main/schemas/toc.schema.json +- name: Übersicht: ListState und ListFeed + uid: DevTKSS.Uno.Mvux-StateManagement.Overview.de + href: HowTo-Binding-ListState-and-ListFeed-de.md +- name: Binding von ListState mit Selection + uid: DevTKSS.Uno.MvuxStateManagement.ListState.Selection.de + href: HowTo-Binding-ListState-Selection-de.md +- name: Aktualisierung von ListState Items + uid: DevTKSS.Uno.MvuxStateManagement.ListState.UpdateItems.de + href: HowTo-Update-ListState-Items-de.md diff --git a/docs/articles/de/MvuxGallery-Overview-de.md b/docs/articles/de/MvuxGallery-Overview-de.md index 2c60f00..c978db1 100644 --- a/docs/articles/de/MvuxGallery-Overview-de.md +++ b/docs/articles/de/MvuxGallery-Overview-de.md @@ -22,13 +22,20 @@ Hier ist eine Liste von Steuerelementen und Funktionen, die Sie in der MvuxGalle - [`ItemOverlayTemplate` DataTemplate](https://github.com/DevTKSS/DevTKSS.Uno.SampleApps/blob/master/src/DevTKSS.Uno.Samples.MvuxGallery/Styles/Generic.xaml) (*Layout repliziert aus WinUI 3 Galerie*) - [TabBar und TabBarItem](https://github.com/DevTKSS/DevTKSS.Uno.SampleApps/blob/master/src/DevTKSS.Uno.Samples.MvuxGallery/Presentation/Views/DashboardPage.xaml) und [Model für das Binden von Elementen an ListFeed](https://github.com/DevTKSS/DevTKSS.Uno.SampleApps/blob/master/src/DevTKSS.Uno.Samples.MvuxGallery/Presentation/ViewModels/DashboardModel.cs) -## Beispielhafte Uno.Extensions +## Beispiele der Verwendung von Uno.Extensions in der MvuxGallery App - Mvux - ListFeed + - [DashboardModel.cs](https://github.com/DevTKSS/DevTKSS.Uno.SampleApps/blob/master/src/DevTKSS.Uno.Samples.MvuxGallery/Presentation/ViewModels/DashboardModel.cs) + - [ListboardModel.cs](https://github.com/DevTKSS/DevTKSS.Uno.SampleApps/blob/master/src/DevTKSS.Uno.Samples.MvuxGallery/Presentation/ViewModels/ListboardModel.cs) + - [GalleryImageService.cs](https://github.com/DevTKSS/DevTKSS.Uno.SampleApps/blob/master/src/DevTKSS.Uno.Samples.MvuxGallery/Models/GalleryImages/GalleryImageService.cs) - State - - --> Fast jedes Model, detaillierte Übersicht folgt. + - [CounterModel.cs](https://github.com/DevTKSS/DevTKSS.Uno.SampleApps/blob/master/src/DevTKSS.Uno.Samples.MvuxGallery/Presentation/ViewModels/CounterModel.cs) + - [DashboardModel.cs](https://github.com/DevTKSS/DevTKSS.Uno.SampleApps/blob/master/src/DevTKSS.Uno.Samples.MvuxGallery/Presentation/ViewModels/DashboardModel.cs) + - [ListboardModel.cs](https://github.com/DevTKSS/DevTKSS.Uno.SampleApps/blob/master/src/DevTKSS.Uno.Samples.MvuxGallery/Presentation/ViewModels/ListboardModel.cs) + - [MainModel.cs](https://github.com/DevTKSS/DevTKSS.Uno.SampleApps/blob/master/src/DevTKSS.Uno.Samples.MvuxGallery/Presentation/ViewModels/MainModel.cs) + - [SimpleCardsModel.cs](https://github.com/DevTKSS/DevTKSS.Uno.SampleApps/blob/master/src/DevTKSS.Uno.Samples.MvuxGallery/Presentation/ViewModels/SimpleCardsModel.cs) + - [ShellModel.cs](https://github.com/DevTKSS/DevTKSS.Uno.SampleApps/blob/master/src/DevTKSS.Uno.Samples.MvuxGallery/Presentation/ShellModel.cs) - Navigation - über Xaml @@ -40,7 +47,7 @@ Hier ist eine Liste von Steuerelementen und Funktionen, die Sie in der MvuxGalle - Hosting - [App.xaml.cs](https://github.com/DevTKSS/DevTKSS.Uno.SampleApps/blob/master/src/DevTKSS.Uno.Samples.MvuxGallery/App.xaml.cs) -- DependencyInjection +- DependencyInjection (Inkl. in `UnoFeature` "Hosting") - Service Registrierung - [App.xaml.cs](https://github.com/DevTKSS/DevTKSS.Uno.SampleApps/blob/master/src/DevTKSS.Uno.Samples.MvuxGallery/App.xaml.cs) - Service Definition diff --git a/docs/articles/de/toc.yml b/docs/articles/de/toc.yml index c159e9c..314d9fe 100644 --- a/docs/articles/de/toc.yml +++ b/docs/articles/de/toc.yml @@ -24,3 +24,5 @@ href: HowTo-Adding-New-VM-Class-Record-de.md - name: "Navigation in Uno Apps" href: Navigation/toc.yml +- name: "MVUX State Management" + href: Mvux-StateManagement/toc.yml diff --git a/docs/articles/en/Mvux-StateManagement/HowTo-Binding-ListState-Selection-en.md b/docs/articles/en/Mvux-StateManagement/HowTo-Binding-ListState-Selection-en.md new file mode 100644 index 0000000..b739b75 --- /dev/null +++ b/docs/articles/en/Mvux-StateManagement/HowTo-Binding-ListState-Selection-en.md @@ -0,0 +1,140 @@ +--- +uid: DevTKSS.Uno.Mvux-StateManagement.ListState.Selection.en +--- +# How to: Binding ListState with Selection + +## Overview + +In this example we show you how to bind a `ListState` from your Model to a `ListView` and how to track the selection of an item. We'll build a simple member list display where users can: + +- View a list of members in a `ListView` +- Select a member from the `ListView` +- See the selected member displayed at the top of the page + +This example demonstrates the fundamentals of the `.Selection(...)` operator, which works with both `IListState` and `IListFeed`. + +## Prerequisites + +Before starting this tutorial, ensure you have: + +- Completed [How to: Create an Uno Platform App](xref:DevTKSS.Uno.Setup.HowTo-CreateNewUnoApp.en) +- Completed [How to: Adding New Pages](xref:DevTKSS.Uno.Setup.HowTo-AddingNewPages.en) +- Completed [How to: Adding New MVUX Model Classes](xref:DevTKSS.Uno.Setup.HowTo-AddingNew-VM-Class-Record.en) +- Basic understanding of dependency injection from [How to: Using DI in Constructor](xref:DevTKSS.Uno.Setup.Using-DI-in-ctor.en) + +## Visual Reference + +![Member List UI with Selection](../../.attachments/Binding-ListState-FeedView.png) + +## The Model Setup + +First, let's define the states needed for display and selection. + +### Initializing ListState + +There are two common ways to initialize the `ListState`: + +#### [Using `ListState.Async(...)`](#tab/Async) + +Using the `ListState.Async(...)` method, we can provide an async method that will be called once to get the initial list of Members. This is useful when you need to load data from an API or database asynchronously. + +```csharp +private readonly IImmutableList _listMembers = ImmutableList.Create( + [ + "Hans", + "Lisa", + "Anke", + "Tom" + ]); + +private async ValueTask> GetMembersAsync(CancellationToken ct) + => _listMembers; + +public IListState Members => ListState.Async(this, GetMembersAsync) + .Selection(SelectedMember); + +public IState SelectedMember => State.Value(this, () => string.Empty); +``` + +The key elements in this code: + +- `_listMembers` - A static immutable list holding our member names +- `GetMembersAsync(...)` - Async method returning the list (even though it's static data) +- `Members` - `IListState` initialized via `Async(...)` with `.Selection(...)` operator +- `SelectedMember` - `IState` that tracks the currently selected member + +> [!NOTE] +> While this approach works, it requires significant boilerplate code (property + async method + ListState property) even for static data that doesn't actually need async loading. + +#### [Using `ListState.Value(...)`](#tab/Value) + +Using the `ListState.Value(...)` method, we can provide a static list of Members directly in a single line. This approach dramatically reduces boilerplate and is perfect for demonstration purposes or when dealing with static data. + +```csharp +public IListState Members => ListState.Value(this, + () => ImmutableList.Create( + [ + "Hans", + "Lisa", + "Anke", + "Tom" + ]) + ).Selection(SelectedMember); + +public IState SelectedMember => State.Value(this, () => string.Empty); +``` + +**Advantages over the Async approach:** + +- **90% less code** - No separate field or async method needed +- **Multi-line definition** - Clear, readable property expression with proper formatting +- **Immediate clarity** - You can see the data right where it's defined +- **Same functionality** - Still gets the `.Selection(...)` operator and all ListState features +- **Ideal for static data** - No unnecessary async overhead for data that's already available + +> [!TIP] +> **When to use Value vs Async:** +> +> - **Use `.Value(...)`** when your data is static, comes from constants, or is computed synchronously +> - **Use `.Async(...)`** when you actually need to fetch data from an API, database, or perform async operations + +*** + +## The View (XAML) + +Now that we have our Model set up with the required states, let's create the UI. Our UI consists of a `TextBlock` displaying the selected member and a `ListView` showing all members: + +```xaml + + + + + + + +``` + +> [!WARNING] +> If you use the `ListView`-Control, make sure to **not** set the `ItemClickCommand` property of the `ListView` simultaneously to the `.Selection(...)` operator of the `ListState`, as it will interfere with the selection behavior and not update the State you use to reflect the current selection as expected. You have to choose either one of the two options. + +Note the key bindings: + +- `ItemsSource="{Binding Path=Members}"` - binds to our `IListState` +- `Text="{Binding Path=SelectedMember, Mode=OneWay}"` - displays the selected member + +## Summary + +This example demonstrates: + +1. Binding `IListState` to a `ListView` with `.Selection(...)` operator (also works with `IListFeed`) +2. Using a separate `IState` to track the selection +3. Displaying the selected item in the UI +4. Two initialization methods: `.Async(...)` for real async data vs `.Value(...)` for static data + +In the next tutorial, you'll learn how to edit and update the selected items. diff --git a/docs/articles/en/Mvux-StateManagement/HowTo-Binding-ListState-and-ListFeed-en.md b/docs/articles/en/Mvux-StateManagement/HowTo-Binding-ListState-and-ListFeed-en.md new file mode 100644 index 0000000..37c6e45 --- /dev/null +++ b/docs/articles/en/Mvux-StateManagement/HowTo-Binding-ListState-and-ListFeed-en.md @@ -0,0 +1,56 @@ +--- +uid: DevTKSS.Uno.Mvux-StateManagement.Overview.en +--- +# Overview: Mvux State Management + +## Introduction + +In this tutorial series you will learn how to use `ListState` and `ListFeed` in your Uno Platform MVUX apps. These components enable you to manage list data reactively with automatic UI updates. + +## What's the difference between ListFeed and ListState? + +- **`IListFeed`** - Read-only async data collections + - Ideal for data you only display but don't edit (e.g., server responses) + - Only supports `RequestRefresh` or `Refresh` (requires new API call) + - Works with the `.Selection(...)` operator + +- **`IListState`** - Read-write data collections + - Allows direct item updates with `UpdateAllAsync(...)` + - You can target specific items with filter criteria + - Also compatible with the `.Selection(...)` operator + - Ideal for lists where you want to edit, add, or remove items + +## Tutorial Series + +This series consists of two progressive tutorials: + +### 1. [Binding ListState with Selection](xref:DevTKSS.Uno.MvuxStateManagement.ListState-Selection.en) + +In this first tutorial you'll learn the basics: + +- How to bind a `ListState` to a `ListView` +- How to use the `.Selection(...)` operator +- How to display the selected item in the UI +- Differences between `.Async(...)` and `.Value(...)` initialization + +### 2. [Updating ListState Items](xref:DevTKSS.Uno.MvuxStateManagement.Update-ListStateItems.en) + +In the second tutorial we extend the functionality: + +- How to edit items in a `ListState` +- Using `UpdateAllAsync(...)` with filter criteria +- Leveraging `[FeedParameter]` for cleaner state handling +- Why `IListState` is required for updates + +## Prerequisites + +Before starting these tutorials, ensure you have: + +- Completed [How to: Create an Uno Platform App](xref:DevTKSS.Uno.Setup.HowTo-CreateNewUnoApp.en) +- Completed [How to: Adding New Pages](xref:DevTKSS.Uno.Setup.HowTo-AddingNewPages.en) +- Completed [How to: Adding New MVUX Model Classes](xref:DevTKSS.Uno.Setup.HowTo-AddingNew-VM-Class-Record.en) +- Basic understanding of dependency injection from [How to: Using DI in Constructor](xref:DevTKSS.Uno.Setup.Using-DI-in-ctor.en) + +## Get Started + +Begin with the first tutorial: [Binding ListState with Selection](xref:DevTKSS.Uno.MvuxStateManagement.ListState-Selection.en) diff --git a/docs/articles/en/Mvux-StateManagement/HowTo-Update-ListState-Items-en.md b/docs/articles/en/Mvux-StateManagement/HowTo-Update-ListState-Items-en.md new file mode 100644 index 0000000..ec9c298 --- /dev/null +++ b/docs/articles/en/Mvux-StateManagement/HowTo-Update-ListState-Items-en.md @@ -0,0 +1,158 @@ +--- +uid: DevTKSS.Uno.Mvux-StateManagement.ListState.UpdateItems.en +--- +# How to: Updating ListState Items + +## Overview + +In this tutorial we extend the previous example by adding the ability to edit items in a `ListState`. You will learn: + +- How to create an additional state for user input +- How to use `UpdateAllAsync(...)` to update items +- How to leverage `[FeedParameter]` for cleaner state handling +- Why we need `IListState` instead of `IListFeed` for updates + +This scenario demonstrates why we need `ListState` instead of `ListFeed`: while `ListFeed` only supports `RequestRefresh` or `Refresh` actions (requiring a new API/service call), `ListState` allows us to directly update items in the list using filter criteria. + +## Prerequisites + +Before starting this tutorial, ensure you have: + +- Completed [How to: Binding ListState with Selection](xref:DevTKSS.Uno.MvuxStateManagement.ListState.Selection.en) successfully. + +## Visual Reference + +![Member List Editor with Update Functionality](../../.attachments/Binding-ListState-FeedView.png) + +## Extending the Model + +We extend our existing Model with an additional state for editing and a method for updating: + +### Additional State for Editing + +We need a state to hold the modified member name that the user is typing: + +```csharp +public IState ModifiedMemberName => State.Empty(this); +``` + +This state is bound two-way to the `TextBox`, capturing user input. + +### Complete Model + +Here's what your complete Model looks like: + +```csharp +public partial record MainModel +{ + public IListState Members => ListState.Value(this, + () => ImmutableList.Create( + [ + "Hans", + "Lisa", + "Anke", + "Tom" + ]) + ).Selection(SelectedMember); + + public IState SelectedMember => State.Value(this, () => string.Empty); + + public IState ModifiedMemberName => State.Empty(this); +} +``` + +## Extended View (XAML) + +Now let's add the editing elements to the UI: + +[!code-xaml[](../../../../src/DevTKSS.Uno.MvuxListApp/Presentation/MainPage.xaml#MembersView?highlight=18,23,27,30)] + +The new bindings: + +- `Text="{Binding Path=ModifiedMemberName, Mode=TwoWay}"` - two-way binding for editing +- `Command="{Binding Path=RenameMemberAsync}"` - triggers the rename operation + +## Implementing the Rename Command + +> [!NOTE] +> We use a button-triggered command rather than a `.ForEach(...)` callback to give users explicit control over when the rename happens. This prevents unintended changes if the user: +> +> - Selected the wrong member +> - Is still looking up the correct spelling +> - Changes their mind about renaming + +Here's the command implementation: + +```csharp +public async ValueTask RenameMemberAsync( + [FeedParameter(nameof(ModifiedMemberName))] string? modName, + [FeedParameter(nameof(SelectedMember))] string? replaceMember, + CancellationToken ct) +{ + if (string.IsNullOrWhiteSpace(modName)) + return; + + await Members.UpdateAllAsync( + match: item => item == replaceMember, + updater: _ => modName, + ct: ct + ); + + await Members.TrySelectAsync(modName, ct); +} +``` + +Key points: + +- **`UpdateAllAsync(...)`** - Updates items in the `ListState` matching the filter criteria +- **`match: item => item == replaceMember`** - Finds the currently selected member +- **`updater: _ => modName`** - Replaces it with the new name +- **`TrySelectAsync(...)`** - Re-selects the member by its new name + +## Using FeedParameter Attribute + +Notice the `[FeedParameter]` attributes on the method parameters. This powerful feature automatically awaits and binds state values to your method parameters, eliminating manual `await` calls: + +```csharp +[FeedParameter(nameof(ModifiedMemberName))] string? modName, +[FeedParameter(nameof(SelectedMember))] string? replaceMember +``` + +> [!TIP] +> **Benefits:** +> +> - No need to manually `await` the states inside the method +> - Parameters can have different names than the original states (improves readability) +> - Cleaner, more focused method implementation + +**Alternative:** Use `[ImplicitFeedParameter]` at the class level to automatically bind all parameters by matching names exactly with your states: + +```csharp +[ImplicitFeedParameter] +public partial record MainModel +{ + public async ValueTask RenameMemberAsync( + string? ModifiedMemberName, + string? SelectedMember, + CancellationToken ct) + { ... } +} +``` + +With `[ImplicitFeedParameter]` on the class, all method parameters are automatically bound by matching their names exactly to your state property names. This means: + +- `ModifiedMemberName` parameter automatically binds to the `ModifiedMemberName` state +- `SelectedMember` parameter automatically binds to the `SelectedMember` state +- No need for individual `[FeedParameter]` attributes on each parameter +- Parameter names must match state names exactly (case-sensitive) + +## Summary + +This example demonstrates: + +1. Using two-way binding for user input via `IState` +2. Updating list items with `UpdateAllAsync(...)` - only available on `IListState` (not `IListFeed`) +3. Command-based updates for explicit user control +4. Leveraging `[FeedParameter]` for cleaner async state handling + +This pattern ensures data consistency while giving users full control over when changes are committed. diff --git a/docs/articles/en/Mvux-StateManagement/toc.yml b/docs/articles/en/Mvux-StateManagement/toc.yml new file mode 100644 index 0000000..7d3ef78 --- /dev/null +++ b/docs/articles/en/Mvux-StateManagement/toc.yml @@ -0,0 +1,10 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/dotnet/docfx/main/schemas/toc.schema.json +- name: Overview: ListState and ListFeed + uid: DevTKSS.Uno.Mvux-StateManagement.Overview.en + href: HowTo-Binding-ListState-and-ListFeed.md +- name: Binding ListState with Selection + uid: DevTKSS.Uno.Mvux-StateManagement.ListState.Selection.en + href: HowTo-Binding-ListState-Selection.md +- name: Updating ListState Items + uid: DevTKSS.Uno.Mvux-StateManagement.ListState.UpdateItems.en + href: HowTo-Update-ListState-Items.md diff --git a/docs/articles/en/toc.yml b/docs/articles/en/toc.yml index f094fcd..4c2d084 100644 --- a/docs/articles/en/toc.yml +++ b/docs/articles/en/toc.yml @@ -24,3 +24,5 @@ href: HowTo-Adding-New-VM-Class-Record-en.md - name: "Navigation in Uno Apps" href: Navigation/toc.yml +- name: "MVUX State Management" + href: Mvux-StateManagement/toc.yml diff --git a/docs/index.md b/docs/index.md index b8ed7f5..5024330 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,6 +1,7 @@ - +--- +uid: DevTKSS.Uno.SampleApps.Home +--- + - + [!INCLUDE [landing-page](../README.md)] diff --git a/docs/toc.yml b/docs/toc.yml index 91d402c..689b15d 100644 --- a/docs/toc.yml +++ b/docs/toc.yml @@ -1,6 +1,7 @@ # yaml-language-server: $schema=https://raw.githubusercontent.com/dotnet/docfx/main/schemas/toc.schema.json - name: Home href: index.md + uid: DevTKSS.Uno.SampleApps.Home - name: "Documentation & Tutorials" href: articles/ - name: API diff --git a/global.json b/global.json index 17ed574..7f2eb3f 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "msbuild-sdks": { - "Uno.Sdk": "6.3.28" + "Uno.Sdk": "6.4.42" }, "sdk": { "allowPrerelease": false diff --git a/src/.run/UnoHotDesignApp1.run.xml b/src/.run/UnoHotDesignApp1.run.xml deleted file mode 100644 index 37ccf9e..0000000 --- a/src/.run/UnoHotDesignApp1.run.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - diff --git a/src/DevTKSS.Extensions.Uno-ExtensionsOnly.slnf b/src/DevTKSS.Extensions.Uno-ExtensionsOnly.slnf index ec2fde0..4242310 100644 --- a/src/DevTKSS.Extensions.Uno-ExtensionsOnly.slnf +++ b/src/DevTKSS.Extensions.Uno-ExtensionsOnly.slnf @@ -2,7 +2,7 @@ "solution": { "path": "..\\DevTKSS.Uno.SampleApps.slnx", "projects": [ - "DevTKSS.Extensions.Uno\\DevTKSS.Extensions.Uno.csproj" + "DevTKSS.Extensions.Uno.Storage\\DevTKSS.Extensions.Uno.Storage.csproj" ] } } \ No newline at end of file diff --git a/src/DevTKSS.Extensions.Uno.Storage/DevTKSS.Extensions.Uno.Storage.csproj b/src/DevTKSS.Extensions.Uno.Storage/DevTKSS.Extensions.Uno.Storage.csproj index d78068f..2b6bc6e 100644 --- a/src/DevTKSS.Extensions.Uno.Storage/DevTKSS.Extensions.Uno.Storage.csproj +++ b/src/DevTKSS.Extensions.Uno.Storage/DevTKSS.Extensions.Uno.Storage.csproj @@ -1,16 +1,16 @@  - - net9.0 + + net9.0 Library - true - + true + DevTKSS.Extensions.Uno DevTKSS.Extensions.Uno Extensions that are extending Uno Platform functionallities - + diff --git a/src/DevTKSS.Uno.MvuxListApp/App.xaml b/src/DevTKSS.Uno.MvuxListApp/App.xaml new file mode 100644 index 0000000..eea3d9a --- /dev/null +++ b/src/DevTKSS.Uno.MvuxListApp/App.xaml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/DevTKSS.Uno.MvuxListApp/App.xaml.cs b/src/DevTKSS.Uno.MvuxListApp/App.xaml.cs new file mode 100644 index 0000000..9150d50 --- /dev/null +++ b/src/DevTKSS.Uno.MvuxListApp/App.xaml.cs @@ -0,0 +1,100 @@ +using DevTKSS.Uno.MvuxListApp.Models; +using DevTKSS.Uno.MvuxListApp.Presentation; +using Uno.Resizetizer; + +namespace DevTKSS.Uno.MvuxListApp; +public partial class App : Application +{ + /// + /// Initializes the singleton application object. This is the first line of authored code + /// executed, and as such is the logical equivalent of main() or WinMain(). + /// + public App() + { + this.InitializeComponent(); + } + + protected Window? MainWindow { get; private set; } + protected IHost? Host { get; private set; } + + protected async override void OnLaunched(LaunchActivatedEventArgs args) + { + var builder = this.CreateBuilder(args) + // Add navigation support for toolkit controls such as TabBar and NavigationView + .UseToolkitNavigation() + .Configure(host => host +#if DEBUG + // Switch to Development environment when running in DEBUG + .UseEnvironment(Environments.Development) +#endif + .UseLogging(configure: (context, logBuilder) => + { + // Configure log levels for different categories of logging + logBuilder + .SetMinimumLevel( + context.HostingEnvironment.IsDevelopment() ? + LogLevel.Information : + LogLevel.Warning) + + // Default filters for core Uno Platform namespaces + .CoreLogLevel(LogLevel.Warning); + + // Uno Platform namespace filter groups + // Uncomment individual methods to see more detailed logging + //// Generic Xaml events + //logBuilder.XamlLogLevel(LogLevel.Debug); + //// Layout specific messages + //logBuilder.XamlLayoutLogLevel(LogLevel.Debug); + //// Storage messages + //logBuilder.StorageLogLevel(LogLevel.Debug); + //// Binding related messages + //logBuilder.XamlBindingLogLevel(LogLevel.Debug); + //// Binder memory references tracking + //logBuilder.BinderMemoryReferenceLogLevel(LogLevel.Debug); + //// DevServer and HotReload related + //logBuilder.HotReloadCoreLogLevel(LogLevel.Information); + //// Debug JS interop + //logBuilder.WebAssemblyLogLevel(LogLevel.Debug); + + }, enableUnoLogging: true) + .UseConfiguration(configure: configBuilder => + configBuilder + .EmbeddedSource() + .Section() + ) + .ConfigureServices((context, services) => + { + // TODO: Register your services + //services.AddSingleton(); + }) + .UseNavigation(ReactiveViewModelMappings.ViewModelMappings, RegisterRoutes) + ); + MainWindow = builder.Window; + +#if DEBUG + MainWindow.UseStudio(); +#endif + MainWindow.SetWindowIcon(); + + Host = await builder.NavigateAsync(); + } + + private static void RegisterRoutes(IViewRegistry views, IRouteRegistry routes) + { + views.Register( + new ViewMap(ViewModel: typeof(ShellModel)), + new ViewMap(), + new DataViewMap() + ); + + routes.Register( + new RouteMap("", View: views.FindByViewModel(), + Nested: + [ + new ("Main", View: views.FindByViewModel(), IsDefault:true), + new ("Second", View: views.FindByViewModel()), + ] + ) + ); + } +} diff --git a/src/DevTKSS.Uno.MvuxListApp/Assets/Icons/icon.svg b/src/DevTKSS.Uno.MvuxListApp/Assets/Icons/icon.svg new file mode 100644 index 0000000..a15af53 --- /dev/null +++ b/src/DevTKSS.Uno.MvuxListApp/Assets/Icons/icon.svg @@ -0,0 +1,42 @@ + + + + + + diff --git a/src/DevTKSS.Uno.MvuxListApp/Assets/Icons/icon_foreground.svg b/src/DevTKSS.Uno.MvuxListApp/Assets/Icons/icon_foreground.svg new file mode 100644 index 0000000..8ffc41a --- /dev/null +++ b/src/DevTKSS.Uno.MvuxListApp/Assets/Icons/icon_foreground.svg @@ -0,0 +1,137 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/DevTKSS.Uno.MvuxListApp/Assets/SharedAssets.md b/src/DevTKSS.Uno.MvuxListApp/Assets/SharedAssets.md new file mode 100644 index 0000000..b1cc4e7 --- /dev/null +++ b/src/DevTKSS.Uno.MvuxListApp/Assets/SharedAssets.md @@ -0,0 +1,32 @@ +# Shared Assets + +See documentation about assets here: https://github.com/unoplatform/uno/blob/master/doc/articles/features/working-with-assets.md + +## Here is a cheat sheet + +1. Add the image file to the `Assets` directory of a shared project. +2. Set the build action to `Content`. +3. (Recommended) Provide an asset for various scales/dpi + +### Examples + +```text +\Assets\Images\logo.scale-100.png +\Assets\Images\logo.scale-200.png +\Assets\Images\logo.scale-400.png + +\Assets\Images\scale-100\logo.png +\Assets\Images\scale-200\logo.png +\Assets\Images\scale-400\logo.png +``` + +### Table of scales + +| Scale | WinUI | iOS | Android | +|-------|:-----------:|:---------------:|:-------:| +| `100` | scale-100 | @1x | mdpi | +| `125` | scale-125 | N/A | N/A | +| `150` | scale-150 | N/A | hdpi | +| `200` | scale-200 | @2x | xhdpi | +| `300` | scale-300 | @3x | xxhdpi | +| `400` | scale-400 | N/A | xxxhdpi | diff --git a/src/DevTKSS.Uno.MvuxListApp/Assets/Splash/splash_screen.svg b/src/DevTKSS.Uno.MvuxListApp/Assets/Splash/splash_screen.svg new file mode 100644 index 0000000..8ffc41a --- /dev/null +++ b/src/DevTKSS.Uno.MvuxListApp/Assets/Splash/splash_screen.svg @@ -0,0 +1,137 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/DevTKSS.Uno.MvuxListApp/DevTKSS.Uno.MvuxListApp.csproj b/src/DevTKSS.Uno.MvuxListApp/DevTKSS.Uno.MvuxListApp.csproj new file mode 100644 index 0000000..f5d3d4d --- /dev/null +++ b/src/DevTKSS.Uno.MvuxListApp/DevTKSS.Uno.MvuxListApp.csproj @@ -0,0 +1,36 @@ + + + net9.0-desktop + + Exe + true + + + Mvux List App + + com.DevTKSS.MvuxListApp + + 1.0 + 1 + + Sonja + + MvuxListApp powered by Uno Platform. + + + + Material; + Hosting; + Toolkit; + Logging; + MVUX; + Configuration; + Navigation; + SkiaRenderer; + + + + diff --git a/src/DevTKSS.Uno.MvuxListApp/GlobalUsings.cs b/src/DevTKSS.Uno.MvuxListApp/GlobalUsings.cs new file mode 100644 index 0000000..9739179 --- /dev/null +++ b/src/DevTKSS.Uno.MvuxListApp/GlobalUsings.cs @@ -0,0 +1,9 @@ +global using System.Collections.Immutable; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Hosting; +global using Microsoft.Extensions.Logging; +global using Microsoft.Extensions.Options; +global using DevTKSS.Uno.MvuxListApp.Models; +global using DevTKSS.Uno.MvuxListApp.Presentation; +global using ApplicationExecutionState = Windows.ApplicationModel.Activation.ApplicationExecutionState; +[assembly: Uno.Extensions.Reactive.Config.BindableGenerationTool(3)] diff --git a/src/DevTKSS.Uno.MvuxListApp/Models/AppConfig.cs b/src/DevTKSS.Uno.MvuxListApp/Models/AppConfig.cs new file mode 100644 index 0000000..f60cdbe --- /dev/null +++ b/src/DevTKSS.Uno.MvuxListApp/Models/AppConfig.cs @@ -0,0 +1,6 @@ +namespace DevTKSS.Uno.MvuxListApp.Models; + +public record AppConfig +{ + public string? Environment { get; init; } +} diff --git a/src/DevTKSS.Uno.MvuxListApp/Models/Entity.cs b/src/DevTKSS.Uno.MvuxListApp/Models/Entity.cs new file mode 100644 index 0000000..7baa304 --- /dev/null +++ b/src/DevTKSS.Uno.MvuxListApp/Models/Entity.cs @@ -0,0 +1,3 @@ +namespace DevTKSS.Uno.MvuxListApp.Models; + +public record Entity(string Name); diff --git a/src/DevTKSS.Uno.MvuxListApp/Package.appxmanifest b/src/DevTKSS.Uno.MvuxListApp/Package.appxmanifest new file mode 100644 index 0000000..9ef3814 --- /dev/null +++ b/src/DevTKSS.Uno.MvuxListApp/Package.appxmanifest @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/DevTKSS.Uno.MvuxListApp/Platforms/Desktop/Program.cs b/src/DevTKSS.Uno.MvuxListApp/Platforms/Desktop/Program.cs new file mode 100644 index 0000000..3f1201a --- /dev/null +++ b/src/DevTKSS.Uno.MvuxListApp/Platforms/Desktop/Program.cs @@ -0,0 +1,20 @@ +using Uno.UI.Hosting; + +namespace DevTKSS.Uno.MvuxListApp.Platforms.Desktop; +internal class Program +{ + [STAThread] + public static void Main(string[] args) + { + + var host = UnoPlatformHostBuilder.Create() + .App(() => new App()) + .UseX11() + .UseLinuxFrameBuffer() + .UseMacOS() + .UseWin32() + .Build(); + + host.Run(); + } +} diff --git a/src/DevTKSS.Uno.MvuxListApp/Presentation/MainModel.cs b/src/DevTKSS.Uno.MvuxListApp/Presentation/MainModel.cs new file mode 100644 index 0000000..ce5e30c --- /dev/null +++ b/src/DevTKSS.Uno.MvuxListApp/Presentation/MainModel.cs @@ -0,0 +1,68 @@ +using Uno.Extensions.Reactive.Commands; + +namespace DevTKSS.Uno.MvuxListApp.Presentation; + +public partial record MainModel +{ + private readonly ILogger _logger; + private readonly INavigator _navigator; + private readonly IRouteNotifier _routeNotifier; + public MainModel( + IOptions appInfo, + INavigator navigator, + IRouteNotifier routeNotifier, + ILogger logger) + { + _navigator = navigator; + _routeNotifier = routeNotifier; + _routeNotifier.RouteChanged += Main_OnRouteChanged; + _logger = logger; + } + + public IState Title => State.Value(this, () => _navigator.Route?.ToString() ?? string.Empty); + private async void Main_OnRouteChanged(object? sender, RouteChangedEventArgs e) + { + await Title.SetAsync(e.Navigator?.Route?.ToString()); + } + + #region MembersView-Value + private readonly IImmutableList _listMembers = ImmutableList.Create( + [ + "Hans", + "Lisa", + "Anke", + "Tom" + ]); + + private async ValueTask> GetMembersAsync(CancellationToken ct) + => _listMembers; + + public IListState Members => ListState.Async(this, GetMembersAsync) + .Selection(SelectedMember); + + public IState SelectedMember => State.Value(this, () => string.Empty); + #endregion + #region MembersView-Update + public IState ModifiedMemberName => State.Empty(this); + + public async ValueTask RenameMemberAsync( + [FeedParameter(nameof(ModifiedMemberName))] string? modName, + [FeedParameter(nameof(SelectedMember))] string? replaceMember, + CancellationToken ct) + { + + _logger.LogInformation("Modified MemberName ist: {modifiedName}", modName); + _logger.LogInformation("SelectedMember ist: {selectedMember}", replaceMember); + if (string.IsNullOrWhiteSpace(modName)) + return; + + await Members.UpdateAllAsync( + match: item => item == replaceMember, + updater: _ => modName, + ct: ct + ); + + // await Members.TrySelectAsync(modName, ct); + } + #endregion +} diff --git a/src/DevTKSS.Uno.MvuxListApp/Presentation/MainPage.xaml b/src/DevTKSS.Uno.MvuxListApp/Presentation/MainPage.xaml new file mode 100644 index 0000000..d384638 --- /dev/null +++ b/src/DevTKSS.Uno.MvuxListApp/Presentation/MainPage.xaml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + +