From aa7ece4ed09729af268a20a7c6d879d179c21438 Mon Sep 17 00:00:00 2001 From: Nitload Date: Wed, 1 Apr 2026 22:20:49 +0800 Subject: [PATCH] advanced userinterface design --- .../Numeric/NumericalModule.cs | 14 +- .../Numeric/Runtime/MatrixParallel.cs | 57 +- .../Numeric/Runtime/MatrixParallelTask.cs | 18 +- .../Numeric/Runtime/MatrixParallelThread.cs | 28 +- .../.$basic-arch.drawio.bkp | 79 +++ .../PanelStateChangeBindingAttribute.cs | 25 + .../Attribute/PanelStateNodeAttribute.cs | 5 + .../Framework/Service.Host.cs | 36 ++ .../Service.LoadAccomplishmentEvent.cs | 15 + .../Framework/Service.LoadCommand.cs | 24 + .../Framework/State.AppState.cs | 27 + .../Framework/State.FrontCommandHost.cs | 32 + .../Framework/State.Host.cs | 63 ++ .../Framework/State.PanelState.cs | 21 + .../Framework/State.ServiceManager.cs | 36 ++ .../Framework/View.CommandResult.cs | 21 + .../Framework/View.ExecuteCommand.cs | 25 + StgSharp.UserInterface/Framework/View.Host.cs | 53 ++ .../Framework/View.StateChangeCommand.cs | 24 + .../Framework/View.StateValueCommand.cs | 19 + .../IncrementalGeneratorFramework.md | 372 +++++++++++ ...PanelStateIncrementalGenerator.Analysis.cs | 50 ++ ...lStateIncrementalGenerator.Architecture.md | 184 ++++++ ...elStateIncrementalGenerator.Diagnostics.cs | 6 + .../PanelStateIncrementalGenerator.Emitter.cs | 66 ++ .../PanelStateIncrementalGenerator.Models.cs | 73 +++ ...PanelStateIncrementalGenerator.Pipeline.cs | 108 ++++ ...ncrementalGenerator.StateChangeBindings.cs | 20 + ...crementalGenerator.StateCommandHandlers.cs | 61 ++ ...lStateIncrementalGenerator.StateSymbols.cs | 20 + .../PanelStateIncrementalGenerator.cs | 37 ++ StgSharp.UserInterface/Global.cs | 5 + .../Sample/PanelStateSample.cs | 65 ++ StgSharp.UserInterface/Service/IService.cs | 12 - StgSharp.UserInterface/State/IState.cs | 37 -- .../StgSharp.UnserInterface.csproj | 1 - .../StgSharp.UserInterface.csproj | 29 + StgSharp.UserInterface/VSS_Arch.md | 580 +++++++----------- StgSharp.UserInterface/VSS_BaseTypes.mmd | 82 +++ StgSharp.UserInterface/VSS_TypeFramework.md | 531 ++++++++++++++++ .../View/PresentorDescription.cs | 12 - StgSharp.UserInterface/basic-arch.drawio | 366 +++++++++++ 42 files changed, 2876 insertions(+), 463 deletions(-) create mode 100644 StgSharp.UserInterface/.$basic-arch.drawio.bkp create mode 100644 StgSharp.UserInterface/Attribute/PanelStateChangeBindingAttribute.cs create mode 100644 StgSharp.UserInterface/Attribute/PanelStateNodeAttribute.cs create mode 100644 StgSharp.UserInterface/Framework/Service.Host.cs create mode 100644 StgSharp.UserInterface/Framework/Service.LoadAccomplishmentEvent.cs create mode 100644 StgSharp.UserInterface/Framework/Service.LoadCommand.cs create mode 100644 StgSharp.UserInterface/Framework/State.AppState.cs create mode 100644 StgSharp.UserInterface/Framework/State.FrontCommandHost.cs create mode 100644 StgSharp.UserInterface/Framework/State.Host.cs create mode 100644 StgSharp.UserInterface/Framework/State.PanelState.cs create mode 100644 StgSharp.UserInterface/Framework/State.ServiceManager.cs create mode 100644 StgSharp.UserInterface/Framework/View.CommandResult.cs create mode 100644 StgSharp.UserInterface/Framework/View.ExecuteCommand.cs create mode 100644 StgSharp.UserInterface/Framework/View.Host.cs create mode 100644 StgSharp.UserInterface/Framework/View.StateChangeCommand.cs create mode 100644 StgSharp.UserInterface/Framework/View.StateValueCommand.cs create mode 100644 StgSharp.UserInterface/Generator/IncrementalGeneratorFramework.md create mode 100644 StgSharp.UserInterface/Generator/PanelStateIncrementalGenerator.Analysis.cs create mode 100644 StgSharp.UserInterface/Generator/PanelStateIncrementalGenerator.Architecture.md create mode 100644 StgSharp.UserInterface/Generator/PanelStateIncrementalGenerator.Diagnostics.cs create mode 100644 StgSharp.UserInterface/Generator/PanelStateIncrementalGenerator.Emitter.cs create mode 100644 StgSharp.UserInterface/Generator/PanelStateIncrementalGenerator.Models.cs create mode 100644 StgSharp.UserInterface/Generator/PanelStateIncrementalGenerator.Pipeline.cs create mode 100644 StgSharp.UserInterface/Generator/PanelStateIncrementalGenerator.StateChangeBindings.cs create mode 100644 StgSharp.UserInterface/Generator/PanelStateIncrementalGenerator.StateCommandHandlers.cs create mode 100644 StgSharp.UserInterface/Generator/PanelStateIncrementalGenerator.StateSymbols.cs create mode 100644 StgSharp.UserInterface/Generator/PanelStateIncrementalGenerator.cs create mode 100644 StgSharp.UserInterface/Global.cs create mode 100644 StgSharp.UserInterface/Sample/PanelStateSample.cs delete mode 100644 StgSharp.UserInterface/Service/IService.cs delete mode 100644 StgSharp.UserInterface/State/IState.cs delete mode 100644 StgSharp.UserInterface/StgSharp.UnserInterface.csproj create mode 100644 StgSharp.UserInterface/StgSharp.UserInterface.csproj create mode 100644 StgSharp.UserInterface/VSS_BaseTypes.mmd create mode 100644 StgSharp.UserInterface/VSS_TypeFramework.md delete mode 100644 StgSharp.UserInterface/View/PresentorDescription.cs create mode 100644 StgSharp.UserInterface/basic-arch.drawio diff --git a/StgSharp.Mathematics/Numeric/NumericalModule.cs b/StgSharp.Mathematics/Numeric/NumericalModule.cs index 754595aa..3fd2adcc 100644 --- a/StgSharp.Mathematics/Numeric/NumericalModule.cs +++ b/StgSharp.Mathematics/Numeric/NumericalModule.cs @@ -2,8 +2,8 @@ // ----------------------------------------------------------------------- // file="NumericalModule" // Project: StgSharp -// AuthorGroup: Nitload Space -// Copyright (c) Nitload Space. All rights reserved. +// AuthorGroup: Nitload +// Copyright (c) Nitload. All rights reserved. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -66,13 +66,15 @@ namespace StgSharp.Mathematics.Numeric internal static SIMDID GlobalIntrinsicMask { get; set; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void InitializeModule() + public static void InitializeModule() { - throw new NotImplementedException(); + MatrixParallel.Init(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void InitializeModule(IModuleInitializeProfile profile) + public void InitializeModule( + IModuleInitializeProfile profile + ) { MatrixParallel.Init(); } @@ -80,7 +82,7 @@ namespace StgSharp.Mathematics.Numeric [MethodImpl(MethodImplOptions.AggressiveInlining)] public void UninitializeModule() { - throw new NotImplementedException(); + MatrixParallel.Deinit(); } } diff --git a/StgSharp.Mathematics/Numeric/Runtime/MatrixParallel.cs b/StgSharp.Mathematics/Numeric/Runtime/MatrixParallel.cs index edf3f372..4d8898f8 100644 --- a/StgSharp.Mathematics/Numeric/Runtime/MatrixParallel.cs +++ b/StgSharp.Mathematics/Numeric/Runtime/MatrixParallel.cs @@ -2,8 +2,8 @@ // ----------------------------------------------------------------------- // file="MatrixParallel" // Project: StgSharp -// AuthorGroup: Nitload Space -// Copyright (c) Nitload Space. All rights reserved. +// AuthorGroup: Nitload +// Copyright (c) Nitload. All rights reserved. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -29,6 +29,7 @@ using StgSharp.Collections; using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Text.RegularExpressions; namespace StgSharp.Mathematics.Numeric { @@ -45,12 +46,28 @@ namespace StgSharp.Mathematics.Numeric public static bool IsParallelSupported { get; } = Environment.ProcessorCount > 6; - public static bool IsParallelMeaningful => IsParallelSupported && IsInitialized && _meaningful; + public static bool IsParallelMeaningful => IsParallelSupported && + IsInitialized && + _meaningful; public static int ContextCount { get; private set; } private static bool IsInitialized { get; set; } + internal static void Deinit() + { + foreach (MatrixParallelThread t in _threads) + { + t.Retire(); + t.Dispose(); + } + foreach (MatrixParallelThread t in _retiredThreads) { + t.Dispose(); + } + ContextCount = 0; + IsInitialized = false; + } + internal static void Init() { Dictionary contextMap = []; @@ -65,8 +82,7 @@ namespace StgSharp.Mathematics.Numeric } contextMap = []; int count = Environment.ProcessorCount; - bool hasGlobalSimd = false; - SIMDID globalSimd = default; + SIMDID globalSimd = new SIMDID(0); _threads = new(count); _retiredThreads = new(RESERVED_THREAD_COUNT); for (int i = 0; i < count; i++) @@ -77,9 +93,10 @@ namespace StgSharp.Mathematics.Numeric SIMDID simd = thread.Simd; ulong mask = simd.Mask; - contextMap[mask] = contextMap.TryGetValue(mask, out int contextCount) ? (contextCount + 1) : 1; - globalSimd = hasGlobalSimd ? UniteSIMDID(globalSimd, simd) : simd; - hasGlobalSimd = true; + contextMap[mask] = contextMap.TryGetValue(mask, out int contextCount) ? + (contextCount + 1) : + 1; + globalSimd = (globalSimd.Mask != 0) ? UniteSIMDID(globalSimd, simd) : simd; } IntrinsicContext globalContext = new(); @@ -96,7 +113,10 @@ namespace StgSharp.Mathematics.Numeric } else { MatrixParallelThread[] allThreads = _threads.PopRange(_threads.Count); - Array.Sort(allThreads, static(left, right) => CompareSIMDID(left.Simd, right.Simd)); + Array.Sort(allThreads, static( + left, + right + ) => CompareSIMDID(left.Simd, right.Simd)); int retireCount = Math.Min(RESERVED_THREAD_COUNT, Math.Max(0, allThreads.Length - 1)); for (int i = 0; i < retireCount; i++) { RetireThread(allThreads[i]); @@ -111,15 +131,23 @@ namespace StgSharp.Mathematics.Numeric [LibraryImport(Native.LibName, EntryPoint = "sn_compare_simd")] [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] [DefaultDllImportSearchPaths(DllImportSearchPath.AssemblyDirectory)] - private static partial int CompareSIMDID(SIMDID left, SIMDID right); + private static partial int CompareSIMDID( + SIMDID left, + SIMDID right + ); [LibraryImport(Native.LibName, EntryPoint = "load_intrinsic_function")] [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] [DefaultDllImportSearchPaths(DllImportSearchPath.AssemblyDirectory)] - private static partial void LoadIntrinsicFunction(IntrinsicContext* context, ulong id); + private static partial void LoadIntrinsicFunction( + IntrinsicContext* context, + ulong id + ); [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void RetireThread(MatrixParallelThread thread) + private static void RetireThread( + MatrixParallelThread thread + ) { thread.Retire(); _retiredThreads.Push(thread); @@ -128,7 +156,10 @@ namespace StgSharp.Mathematics.Numeric [LibraryImport(Native.LibName, EntryPoint = "sn_get_unite_simd")] [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] [DefaultDllImportSearchPaths(DllImportSearchPath.AssemblyDirectory)] - private static partial SIMDID UniteSIMDID(SIMDID left, SIMDID right); + private static partial SIMDID UniteSIMDID( + SIMDID left, + SIMDID right + ); } } diff --git a/StgSharp.Mathematics/Numeric/Runtime/MatrixParallelTask.cs b/StgSharp.Mathematics/Numeric/Runtime/MatrixParallelTask.cs index 9d2b3b98..ba1dba12 100644 --- a/StgSharp.Mathematics/Numeric/Runtime/MatrixParallelTask.cs +++ b/StgSharp.Mathematics/Numeric/Runtime/MatrixParallelTask.cs @@ -2,8 +2,8 @@ // ----------------------------------------------------------------------- // file="MatrixParallelTask" // Project: StgSharp -// AuthorGroup: Nitload Space -// Copyright (c) Nitload Space. All rights reserved. +// AuthorGroup: Nitload +// Copyright (c) Nitload. All rights reserved. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -49,7 +49,13 @@ namespace StgSharp.Mathematics.Numeric.Runtime [FieldOffset(0)] public MatrixKernel* Mat1; [FieldOffset(8)] public MatrixKernel* Mat2; [FieldOffset(16)] public MatrixKernel* Mat3; + /// + /// Being offset of first kernel in a L4 cache line. + /// [FieldOffset(24)] public int CacheIndex; + /// + /// Count of kernels to be calculated in a L4 cache line. + /// [FieldOffset(28)] public int CacheLength; [FieldOffset(32)] public int CommonK; [FieldOffset(36)] public int CommonX; @@ -66,9 +72,13 @@ namespace StgSharp.Mathematics.Numeric.Runtime } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Copy(MatrixParallelTask* source, MatrixParallelTask* target) + public static void Copy( + MatrixParallelTask* source, + MatrixParallelTask* target + ) { - Buffer.MemoryCopy(source, target, sizeof(MatrixParallelTask), sizeof(MatrixParallelTask)); + Buffer.MemoryCopy(source, target, sizeof(MatrixParallelTask), + sizeof(MatrixParallelTask)); } } diff --git a/StgSharp.Mathematics/Numeric/Runtime/MatrixParallelThread.cs b/StgSharp.Mathematics/Numeric/Runtime/MatrixParallelThread.cs index cc4ddf76..9934e85b 100644 --- a/StgSharp.Mathematics/Numeric/Runtime/MatrixParallelThread.cs +++ b/StgSharp.Mathematics/Numeric/Runtime/MatrixParallelThread.cs @@ -2,8 +2,8 @@ // ----------------------------------------------------------------------- // file="MatrixParallelThread" // Project: StgSharp -// AuthorGroup: Nitload Space -// Copyright (c) Nitload Space. All rights reserved. +// AuthorGroup: Nitload +// Copyright (c) Nitload. All rights reserved. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -31,6 +31,7 @@ using System; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Threading; +using System.Threading.Tasks; namespace StgSharp.Mathematics.Numeric { @@ -54,7 +55,9 @@ namespace StgSharp.Mathematics.Numeric private readonly ManualResetEventSlim _resetEvent; private Thread _managedThread; - internal MatrixParallelThread(int poolId) + internal MatrixParallelThread( + int poolId + ) { _resetEvent = new ManualResetEventSlim(false); _managedThread = new Thread(MatrixParallelWorkLoad); @@ -119,14 +122,18 @@ namespace StgSharp.Mathematics.Numeric } [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal void SetSingleThreadTask(ref MatrixParallelTask task) + internal void SetSingleThreadTask( + ref MatrixParallelTask task + ) { if (IsSingle) { *_currentTask = task; } } - protected virtual void Dispose(bool disposing) + protected virtual void Dispose( + bool disposing + ) { if (!_disposed) { @@ -183,6 +190,8 @@ namespace StgSharp.Mathematics.Numeric if (IsSingle) { MatrixParallelTask* outer = _currentTask; + + // compute here IsSingle = false; } else { @@ -217,12 +226,17 @@ namespace StgSharp.Mathematics.Numeric [LibraryImport(Native.LibName, EntryPoint = "sn_set_thread_affinity")] [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] [DefaultDllImportSearchPaths(DllImportSearchPath.AssemblyDirectory)] - internal static partial void SetThreadAffinity(int coreId); + internal static partial void SetThreadAffinity( + int coreId + ); [LibraryImport(Native.LibName, EntryPoint = "load_intrinsic_function")] [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] [DefaultDllImportSearchPaths(DllImportSearchPath.AssemblyDirectory)] - private static unsafe partial void LoadIntrinsicFunction(IntrinsicContext* context, ulong id); + private static unsafe partial void LoadIntrinsicFunction( + IntrinsicContext* context, + ulong id + ); #pragma warning restore CA5393 } } diff --git a/StgSharp.UserInterface/.$basic-arch.drawio.bkp b/StgSharp.UserInterface/.$basic-arch.drawio.bkp new file mode 100644 index 00000000..54c69536 --- /dev/null +++ b/StgSharp.UserInterface/.$basic-arch.drawio.bkp @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/StgSharp.UserInterface/Attribute/PanelStateChangeBindingAttribute.cs b/StgSharp.UserInterface/Attribute/PanelStateChangeBindingAttribute.cs new file mode 100644 index 00000000..871dbb4a --- /dev/null +++ b/StgSharp.UserInterface/Attribute/PanelStateChangeBindingAttribute.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace StgSharp.UserInterface +{ + [AttributeUsage(AttributeTargets.Method, Inherited = true)] + public sealed class PanelStateChangeBindingAttribute( + string statePath, + string panelHostPath + ) : Attribute + { + + public string StatePath { get; } = string.IsNullOrWhiteSpace(statePath) ? + throw new ArgumentException("Value cannot be null or whitespace.", nameof(statePath)) : + statePath; + + public string PanelHostPath { get; } = string.IsNullOrWhiteSpace(panelHostPath) ? + throw new ArgumentException("Value cannot be null or whitespace.", nameof(panelHostPath)) : + panelHostPath; + + } +} diff --git a/StgSharp.UserInterface/Attribute/PanelStateNodeAttribute.cs b/StgSharp.UserInterface/Attribute/PanelStateNodeAttribute.cs new file mode 100644 index 00000000..96de2a06 --- /dev/null +++ b/StgSharp.UserInterface/Attribute/PanelStateNodeAttribute.cs @@ -0,0 +1,5 @@ +namespace StgSharp.UserInterface +{ + [AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = true)] + public sealed class PanelStateNodeAttribute : Attribute { } +} diff --git a/StgSharp.UserInterface/Framework/Service.Host.cs b/StgSharp.UserInterface/Framework/Service.Host.cs new file mode 100644 index 00000000..8313e47c --- /dev/null +++ b/StgSharp.UserInterface/Framework/Service.Host.cs @@ -0,0 +1,36 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace StgSharp.UserInterface +{ + public abstract class ServiceHost + { + + protected ServiceHost( + string name + ) + { + Name = string.IsNullOrWhiteSpace(name) ? + throw new ArgumentException("Value cannot be null or whitespace.", nameof(name)) : + name; + } + + public string Name { get; } + + public abstract Task ExecuteAsync( + ExecuteCommand command, + CancellationToken cancellationToken = default + ); + + public abstract ValueTask InitializeAsync( + ServiceLoadCommand command, + CancellationToken cancellationToken = default + ); + + public abstract ValueTask StopAsync( + CancellationToken cancellationToken = default + ); + + } +} diff --git a/StgSharp.UserInterface/Framework/Service.LoadAccomplishmentEvent.cs b/StgSharp.UserInterface/Framework/Service.LoadAccomplishmentEvent.cs new file mode 100644 index 00000000..54a95a59 --- /dev/null +++ b/StgSharp.UserInterface/Framework/Service.LoadAccomplishmentEvent.cs @@ -0,0 +1,15 @@ +namespace StgSharp.UserInterface +{ + public sealed record ServiceLoadAccomplishmentEvent + { + + public string? Message { get; init; } + + public ServiceHost? Host { get; init; } + + public bool IsLoaded { get; init; } + + public string ServiceName { get; init; } = string.Empty; + + } +} diff --git a/StgSharp.UserInterface/Framework/Service.LoadCommand.cs b/StgSharp.UserInterface/Framework/Service.LoadCommand.cs new file mode 100644 index 00000000..b2bff7c5 --- /dev/null +++ b/StgSharp.UserInterface/Framework/Service.LoadCommand.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; + +namespace StgSharp.UserInterface +{ + public abstract record ServiceLoadCommand + { + + protected ServiceLoadCommand( + string serviceName + ) + { + ServiceName = string.IsNullOrWhiteSpace(serviceName) ? + throw new ArgumentException("Value cannot be null or whitespace.", nameof(serviceName)) : + serviceName; + } + + public IReadOnlyDictionary Parameters { get; init; } = + new Dictionary(StringComparer.Ordinal); + + public string ServiceName { get; init; } + + } +} diff --git a/StgSharp.UserInterface/Framework/State.AppState.cs b/StgSharp.UserInterface/Framework/State.AppState.cs new file mode 100644 index 00000000..2ee5a48b --- /dev/null +++ b/StgSharp.UserInterface/Framework/State.AppState.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; + +namespace StgSharp.UserInterface +{ + public abstract class AppState + { + + protected AppState( + string name + ) + { + Name = string.IsNullOrWhiteSpace(name) ? + throw new ArgumentException("Value cannot be null or whitespace.", nameof(name)) : + name; + } + + public IDictionary Panels { get; } = + new Dictionary(StringComparer.Ordinal); + + public IDictionary SharedContext { get; } = + new Dictionary(StringComparer.Ordinal); + + public string Name { get; } + + } +} diff --git a/StgSharp.UserInterface/Framework/State.FrontCommandHost.cs b/StgSharp.UserInterface/Framework/State.FrontCommandHost.cs new file mode 100644 index 00000000..a0418f9b --- /dev/null +++ b/StgSharp.UserInterface/Framework/State.FrontCommandHost.cs @@ -0,0 +1,32 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace StgSharp.UserInterface +{ + public abstract class FrontCommandHost + { + + public StateHost StateHost { get; private set; } = null!; + + public abstract Task AcceptAsync( + ViewHost source, + StateChangeCommand command, + CancellationToken cancellationToken = default + ); + + public abstract Task AcceptAsync( + ViewHost source, + ExecuteCommand command, + CancellationToken cancellationToken = default + ); + + internal void Attach( + StateHost stateHost + ) + { + StateHost = stateHost ?? throw new ArgumentNullException(nameof(stateHost)); + } + + } +} diff --git a/StgSharp.UserInterface/Framework/State.Host.cs b/StgSharp.UserInterface/Framework/State.Host.cs new file mode 100644 index 00000000..7b31e3dd --- /dev/null +++ b/StgSharp.UserInterface/Framework/State.Host.cs @@ -0,0 +1,63 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace StgSharp.UserInterface +{ + public abstract class StateHost + { + + protected StateHost( + FrontCommandHost frontCommandHost, + AppState appState, + ServiceManager serviceManager + ) + { + FrontCommandHost = frontCommandHost ?? + throw new ArgumentNullException(nameof(frontCommandHost)); + AppState = appState ?? throw new ArgumentNullException(nameof(appState)); + ServiceManager = serviceManager ?? + throw new ArgumentNullException(nameof(serviceManager)); + + FrontCommandHost.Attach(this); + ServiceManager.Attach(this); + } + + public AppState AppState { get; } + + public FrontCommandHost FrontCommandHost { get; } + + public ServiceManager ServiceManager { get; } + + public virtual async Task PostAsync( + ViewHost source, + StateChangeCommand command, + CancellationToken cancellationToken = default + ) + { + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(command); + + CommandResult result = await FrontCommandHost.AcceptAsync(source, command, cancellationToken) + .ConfigureAwait(false); + await source.ReceiveCommandResultAsync(result, cancellationToken).ConfigureAwait(false); + return result; + } + + public virtual async Task PostAsync( + ViewHost source, + ExecuteCommand command, + CancellationToken cancellationToken = default + ) + { + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(command); + + CommandResult result = await FrontCommandHost.AcceptAsync(source, command, cancellationToken) + .ConfigureAwait(false); + await source.ReceiveCommandResultAsync(result, cancellationToken).ConfigureAwait(false); + return result; + } + + } +} diff --git a/StgSharp.UserInterface/Framework/State.PanelState.cs b/StgSharp.UserInterface/Framework/State.PanelState.cs new file mode 100644 index 00000000..0b5e031d --- /dev/null +++ b/StgSharp.UserInterface/Framework/State.PanelState.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; + +namespace StgSharp.UserInterface +{ + public abstract class PanelState + { + + protected PanelState( + string name + ) + { + Name = string.IsNullOrWhiteSpace(name) ? + throw new ArgumentException("Value cannot be null or whitespace.", nameof(name)) : + name; + } + + public string Name { get; } + + } +} diff --git a/StgSharp.UserInterface/Framework/State.ServiceManager.cs b/StgSharp.UserInterface/Framework/State.ServiceManager.cs new file mode 100644 index 00000000..34f2a072 --- /dev/null +++ b/StgSharp.UserInterface/Framework/State.ServiceManager.cs @@ -0,0 +1,36 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace StgSharp.UserInterface +{ + public abstract class ServiceManager + { + + public StateHost StateHost { get; private set; } = null!; + + public abstract Task ExecuteAsync( + ExecuteCommand command, + CancellationToken cancellationToken = default + ); + + public abstract Task LoadAsync( + ServiceLoadCommand command, + CancellationToken cancellationToken = default + ); + + public abstract ValueTask RegisterAsync( + string serviceName, + ServiceHost serviceHost, + CancellationToken cancellationToken = default + ); + + internal void Attach( + StateHost stateHost + ) + { + StateHost = stateHost ?? throw new ArgumentNullException(nameof(stateHost)); + } + + } +} diff --git a/StgSharp.UserInterface/Framework/View.CommandResult.cs b/StgSharp.UserInterface/Framework/View.CommandResult.cs new file mode 100644 index 00000000..c21d429b --- /dev/null +++ b/StgSharp.UserInterface/Framework/View.CommandResult.cs @@ -0,0 +1,21 @@ +using System; + +namespace StgSharp.UserInterface +{ + public sealed record CommandResult + { + + public string? Message { get; init; } + + public object? Data { get; init; } + + public bool IsSuccess { get; init; } + + public bool IsRejected { get; init; } + + public bool IsAborted { get; init; } + + public Guid CommandId { get; init; } + + } +} diff --git a/StgSharp.UserInterface/Framework/View.ExecuteCommand.cs b/StgSharp.UserInterface/Framework/View.ExecuteCommand.cs new file mode 100644 index 00000000..6681aada --- /dev/null +++ b/StgSharp.UserInterface/Framework/View.ExecuteCommand.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; + +namespace StgSharp.UserInterface +{ + public abstract record ExecuteCommand + { + + protected ExecuteCommand( + string operationName + ) + { + OperationName = string.IsNullOrWhiteSpace(operationName) ? + throw new ArgumentException("Value cannot be null or whitespace.", nameof(operationName)) : + operationName; + } + + public Guid CommandId { get; init; } = Guid.NewGuid(); + + public IReadOnlyList Parameters { get; init; } = Array.Empty(); + + public string OperationName { get; init; } + + } +} diff --git a/StgSharp.UserInterface/Framework/View.Host.cs b/StgSharp.UserInterface/Framework/View.Host.cs new file mode 100644 index 00000000..d34a779d --- /dev/null +++ b/StgSharp.UserInterface/Framework/View.Host.cs @@ -0,0 +1,53 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace StgSharp.UserInterface +{ + public abstract class ViewHost + { + + protected ViewHost( + string name, + StateHost stateHost + ) + { + Name = string.IsNullOrWhiteSpace(name) ? + throw new ArgumentException("Value cannot be null or whitespace.", nameof(name)) : + name; + StateHost = stateHost ?? throw new ArgumentNullException(nameof(stateHost)); + } + + public string Name { get; } + + protected StateHost StateHost { get; } + + public virtual Task PostAsync( + StateChangeCommand command, + CancellationToken cancellationToken = default + ) + { + ArgumentNullException.ThrowIfNull(command); + return StateHost.PostAsync(this, command, cancellationToken); + } + + public virtual Task PostAsync( + ExecuteCommand command, + CancellationToken cancellationToken = default + ) + { + ArgumentNullException.ThrowIfNull(command); + return StateHost.PostAsync(this, command, cancellationToken); + } + + public virtual ValueTask ReceiveCommandResultAsync( + CommandResult result, + CancellationToken cancellationToken = default + ) + { + ArgumentNullException.ThrowIfNull(result); + return ValueTask.CompletedTask; + } + + } +} diff --git a/StgSharp.UserInterface/Framework/View.StateChangeCommand.cs b/StgSharp.UserInterface/Framework/View.StateChangeCommand.cs new file mode 100644 index 00000000..ce04d154 --- /dev/null +++ b/StgSharp.UserInterface/Framework/View.StateChangeCommand.cs @@ -0,0 +1,24 @@ +using System; + +namespace StgSharp.UserInterface +{ + public abstract record StateChangeCommand + { + + protected StateChangeCommand( + string statePath + ) + { + StatePath = string.IsNullOrWhiteSpace(statePath) ? + throw new ArgumentException("Value cannot be null or whitespace.", nameof(statePath)) : + statePath; + } + + public object? Value { get; init; } + + public Guid CommandId { get; init; } = Guid.NewGuid(); + + public string StatePath { get; init; } + + } +} diff --git a/StgSharp.UserInterface/Framework/View.StateValueCommand.cs b/StgSharp.UserInterface/Framework/View.StateValueCommand.cs new file mode 100644 index 00000000..e0b0f8e8 --- /dev/null +++ b/StgSharp.UserInterface/Framework/View.StateValueCommand.cs @@ -0,0 +1,19 @@ +using System; + +namespace StgSharp.UserInterface +{ + public sealed record StateValueCommand : StateChangeCommand + { + + public StateValueCommand( + string statePath, + TValue value + ) : base(statePath) + { + Value = value; + } + + public new TValue Value { get; init; } + + } +} \ No newline at end of file diff --git a/StgSharp.UserInterface/Generator/IncrementalGeneratorFramework.md b/StgSharp.UserInterface/Generator/IncrementalGeneratorFramework.md new file mode 100644 index 00000000..4378b29a --- /dev/null +++ b/StgSharp.UserInterface/Generator/IncrementalGeneratorFramework.md @@ -0,0 +1,372 @@ +# 增量生成器框架说明 + +本文档说明当前 Generator 文件夹里这套增量生成器骨架为什么这样拆,以及每一层现在承担什么职责。 + +这里讲的是“结构设计”,不是“最终 VSS 业务代码怎么生成”。 + +--- + +## 1. 当前结论 + +当前这套结构不再是“一个字段触发一个小生成器”,而是: + +1. 一个统一入口生成器 +2. 一个类型级候选识别管线 +3. 一个共享中间模型 +4. 三个内部分析模块 +5. 一个统一发射器 +6. 一个诊断层预留点 + +也就是说,设计目标已经从“节点枚举小功能”升级成“围绕一个状态类型进行多维分析”。 + +--- + +## 2. 当前文件与职责 + +当前 Generator 文件夹中的核心文件是: + +1. `PanelStateIncrementalGenerator.cs` +2. `PanelStateIncrementalGenerator.Pipeline.cs` +3. `PanelStateIncrementalGenerator.Models.cs` +4. `PanelStateIncrementalGenerator.Analysis.cs` +5. `PanelStateIncrementalGenerator.StateSymbols.cs` +6. `PanelStateIncrementalGenerator.StateChangeBindings.cs` +7. `PanelStateIncrementalGenerator.StateCommandHandlers.cs` +8. `PanelStateIncrementalGenerator.Emitter.cs` +9. `PanelStateIncrementalGenerator.Diagnostics.cs` + +它们的职责可以概括成下面这张表: + +| 文件 | 角色 | 负责什么 | +| --- | --- | --- | +| `PanelStateIncrementalGenerator.cs` | 入口层 | 把 Roslyn 的增量管线接起来 | +| `PanelStateIncrementalGenerator.Pipeline.cs` | 候选管线层 | 识别类型级候选,并收集已知标记成员 | +| `PanelStateIncrementalGenerator.Models.cs` | 中间模型层 | 定义共享输入、目标、分析结果模型 | +| `PanelStateIncrementalGenerator.Analysis.cs` | 协调层 | 驱动三个内部分析模块并汇总输出 | +| `PanelStateIncrementalGenerator.StateSymbols.cs` | 分析模块 1 | 分析状态符号相关成员 | +| `PanelStateIncrementalGenerator.StateChangeBindings.cs` | 分析模块 2 | 分析状态变化绑定相关成员 | +| `PanelStateIncrementalGenerator.StateCommandHandlers.cs` | 分析模块 3 | 基于前两部分结果派生状态变化命令处理结果 | +| `PanelStateIncrementalGenerator.Emitter.cs` | 发射层 | 把分析结果输出成 `.g.cs` | +| `PanelStateIncrementalGenerator.Diagnostics.cs` | 诊断层 | 预留统一诊断定义位置 | + +一句话总结现在的结构: + +> 一个入口识别类型声明,进入统一模型,再在内部拆成三个并列分析部分。 + +--- + +## 3. 入口层 + +对应文件: + +`PanelStateIncrementalGenerator.cs` + +它的职责仍然很单纯: + +1. 建立 SyntaxProvider +2. 收集类型级候选 +3. 与 Compilation 合并 +4. 调用统一分析器 +5. 把结果交给发射器 + +它不应该承担这些事情: + +1. 不应该写复杂业务规则 +2. 不应该直接拼大段生成代码 +3. 不应该依赖某一块具体的状态逻辑 + +入口层越薄,后面三部分演化时越安全。 + +--- + +## 4. Pipeline 层 + +对应文件: + +`PanelStateIncrementalGenerator.Pipeline.cs` + +这层的重点已经从“字段筛选”切换成“类型筛选”。 + +### 4.1 `IsCandidate` + +这一步只做便宜的语法级判断。 + +当前策略是: + +1. 只看 `TypeDeclarationSyntax` +2. 该类型内部至少存在一个带 Attribute 的字段、属性或方法 + +这一步允许误报,但必须便宜。 + +### 4.2 `Transform` + +这一步开始做语义级确认。 + +当前逻辑是: + +1. 拿到类型符号 `INamedTypeSymbol` +2. 遍历该类型成员 +3. 识别已知 Attribute +4. 收集所有命中的成员 +5. 构造一个类型级目标 `PanelStateIncrementalGeneratorTypeTarget` + +这一步的核心意义是: + +1. 根输入单位是“类型” +2. 成员只是这个类型上的分析素材 +3. 后面的三部分都共享同一份素材,而不是各自重新扫一遍类型 + +--- + +## 5. Models 层 + +对应文件: + +`PanelStateIncrementalGenerator.Models.cs` + +中间模型的作用,是把 Roslyn 的语义对象整理成稳定的数据结构,供后面多个模块共享。 + +当前关键模型包括: + +1. `PanelStateIncrementalGeneratorMarkedMember` +2. `PanelStateIncrementalGeneratorTypeTarget` +3. `PanelStateIncrementalGeneratorInput` +4. `PanelStateIncrementalGeneratorTypeAnalysis` +5. `PanelStateIncrementalGeneratorOutput` + +以及三个内部分析结果: + +1. `PanelStateSymbolPartAnalysis` +2. `StateChangeBindingPartAnalysis` +3. `StateChangeCommandHandlerPartAnalysis` + +其中第三个结果不再对应一个额外的用户声明 attribute,而是前两部分分析完成后自动推导出来的调度结果。 + +这意味着现在共享的不是某个模块实例里的私有字段,而是显式模型。 + +这点很重要,因为源生成器里真正稳定的共享方式就是: + +1. 共享模型 +2. 共享编译上下文 +3. 共享归一化后的分析结果 + +而不是模块之间互相暴露运行时对象。 + +--- + +## 6. Analysis 层 + +对应文件: + +1. `PanelStateIncrementalGenerator.Analysis.cs` +2. `PanelStateIncrementalGenerator.StateSymbols.cs` +3. `PanelStateIncrementalGenerator.StateChangeBindings.cs` +4. `PanelStateIncrementalGenerator.StateCommandHandlers.cs` + +这一层是当前结构里最关键的变化。 + +以前如果只做一个节点枚举生成器,那么 Pipeline 之后可以直接进 Emitter。 + +但现在不行,因为你已经明确有三块并列需求: + +1. StateSymbols +2. StateChangeBindings +3. StateChangeCommandHandlers + +所以中间必须有一个统一分析层。 + +它负责: + +1. 对每个类型目标进行统一调度 +2. 先调用前两个显式分析模块 +3. 再基于前两部分结果运行第三个派生调度阶段 +4. 把三个分析结果装配成一个类型级总分析结果 +5. 保证最终发射器看到的是完整上下文 + +这就是“一入口,三部分”的真正落点。 + +--- + +## 7. Emitter 层 + +对应文件: + +`PanelStateIncrementalGenerator.Emitter.cs` + +发射器当前还是一个骨架,但它已经不再只是输出“发现了哪些字段”,而是输出: + +1. 发现了哪些类型 +2. 每个类型在三部分里各命中了多少成员 + +这类 manifest 的作用不是业务交付,而是结构校验: + +1. 证明统一入口已经接通 +2. 证明类型级分析结果已经被汇总 +3. 证明发射器能看到三部分的完整结果 + +等后面要真正生成 enum、binding 路由、handler 路由时,这个文件会自然长大。 + +--- + +## 8. Diagnostics 层 + +对应文件: + +`PanelStateIncrementalGenerator.Diagnostics.cs` + +当前它依然很薄,这是正常的。 + +它的存在价值是为后续统一诊断留一个稳定出口,例如: + +1. 标记位置不合法 +2. 同一类型内声明冲突 +3. 某个 handler 签名不符合规则 +4. 某个 binding 指向不存在的状态路径 +5. 同一路由被重复生成 + +真正重要的不是现在有没有很多诊断,而是不要等逻辑长大后再到处散着写诊断定义。 + +--- + +## 9. 为什么必须是“一入口三部分” + +如果把这三块拆成三个彼此独立的生成器,会立刻遇到两个问题: + +1. 元数据重复扫描 +2. 诊断信息无法自然共享 + +更严重的是,多个生成器之间并不适合靠实例成员互相通信。 + +因此更合理的结构是: + +1. 一个根入口 +2. 一个共享模型 +3. 三个内部分析部分 +4. 一个统一发射出口 + +这就是现在这套结构背后的核心原则。 + +--- + +## 10. 当前阶段的定位 + +现在这套代码还只是“结构骨架”,不是“业务生成完成版”。 + +它已经完成的事情是: + +1. 根入口已经统一 +2. 输入粒度已经切到类型级 +3. 三部分内部分析已经独立成模块 +4. 发射器已经能看到完整汇总结果 + +还没有完成的事情是: + +1. 真实的状态符号生成 +2. 真实的状态变化绑定生成 +3. 真实的基于前两部分推导出的状态命令路由生成 +4. 真实的统一诊断规则 + +也就是说,当前目标不是“把功能做完”,而是先把以后不会推翻的结构搭稳。 + +但从工程实现看,它其实是几件不同的事情: + +1. Roslyn 入口接线 +2. 低成本筛候选 +3. 高成本做语义确认 +4. 把结果整理成稳定模型 +5. 输出代码 +6. 在不合法时报诊断 + +如果这几件事全写在一个文件里,短期看更短,长期一定更难改。 + +所以你可以把这套拆分理解成: + +> 它不是为了“显得标准”,而是为了让后面真正加业务逻辑时,不需要每次都从一坨 Roslyn 接线代码里翻东西。 + +--- + +## 9. 一个更直观的心智模型 + +你可以把当前框架理解成一条流水线: + +1. 入口层:把流水线接起来 +2. Pipeline:把矿石挑出来 +3. Models:把矿石做成标准坯料 +4. Emitter:把坯料加工成成品代码 +5. Diagnostics:发现废品时给出报错 + +如果写成一句更短的话: + +1. 找目标 +2. 抽模型 +3. 发代码 +4. 报问题 + +--- + +## 10. 你后面最可能怎么继续扩展 + +如果按你现在的 VSS 方向继续,这套框架下一步通常会往下面长: + +### 第一步:把 Target 变得更像“节点描述” + +例如不只是保存字段名,还保存: + +1. 节点名 +2. 所属 PanelState +3. 路径前缀 +4. 可生成成员集合 + +### 第二步:在 Emitter 中真正生成 enum + +例如: + +1. 每个 PanelState 对应一个 `xxxNode` enum +2. enum 成员由被标记字段自动生成 + +### 第三步:继续生成路由代码 + +例如: + +1. string path -> enum +2. enum -> setter/handler + +### 第四步:再接入受控状态写入与分布式 observer hook + +例如: + +1. pre-change handler 调用点 +2. set property +3. 由源生成器织入的轻量 post-change observer hook +4. 局部状态广播或内部同步 + +这里的 observer hook 只负责很薄的一层内部广播,不负责 view 重绘或业务执行。 +在 VSS 中,这两部分分别由 command/result 同步链路与 service 逻辑承担。 + +--- + +## 11. 对你当前项目的最简理解 + +如果你觉得这套骨架有点“过工程化”,可以先只记住下面这五句话: + +1. `IncrementalGenerator` 文件只是总入口,不负责细活。 +2. `Pipeline` 文件负责找出哪些代码需要生成。 +3. `Models` 文件负责把识别结果整理成稳定的数据结构。 +4. `Emitter` 文件负责把这些数据真正输出成 `.g.cs`。 +5. `Diagnostics` 文件负责以后统一放错误和警告定义。 + +你后面要改业务逻辑,主要会集中在 `Pipeline`、`Models`、`Emitter` 这三层。 + +--- + +## 12. 最后一句话 + +当前这套框架不是“生成器已经很复杂了”,而是“先把复杂度分栏放好”。 + +你后面真正写逻辑时,应该优先记住这个原则: + +1. Pipeline 决定“找谁” +2. Models 决定“怎么表示它” +3. Emitter 决定“怎么生成它” + +只要这三个边界不乱,后面的业务逻辑就算加很多,也还会比较稳。 diff --git a/StgSharp.UserInterface/Generator/PanelStateIncrementalGenerator.Analysis.cs b/StgSharp.UserInterface/Generator/PanelStateIncrementalGenerator.Analysis.cs new file mode 100644 index 00000000..ca0a146a --- /dev/null +++ b/StgSharp.UserInterface/Generator/PanelStateIncrementalGenerator.Analysis.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; + +namespace StgSharp.UserInterface.Generator; + +internal static class PanelStateIncrementalGeneratorAnalysis +{ + public static PanelStateIncrementalGeneratorOutput Analyze( + PanelStateIncrementalGeneratorInput input + ) + { + if (input.TypeTargets.IsDefaultOrEmpty) + { + return new PanelStateIncrementalGeneratorOutput( + input.Compilation, + ImmutableArray.Empty); + } + + HashSet emittedTypes = new(StringComparer.Ordinal); + ImmutableArray.Builder builder = + ImmutableArray.CreateBuilder(); + + foreach (PanelStateIncrementalGeneratorTypeTarget target in input.TypeTargets + .OrderBy(static item => item.TypeDisplayName, StringComparer.Ordinal)) + { + if (!emittedTypes.Add(target.TypeDisplayName)) + { + continue; + } + + PanelStateSymbolPartAnalysis stateSymbols = PanelStateIncrementalGeneratorStateSymbols.Analyze(target); + StateChangeBindingPartAnalysis stateChangeBindings = PanelStateIncrementalGeneratorStateChangeBindings.Analyze(target); + + builder.Add( + new PanelStateIncrementalGeneratorTypeAnalysis( + target, + stateSymbols, + stateChangeBindings, + PanelStateIncrementalGeneratorStateCommandHandlers.Analyze( + target, + stateSymbols, + stateChangeBindings))); + } + + return new PanelStateIncrementalGeneratorOutput( + input.Compilation, + builder.ToImmutable()); + } +} \ No newline at end of file diff --git a/StgSharp.UserInterface/Generator/PanelStateIncrementalGenerator.Architecture.md b/StgSharp.UserInterface/Generator/PanelStateIncrementalGenerator.Architecture.md new file mode 100644 index 00000000..7fa6a242 --- /dev/null +++ b/StgSharp.UserInterface/Generator/PanelStateIncrementalGenerator.Architecture.md @@ -0,0 +1,184 @@ +# PanelStateIncrementalGenerator 结构草案 + +本文档说明当前 `PanelStateIncrementalGenerator` 的设计目标: + +1. 只有一个增量生成器入口点 +2. 入口识别的是“类型声明”,不是单个字段 +3. 类型进入统一上下文后,内部拆成三个独立分析部分 +4. 三部分不互相依赖实例成员,只共享同一份中间模型 + +--- + +## 1. 为什么入口要识别类型声明 + +VSS 后续的生成需求不再只是“某个字段生成 enum”,而是围绕某个状态类型展开: + +1. 该类型有哪些公开状态符号 +2. 该类型有哪些状态变化绑定 +3. 基于前两者可以推导出哪些 command handler 路由入口 + +因此,最合适的根单位不是单个字段,而是一个类型声明。 + +也就是说,入口阶段做的事情应该是: + +1. 找出值得参与 VSS 生成的类型 +2. 收集该类型上所有已知标记成员 +3. 把这份类型级目标交给前两部分分析,再进入第三部分派生调度 + +--- + +## 2. 三部分内部分析 + +当前骨架把内部分析拆成三块: + +1. `StateSymbols` +2. `StateChangeBindings` +3. `StateChangeCommandHandlers` + +### 2.1 StateSymbols + +这一部分负责状态符号层。 + +目标是为后续生成这些东西打基础: + +1. 状态节点枚举 +2. 状态项元数据 +3. state 向量上的属性节点 +4. 节点到属性的静态映射 + +### 2.2 StateChangeBindings + +这一部分负责绑定层。 + +目标是分析: + +1. 哪些状态变化需要绑定到 handler +2. 哪些声明使用了 `StateChangeBinding` 之类的标记 +3. 后续需要生成怎样的静态接线代码 + +### 2.3 StateChangeCommandHandlers + +这一部分负责调度层。 + +它不是一个额外的 attribute 输入层,而是前两部分完成后自动运行的派生阶段。 + +目标是分析: + +1. 哪些状态符号和 binding 结果可以组成 command handler 路由入口 +2. 后续如何生成状态变化命令到 handler 的静态路由 +3. 如何在不依赖运行时字符串解释器的前提下构建调度表 + +--- + +## 3. 为什么三部分不能互相暴露实例成员 + +原因很简单: + +1. Roslyn 生成器不是运行时 IOC 容器 +2. 不应该设计成多个生成器实例互相通信 +3. 也不应该让一个分析模块依赖另一个模块的内部字段状态 + +正确方式是: + +1. 统一入口构建共享中间模型 +2. 三部分都基于这份中间模型分析 +3. 最终统一做诊断和发射 + +也就是说,共享的应该是: + +1. 类型级目标 +2. 已知标记成员 +3. 编译上下文 +4. 归一化后的分析结果 + +而不是: + +1. 某个模块里的字典字段 +2. 某个模块先运行出来的实例状态 +3. 依赖执行顺序的临时对象 + +--- + +## 4. 当前骨架中的文件分工 + +当前相关文件如下: + +1. `PanelStateIncrementalGenerator.cs` +2. `PanelStateIncrementalGenerator.Pipeline.cs` +3. `PanelStateIncrementalGenerator.Models.cs` +4. `PanelStateIncrementalGenerator.Analysis.cs` +5. `PanelStateIncrementalGenerator.StateSymbols.cs` +6. `PanelStateIncrementalGenerator.StateChangeBindings.cs` +7. `PanelStateIncrementalGenerator.StateCommandHandlers.cs` +8. `PanelStateIncrementalGenerator.Emitter.cs` +9. `PanelStateIncrementalGenerator.Diagnostics.cs` + +它们各自的职责是: + +1. `PanelStateIncrementalGenerator.cs` +负责总入口接线 + +2. `PanelStateIncrementalGenerator.Pipeline.cs` +负责从语法树中找出类型级候选,并收集已知标记成员 + +3. `PanelStateIncrementalGenerator.Models.cs` +负责共享中间模型和三部分分析结果模型 + +4. `PanelStateIncrementalGenerator.Analysis.cs` +负责协调三部分分析模块,并汇总出统一输出上下文 + +5. `PanelStateIncrementalGenerator.StateSymbols.cs` +负责状态符号层分析 + +6. `PanelStateIncrementalGenerator.StateChangeBindings.cs` +负责状态变化绑定层分析 + +7. `PanelStateIncrementalGenerator.StateCommandHandlers.cs` +负责基于前两部分结果派生状态变化命令处理层 + +8. `PanelStateIncrementalGenerator.Emitter.cs` +负责真正输出当前轮次的生成代码 + +9. `PanelStateIncrementalGenerator.Diagnostics.cs` +负责后续统一挂诊断定义 + +--- + +## 5. 当前状态 + +现在这套代码还只是骨架,业务逻辑没有真正接进去。 + +当前它已经具备的结构能力是: + +1. 入口已经从字段级切到类型级 +2. 已知标记成员会在 Pipeline 阶段被收集 +3. 内部已经形成“前两部分显式分析 + 第三部分派生调度”的结构 +4. 发射器已经能看到每个类型在三部分里各自命中了多少成员 + +也就是说,现在已经完成的是“结构立住”,还没有进入“具体生成 VSS 代码”的阶段。 + +--- + +## 6. 后续最自然的实现顺序 + +如果继续往下做,建议顺序是: + +1. 先完善 `StateSymbols` +2. 再完善 `StateChangeBindings` +3. 最后完善 `StateChangeCommandHandlers` + +原因是: + +1. 状态符号层是基础设施 +2. 绑定层建立在状态符号层之上 +3. command handler 调度层由前两层自动派生 + +所以三部分虽然是并列模块,但实现优先级并不是并列的。 + +--- + +## 7. 一句话总结 + +当前设计不是“三个生成器共享上下文”,而是: + +一个 `PanelStateIncrementalGenerator` 统一入口,识别类型声明,先完成前两部分分析,再自动进入第三部分派生调度。 diff --git a/StgSharp.UserInterface/Generator/PanelStateIncrementalGenerator.Diagnostics.cs b/StgSharp.UserInterface/Generator/PanelStateIncrementalGenerator.Diagnostics.cs new file mode 100644 index 00000000..fcc9f2df --- /dev/null +++ b/StgSharp.UserInterface/Generator/PanelStateIncrementalGenerator.Diagnostics.cs @@ -0,0 +1,6 @@ +namespace StgSharp.UserInterface.Generator; + +internal static class PanelStateIncrementalGeneratorDiagnostics +{ + public const string Category = "StgSharp.UserInterface.Generator"; +} \ No newline at end of file diff --git a/StgSharp.UserInterface/Generator/PanelStateIncrementalGenerator.Emitter.cs b/StgSharp.UserInterface/Generator/PanelStateIncrementalGenerator.Emitter.cs new file mode 100644 index 00000000..8b615b17 --- /dev/null +++ b/StgSharp.UserInterface/Generator/PanelStateIncrementalGenerator.Emitter.cs @@ -0,0 +1,66 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; +using System.Linq; +using System.Text; + +namespace StgSharp.UserInterface.Generator +{ + internal static class PanelStateIncrementalGeneratorEmitter + { + + public static void Emit( + SourceProductionContext context, + PanelStateIncrementalGeneratorOutput source + ) + { + if (source.TypeAnalyses.IsDefaultOrEmpty) { + return; + } + + StringBuilder builder = new(); + _ = builder.AppendLine("// "); + _ = builder.AppendLine("#nullable enable"); + _ = builder.AppendLine(); + _ = builder.AppendLine("namespace StgSharp.UserInterface.Generator"); + _ = builder.AppendLine("{"); + _ = builder.AppendLine(" internal static class PanelStateIncrementalGeneratorManifest"); + _ = builder.AppendLine(" {"); + _ = builder.AppendLine(" /*"); + _ = builder.AppendLine(" internal static string[] DiscoveredTypes { get; } = new string[]"); + _ = builder.AppendLine(" {"); + + foreach (string typeName in source.TypeAnalyses + .Select(static analysis => analysis.Target.TypeDisplayName) + .Distinct() + .OrderBy(static name => name, StringComparer.Ordinal)) + { + _ = builder.Append(" \""); + _ = builder.Append(typeName.Replace("\"", "\"\"", StringComparison.Ordinal)); + _ = builder.AppendLine("\","); + } + + _ = builder.AppendLine(" };"); + _ = builder.AppendLine(); + _ = builder.AppendLine(" internal static string[] AnalysisSummary { get; } = new string[]"); + _ = builder.AppendLine(" {"); + + foreach (PanelStateIncrementalGeneratorTypeAnalysis analysis in source.TypeAnalyses + .OrderBy(static item => item.Target.TypeDisplayName, StringComparer.Ordinal)) + { + string summary = $"{analysis.Target.TypeDisplayName} | symbols={analysis.StateSymbols.Members.Length} | bindings={analysis.StateChangeBindings.Members.Length} | handlers={analysis.StateChangeCommandHandlers.Members.Length}"; + _ = builder.Append(" \""); + _ = builder.Append(summary.Replace("\"", "\"\"", StringComparison.Ordinal)); + _ = builder.AppendLine("\","); + } + + _ = builder.AppendLine(" };"); + _ = builder.AppendLine(" }"); + _ = builder.AppendLine("}"); + + context.AddSource( + "PanelStateIncrementalGeneratorManifest.g.cs", + SourceText.From(builder.ToString(), Encoding.UTF8)); + } + + } +} diff --git a/StgSharp.UserInterface/Generator/PanelStateIncrementalGenerator.Models.cs b/StgSharp.UserInterface/Generator/PanelStateIncrementalGenerator.Models.cs new file mode 100644 index 00000000..c2846e99 --- /dev/null +++ b/StgSharp.UserInterface/Generator/PanelStateIncrementalGenerator.Models.cs @@ -0,0 +1,73 @@ +using Microsoft.CodeAnalysis; +using System.Collections.Immutable; + +namespace StgSharp.UserInterface.Generator +{ + internal static class PanelStateIncrementalGeneratorKnownAttributes + { + + public const string PanelStateNodeAttribute = "StgSharp.UserInterface.PanelStateNodeAttribute"; + + public const string StateChangeBindingAttribute = "StgSharp.UserInterface.StateChangeBindingAttribute"; + + public static bool IsKnown( + string? metadataName + ) + { + return IsStateSymbolAttribute(metadataName) || metadataName == StateChangeBindingAttribute; + } + + public static bool IsStateSymbolAttribute( + string? metadataName + ) + { + return metadataName == PanelStateNodeAttribute; + } + + } + + internal sealed record PanelStateIncrementalGeneratorMarkedMember( + ISymbol Symbol, + string AttributeMetadataName, + string SymbolName, + string SymbolTypeName + ); + + internal sealed record PanelStateIncrementalGeneratorTypeTarget( + INamedTypeSymbol Type, + string TypeDisplayName, + ImmutableArray MarkedMembers + ); + + internal readonly record struct PanelStateIncrementalGeneratorInput( + Compilation Compilation, + ImmutableArray TypeTargets + ); + + internal sealed record PanelStateIncrementalGeneratorTypeAnalysis( + PanelStateIncrementalGeneratorTypeTarget Target, + PanelStateSymbolPartAnalysis StateSymbols, + StateChangeBindingPartAnalysis StateChangeBindings, + StateChangeCommandHandlerPartAnalysis StateChangeCommandHandlers + ); + + internal readonly record struct PanelStateIncrementalGeneratorOutput( + Compilation Compilation, + ImmutableArray TypeAnalyses + ); + + internal sealed record PanelStateSymbolPartAnalysis( + string OwnerTypeName, + ImmutableArray Members + ); + + internal sealed record StateChangeBindingPartAnalysis( + string OwnerTypeName, + ImmutableArray Members + ); + + internal sealed record StateChangeCommandHandlerPartAnalysis( + string OwnerTypeName, + ImmutableArray Members + ); +} diff --git a/StgSharp.UserInterface/Generator/PanelStateIncrementalGenerator.Pipeline.cs b/StgSharp.UserInterface/Generator/PanelStateIncrementalGenerator.Pipeline.cs new file mode 100644 index 00000000..8d6d5948 --- /dev/null +++ b/StgSharp.UserInterface/Generator/PanelStateIncrementalGenerator.Pipeline.cs @@ -0,0 +1,108 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System.Threading; + +namespace StgSharp.UserInterface.Generator +{ + internal static class PanelStateIncrementalGeneratorPipeline + { + + public static bool IsCandidate( + SyntaxNode node + ) + { + return node is TypeDeclarationSyntax typeDeclaration && HasAttributedMembers(typeDeclaration); + } + + public static PanelStateIncrementalGeneratorTypeTarget? Transform( + GeneratorSyntaxContext context, + CancellationToken cancellationToken + ) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (context.Node is not TypeDeclarationSyntax typeDeclaration) { + return null; + } + + if (context.SemanticModel.GetDeclaredSymbol(typeDeclaration, cancellationToken) is not INamedTypeSymbol typeSymbol) + { + return null; + } + + ImmutableArray markedMembers = CollectMarkedMembers(typeSymbol); + if (markedMembers.IsDefaultOrEmpty) + { + return null; + } + + return new PanelStateIncrementalGeneratorTypeTarget( + typeSymbol, + typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + markedMembers); + } + + private static bool HasAttributedMembers( + TypeDeclarationSyntax typeDeclaration + ) + { + foreach (MemberDeclarationSyntax member in typeDeclaration.Members) + { + switch (member) + { + case FieldDeclarationSyntax { AttributeLists.Count: > 0 }: + case PropertyDeclarationSyntax { AttributeLists.Count: > 0 }: + case MethodDeclarationSyntax { AttributeLists.Count: > 0 }: + return true; + } + } + + return false; + } + + private static ImmutableArray CollectMarkedMembers( + INamedTypeSymbol typeSymbol + ) + { + ImmutableArray.Builder builder = + ImmutableArray.CreateBuilder(); + + foreach (ISymbol member in typeSymbol.GetMembers()) + { + foreach (AttributeData attributeData in member.GetAttributes()) + { + string? metadataName = attributeData.AttributeClass?.ToDisplayString(); + if (!PanelStateIncrementalGeneratorKnownAttributes.IsKnown(metadataName)) + { + continue; + } + + builder.Add( + new PanelStateIncrementalGeneratorMarkedMember( + member, + metadataName!, + member.Name, + GetSymbolTypeName(member))); + break; + } + } + + return builder.ToImmutable(); + } + + private static string GetSymbolTypeName( + ISymbol symbol + ) + { + return symbol switch + { + IFieldSymbol fieldSymbol => fieldSymbol.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + IPropertySymbol propertySymbol => propertySymbol.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + IMethodSymbol methodSymbol => methodSymbol.ReturnType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + _ => symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) + }; + } + + } +} diff --git a/StgSharp.UserInterface/Generator/PanelStateIncrementalGenerator.StateChangeBindings.cs b/StgSharp.UserInterface/Generator/PanelStateIncrementalGenerator.StateChangeBindings.cs new file mode 100644 index 00000000..3df2bc18 --- /dev/null +++ b/StgSharp.UserInterface/Generator/PanelStateIncrementalGenerator.StateChangeBindings.cs @@ -0,0 +1,20 @@ +using System.Collections.Immutable; +using System.Linq; + +namespace StgSharp.UserInterface.Generator; + +internal static class PanelStateIncrementalGeneratorStateChangeBindings +{ + public static StateChangeBindingPartAnalysis Analyze( + PanelStateIncrementalGeneratorTypeTarget target + ) + { + ImmutableArray members = target.MarkedMembers + .Where(static member => member.AttributeMetadataName == PanelStateIncrementalGeneratorKnownAttributes.StateChangeBindingAttribute) + .ToImmutableArray(); + + return new StateChangeBindingPartAnalysis( + target.TypeDisplayName, + members); + } +} \ No newline at end of file diff --git a/StgSharp.UserInterface/Generator/PanelStateIncrementalGenerator.StateCommandHandlers.cs b/StgSharp.UserInterface/Generator/PanelStateIncrementalGenerator.StateCommandHandlers.cs new file mode 100644 index 00000000..c22dc4f6 --- /dev/null +++ b/StgSharp.UserInterface/Generator/PanelStateIncrementalGenerator.StateCommandHandlers.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; + +namespace StgSharp.UserInterface.Generator; + +internal static class PanelStateIncrementalGeneratorStateCommandHandlers +{ + public static StateChangeCommandHandlerPartAnalysis Analyze( + PanelStateIncrementalGeneratorTypeTarget target, + PanelStateSymbolPartAnalysis stateSymbols, + StateChangeBindingPartAnalysis stateChangeBindings + ) + { + ImmutableArray members = MergeMembers( + stateChangeBindings.Members, + stateSymbols.Members); + + return new StateChangeCommandHandlerPartAnalysis( + target.TypeDisplayName, + members); + } + + private static ImmutableArray MergeMembers( + ImmutableArray primaryMembers, + ImmutableArray secondaryMembers + ) + { + if (primaryMembers.IsDefaultOrEmpty && secondaryMembers.IsDefaultOrEmpty) + { + return ImmutableArray.Empty; + } + + HashSet seen = new(StringComparer.Ordinal); + ImmutableArray.Builder builder = + ImmutableArray.CreateBuilder(); + + AddMembers(builder, seen, primaryMembers); + AddMembers(builder, seen, secondaryMembers); + + return builder.ToImmutable(); + } + + private static void AddMembers( + ImmutableArray.Builder builder, + HashSet seen, + ImmutableArray members + ) + { + foreach (PanelStateIncrementalGeneratorMarkedMember member in members) + { + string key = member.Symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + if (!seen.Add(key)) + { + continue; + } + + builder.Add(member); + } + } +} \ No newline at end of file diff --git a/StgSharp.UserInterface/Generator/PanelStateIncrementalGenerator.StateSymbols.cs b/StgSharp.UserInterface/Generator/PanelStateIncrementalGenerator.StateSymbols.cs new file mode 100644 index 00000000..486d0c89 --- /dev/null +++ b/StgSharp.UserInterface/Generator/PanelStateIncrementalGenerator.StateSymbols.cs @@ -0,0 +1,20 @@ +using System.Collections.Immutable; +using System.Linq; + +namespace StgSharp.UserInterface.Generator; + +internal static class PanelStateIncrementalGeneratorStateSymbols +{ + public static PanelStateSymbolPartAnalysis Analyze( + PanelStateIncrementalGeneratorTypeTarget target + ) + { + ImmutableArray members = target.MarkedMembers + .Where(static member => PanelStateIncrementalGeneratorKnownAttributes.IsStateSymbolAttribute(member.AttributeMetadataName)) + .ToImmutableArray(); + + return new PanelStateSymbolPartAnalysis( + target.TypeDisplayName, + members); + } +} \ No newline at end of file diff --git a/StgSharp.UserInterface/Generator/PanelStateIncrementalGenerator.cs b/StgSharp.UserInterface/Generator/PanelStateIncrementalGenerator.cs new file mode 100644 index 00000000..c47eb069 --- /dev/null +++ b/StgSharp.UserInterface/Generator/PanelStateIncrementalGenerator.cs @@ -0,0 +1,37 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; + +namespace StgSharp.UserInterface.Generator; + +[Generator(LanguageNames.CSharp)] +public sealed class PanelStateIncrementalGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + IncrementalValuesProvider typeTargets = context.SyntaxProvider + .CreateSyntaxProvider( + static (node, _) => PanelStateIncrementalGeneratorPipeline.IsCandidate(node), + static (syntaxContext, cancellationToken) => + PanelStateIncrementalGeneratorPipeline.Transform(syntaxContext, cancellationToken)) + .Where(static target => target is not null) + .Select(static (target, _) => target!); + + IncrementalValueProvider> collectedTypeTargets = typeTargets.Collect(); + + IncrementalValueProvider generatorInput = context.CompilationProvider + .Combine(collectedTypeTargets) + .Select( + static (pair, _) => new PanelStateIncrementalGeneratorInput( + pair.Left, + pair.Right)); + + IncrementalValueProvider generatorOutput = generatorInput + .Select( + static (input, _) => PanelStateIncrementalGeneratorAnalysis.Analyze(input)); + + context.RegisterSourceOutput( + generatorOutput, + static (productionContext, source) => + PanelStateIncrementalGeneratorEmitter.Emit(productionContext, source)); + } +} \ No newline at end of file diff --git a/StgSharp.UserInterface/Global.cs b/StgSharp.UserInterface/Global.cs new file mode 100644 index 00000000..fc3655a4 --- /dev/null +++ b/StgSharp.UserInterface/Global.cs @@ -0,0 +1,5 @@ + +global using System; +global using System.Collections.Generic; +global using System.Runtime.CompilerServices; +global using System.Runtime.InteropServices; diff --git a/StgSharp.UserInterface/Sample/PanelStateSample.cs b/StgSharp.UserInterface/Sample/PanelStateSample.cs new file mode 100644 index 00000000..673b278c --- /dev/null +++ b/StgSharp.UserInterface/Sample/PanelStateSample.cs @@ -0,0 +1,65 @@ +using System; + +namespace StgSharp.UserInterface.Sample +{ + public sealed class PanelStateSample : PanelState + { + public const string SampleStatePath = nameof(PanelStateSample) + "." + nameof(SampleState); + + private int sampleState; + + public PanelStateSample() + : base(nameof(PanelStateSample)) + { + } + + public int SampleState => sampleState; + + // 生成器拆分后,这里相当于某个节点 handler 的 guard, + // 对应 ICommand 语义中的 CanExecute。 + private static bool CanApplySampleStateChange( + int origin, + int current, + StateValueCommand command + ) + { + return string.Equals(command.StatePath, SampleStatePath, StringComparison.Ordinal) + && current >= 0 + && origin != current; + } + + // 实际项目里这整个 StateChangeHandler 都由源生成器产出; + // 这里手写展开,只是为了展示“没有生成器时 VSS 代码长什么样”。 + public void ApplySampleStateChangeCommand( + StateValueCommand command + ) + { + ArgumentNullException.ThrowIfNull(command); + + int origin = sampleState; + int current = command.Value; + if (!CanApplySampleStateChange(origin, current, command)) + { + return; + } + + sampleState = current; + BroadcastSampleStateChanged(origin, current); + } + + // 生成器拆分后,这里相当于某个节点写入后的 post-change hook, + // 对应手写体系里常见的 OnChange 位置。 + private static void BroadcastSampleStateChanged( + int origin, + int current + ) + { + _ = origin; + _ = current; + + // event 已禁用。手写版本若还需要状态后置联动, + // 这里只能继续手写内部同步/广播逻辑;正式版本由生成器织入。 + } + + } +} diff --git a/StgSharp.UserInterface/Service/IService.cs b/StgSharp.UserInterface/Service/IService.cs deleted file mode 100644 index 5ee1ec47..00000000 --- a/StgSharp.UserInterface/Service/IService.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace StgSharp.UnserInterface.Service -{ - internal interface IService - { - } -} diff --git a/StgSharp.UserInterface/State/IState.cs b/StgSharp.UserInterface/State/IState.cs deleted file mode 100644 index ae238bf8..00000000 --- a/StgSharp.UserInterface/State/IState.cs +++ /dev/null @@ -1,37 +0,0 @@ -//----------------------------------------------------------------------- -// ----------------------------------------------------------------------- -// file="IState" -// Project: StgSharp -// AuthorGroup: Nitload Space -// Copyright (c) Nitload Space. All rights reserved. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// -// ----------------------------------------------------------------------- -// ----------------------------------------------------------------------- -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace StgSharp.UnserInterface.State -{ - internal class IState { } -} diff --git a/StgSharp.UserInterface/StgSharp.UnserInterface.csproj b/StgSharp.UserInterface/StgSharp.UnserInterface.csproj deleted file mode 100644 index b4b1d3ea..00000000 --- a/StgSharp.UserInterface/StgSharp.UnserInterface.csproj +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/StgSharp.UserInterface/StgSharp.UserInterface.csproj b/StgSharp.UserInterface/StgSharp.UserInterface.csproj new file mode 100644 index 00000000..7af58732 --- /dev/null +++ b/StgSharp.UserInterface/StgSharp.UserInterface.csproj @@ -0,0 +1,29 @@ + + + + true + true + true + true + $(BaseIntermediateOutputPath)Generated + $(SourceRoot)StgSharp.Common\bin\$(Platform)\$(Configuration)\$(TargetFramework)\StgSharp.Common.dll + $(SourceRoot)StgSharp.Common\bin\$(Platform)\$(Configuration)\$(TargetFramework)\StgSharp.Common.pdb + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/StgSharp.UserInterface/VSS_Arch.md b/StgSharp.UserInterface/VSS_Arch.md index f05583a8..2757e692 100644 --- a/StgSharp.UserInterface/VSS_Arch.md +++ b/StgSharp.UserInterface/VSS_Arch.md @@ -3,473 +3,311 @@ > **作者**:Nitload-NSI > **创建日期**:2026-03-17 > **所属项目**:StgSharp 框架 / 工业上位机架构预演 -> **文档状态**:初稿 +> **文档状态**:精简版草案 --- -## 目录 +## 1. 文档定位 -1. [背景与动机](#1-背景与动机) -2. [核心思想](#2-核心思想) -3. [架构总览](#3-架构总览) -4. [V 层:I/O 适配层](#4-v-层io-适配层) -5. [S 层:状态机](#5-s-层状态机) -6. [S 层:服务层](#6-s-层服务层) -7. [层间通信协议](#7-层间通信协议) -8. [与 MVVM 的对比](#8-与-mvvm-的对比) -9. [适用场景](#9-适用场景) -10. [参考实现索引](#10-参考实现索引) +本文档用于说明 VSS 的整体架构思想、三层职责划分与层间关系。 +具体类型定义、命名与边界,以《VSS 三层类型基座设计草案》为准。 + +当前版本需要特别强调一点: +State 层不再被描述为几个并列的中枢对象,而是对外统一表现为一个单例 `StateHost`。 --- -## 1. 背景与动机 +## 2. 设计评估 -### 1.1 MVVM 的局限 +把整个 State 层对外收敛为 `StateHost` 单例,是比旧描述更合理的做法。 -MVVM(Model-View-ViewModel)是一种面向"轻型商用软件"设计的 UI 架构模式。其隐含前提为: +原因有三点: -- **Model 是被动的数据容器**,不具备独立生命周期 -- **ViewModel 是核心调度者**,负责连接 View 与 Model -- **View 由数据变化驱动**(双向绑定) +- 它明确了唯一中枢,避免旧版文档中的多个中枢概念同时被读成并列核心。 +- 它明确了所有权,说明命令接入、状态抽象和服务管理都属于同一个运行时边界。 +- 它更符合宿主型系统的实际组织方式,对外暴露一个主机,对内再展开结构,比暴露多个概念性中枢更稳定。 -这一前提在以下场景中失效: +这个设计也有一个边界需要保留: +虽然 `ServiceManager` 现在被放入 `StateHost` 的下层,但这并不意味着状态与能力本体被混为一层。 +`AppState` 仍然是状态抽象,`ServiceManager` 仍然只是服务管理结构,具体服务对象依然属于能力层。 -| 场景 | 失效原因 | -|---|---| -| 工业上位机 | 设备/硬件具有独立生命周期和状态机,不是"被动数据" | -| 仿真集群软件 | 计算节点有自己的异步事件流,不受 ViewModel 调度 | -| 多输入源 HMI | 鼠标、专用手柄、实体按钮等多源输入在 View 层打架 | -| 重型可视化软件 | ViewModel 膨胀为"上帝类",承担了所有业务逻辑 | - -### 1.2 直接动机 - -本架构的直接动机来源于维护一套 WinForms + MVP 上位机项目时,**鼠标与专用手柄两种输入模式相互干扰**的问题。 - -解决思路的核心转变: - -> **外设事件不应提交给 View,而应提交给 State。** -> View 只负责将当前 State 状态呈现出来。 - -这一转变最终演化为完整的 VSS 架构。 - -### 1.3 设计血统 - -VSS 的部分设计思路借鉴自**游戏引擎架构**,核心哲学为: - -> **以输入为核心,以逻辑为驱动,界面显示是被动的副产品。** - -这与 Unity New Input System、虚幻引擎 Enhanced Input 等现代游戏输入系统的设计理念一脉相承。 - ---- - -## 2. 核心思想 - -### 2.1 两个根本原则 - -**原则一:所有"意图"必须经过 State 归一化** - -``` -鼠标点击 ──┐ -手柄按键 ──┼──→ State.Dispatch(Intent) ──→ 统一的状态转移逻辑 -实体按钮 ──┤ -网络指令 ──┘ -``` - -无论信号来源如何,进入 State 之前都转化为统一的 `Intent`。State 是**唯一的语义解释者**。 - -**原则二:View 层可以完全换掉或关闭** - -``` -有头模式: V(WPF) + State + Service (操作员在场) -无头模式: State + Service (自动化运行/集群节点) -远程模式: V(薄客户端) + State + Service (远程接管显示) -HMI 模式: V(实体面板) + State + Service (无屏幕操作台) -``` - -State 和 Service 在任何模式下**零改动**。 - -### 2.2 职责边界的时间维度 - -不同层次的操作具有根本不同的时间特征,这是分层的真正依据: - -| 层 | 时间特征 | 驱动方式 | -|---|---|---| -| V(渲染输出) | 帧级,毫秒级刷新 | 时钟主动驱动 | -| V(输入采集) | 事件级,异步到达 | 中断/回调驱动 | -| State | 事件队列消费,微秒~毫秒 | 串行队列驱动 | -| Service | 业务流程,毫秒~分钟 | Command 触发 | -| 设备/集群节点 | 独立生命周期,跨越整个运行期 | 自驱动 | - -让 ViewModel 同时承担所有时间维度的调度,是 MVVM 在重型软件中失败的根本原因。 +另外,前侧命令链路只定义命令而不定义结果也是不完整的。 +如果 `FrontCommandHost` 已经区分 `StateChangeCommand` 与 `ExecuteCommand`,那么还必须存在统一的 `CommandResult`,用于把执行完成状态回传给 View。 --- ## 3. 架构总览 -``` -┌────────────────────────────────────────────────────────────┐ -│ V — View Layer(I/O 适配层集合) │ -│ │ -│ ┌─────────────────────────┐ ┌────────────────────────┐ │ -│ │ 输入适配器 │ │ 输出适配器 │ │ -│ │ IInputAdapter │ │ IOutputAdapter │ │ -│ │ │ │ │ │ -│ │ · WpfInputAdapter │ │ · WpfPresenter │ │ -│ │ 鼠标/键盘/触摸 │ │ 帧时钟驱动 XAML 刷新 │ │ -│ │ │ │ │ │ -│ │ · HmiInputAdapter │ │ · HmiOutputAdapter │ │ -│ │ 实体按钮/旋钮/急停 │ │ LED/炫彩/面板小屏 │ │ -│ │ │ │ │ │ -│ │ · DeviceInputAdapter │ │ · NullOutputAdapter │ │ -│ │ 专用手柄/外接设备 │ │ 无头模式,丢弃输出 │ │ -│ └──────────┬──────────────┘ └───────────┬────────────┘ │ -└─────────────┼──────────────────────────────┼───────────────┘ - │ Intent(原始输入,无语义) │ ← Snapshot(只读快照) - ↓ ↑ -┌─────────────────────────────────────────────────────────────┐ -│ S — State(抽象状态机) │ -│ │ -│ · 串行 Intent 队列,保证状态转移原子性 │ -│ · 持有所有 UI 可见状态,生成 Snapshot 通知 V 层 │ -│ · 接收所有输入源:V 层 Intent + Service Event │ -│ · 向 Service 发送 Command(携带完整上下文) │ -│ · 不持有 Service 具体实现引用(依赖接口) │ -│ · 不知道 V 层的存在 │ -└───────────────────────────┬─────────────────────────────────┘ - │ Command(有语义指令,携带完整上下文) - ↕ Event(异步完成通知) -┌───────────────────────────────────────────────────────────────┐ -│ S — Service Layer(业务服务层) │ -│ │ -│ · 每个 Service 拥有独立生命周期和内部状态机 │ -│ · 执行具体业务:设备控制、集群调度、数据持久化、通信协议等 │ -│ · 通过 Event 将异步结果回传给 State │ -│ · 完全不知道 V 层的存在 │ -│ · Command 应自描述,Service 执行时无需回调 State │ -└───────────────────────────────────────────────────────────────┘ -``` +VSS 当前建议的核心结构如下: + +View / Electron / HMI / 外设 / 其他宿主 +↓ +StateHost(单例) +├─ 上层:FrontCommandHost +├─ 中层:AppState +└─ 下层:ServiceManager +↓ +Service / ServiceHost / 具体能力实例 + +这个结构表达的是: + +1. 对外,State 层只有一个统一入口,即 `StateHost`。 +2. 对内,`StateHost` 全息展开为上中下三层。 +3. 服务能力通过 `ServiceManager` 被纳入统一调度,而不是直接散落在外部结构中。 + +其基本流程可以概括为: + +1. 外部宿主产生交互或控制请求。 +2. 请求进入 `StateHost`。 +3. `StateHost` 的上层 `FrontCommandHost` 接收并区分 `StateChangeCommand` 与 `ExecuteCommand`。 +4. `StateHost` 的中层 `AppState` 参与状态判断与状态更新。 +5. 当命令涉及能力执行时,`StateHost` 的下层 `ServiceManager` 负责调度服务。 +6. 命令处理完成后,`StateHost` 统一生成 `CommandResult` 回传给 View。 +7. 状态变化与结果通知分别承担状态同步和执行完成通知两类职责。 --- -## 4. V 层:I/O 适配层 +## 4. View 层 -### 4.1 重新定义"View" +### 4.1 定位 -VSS 中的 V **不是"界面"**,而是**"所有与外部世界的 I/O 适配层的集合"**。 +View 层是前侧交互来源的统一抽象。 +它不等同于传统窗口或页面,而是所有外部交互入口与宿主环境的集合。 -``` -V 层 = 输入适配器集合 ∪ 输出适配器集合 -``` +View 层只负责两件事: -这一定义来源于对"实体 HMI 也是一种 View"的认识: +- 承接交互 +- 将交互转化为命令并送入 `StateHost` -- 实体按钮面板:有输入(按键),有输出(指示灯、小屏幕、炫彩灯带) -- WPF 窗口:有输入(鼠标/键盘/触摸),有输出(屏幕渲染) -- 无头节点:无 V 层,State + Service 直接运行 +同时,View 还需要能够接收由 `StateHost` 回传的 `CommandResult`,从而知道某次执行型命令是否已经完成。 -### 4.2 输入适配器(IInputAdapter) +它不再承担中枢职责,也不直接持有服务调度责任。 -```csharp -public interface IInputAdapter -{ - void Start(Action dispatch); - void Stop(); -} -``` +### 4.2 基础类型 -职责: -- 监听来自特定输入源的原始信号 -- 将原始信号转化为 `Intent`(**不做语义判断**) -- 通过 `dispatch` 委托投递到 State 的事件队列 +#### ViewHost -**关键约束**:输入适配器只描述"发生了什么",不判断"应该做什么"。 +`ViewHost` 是前侧宿主的统一抽象,用于表示任何可以承接界面表现、输入来源或外部交互来源的宿主对象。 -### 4.3 输出适配器(IOutputAdapter)与 Presenter +#### StateChangeCommand -```csharp -public interface IOutputAdapter -{ - void Start(); - void Stop(); - void AcceptSnapshot(StateSnapshot snapshot); // 可能在任意线程调用 -} -``` +`StateChangeCommand` 用于表达对系统状态的直接修改请求。 -**Presenter 是输出适配器的一种**,负责屏幕渲染输出。 +#### ExecuteCommand -WPF 环境下,Presenter 挂载于 `CompositionTarget.Rendering`——这是 WPF 暴露的帧循环钩子,与显示器刷新率同步(通常 60fps)。 +`ExecuteCommand` 用于表达需要进入执行流程的命令请求。 -``` -State.Commit() - ↓ AcceptSnapshot()(任意线程,原子写 _pending) -CompositionTarget.Rendering(WPF 帧时钟,UI线程) - ↓ 检测 _pending 变化 - ↓ PushToView() -View.DataContext = snapshot(触发 XAML 绑定刷新) -``` +#### CommandResult -**帧合并机制**:State 在两帧之间可能产生多个 Snapshot(如高频设备数据更新),Presenter 的帧循环只取最新的 `_pending`,自动合并中间状态,保持渲染频率稳定。 +`CommandResult` 用于表达某次前侧命令处理完成后的统一结果通知。 -非屏幕输出适配器(如 HMI 灯带)使用独立低频定时器,无需挂载帧时钟。 +### 4.3 小结 -### 4.4 ViewLayer 容器 +View 层的职责不是承载业务逻辑,而是把复杂、多样、异构的交互来源收敛成稳定命令,然后统一送入 `StateHost`。 -```csharp -public class ViewLayer -{ - public ViewLayer Add(IInputAdapter adapter); - public ViewLayer Add(IOutputAdapter adapter); - public void Start(Action dispatch); - public void Stop(); - public void AcceptSnapshot(StateSnapshot snapshot); // 广播给所有输出适配器 -} -``` - -V 层对外只暴露两个接口: -- `Start(dispatch)`:启动所有适配器,传入 State 的 Dispatch 入口 -- `AcceptSnapshot(snapshot)`:接收新状态,广播给所有输出适配器 - -### 4.5 WPF 中的 XAML 规范 - -在 VSS 架构下,WPF 的 XAML 层应遵循以下规范: - -``` -✅ 允许: - · 纯布局声明(Grid、StackPanel、Border 等) - · DataContext 绑定({Binding PropertyName},单向或双向均可) - · Code-behind 中的事件转发(OnXxxClicked → Dispatch(Intent)) - -❌ 禁止: - · XAML 中的 Command 绑定(ICommand、RelayCommand 等) - · Code-behind 中的业务逻辑判断 - · ViewModel 类(无 ViewModel 层) - · Code-behind 直接调用 Service -``` - -Code-behind 的唯一职责: - -```csharp -// 用户操作 → 转发为 Intent,不做任何判断 -private void OnPrintClicked(object s, RoutedEventArgs e) - => _dispatch?.Invoke(new PrintRequestedIntent()); -``` +对于执行类命令,View 不能只依赖状态变化判断是否结束,还应通过 `CommandResult` 获得明确的完成通知。 --- -## 5. S 层:状态机 +## 5. State 层 -### 5.1 职责定义 +### 5.1 定位 -State 是 VSS 的核心,也是**唯一持有应用状态的地方**。 +State 层是 VSS 的唯一状态与调度中枢。 +它对外统一表现为 `StateHost` 单例,对内展开为三层结构。 -``` -State 做且只做: - 1. 接收 Intent - 2. 决策状态如何转移 - 3. 向 Service 发 Command - 4. 生成新 Snapshot 通知 V 层 -``` +### 5.2 内部结构 -State **不做**: -- 任何 IO 操作 -- 任何耗时计算 -- 直接操作 UI 控件 -- 持有 Service 的具体实现(依赖接口) +#### StateHost -### 5.2 串行事件队列 +`StateHost` 是 State 层唯一对外暴露的运行时主机。 -State 内部维护一个串行消费队列,**所有输入源**(V 层 Intent + Service Event)都通过同一个队列进入: +它负责: -``` -[WPF鼠标事件] ──┐ -[手柄按键] ──┤ -[实体按钮] ──┼──→ Channel ──→ State(单线程消费) -[Service完成] ──┤ -[定时器触发] ──┘ -``` +- 接收来自外部宿主的命令 +- 统一组织命令接入、状态抽象和服务管理 +- 统一处理状态同步、事件广播、服务结果回流和 `CommandResult` 回传 -**意义**:状态转移是原子的,不会因并发输入导致内部状态撕裂。这是简化版的 **Actor 模型**。 +#### 上层:FrontCommandHost -### 5.3 StateSnapshot +`FrontCommandHost` 是 `StateHost` 的上层,用于承接前侧命令输入。 +它属于 State 内部结构,不再被视为 View 层自己的中介中心。 -Snapshot 是 State 暴露给 V 层的**只读数据快照**,使用 C# `record` 定义以保证不可变性: +在命令协议上,`FrontCommandHost` 至少要面对两类前向命令: -```csharp -public record StateSnapshot -{ - // 所有字段均为 init-only,外部无法修改 - public bool IsBusy { get; init; } - public string StatusMessage { get; init; } = ""; - // ... 其他字段 -} -``` +- `StateChangeCommand` +- `ExecuteCommand` -State 内部通过 `with` 表达式生成新 Snapshot: +对应地,State 在处理完成后还需要回传统一的 `CommandResult`。 -```csharp -Commit(_snapshot with { IsBusy = true, StatusMessage = "处理中..." }); -``` +#### 中层:AppState -### 5.4 Intent 与 Command 的区别 +`AppState` 是 `StateHost` 的中层状态抽象,是全局状态树根。 +它代表状态本体,而不是状态宿主。 -| | Intent | Command | -|---|---|---| -| **方向** | V → State | State → Service | -| **语义** | 无(描述事实) | 有(描述意图) | -| **产生者** | V 层适配器 | State 状态机 | -| **消费者** | State | Service | -| **示例** | `MouseClickedIntent(x, y)` | `StartSimulationCommand(config)` | +#### 下层:ServiceManager -**同一个 Command 可以由多种 Intent 触发**,这是多输入源统一的关键。 +`ServiceManager` 是 `StateHost` 的下层服务管理结构。 +它负责把命令向下转化为具体能力调度,并将结果回流状态体系。 + +#### 配套结构 + +State 层还包括: + +- `PanelState`,用于表示局部状态单元 + +此外,observer 语义仍然存在,但在 `StgSharp.UserInterface` 中不再通过 `OnStateChanged` / `StateChangeObserver` 这类手写公共类型表达。 + +这里更准确的说法是:状态写入后的轻量 observer 逻辑,改为由源生成器围绕具体状态节点直接分布式织入。 + +在传统 MVVM 中,observer 往往同时承担: + +- 内部逻辑回调 +- VM 内部广播变化 +- 通知 View 重绘 +- 通知 Data / 业务执行 + +而在 VSS 中,这四项职责被拆开处理: + +- 内部逻辑回调:由 Service 或具体业务实现承担 +- VM 内部广播变化:由源生成器生成的轻量 observer hook 承担 +- 通知 View 重绘:由 command/result 与同步符号链路承担 +- 通知 Data / 业务执行:由 command/result 与 service 调度承担 + +其中 `CommandResult` 仍用于通知某次命令已经处理完成。 + +### 5.3 小结 + +State 层现在应被理解为一个统一主机,而不是多个同级中心。 +`StateHost` 对外是单例边界,对内则由 `FrontCommandHost`、`AppState` 和 `ServiceManager` 三层共同构成。 --- -## 6. S 层:服务层 +## 6. Service 层 -### 6.1 职责定义 +### 6.1 定位 -Service 负责所有"重活": +Service 层仍然是系统能力的承载层,但其管理入口已经被明确收敛到 `StateHost` 的下层 `ServiceManager`。 -- 设备驱动与通信协议 -- 集群节点调度 -- 数据持久化与查询 -- 耗时计算与文件 IO -- 网络请求 +因此,Service 层关注的重点是: -Service 的核心特征:**有自己独立的生命周期和内部状态机**,不受 State 控制,只响应 Command。 +- 被调度的具体服务对象 +- 服务装载与完成事件 +- 服务运行宿主与能力实例 -### 6.2 Command 自描述原则 +Service 内部完成事件可以回流到 `StateHost`,但前侧最终接收到的仍应是统一的 `CommandResult`,而不是直接暴露服务内部事件。 -Command 应携带执行所需的**完整上下文**,Service 执行时无需回调 State 查询任何信息: +### 6.2 基础类型 -```csharp -// ❌ 错误设计:Service 执行到一半还要问 State -service.Execute(cmd); -// Service 内部:var config = state.GetCurrentConfig(); ← 违反原则 +#### ServiceLoadCommand -// ✅ 正确设计:Command 是自描述的 -service.Execute(new StartSimulationCommand -{ - NodeCount = 8, - ConfigSnapshot = currentConfig, // 执行时的完整上下文快照 - ResultEvent = typeof(SimulationStartedEvent) // 完成后回传的 Event 类型 -}); -``` +`ServiceLoadCommand` 用于表达服务装载请求。 -**意义**:当某个 Service 大到需要拆分为独立微服务时,Command 的接口几乎不需要修改——这是架构可扩展性的基础。 +#### ServiceLoadAccomplishmentEvent -### 6.3 Service 与 State 解耦 +`ServiceLoadAccomplishmentEvent` 用于表达服务装载完成后的结果通知。 -``` -State → Service:Command(State 主动发起) -Service → State:Event(Service 异步回传) +#### ServiceHost -Service 持有 State 的 Dispatch 委托(或事件总线) -Service 不持有 State 的引用 -State 持有 Service 的接口引用 -Service 完全不知道 V 层的存在 -``` +`ServiceHost` 用于表达某一类服务实例的直接宿主或执行载体。 + +### 6.3 小结 + +`ServiceManager` 属于 State 的下层管理结构,而 `ServiceHost` 与具体服务实例才属于能力层主体。 +这样可以在统一调度和能力解耦之间保持清晰边界。 --- -## 7. 层间通信协议 +## 7. 层间关系 -### 7.1 完整数据流 +### 7.1 View → State -``` -[用户操作 / 外设信号] - ↓ -IInputAdapter.OnXxx() - ↓ dispatch(Intent) -State 的串行 Channel - ↓ HandleIntent(intent) -State 内部状态转移 - ↓ Commit(newSnapshot) -IOutputAdapter.AcceptSnapshot() ← 可能在非 UI 线程 - ↓ (各适配器自行处理线程安全) -屏幕渲染 / LED 更新 / 面板刷新 ← 各自的驱动时钟 -``` +View 层通过命令把交互送入 `StateHost`。 +命令首先由 `FrontCommandHost` 接入,再由 `StateHost` 统一协调后续动作。 +当前前侧协议至少应包含: -### 7.2 线程模型 +- `StateChangeCommand` +- `ExecuteCommand` +- `CommandResult` -| 操作 | 线程 | -|---|---| -| `IInputAdapter` 事件采集 | 各自线程(WPF UI 线程 / 后台轮询线程) | -| `State.Dispatch(Intent)` | 任意线程,线程安全(Channel 写入是原子的) | -| `State` 内部消费循环 | 单一后台线程 | -| `IOutputAdapter.AcceptSnapshot()` | State 的消费线程(非 UI 线程) | -| `WpfPresenter.PushToView()` | WPF UI 线程(由 `CompositionTarget.Rendering` 保证) | -| `HmiOutputAdapter.OnTick()` | Timer 回调线程 | +### 7.2 State 内部关系 -**核心结论**:整个架构中**只有 WpfPresenter 需要关心 UI 线程**,其余所有层次天然线程安全,无需到处写 `Dispatcher.Invoke`。 +State 内部的关系是: -### 7.3 组装示例 +- 上层 `FrontCommandHost` 负责接命令 +- 中层 `AppState` 负责持状态 +- 下层 `ServiceManager` 负责管服务 -```csharp -// Program / App.xaml.cs -var window = new MainWindow(); +三者共同组成一个统一的 State 运行时主机。 -// 组装 V 层 -var view = new ViewLayer() - .Add(new WpfInputAdapter(window)) - .Add(new HmiInputAdapter(hmiDriver)) - .Add(new WpfPresenter(window)) - .Add(new HmiOutputAdapter(hmiDriver)); +### 7.3 State → Service -// 组装 Service 层 -var excelService = new ExcelService(); -var printerService = new PrinterService(); -// ... - -// 组装 State -var state = new AppState(view.AcceptSnapshot, excelService, printerService); - -// 启动 -view.Start(state.Dispatch); -window.Show(); -``` +当命令涉及能力执行时,`StateHost` 通过下层 `ServiceManager` 调度服务对象。 +服务执行结果再统一回流 `StateHost`,由其决定状态更新与通知广播。 +对于 View 而言,某次执行是否完成,应以 `CommandResult` 为准,而不是直接依赖服务内部事件。 --- -## 8. 与 MVVM 的对比 +## 8. 与 MVVM 的区别 -| 维度 | MVVM | VSS | -|---|---|---| -| **核心驱动** | 数据变化驱动视图 | 输入驱动状态,状态驱动视图 | -| **View 的角色** | 双向绑定的参与者 | 纯粹的 I/O 适配层 | -| **逻辑入口** | ICommand 散落在 ViewModel | 统一的 Intent → State | -| **多输入源** | 无原生支持,需自行处理 | 天然归一化到 State | -| **ViewModel/State 职责** | 常膨胀为上帝类 | State 只做状态转移决策 | -| **业务复杂度承载** | Model 层,无规范 | Service 层,有明确生命周期规范 | -| **线程模型** | 需大量 Dispatcher.Invoke | 只有 Presenter 需要关心 UI 线程 | -| **View 可替换性** | View 和 VM 生命周期耦合 | View 层可任意替换或完全关闭 | -| **无头运行** | 困难 | 天然支持(移除 V 层即可) | -| **适合场景** | 纯前端、纯数据展示类应用 | 工业上位机、仿真集群、多 HMI 软件 | +相较于 MVVM,VSS 更强调统一运行时主机,而不是围绕界面绑定建立系统中心。 + +主要区别在于: + +- MVVM 通常以界面和 ViewModel 配合为中心,VSS 以 `StateHost` 为唯一中枢 +- MVVM 更适合界面驱动型应用,VSS 更适合多输入源、多宿主、强状态调度的软件 +- VSS 明确区分状态本体、命令接入和服务管理,但又把它们统一收束在同一个 State 边界内 --- ## 9. 适用场景 -### ✅ VSS 强烈推荐 +VSS 更适合以下场景: -- 工业上位机(需要驱动硬件设备) -- 多输入源 HMI(鼠标 + 触摸 + 手柄 + 实体按钮) -- 仿真计算软件(既可单机也可集群) -- 需要"无头模式"运行的软件(自动化测试、远程节点) -- 某个功能模块已大到可以单独拆出做服务的软件 +- 工业上位机 +- 多输入源 HMI +- 仿真与控制软件 +- 支持无头运行的软件 +- 需要多宿主接入的系统 -### ⚠️ MVVM 仍然适用 - -- 纯数据展示(Dashboard、报表工具) -- 纯前端业务(表单填写、内容管理) -- 业务逻辑简单,Model 没有独立生命周期 - -### ❌ 两者都不适合 - -- 分布式系统的后端服务(应使用 DDD / Clean Architecture) -- 微服务内部逻辑(应使用领域模型 + CQRS) +对于纯数据展示、轻量表单或前端页面驱动型应用,MVVM 仍可能是更直接的选择。 --- +## 10. 当前建议保留的最小类型清单 -*本文档随项目演进持续更新。架构思想的提出与设计版权归 Nitload 所有。* +### View 层 + +- `ViewHost` +- `StateChangeCommand` +- `ExecuteCommand` +- `CommandResult` + +### State 层 + +- `StateHost` +- `FrontCommandHost` +- `AppState` +- `PanelState` +- `ServiceManager` + +### Service 层 + +- `ServiceLoadCommand` +- `ServiceLoadAccomplishmentEvent` +- `ServiceHost` + +--- + +## 11. 结论 + +VSS 的重点不在于先定义某一种具体界面实现,而在于先定义稳定的运行时骨架。 +在当前阶段,更准确的表达应当是: + +- View 层负责交互来源 +- State 层对外统一表现为 `StateHost` 单例 +- `StateHost` 对内全息展开为 `FrontCommandHost`、`AppState`、`ServiceManager` 三层 +- 前侧命令协议由 `StateChangeCommand`、`ExecuteCommand`、`CommandResult` 构成闭环 +- Service 层负责承载被调度的具体能力对象 + +这一定义比旧版本更清楚地表达了唯一中枢、运行时边界和内部层次关系。 \ No newline at end of file diff --git a/StgSharp.UserInterface/VSS_BaseTypes.mmd b/StgSharp.UserInterface/VSS_BaseTypes.mmd new file mode 100644 index 00000000..377f6f5e --- /dev/null +++ b/StgSharp.UserInterface/VSS_BaseTypes.mmd @@ -0,0 +1,82 @@ +%% VSS 基座类型关系图 +%% 这份图优先表达“包含/从属关系”,其次才表达“交互关系”。 +%% 实线表示结构归属,虚线表示交互/消息流,不等同于当前代码里的具体成员实现。 +%% 整体按 View -> State -> Service 自上而下排列,避免横向摊开后破坏层次关系。 +%% observer 语义仍然存在,但在 UserInterface 中以源生成器织入到状态节点代码里的分布式 hooks 形式出现, +%% 不再作为手写的公共基座类型单独暴露。 + +flowchart TB + subgraph V[View 层] + direction LR + VH[ViewHost] + SCC[StateChangeCommand] + EC[ExecuteCommand] + CR[CommandResult] + end + + subgraph S[State 层] + direction TB + subgraph SHBOX[StateHost] + direction TB + SH[唯一对外主机] + + subgraph SU[上层] + direction LR + FCH[FrontCommandHost] + end + + subgraph SM[中层] + direction LR + AS[AppState] + PS[PanelState] + GOBS[生成器织入的分布式 Observer Hooks] + end + + subgraph SL[下层] + direction LR + MGR[ServiceManager] + end + end + end + + subgraph SV[Service 层] + SLC[ServiceLoadCommand] + SHOST[ServiceHost] + SLAE[ServiceLoadAccomplishmentEvent] + end + + SH --> FCH + SH --> AS + SH --> MGR + AS -->|聚合| PS + PS -. 节点写入后触发 .-> GOBS + MGR -->|管理| SHOST + + VH -. 产生/投递 .-> SCC + VH -. 产生/投递 .-> EC + VH -. 命令送入 .-> SH + SH -. 统一回传 .-> CR + CR -. 返回给 .-> VH + + FCH -. 接入/区分 .-> SCC + FCH -. 接入/区分 .-> EC + FCH -. 前侧结果组织 .-> CR + + SH -. 下传执行请求 .-> MGR + MGR -. 调度具体服务 .-> SHOST + MGR -. 装载请求 .-> SLC + SHOST -. 执行结果回流 .-> MGR + SLC -. 初始化依据 .-> SHOST + SHOST -. 装载完成产出 .-> SLAE + + classDef host fill:#1d3557,stroke:#f1faee,color:#f1faee,stroke-width:2px; + classDef state fill:#264653,stroke:#e9f5db,color:#f1faee; + classDef view fill:#3d405b,stroke:#f2cc8f,color:#f1faee; + classDef service fill:#4a4e69,stroke:#c9ada7,color:#f1faee; + classDef generated fill:#6d597a,stroke:#ffcad4,color:#f1faee; + + class SH host; + class FCH,AS,PS,MGR state; + class GOBS generated; + class VH,SCC,EC,CR view; + class SHOST,SLC,SLAE service; diff --git a/StgSharp.UserInterface/VSS_TypeFramework.md b/StgSharp.UserInterface/VSS_TypeFramework.md new file mode 100644 index 00000000..4b69f9a0 --- /dev/null +++ b/StgSharp.UserInterface/VSS_TypeFramework.md @@ -0,0 +1,531 @@ +# VSS 三层类型基座设计草案 + +## 目录 + +### 总览 +- [1. 设计目标](#1-设计目标) +- [2. 整体结构概览](#2-整体结构概览) +- [3. View 层类型基座](#3-view-层类型基座) +- [4. State 层类型基座](#4-state-层类型基座) +- [5. Service 层类型基座](#5-service-层类型基座) +- [6. 三层之间的关系](#6-三层之间的关系) +- [7. 当前建议保留的最小类型清单](#7-当前建议保留的最小类型清单) +- [8. 后续扩展建议](#8-后续扩展建议) +- [9. 结论](#9-结论) + +### 已明确类型 + +#### View 层 +- [ViewHost](#321-viewhost) +- [StateChangeCommand](#322-statechangecommand) +- [ExecuteCommand](#323-executecommand) +- [CommandResult](#324-commandresult) + +#### State 层 +- [StateHost](#421-statehost) +- [FrontCommandHost](#422-frontcommandhost) +- [AppState](#423-appstate) +- [PanelState](#424-panelstate) +- [ServiceManager](#425-servicemanager) + +#### Service 层 +- [ServiceLoadCommand](#521-serviceloadcommand) +- [ServiceLoadAccomplishmentEvent](#522-serviceloadaccomplishmentevent) +- [ServiceHost](#523-servicehost) + +## 1. 设计目标 + +VSS 面向的是多宿主、多运行环境、多交互来源的系统,不应将整体架构绑定到单一 GUI 框架、单一线程模型或单一输入方式上。 +因此,VSS 的基础设计不以传统 ViewModel-View 对应关系为核心,而以统一状态中枢为运行时骨架。 + +在当前版本中,VSS 仍然保留 View、State、Service 三种职责域,但其运行时组织方式需要进一步明确: + +- View 层负责承接外部交互来源 +- State 层负责作为唯一状态与调度中枢 +- Service 层负责承载系统能力与执行对象 + +其中,State 层不再被描述为若干并列对象的松散组合,而是对外统一表现为一个单例运行时主机:`StateHost`。 + +--- + +## 2. 整体结构概览 + +VSS 当前建议的核心运行结构如下: + +View / Electron / HMI / 外设 / 其他宿主 +↓ +StateHost(单例) +├─ 上层:FrontCommandHost +├─ 中层:AppState +└─ 下层:ServiceManager +↓ +Service / ServiceHost / 具体能力实例 + +这个结构表达的是两层含义: + +1. 对外,整个 State 层只暴露一个统一中枢,即 `StateHost`。 +2. 对内,`StateHost` 全息展开为上中下三层:上层接命令,中层持状态,下层管服务。 + +其基本运行逻辑如下: + +1. 外部宿主、界面、手柄、HMI 或进程外系统发出命令。 +2. 命令进入 `StateHost` 的上层,即 `FrontCommandHost`。 +3. `FrontCommandHost` 完成命令接入、规范化与前置转发。 +4. `StateHost` 的中层 `AppState` 作为全局状态抽象参与状态判断与状态更新。 +5. 当命令涉及能力执行时,`StateHost` 的下层 `ServiceManager` 负责服务管理与调度。 +6. 服务结果回流 `StateHost`,再统一同步到状态体系与观察者体系。 + +--- + +# 3. View 层类型基座 + +## 3.1 设计定位 + +View 层表示前侧宿主与交互来源,而不是具体控件集合。 +它只负责承接输入、形成命令,并与 `StateHost` 建立稳定的交互接口。 + +因此,View 层不承担系统中枢职责,也不直接管理服务。 + +--- + +## 3.2 View 层基础类型 + +### 3.2.1 `ViewHost` + +#### 定义 +`ViewHost` 是 VSS 对前侧宿主的统一抽象,用于表示任何可以承接界面表现、输入来源或外部交互来源的宿主对象。 + +#### 职责 +- 承载某一类前端或外部交互上下文; +- 接收宿主内产生的交互行为; +- 将交互行为转为标准命令; +- 将命令提交给 `StateHost`; +- 接收由 `StateHost` 回传的 `CommandResult`; +- 在必要时订阅状态变化并做宿主内部同步。 + +#### 说明 +`ViewHost` 不是具体 UI 控件,也不是具体页面对象。 +它更偏向交互宿主,例如图形界面宿主、HMI 宿主、外设宿主或远程宿主。 + +--- + +### 3.2.2 `StateChangeCommand` + +#### 定义 +`StateChangeCommand` 用于表达对系统状态的直接修改请求。 + +#### 职责 +- 描述某项状态应当如何被修改; +- 携带状态修改目标与修改参数; +- 由 `StateHost` 接收并执行; +- 执行后触发状态变更通知。 + +#### 说明 +这类命令本质上是确定性状态修改命令,适合用于审计、回放和状态同步。 + +对于高频、值类型占比高的状态节点,不应长期停留在 `object` 中继模型上。 + +更合理的做法是: + +- `StateChangeCommand` 继续承担统一路由 envelope; +- 具体节点更新优先落到类型化的 `StateValueCommand`; +- 由源生成器直接生成对应的 typed handler,避免在热点路径中反复装箱、拆箱和类型判断。 + +如果一次命令需要协调多个状态节点的原子更新,则不应继续复用单节点 `StateChangeCommand`,而应单独设计“向量命令”家族,将批量节点写入、冲突检测、原子性与回滚语义显式建模。 + +--- + +### 3.2.3 `ExecuteCommand` + +#### 定义 +`ExecuteCommand` 用于表达需要进入执行流程的命令请求。 + +#### 职责 +- 表达需要被执行而不是被直接改写为状态的动作; +- 以当前状态为上下文触发流程、能力调用或服务调度; +- 由 `StateHost` 判断是否需要下传到 `ServiceManager`; +- 在执行结束后对应产生一个 `CommandResult`。 + +#### 说明 +这类命令更接近执行请求,而不是单一字段修改。 + +--- + +### 3.2.4 `CommandResult` + +#### 定义 +`CommandResult` 用于表达某次前侧命令处理完成后的统一结果通知。 + +#### 职责 +- 作为 `StateHost` 回传给 `ViewHost` 的统一结果对象; +- 通知某次命令是否已经处理完成; +- 描述执行成功、失败、拒绝或中止等结果状态; +- 在必要时携带消息、结果数据或关联标识。 + +#### 说明 +如果没有 `CommandResult`,View 只能看到状态变化,却无法明确知道某次命令是否已经结束。 +因此,`CommandResult` 应被视为前侧命令协议中必需的回向对象。 + +--- + +## 3.3 View 层总结 + +View 层的基础作用只有三点: + +1. 提供统一的前侧宿主抽象; +2. 将复杂交互来源统一转换为命令; +3. 将命令稳定送入 `StateHost`。 + +当前基础类型包括: + +- `ViewHost` +- `StateChangeCommand` +- `ExecuteCommand` +- `CommandResult` + +--- + +# 4. State 层类型基座 + +## 4.1 设计定位 + +State 层是 VSS 的唯一状态与调度中枢。 +它对外统一表现为 `StateHost` 单例,而不是多个并列对象分别对外承担中枢职责。 + +`StateHost` 的内部结构分为三层: + +- 上层:`FrontCommandHost` +- 中层:`AppState` +- 下层:`ServiceManager` + +因此,State 层既是统一入口,也是统一协调边界。 + +--- + +## 4.2 State 层基础类型 + +### 4.2.1 `StateHost` + +#### 定义 +`StateHost` 是 VSS 对外暴露的唯一状态主机,建议以单例形式存在。 + +#### 职责 +- 作为整个 State 层的唯一外观; +- 对外承接来自 `ViewHost` 或其他入口的命令; +- 对内组织 `FrontCommandHost`、`AppState` 与 `ServiceManager`; +- 统一管理状态判断、状态同步、服务调度与结果回流; +- 作为状态变化通知与观察者体系的汇聚点。 + +#### 说明 +`StateHost` 是运行时主机概念,不等价于某一个状态对象。 +它的价值在于为整个 State 层提供唯一边界与唯一所有权。 + +--- + +### 4.2.2 `FrontCommandHost` + +#### 定义 +`FrontCommandHost` 是 `StateHost` 的上层结构,用于承接前侧命令输入。 + +#### 职责 +- 接收来自不同 `ViewHost` 的命令; +- 统一前侧命令入口; +- 区分 `StateChangeCommand` 与 `ExecuteCommand` 两类命令; +- 对命令进行初步规范化; +- 执行必要的前置校验; +- 将命令转交给 `StateHost` 的中层与下层进行后续处理; +- 参与将处理结果组织为 `CommandResult` 回传前侧。 + +#### 说明 +`FrontCommandHost` 是 State 的上边界,不再被视为独立于 State 层之外的中介对象。 + +--- + +- 统一向前侧回传 `CommandResult`; +### 4.2.3 `AppState` + +#### 定义 +`AppState` 是 `StateHost` 中层所持有的全局状态抽象,是 VSS 的唯一可信状态树根。 + +#### 职责 +- 聚合多个 `PanelState`; +- 保存全局运行时配置; +- 保存当前会话状态; +- 保存系统共享上下文; +- 作为状态判断和状态变更的核心依据; +- 为状态广播与观察提供基础数据。 + +#### 说明 +`AppState` 代表状态本体,而不是状态宿主。 +它由 `StateHost` 持有和管理。 + +--- + +### 4.2.4 `PanelState` + +#### 定义 +`PanelState` 是面板级状态单元,用于表示某个功能域的局部状态。 + +#### 职责 +- 保存局部运行状态; +- 作为 `AppState` 的子状态节点; +- 为不同功能模块提供独立状态边界; +- 支持局部状态的独立演进与扩展。 + +#### 说明 +`PanelState` 使状态结构具备模块化能力,避免所有字段直接堆叠在 `AppState` 根节点上。 + +如果沿用状态机术语,更准确的说法应当是:VSS 当前更接近“向量状态存储 + 受控变更路由”,而不是经典有限状态机。 + +这里的典型映射关系是: + +- 状态节点值对应状态向量的各分量; +- `CanApply` 更接近 transition guard; +- 实际赋值是状态向量写入; +- `BroadcastChange` / observer hook 更接近 transition effect; +- route 或 command path 只是进入某条受控变更管线的路由键,不等于经典状态机中的单一状态迁移边。 + +--- + +当前版本仍然保留 observer 语义,但不再保留 `OnStateChanged` 与 `StateChangeObserver` 这两个手写的公开类型。 + +在 `StgSharp.UserInterface` 中,状态写入后的前置校验、实际赋值与后置 observer 联动,统一改为由源生成器围绕具体状态节点分布式生成。 + +在传统 MVVM 中,observer 往往同时承担: + +- 内部逻辑回调 +- VM 内部广播变化 +- 通知 View 重绘 +- 通知 Data / 业务执行 + +而在 VSS 中,这四项职责被拆开后分别落到不同位置: + +- 内部逻辑回调:由 Service 或具体业务实现承担 +- VM 内部广播变化:由源生成器织入的轻量 observer hook 承担 +- 通知 View 重绘:由 command/result 与同步符号链路承担 +- 通知 Data / 业务执行:由 command/result 与 service 调度承担 + +因此,observer 在 VSS 中并没有消失,只是从一个中心化、手写的公共类型,变成了围绕各状态节点分布实现的生成代码职责。 + +--- + +### 4.2.5 `ServiceManager` + +#### 定义 +`ServiceManager` 是 `StateHost` 的下层结构,用于管理服务体系。 + +#### 职责 +- 管理服务注册、装载、卸载与生命周期; +- 维护服务实例与执行上下文; +- 接收 `StateHost` 下传的能力请求; +- 将服务执行结果回流给 `StateHost`; +- 作为 State 层通往具体能力层的下边界。 + +#### 说明 +`ServiceManager` 虽然位于 `StateHost` 内部下层,但它管理的仍然是能力体系,而不是状态本体。 +这一点必须与 `AppState` 严格区分。 + +--- + +## 4.3 State 层总结 + +State 层应被理解为一个统一主机,而不是若干同级对象的松散拼装。 +`StateHost` 对外是单例中枢,对内全息展开为上中下三层: + +- 上层负责命令接入 +- 中层负责状态抽象 +- 下层负责服务管理 + +同时,State 对前侧的完整协议应当至少包含: + +- `StateChangeCommand` +- `ExecuteCommand` +- `CommandResult` + +当前基础类型包括: + +- `StateHost` +- `FrontCommandHost` +- `AppState` +- `PanelState` +- `ServiceManager` + +此外,还存在由源生成器分布式织入的 observer 机制,用于承担轻量的状态节点内部广播,但它不再作为单独的公开基座类型出现。 + +--- + +# 5. Service 层类型基座 + +## 5.1 设计定位 + +Service 层是系统能力的承载层。 +这里的重点不再是把 `ServiceManager` 视为一个独立的外部中枢,而是将其视为 `StateHost` 下层所管理的能力入口。 + +因此,Service 层更关注的是被管理的服务对象、服务装载命令和服务完成事件。 + +--- + +## 5.2 Service 层基础类型 + +### 5.2.1 `ServiceLoadCommand` + +#### 定义 +`ServiceLoadCommand` 用于表达服务装载请求。 + +#### 职责 +- 描述需要装载的服务; +- 描述装载目标与装载参数; +- 由 `StateHost` 下传给 `ServiceManager`; +- 驱动服务实例创建、初始化或激活。 + +--- + +### 5.2.2 `ServiceLoadAccomplishmentEvent` + +#### 定义 +`ServiceLoadAccomplishmentEvent` 用于表达服务装载动作完成后的结果通知。 + +#### 职责 +- 通知服务是否完成装载; +- 通知装载是否成功; +- 承载失败信息或附加上下文; +- 支持结果回流到 `StateHost` 并同步状态。 + +--- + +### 5.2.3 `ServiceHost` + +#### 定义 +`ServiceHost` 用于表达某一类服务实例的直接宿主或执行载体。 + +#### 职责 +- 承接某类服务的运行实例; +- 为服务提供执行上下文; +- 支持未来多宿主、多环境的服务承载模式; +- 作为 `ServiceManager` 管理的底层运行单元之一。 + +--- + +## 5.3 Service 层总结 + +Service 层负责真正承载系统能力。 +`ServiceManager` 属于 `StateHost` 的下层管理结构,而 `ServiceHost` 与具体服务实例才构成能力层主体。 + +当前基础类型包括: + +- `ServiceLoadCommand` +- `ServiceLoadAccomplishmentEvent` +- `ServiceHost` + +--- + +# 6. 三层之间的关系 + +## 6.1 View → State + +View 层不直接管理服务,也不直接改写系统运行结构。 +其职责是通过命令将外部交互送入 `StateHost`。 + +即: + +- `ViewHost` 产生命令; +- 命令分为 `StateChangeCommand` 与 `ExecuteCommand`; +- 命令进入 `StateHost`; +- `StateHost` 的上层 `FrontCommandHost` 负责接入和前置处理; +- 命令处理完成后由 `StateHost` 向 `ViewHost` 回传 `CommandResult`。 + +--- + +## 6.2 State 内部三层关系 + +`StateHost` 的内部关系如下: + +- `FrontCommandHost` 负责接命令; +- `AppState` 负责持状态; +- `ServiceManager` 负责管服务。 + +这三者不是并列对外暴露,而是共同组成一个统一的 State 运行时主机。 + +--- + +## 6.3 State → Service + +当命令涉及能力执行时,`StateHost` 通过下层 `ServiceManager` 调度具体服务对象。 +服务执行结果再统一回流 `StateHost`,由其决定状态更新与事件广播。 + +--- + +# 7. 当前建议保留的最小类型清单 + +## View 层 +- `ViewHost` +- `StateChangeCommand` +- `ExecuteCommand` +- `CommandResult` + +## State 层 +- `StateHost` +- `FrontCommandHost` +- `AppState` +- `PanelState` +- `ServiceManager` + +## Service 层 +- `ServiceLoadCommand` +- `ServiceLoadAccomplishmentEvent` +- `ServiceHost` + +--- + +# 8. 后续扩展建议 + +在当前基础类型稳定后,建议后续按以下顺序扩展: + +## 第一阶段:固定唯一中枢边界 +- 明确 `StateHost` 的单例角色; +- 明确 `FrontCommandHost` 是 `StateHost` 的上层; +- 明确 `AppState` 是 `StateHost` 的中层状态抽象; +- 明确 `ServiceManager` 是 `StateHost` 的下层服务管理结构。 + +## 第二阶段:扩展具体子类型 + +### View 侧 +- 图形界面宿主 +- HMI 宿主 +- 外设宿主 +- 远程宿主 + +### State 侧 +- 更多 `PanelState` 子类型 +- 更明确的状态变化事件类型 +- 更完整的观察者与同步机制 + +### Service 侧 +- 具体服务实例 +- 具体 `ServiceHost` 子类型 +- 更细化的服务装载与能力描述体系 + +## 第三阶段:补充运行时机制 +- 命令上下文; +- 状态快照; +- 事件总线; +- 服务能力描述; +- 审计与回放; +- 服务结果同步协议。 + +--- + +# 9. 结论 + +VSS 当前不应先从具体界面技术切入,而应先定义稳定的运行时类型基座。 +在这一前提下,State 层需要被明确为唯一统一中枢,而不是若干对象之间的松散协作。 + +因此,当前版本更合适的表述是: + +- View 层负责交互来源 +- State 层对外统一表现为 `StateHost` 单例 +- `StateHost` 对内全息展开为 `FrontCommandHost`、`AppState`、`ServiceManager` 三层 +- 前侧命令协议由 `StateChangeCommand`、`ExecuteCommand`、`CommandResult` 共同构成 +- Service 层负责承载被 `ServiceManager` 管理的具体能力对象 + +这一定义能更清楚地表达所有权、调度边界与运行时结构。 \ No newline at end of file diff --git a/StgSharp.UserInterface/View/PresentorDescription.cs b/StgSharp.UserInterface/View/PresentorDescription.cs deleted file mode 100644 index 0925443c..00000000 --- a/StgSharp.UserInterface/View/PresentorDescription.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace StgSharp.UnserInterface.View -{ - internal class PresenterDescription - { - } -} diff --git a/StgSharp.UserInterface/basic-arch.drawio b/StgSharp.UserInterface/basic-arch.drawio new file mode 100644 index 00000000..4f4fcc0a --- /dev/null +++ b/StgSharp.UserInterface/basic-arch.drawio @@ -0,0 +1,366 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +