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..90efd7c --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,453 @@ +# Uno Platform MVUX Sample Apps - AI Coding Guide + +## Project Context + +This is a (primary) **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**. + +### 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 +``` + +### UnoFeatures in .csproj +Apps declare capabilities via ``: +- Material: Theming, Relyable for `ColorPaletteOverride` +- MVUX +- MVVM (prefer not using this at the same time as MVUX) +- Navigation +- Hosting (Hosting + DI) +- Configuration - App settings with `IOptions` and Configuration binding, is supporting `IOptionsMonitor` but using `IConfigBuilder.Section()` does set `reloadOnChange: false`! Do not manually change, the user will do if needed! +- Localization +- Serialization +- Storage +- Toolkit - provides powerful controls and binding helpers e.g.: + - `AncestorBinding`: to access parent DataContexts or other elements in the visual tree. + - `ItemsControlBinding`: advanced binding scenarios for ItemsControls. + - `CommandExtensions`: enhanced command binding capabilities, especially useful for MVUX. Allowes binding Commands to async Task/ValueTask methods in the Model on controls which do not support Command binding out of the box. +- ThemeService +- SkiaRenderer - Performanct graphic rendering. **ONLY** non-supported platform is Windows! This will **always** use Native Rendering! + +## Strict Constraints you MUST comply with +The following are ABSOLUTE RULES you MUST always follow when editing code in this repository, except from explicitly requested otherwise! + +- **Do not manually add implicit package references** - they're managed by Uno.Sdk. +- We are using Global Usings via `GlobalUsings.cs` in each project. If you need additional usings, add them there and not in individual files. +- Using MVUX Architecture (the `csproj` will contain `MVUX`) Always lookup the section in this Instructions! +- Using Uno Extensions Navigation (the `csproj` will contain `Navigation`): + - Ensure all Views and Models are properly registered in `App.xaml.cs` + +**Facts and Strict Constraints in Context of `this.InitializeComponent()`:** +- You MUST NOT remove this from anywhere you find this line! Removing it will break the app and prevent any XAML from loading. +- No matter what errors or warnings you see, this line is always *required to successfully initialize* the XAML components of the View/Page/App and in absolutely **NO** Situation the issue source! +- NEVER even consider deleting or comment out this line! +- Never Remove or Replace any lines with Placeholders, comments (e.g. `// ...existing code...`) or Sample content! +- Especially the `net*.0-windows*` TargetFramework is known to trigger false warnings/errors about this line would be unknown or missing using directives. In case of the Project is brand new, this will potentially be present in 99% of the cases and you MUST ignore those warnings/errors! +- Even if you just remove the "unnesseary" `this.` prefix, this is NOT wanted! +- You MUST always keep this line as-is! + +What you will check and fix instead: +- There are no typos or mismatches in file/class names or XAML root element names. +- The `*Page.xaml.cs` (Codebehind) class: + - Has the correct `partial` modifier + - Matches the `x:Class` attribute in the corresponding `*Page.xaml` root element + - The constructor calls `this.InitializeComponent();` before you may access any fields or properties from the to-be-connected xaml! Even prefer having this the first call in the constructor body! + - If using MVUX: + - The Page **MUST NOT** have *any* direct references (e.g. fields, properties, events... whatever!) to the Model in codebehind! + - Always use `{Binding}` to the MVUX-generated ViewModel *in XAML* instead. Properties are equally named as in the Model's stateful properties. Commands bind to async Task/ValueTask method names. + - Except from special User Interaction (Keyboard events, but NOT simple Click/Tap events), codebehind is rarely needed! MVUX is designed to avoid codebehind usage via powerful binding and command patterns. + - In 90% of cases, achieve what you want via XAML Bindings only. Use Uno.Toolkit controls and binding helpers. + - **Before** accessing `DataContext` in codebehind: Ask the Developer (via VS Copilot Chat) for agreement and provide a reasoned explanation. + +## MVUX Architecture + +**Known false warnings/errors you MUST ignore:** + +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/`. + +**Conventions and Patterns:** + +- **All "Model"'s are defined as `partial record` types** named `*Model` (e.g., `DashboardModel`, `MainModel`). +- Models are not the ViewModels themselves per definition. The bindable **ViewModel is auto-generated** from each `*Model` by the MVUX source generators at compile time. +- The User you speak to in chat or documentation may refer to "ViewModel". This is impling the Model when working with MVUX, NOT the actual Source Generated ViewModel! You may only edit `*Model` code. +- You should never edit or depend on the generated files directly. +- Models use constructor injection for services (DI via Uno.Extensions.DependencyInjection -> `Hosting`). Pages NOT! +- Models define **stateful properties** using `IState`, `IListState`, `IFeed`, and `IListFeed` from `Uno.Extensions.Reactive`. + +**Strict Constraints you MUST ALWAYS adhere to:** +- **Fields and Properties in Models:** + - Models might have fields for injected services, `IOptions` provided Configuration *objects* (**DO NOT** set the field as `IOptions` if it is not a `IOptionsMonitor`) and Properties that are not expected to be updated/reactive. + - Properties that the UI binds to and are expected to be changing are exposed as `IFeed`, `IListFeed`, `IState`, or `IListState`. + - You **MUST NOT** use `INotifyPropertyChanged` or Events for those Statefull properties! -> MVUX generates reactive bindings automatically. + - Instead of using Events or `INotifyPropertyChanged` triggered by a Property Change in a MVVM app, you will migrate them to `IState` or `IListState`. + - If the Property is expected to be fed from external data sources / services, but your code is not expected or allowed to change the data, you will use `IFeed` or `IListFeed`. + - The `` generic type parameter of a Statefull Property like those representing the data type (e.g., `string`, `int`, `MyCustomType`, etc.) is **NEVER** nullable! + - This is **WRONG!!!:** `IState`, `IListState` + - If you would expect the value eventually beeing null, or see compiler `possible null reference` warnings, use the `Option` pattern instead (e.g., `Option.Some(value)` / `Option.SomeOrDefault(value)`). + - If you connect Callback Handlers of e.g. `IState` or `IListState` via the MVUX **`ForEach` operator**, you may still recieve `null` value in the Handling Method, so before you use `Update*`-Methods on those Statefull properties, always check for `null` and rather not update then update with null values! +- **UI Layer Constraints:** + - Where the UI needs to bind to these use `{Binding}` in XAML. + - If your bound UI control has an `ItemsSource` (e.g. `ListView` or `ItemsRepeater`), you **MUST** use `IListFeed` or `IListState`, **NOT** `ObservableCollection` or `List`! + - For Selection support, see the "Selection with IListState/IListFeed" section below in Feed vs State. + +#### 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) + - Initialize: + - Async: `IListFeed People => ListFeed.Async(_service.GetPeopleAsync);` + - Async Enumerable: `IListFeed People => ListFeed.AsyncEnumerable(_service.GetPeopleAsyncEnumerable);` - make sure the Service Method uses `[AsyncEnumerableCancellationToken]CancellationToken`! + - Value: **NOT SUPPORTED** - Feeds are always async! + - Empty list: `IListFeed Items => ListFeed.Empty(this);` + - **IMPORTANT:** The `.Empty()` initializer is only available at `ListFeed.Empty(this)` but is not at a non-typed `ListFeed.Empty(...)`! + - Request refresh with: + - `public IListFeed People => ListFeed.Async(_service.GetPeopleAsync);` -> call `await People.TryRefreshAsync(cancellationToken);` to request a refresh + - `public IFeed Title => Feed.Async(async () => "Hello MVUX");` -> call `await Title.TryRefreshAsync(cancellationToken);` to request a refresh + - `IListFeed` supports `.Selection(...)`, but **NOT** supports `.ForEach(...)`! +- **States (`IState`, `IListState`)**: Stateful feeds with update capabilities + - Replay current value + allow modifications + - Use for data you usually would use Event based / INotifyPropertyChanged on. Bind from Xaml UI via `{Binding MyState, Mode=OneWay}` or with `{Binding MyState, Mode=TwoWay}`. + - Initialize: + - Value: `IState Counter => State.Value(this, () => 0);` -> fills this with the Value `0` initially. Internally MVUX will set this: `Option.Some(0)` + - Empty: `IListState Items => ListState.Empty(this);` -> fills this with `Option.None()` initially. + - **INVALID:** The `.Empty()` initializer is only available at `State.Empty(this)` or `ListState.Empty(this)` but is not at a non-typed `State.Empty(...)` / `ListState.Empty(...)`! + - Selection of Items in a `IListState` *OR* `IListFeed`: + - single or multi selection possible - make sure the UI element, e.g. ListView has SelectionMode set appropriately! + - *Transfer* the selection to another `IState` via: + - `.Selection(...)` + - Example: + ```csharp + public IListState People => ListState.Empty(this) + .Selection(OtherState); + public IState OtherState => State.Empty(this); + ``` + Expect `OtherState` to be updated when: + - You call from Model-Side `await People.TrySelectAsync(person)` + - The User selects an item in the bound UI control. + You **MUST NOT** manually update `OtherState` either trought Model or Command + CommandParameter via UI except from explicitly Requested! Expect MVUX to handle updating and triggering `People.Selection(...)` automatically! + - `MyListState.ClearSelection(...)` allows to clear the selection programmatically. + - `.ForEach(...)` to react to selection changes (**NOT** for `IListFeed`/`IFeed`, only for `IListState` / `IState`): + ```csharp + public IListState People => ListState.Empty(this) + .Selection(SelectedPerson) + .ForEach(OnPersonSelected); + public IState SelectedPerson => State.Empty(this); + + private Task OnPersonSelected(Person? person, CancellationToken ct) + { + // React to selection change here + } + ``` + - Update state values: + - `State`: (multiple overloads available! some of them are listed below) + - `await CounterState.UpdateAsync(updater: _ => v + 1, ct);` (drop old value, set new) + - `await PersonState.UpdateAsync(updater: p => p with { Name = "New Name" }, ct);` (with-record non-destructive syntax) + - `ListState` + - `await Members.UpdateAllAsync(match: item => item == replaceMember, updater: _ => modifiedName, ct: ct);` + - `await PersonListState.UpdateItemAsync(match: p => p.Id == updatedPerson.Id, updater: p => updatedPerson, ct);` + - **IMPORTANT:** `UpdateItemAsync(...)` is only available if the `T` type is a complex type like a `record` or `class`, which is implementing the `IKeyEquatable` or `IEquatable` interface! It is **NOT** available for primitive types like `int`, `string`, etc.! + +**Attributes coming from `Uno.Extensions.Reactive`:** +- `FeedParameterAttribute`: allowes Methods in the Model to get Data from a Statefull property (Feed* or State*) + - Example usage: `public async ValueTask RenameMemberAsync([FeedParameter(nameof(ModifiedMemberName))] string modName, CancellationToken ct)` - will provide the current value of the `IState` Property `ModifiedMemberName` when the Method is called via Command from the UI, without needing to pass it as CommandParameter! + +### Uno.Extensions.Navigation Architecture + +**Identify the use of the Uno.Extensions.Navigation system with:** +- The `csproj` ``-Entry: `...Navigation...` +- The presence of navigation registration code in `App.xaml.cs`: + 1. MVUX App: `.UseNavigation(ReactiveViewModelMappings.ViewModelMappings, RegisterRoutes)` / MVVM App: `.UseNavigation(RegisterRoutes)` + 2. `private static void RegisterRoutes(IViewRegistry views, IRouteRegistry routes)` method defining Views and Routes + 3. Navigation startup at the end of `OnLaunched` with `Host = await builder.NavigateAsync();` + 3. Optional, when using Toolkit Navigation Controls: `.UseToolkitNavigation()` + +**Conventions and Patterns:** +- **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); +} +``` + +**IMPORTANT:** Do not attempt to use `Shell` like a regular Page or View! It is the main navigation container. The sample above shows the default setup, where you are NOT allowed to modify anything in `Shell.xaml`, `Shell.xaml.cs` or the `ShellModel.cs`! All navigation happens in the nested routes below Shell. + +### Configuration Pattern +- Load sections from `appsettings.json` using `.EmbeddedSource().Section()` +- Keyed services for multiple code sample collections: `.AddKeyedSingletonCodeService("SampleName")` + +### XAML Binding and Views + +#### FeedView Control +- **FeedView** wraps async data with loading/error states + - `Source="{Binding Feed}"` binds to IFeed/IState + - `ValueTemplate` for successful data display + - `ErrorTemplate` and `ProgressTemplate` for loading/error states + - `{Binding Data}` accesses feed value in template + - `{Binding Refresh}` command triggers feed refresh + - FeedView `State` property is auto-set as DataContext for templates + +#### Binding Best Practices +- The MVUX ViewModel is generated and provided at runtime as the `DataContext` +- **Prefer `{Binding}`** over `{x:Bind}` for MVUX feeds (runtime-reactive); `{x:Bind}` commonly leads to NullReferenceExceptions +- Access parent model in templates: `{Binding Parent.PropertyName}` +- Refresh commands: `{utu:AncestorBinding AncestorType=mvux:FeedView, Path=Refresh}` +- Use `{utu:AncestorBinding}` from Uno.Toolkit for parent access +- Centralize DataTemplates in ResourceDictionaries (see `Styles/GalleryTemplates.xaml`) +- Feeds are **awaitable** in code: `var data = await this.MyFeed;` + +#### Views and Code-Behind (Critical Constraints) +- **Page constructors must have NO arguments** when using `Navigation` with Uno.Extensions. The navigation and DI system only instantiates Pages via the default parameterless constructor. Adding arguments (e.g., `MainPage(MainViewModel vm)` or `MainPage(IService svc)`) will cause build to fail. +- Do not inject or expect the MVUX-generated `*ViewModel` in a Page constructor +- Do not rely on `DataContextChanged` to grab the ViewModel early. The `INavigator` sets `DataContext` after the view initializes; accessing 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/IListFeed (ListView/GridView) +- When binding a `ListView` or `GridView` to an `IListState` or `IListFeed` 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 working. +- Correct pattern: + - Model: + ```csharp + public partial record PeopleModel(IPeopleService Service) + { + public IListFeed People => ListFeed.Async(Service.GetPeopleAsync) + .Selection(SelectedPerson); + public IState SelectedPerson => State.Empty(this); + } + ``` + - XAML: + ```xml + + + + + + ``` + - **Warning:** Avoid setting `ItemClick`, `IsItemClickEnabled`, `ItemClickCommand`, or `SelectionChanged` on the list control when using `.Selection(...)` + +### 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 + ``` + +#### 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/WinUI3) +- Targets: iOS/iPadOS, Android, macOS, Desktop, (Windows), Linux, WebAssembly +- Free C# and XAML Hot Reload support 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 552f79a..42a8e67 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,7 +5,7 @@ "version": "0.2.0", "configurations": [ { - "name": "Uno Platform Desktop", + "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 @@ -47,6 +47,26 @@ // 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/net10.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 }, { "name": "Uno Platform Desktop Debug (SimpleMemberSelectionApp)", diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 8c13cc9..e5da2a4 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -53,7 +53,34 @@ ], "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=net10.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=net10.0-desktop", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { "label": "build-desktop-SimpleMemberSelectionApp", "command": "dotnet", "type": "process", diff --git a/DevTKSS.Uno.SampleApps.slnx b/DevTKSS.Uno.SampleApps.slnx index 3179580..28a6f9e 100644 --- a/DevTKSS.Uno.SampleApps.slnx +++ b/DevTKSS.Uno.SampleApps.slnx @@ -26,9 +26,14 @@ + + + - + + + diff --git a/README.md b/README.md index c3e006a..c5690cc 100644 --- a/README.md +++ b/README.md @@ -94,9 +94,19 @@ ![Image of final Simple Member Selection App](https://raw.githubusercontent.com/DevTKSS/DevTKSS.Uno.SampleApps/master/docs/articles/.attachments/SimpleMemberSelectionApp.png) +**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) +- **[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 + | | | | --- | --- | -| Die Simple Member Selection Anwendung demonstriert die Auswahl und Anzeige von Mitgliedernamen in einer `ListView`, gebunden an einen `ListState` im Modell mittels MVUX. | The Simple Member Selection application demonstrates selection and display of member names in a `ListView` bound to a `ListState` in the Model using MVUX. | +| **Die `SimpleMemberSelection` Beispiel Anwendung zeigt, wie man:**
- Einer Sammlungs/Listen-Eigenschaft (`ListState`), bestehend aus ganz bewusst *einfachen* Text-Elementen, im MVUX-Model (Daten/Logik-Layer) definieren kann
- Die komplette Sammlung in einem `ListView`-Steuerelement innerhalb der Benutzeroberfläche (UI / View) einbinden kann, unter Verwendung der `ListView.ItemSource`-Eigenschaft.
- Das aktuell selektierte / ausgewählte Mitglieds, bzw. dessen Namen, nach Nutzer-Auswahl (View -> MVUX Model) mittels des `ListState.Selection(...)`-Operators *automatisch* in eine `State`-Eigenschaft transferieren lässt.
- Das ausgewählte Element bzw. Mitglied aus dem `State` wiederum im UI/View-Layer mittels `TextBox`/`TextBlock`-Steuerelement anzeigen lassen kann. | The `SimpleMemberSelection` Sample Application demonstrates how to setup selection between the Userinterface and the MVUX Model, by:
- Defining a `ListState`-Property in the MVUX Model
- Binding it to a `ListView`-Control to display the collection of member names in the Page (View-Layer/UI)
- Using the `ListState.Selection(...)` Operator to get the currently selected Item automatically transfered to another `State`-Property in the Model by the MVUX Engine
- Displaying the currently Selected Member in a `TextBlock`/`TextBox`-Control binding to the `State`-Property of the MVUX-Model. | #### Tutorial-Inhalte / Tutorial Content @@ -140,8 +150,8 @@ Falls du gerade erst mit **C#** anfangen möchtest zu lernen, empfehle ich die K | .NET Guide (kostenlos) | Produktionsreife .NET Anwendungen – Umgang mit professionellen .NET Anwendungen | [Zum Kurs](https://codingmitjannick.de/s/coding-mit-jannick/leitfaden) | | C# Bootcamp 2024 | Vom Anfänger bis zum Profi – Umfassendes Trainingsprogramm | [Zum Kurs](https://codingmitjannick.de/s/coding-mit-jannick/csharp-bootcamp) | | C# Expertise | Design Patterns und Clean Code – Fortgeschrittene Konzepte für professionelle Entwicklung | [Zum Kurs](https://codingmitjannick.de/s/coding-mit-jannick/csharp-expertise) | -| Alle Kurse | Komplette Kursübersicht | [Alle Kurse ansehen](https://codingmitjannick.de/s/coding-mit-jannick/kurse) | + > [!NOTE] > **Transparenzhinweis:** Ich habe selbst an diesen Kursen teilgenommen und empfehle sie aus Überzeugung. Ich erhalte für diese Weiterempfehlung kein Geld oder andere Vergütung. -> Die Preise und Verfügbarkeit der Kurse können sich ändern. Bitte überprüfe die Kursseiten für die aktuellsten Informationen. +> Die Verfügbarkeit der Kurse können sich ändern. Bitte überprüfe die [Kursseiten](https://codingmitjannick.de/s/coding-mit-jannick/kurse) immer für aktuellste Informationen. 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..9dd8004 --- /dev/null +++ b/docs/articles/de/Mvux-StateManagement/HowTo-Binding-ListState-Selection-de.md @@ -0,0 +1,141 @@ +--- +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 + +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..d652851 --- /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 0c2b09d..c809689 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..8a27808 --- /dev/null +++ b/docs/articles/en/Mvux-StateManagement/HowTo-Binding-ListState-Selection-en.md @@ -0,0 +1,140 @@ +--- +uid: DevTKSS.Uno.MvuxStateManagement.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..b83829f --- /dev/null +++ b/src/DevTKSS.Uno.MvuxListApp/DevTKSS.Uno.MvuxListApp.csproj @@ -0,0 +1,36 @@ + + + net10.0-desktop + + Exe + true + + + Mvux List App + + com.DevTKSS.MvuxListApp + + 1.0 + 1 + + Sonja + + MvuxListApp powered by Uno Platform. + 7.1.0-dev.62 + + + 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..9d2b588 --- /dev/null +++ b/src/DevTKSS.Uno.MvuxListApp/Presentation/MainModel.cs @@ -0,0 +1,99 @@ +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) + .ForEach(async (member, ct) => + { + _logger.LogInformation("Selected Member changed to: {member}", member); + await ValueTask.CompletedTask; + }); + #endregion + #region MembersView-Update + public IState ModifiedMemberName => State.Empty(this); + public async ValueTask RenameOtherMemberAsync( + CancellationToken ct) + { + + var selectedMember = await SelectedMember; + _logger.LogInformation("Selected Member in RenameOtherMemberAsync: {selectedMember}", selectedMember); + if (selectedMember is not string replaceMember) + { + if(_logger.IsEnabled(LogLevel.Warning)) + _logger.LogWarning("Selected Member is not a string. Is null? '{selectedMember}'", selectedMember is null); + + return; + } + var modName = await ModifiedMemberName; + if (modName is null) + { + if (_logger.IsEnabled(LogLevel.Warning)) + _logger.LogWarning("Modified Member Name is null."); + return; + } + + await Members.UpdateAllAsync( + match: item => item == replaceMember, + updater: _ => modName, + ct: ct + ); + } + 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) || string.IsNullOrWhiteSpace(replaceMember)) + return; + + await Members.UpdateAllAsync( + match: item => item == replaceMember, + updater: oldName => 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..ae5b7fc --- /dev/null +++ b/src/DevTKSS.Uno.MvuxListApp/Presentation/MainPage.xaml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + +