Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .github/rulesets/protect-main.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "Protect main",
"target": "branch",
"enforcement": "active",
"bypass_actors": [],
"conditions": {
"ref_name": { "include": ["refs/heads/main"], "exclude": [] }
},
"rules": [
{ "type": "deletion" },
{ "type": "non_fast_forward" },
{
"type": "pull_request",
"parameters": {
"required_approving_review_count": 0,
"dismiss_stale_reviews_on_push": false,
"require_code_owner_review": false,
"require_last_push_approval": false,
"required_review_thread_resolution": false,
"allowed_merge_methods": ["merge", "squash"]
}
}
]
}
34 changes: 34 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Pull request validation: builds Release. Publishes nothing.
# There is no unit test project; MVVMSample and MauiSample are manual samples.
name: CI

on:
pull_request:
branches: [main, development]
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v7

- uses: actions/setup-dotnet@v6
with:
dotnet-version: '10.x'

- name: Restore workloads
run: dotnet workload restore System.Mvvm.slnx

- name: Restore
run: dotnet restore System.Mvvm.slnx -p:Configuration=Release

- name: Build
run: dotnet build System.Mvvm.slnx --configuration Release --no-restore
97 changes: 97 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Release: on every push to main, builds Release and pushes the DSoft.*.nupkg
# packages to nuget.org as 3.6.yyMM.<run number>, then tags the commit and
# creates a GitHub release with the packages attached.
# RELEASE_SUFFIX is empty for a stable version; set it to a suffix such as
# '-prerelease' to publish a prerelease.
# Signs in to nuget.org with Trusted Publishing (OIDC), so no API key is stored.
name: Release

on:
push:
branches: [main]
paths-ignore:
- '**.md'
- '.github/workflows/**'
workflow_dispatch:

permissions:
contents: read

concurrency:
group: release
cancel-in-progress: false

env:
RELEASE_SUFFIX: ''

jobs:
release:
runs-on: windows-latest
environment: nuget
permissions:
contents: write # GitHub release and tag
id-token: write # Trusted Publishing
steps:
- uses: actions/checkout@v7

- uses: actions/setup-dotnet@v6
with:
dotnet-version: '10.x'

- name: Version
id: version
shell: bash
run: |
version="3.6.$(date -u +%y%m).${{ github.run_number }}"
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "package=$version$RELEASE_SUFFIX" >> "$GITHUB_OUTPUT"
echo "Version: $version$RELEASE_SUFFIX"

- name: Restore workloads
run: dotnet workload restore System.Mvvm.slnx

- name: Restore
run: dotnet restore System.Mvvm.slnx -p:Configuration=Release

- name: Build
run: >
dotnet build System.Mvvm.slnx --configuration Release --no-restore
/p:Version=${{ steps.version.outputs.package }}
/p:AssemblyVersion=${{ steps.version.outputs.version }}
/p:FileVersion=${{ steps.version.outputs.version }}

- name: Collect packages
shell: bash
run: |
mkdir -p artifacts
find . -path '*/bin/Release/*' -name 'DSoft.*.nupkg' -exec cp {} artifacts/ \;
ls artifacts

- uses: actions/upload-artifact@v7
with:
name: drop
path: artifacts/*.nupkg

- name: NuGet login
id: nuget
uses: NuGet/login@v1
with:
user: ${{ secrets.NUGET_USER }}

- name: Push to nuget.org
shell: bash
run: |
for package in artifacts/*.nupkg; do
dotnet nuget push "$package" --api-key "${{ steps.nuget.outputs.NUGET_API_KEY }}" --source https://api.nuget.org/v3/index.json --skip-duplicate
done

- name: GitHub release
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release create "v${{ steps.version.outputs.package }}" artifacts/*.nupkg \
--target "${{ github.sha }}" \
--title "${{ steps.version.outputs.package }}" \
--generate-notes \
${RELEASE_SUFFIX:+--prerelease}
78 changes: 78 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# AGENTS.md

This file provides guidance to AI coding agents (Claude Code, Codex, Copilot and others) when working with code in this repository.

## Branching

- `development` is the working branch. **Every feature and bug fix goes to `development`**: branch from it and open pull requests into it. Never commit or open a pull request directly against `main`.
- `main` is the release branch. Every push to `main` runs `release.yml`, which publishes the packages to nuget.org and creates a GitHub release, so only merge `development` into `main` when a release is intended, and only when asked to release.

## Overview

DSoft.System.Mvvm is a cross-platform MVVM class library for .NET, published as a family of NuGet packages (`DSoft.System.Mvvm.*`). Core MVVM lives in one base library; UI dialog/threading support is layered on top per-platform (WPF, WinUI/UWP, MAUI, Xamarin.Forms). Built-in DI is provided via a `Microsoft.Extensions.Hosting` wrapper.

## Build & Test

The solution uses the newer `.slnx` format (the old `.sln` was removed):

```powershell
dotnet restore System.Mvvm.slnx
dotnet build System.Mvvm.slnx -c Release
```

- No unit test project exists; `MVVMSample` (WPF) and `MauiSample` are the manual runnable samples.
- Multi-targeted builds (especially MAUI/WinUI) require workloads: `dotnet workload restore`.
- CI is GitHub Actions (`.github/workflows/`): `ci.yml` builds Release for pull requests into `main` or `development` and publishes nothing; `release.yml` runs on every push to `main` (changes only to Markdown or workflow files are skipped; run it by hand to test a workflow change), builds Release as `3.6.<yyMM>.<run number>` plus `RELEASE_SUFFIX` (empty for a stable version, set it to `-prerelease` in the workflow to publish a prerelease), uploads the packages as the `drop` artifact, pushes them to nuget.org with Trusted Publishing (OIDC, `NUGET_USER` secret, `nuget` environment), then tags the commit `v<version>` and creates a GitHub release with the packages attached.
- The core library targets `netstandard2.0;net10.0`. Platform UI libs add windows-specific TFMs (e.g. `net462;net10.0-windows7.0;net10.0-windows10.0.18362.0;...`).
- Assemblies are strong-named/signed (`DSoft.snk`); `Release` builds enable SourceLink and pack symbols. `GeneratePackageOnBuild` is on, so building produces `.nupkg` files.

## Shared build configuration

`Directory.Build.props` (root) centralizes signing, license, SourceLink, and `NoWarn` for **all** projects. Per-project `.csproj` files only set package id/description/TFMs. Change copyright/signing here, not per-project. No project sets a version: the release workflow injects `/p:Version`, `/p:AssemblyVersion` and `/p:FileVersion`, so a local build produces `1.0.0` packages.

## Architecture

### Core MVVM (`System.Mvvm/`, namespace `System.Mvvm`, assembly `System.Mvvm`)

ViewModel inheritance hierarchy — pick the closest base:
- `ViewModel` — base. Implements `INotifyPropertyChanged` + `INotifyDataErrorInfo`. Provides `IsBusy`/`IsLoaded`/`IsEditable`/`IsValid` state, validation, and the central `static ViewModel.OnErrorOccurred` error event.
- `ListViewModel<T,T2>` → adds list management.
- `SearchViewModel<T,T2>` → adds search/filtering.
- `SearchTreeViewModel<T,T2>` → adds tree-path prep.

Key patterns inside `ViewModel`:
- Property change: call `NotifyPropertyChanged()` (uses `[CallerMemberName]`), `SetProperty(ref field, value)`, or `UpdateValueAndNotify(...)`. `WhenPropertyChanged(name, action)` registers a side-effect action instead of overriding the setter.
- Validation: register via `AddValidator(...)` or `[Validated]` attributes; `Validator` (see `Validator.cs`) holds rules and drives `INotifyDataErrorInfo`.
- Commands: `DelegateCommand` (`ICommand`). Static toggles control auto-requery on property change: `DelegateCommand.RequeryCommandsOnChange` (default true, scans `ICommand` *properties*) vs `UpdateICommandFields` (scans `ICommand` *fields*). These reflection scans run on every notify — relevant for perf.

### Source generator (`DSoft.System.Mvvm.SourceGenerators/`)

`ViewModelGenerator` is an `ISourceGenerator` that injects the `[MVVMViewModel]` attribute and generates boilerplate for classes marked with it. Targets `netstandard2.0` (required for analyzers). Referenced as an analyzer, not a normal dependency.

### DI / Hosting (`DSoft.System.Mvvm.Hosting/`)

`ServiceHost` is a **static** wrapper around `Microsoft.Extensions.Hosting`. Set `ServiceHost.Host = builtHost` once, then resolve anywhere via `ServiceHost.GetRequiredService<T>()`. Supports a dual Primary/Secondary host (`HostMode`) and reusable fixed scopes. The `Initialize(...)` overloads are `[Obsolete]` — build the `IHost` yourself and assign it. `ViewModel.GetRequiredService<T>()` extension pulls from `ServiceHost`.

### UI abstraction (`System.Mvvm.Ui/`, package `DSoft.System.Mvvm.Ui`)

This is the platform-agnostic contract for calling UI from shared/non-UI code:
- `IPlatformCoreUIProvider` — `ShowAlertAsync`, `ShowConfirmationDialogAsync`, `InvokeOnUIThread(Async)`.
- `UI` — static facade that forwards to the registered provider (`UI.ShowAlertAsync(...)`, etc.).
- Uses `InternalsVisibleTo` (with the strong-name public key) to expose `UI.PlatformProvider` internally to each platform UI assembly.

### Platform UI implementations (`System.Mvvm.UI.Wpf/`, `System.Mvvm.UI.WinUI/`, `DSoft.System.Mvvm.UI.MAUI/`)

Each platform package implements the abstraction the same way — when adding/maintaining a platform, mirror this structure:
1. `PlatformUIProvider` — internal singleton implementing the platform-specific provider interface (e.g. `IWPFPlatformUIProvider`) plus `IPlatformCoreUIProvider`.
2. `MvvmManager.Init()` — wires everything: sets `UI.PlatformProvider`, subscribes to `ViewModel.OnErrorOccurred` to show alerts (special-casing `TitledException`), and sets `DelegateCommand.RunOnUiThreadAction`. **App startup must call `MvvmManager.Init()`.**
3. `ServiceCollectionExtensions.AddCoreUI()` — registers the provider into DI via `TryAddSingleton`.

Platform projects use `<DefineConstants>` (e.g. `WPF`) and `.shared.cs` filename suffixes for cross-platform code sharing within a multi-targeted project.

## Conventions

- Target `development` for all changes; `main` is only ever updated by merging `development` for a release (see Branching).

- Root namespace is `System.Mvvm` across most projects regardless of folder name (note folder/file names like `DSoft.System.Mvvm.UI.WPF.csproj` differ from assembly name `System.Mvvm.UI.WPF`).
- DI registration extensions live in namespace `Microsoft.Extensions.DependencyInjection` so `AddCoreUI()` surfaces on `IServiceCollection` without extra usings.
- Do not add `Version`/`AssemblyVersion`/`FileVersion` to a `.csproj`; the version comes from `.github/workflows/release.yml`. Bump the `3.6` prefix there.
72 changes: 1 addition & 71 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,71 +1 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Overview

DSoft.System.Mvvm is a cross-platform MVVM class library for .NET, published as a family of NuGet packages (`DSoft.System.Mvvm.*`). Core MVVM lives in one base library; UI dialog/threading support is layered on top per-platform (WPF, WinUI/UWP, MAUI, Xamarin.Forms). Built-in DI is provided via a `Microsoft.Extensions.Hosting` wrapper.

## Build & Test

The solution uses the newer `.slnx` format (the old `.sln` was removed):

```powershell
dotnet restore System.Mvvm.slnx
dotnet build System.Mvvm.slnx -c Release
```

- No unit test project exists; `MVVMSample` (WPF) and `MauiSample` are the manual runnable samples.
- Multi-targeted builds (especially MAUI/WinUI) require workloads: `dotnet workload restore`.
- Azure pipelines (`azure-pipelines-*.yml`) build via the `solution: 'System.Mvvm.slnx'` variable.
- The core library targets `netstandard2.0;net10.0`. Platform UI libs add windows-specific TFMs (e.g. `net462;net10.0-windows7.0;net10.0-windows10.0.18362.0;...`).
- Assemblies are strong-named/signed (`DSoft.snk`); `Release` builds enable SourceLink and pack symbols. `GeneratePackageOnBuild` is on, so building produces `.nupkg` files.

## Shared build configuration

`Directory.Build.props` (root) centralizes versioning, signing, license, SourceLink, and `NoWarn` for **all** projects. Per-project `.csproj` files only set package id/description/TFMs. Change version/copyright/signing here, not per-project.

## Architecture

### Core MVVM (`System.Mvvm/`, namespace `System.Mvvm`, assembly `System.Mvvm`)

ViewModel inheritance hierarchy — pick the closest base:
- `ViewModel` — base. Implements `INotifyPropertyChanged` + `INotifyDataErrorInfo`. Provides `IsBusy`/`IsLoaded`/`IsEditable`/`IsValid` state, validation, and the central `static ViewModel.OnErrorOccurred` error event.
- `ListViewModel<T,T2>` → adds list management.
- `SearchViewModel<T,T2>` → adds search/filtering.
- `SearchTreeViewModel<T,T2>` → adds tree-path prep.

Key patterns inside `ViewModel`:
- Property change: call `NotifyPropertyChanged()` (uses `[CallerMemberName]`), `SetProperty(ref field, value)`, or `UpdateValueAndNotify(...)`. `WhenPropertyChanged(name, action)` registers a side-effect action instead of overriding the setter.
- Validation: register via `AddValidator(...)` or `[Validated]` attributes; `Validator` (see `Validator.cs`) holds rules and drives `INotifyDataErrorInfo`.
- Commands: `DelegateCommand` (`ICommand`). Static toggles control auto-requery on property change: `DelegateCommand.RequeryCommandsOnChange` (default true, scans `ICommand` *properties*) vs `UpdateICommandFields` (scans `ICommand` *fields*). These reflection scans run on every notify — relevant for perf.

### Source generator (`DSoft.System.Mvvm.SourceGenerators/`)

`ViewModelGenerator` is an `ISourceGenerator` that injects the `[MVVMViewModel]` attribute and generates boilerplate for classes marked with it. Targets `netstandard2.0` (required for analyzers). Referenced as an analyzer, not a normal dependency.

### DI / Hosting (`DSoft.System.Mvvm.Hosting/`)

`ServiceHost` is a **static** wrapper around `Microsoft.Extensions.Hosting`. Set `ServiceHost.Host = builtHost` once, then resolve anywhere via `ServiceHost.GetRequiredService<T>()`. Supports a dual Primary/Secondary host (`HostMode`) and reusable fixed scopes. The `Initialize(...)` overloads are `[Obsolete]` — build the `IHost` yourself and assign it. `ViewModel.GetRequiredService<T>()` extension pulls from `ServiceHost`.

### UI abstraction (`System.Mvvm.Ui/`, package `DSoft.System.Mvvm.Ui`)

This is the platform-agnostic contract for calling UI from shared/non-UI code:
- `IPlatformCoreUIProvider` — `ShowAlertAsync`, `ShowConfirmationDialogAsync`, `InvokeOnUIThread(Async)`.
- `UI` — static facade that forwards to the registered provider (`UI.ShowAlertAsync(...)`, etc.).
- Uses `InternalsVisibleTo` (with the strong-name public key) to expose `UI.PlatformProvider` internally to each platform UI assembly.

### Platform UI implementations (`System.Mvvm.UI.Wpf/`, `System.Mvvm.UI.WinUI/`, `DSoft.System.Mvvm.UI.MAUI/`)

Each platform package implements the abstraction the same way — when adding/maintaining a platform, mirror this structure:
1. `PlatformUIProvider` — internal singleton implementing the platform-specific provider interface (e.g. `IWPFPlatformUIProvider`) plus `IPlatformCoreUIProvider`.
2. `MvvmManager.Init()` — wires everything: sets `UI.PlatformProvider`, subscribes to `ViewModel.OnErrorOccurred` to show alerts (special-casing `TitledException`), and sets `DelegateCommand.RunOnUiThreadAction`. **App startup must call `MvvmManager.Init()`.**
3. `ServiceCollectionExtensions.AddCoreUI()` — registers the provider into DI via `TryAddSingleton`.

Platform projects use `<DefineConstants>` (e.g. `WPF`) and `.shared.cs` filename suffixes for cross-platform code sharing within a multi-targeted project.

## Conventions

- Root namespace is `System.Mvvm` across most projects regardless of folder name (note folder/file names like `DSoft.System.Mvvm.UI.WPF.csproj` differ from assembly name `System.Mvvm.UI.WPF`).
- DI registration extensions live in namespace `Microsoft.Extensions.DependencyInjection` so `AddCoreUI()` surfaces on `IServiceCollection` without extra usings.
- Version numbers are duplicated across `.csproj` files and `Directory.Build.props` — keep them in sync when bumping.
@AGENTS.md
5 changes: 1 addition & 4 deletions DSoft.System.Mvvm.Hosting/DSoft.System.Mvvm.Hosting.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,6 @@
<RootNamespace>System.Mvvm</RootNamespace>
<Product>$(AssemblyName) ($(TargetFramework))</Product>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
<AssemblyVersion>2.2.2102.142</AssemblyVersion>
<Version>2.2.2102.142-prerelease</Version>
<FileVersion>2.2.2102.142</FileVersion>
<PackageReadmeFile>readme.md</PackageReadmeFile>
</PropertyGroup>

Expand All @@ -22,7 +19,7 @@
<Compile Include="**\*.shared.*.cs" />
<Compile Include="HostMode.cs" />
<ProjectReference Include="..\System.Mvvm\DSoft.System.Mvvm.csproj" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.9" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.12" />
</ItemGroup>

<ItemGroup Condition=" $(TargetFramework.Equals('netstandard2.0'))">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,6 @@
<PackageReleaseNotes>Updated to .NET 10 and latest packages. Removed .NET 8 and .NET 9 support. Performance improvements.</PackageReleaseNotes>
<AssemblyName>System.Mvvm.SourceGenerators</AssemblyName>
<Product>$(AssemblyName) ($(TargetFramework))</Product>
<AssemblyVersion>2.2.2102.142</AssemblyVersion>
<Version>2.2.2102.142-prerelease</Version>
<FileVersion>2.2.2102.142</FileVersion>
<PackageReadmeFile>readme.md</PackageReadmeFile>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
</PropertyGroup>
Expand Down
9 changes: 3 additions & 6 deletions DSoft.System.Mvvm.UI.MAUI/DSoft.System.Mvvm.UI.MAUI.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,11 @@
<ImplicitUsings>enable</ImplicitUsings>
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
<PackageId>DSoft.System.Mvvm.UI.MAUI</PackageId>
<Version>2.2.2102.142-prerelease</Version>
<Description>MAUI platform support for System.Mvvm.UI</Description>
<PackageReleaseNotes>Updated to .NET 10 and latest packages. Removed .NET 8 and .NET 9 support. Performance improvements.</PackageReleaseNotes>
<PackageTags>MAUI NET</PackageTags>
<AssemblyName>System.Mvvm.UI.MAUI</AssemblyName>
<Product>$(AssemblyName) ($(TargetFramework))</Product>
<AssemblyVersion>2.2.2102.142</AssemblyVersion>
<FileVersion>2.2.2102.142</FileVersion>
<RootNamespace>System.Mvvm</RootNamespace>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">16.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst'">15.0</SupportedOSPlatformVersion>
Expand All @@ -31,9 +28,9 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Maui.Controls" Version="10.0.71" />
<PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="10.0.71" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="10.0.9" />
<PackageReference Include="Microsoft.Maui.Controls" Version="10.0.110" />
<PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="10.0.110" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="10.0.12" />
</ItemGroup>

<ItemGroup>
Expand Down
Loading
Loading