advanced userinterface design

This commit is contained in:
Nitload
2026-04-01 22:20:49 +08:00
parent 24498c2717
commit aa7ece4ed0
42 changed files with 2876 additions and 463 deletions
@@ -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();
}
}
@@ -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<ulong, int> 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
);
}
}
@@ -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;
/// <summary>
/// Being offset of first kernel in a L4 cache line.
/// </summary>
[FieldOffset(24)] public int CacheIndex;
/// <summary>
/// Count of kernels to be calculated in a L4 cache line.
/// </summary>
[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));
}
}
@@ -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
}
}
@@ -0,0 +1,79 @@
<mxfile host="Electron" agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/28.2.5 Chrome/138.0.7204.251 Electron/37.6.1 Safari/537.36" version="28.2.5">
<diagram name="第 1 页" id="cbTJhW8NyRPDnIMFj9ZQ">
<mxGraphModel dx="1425" dy="828" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="oCZHK8Ia19RPNdtHcHY8-36" value="" style="verticalLabelPosition=bottom;verticalAlign=top;html=1;shape=mxgraph.basic.rect;fillColor2=none;strokeWidth=1;size=20;indent=5;fillColor=none;dashed=1;dashPattern=12 12;" vertex="1" parent="1">
<mxGeometry x="171" y="355" width="410" height="170" as="geometry" />
</mxCell>
<mxCell id="oCZHK8Ia19RPNdtHcHY8-37" value="呈现器VP&lt;br&gt;View Presenter" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="251" y="140" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="oCZHK8Ia19RPNdtHcHY8-38" value="试图描述VD&lt;br&gt;View Descriptor" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="251" y="200" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="oCZHK8Ia19RPNdtHcHY8-39" value="用户输入 I" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="391" y="140" width="120" height="120" as="geometry" />
</mxCell>
<mxCell id="oCZHK8Ia19RPNdtHcHY8-40" value="输入抽象 Intent" style="shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;" vertex="1" parent="1">
<mxGeometry x="421" y="300" width="120" height="30" as="geometry" />
</mxCell>
<mxCell id="oCZHK8Ia19RPNdtHcHY8-41" value="状态快照 Snapshot" style="shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;" vertex="1" parent="1">
<mxGeometry x="181" y="300" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="oCZHK8Ia19RPNdtHcHY8-42" value="状态机" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="251" y="380" width="190" height="120" as="geometry" />
</mxCell>
<mxCell id="oCZHK8Ia19RPNdtHcHY8-43" value="仲裁器" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="451" y="380" width="50" height="50" as="geometry" />
</mxCell>
<mxCell id="oCZHK8Ia19RPNdtHcHY8-44" value="并发控制" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="451" y="440" width="50" height="60" as="geometry" />
</mxCell>
<mxCell id="oCZHK8Ia19RPNdtHcHY8-45" value="" style="html=1;shadow=0;dashed=0;align=center;verticalAlign=middle;shape=mxgraph.arrows2.arrow;dy=0.78;dx=28.86;direction=south;notch=0;" vertex="1" parent="1">
<mxGeometry x="391" y="260" width="20" height="120" as="geometry" />
</mxCell>
<mxCell id="oCZHK8Ia19RPNdtHcHY8-46" value="" style="html=1;shadow=0;dashed=0;align=center;verticalAlign=middle;shape=mxgraph.arrows2.arrow;dy=0.78;dx=28.86;direction=south;notch=0;rotation=-180;" vertex="1" parent="1">
<mxGeometry x="341" y="260" width="20" height="120" as="geometry" />
</mxCell>
<mxCell id="oCZHK8Ia19RPNdtHcHY8-47" value="" style="verticalLabelPosition=bottom;verticalAlign=top;html=1;shape=mxgraph.basic.rect;fillColor2=none;strokeWidth=1;size=20;indent=5;fillColor=none;dashed=1;dashPattern=12 12;" vertex="1" parent="1">
<mxGeometry x="171" y="110" width="410" height="170" as="geometry" />
</mxCell>
<mxCell id="oCZHK8Ia19RPNdtHcHY8-48" value="" style="verticalLabelPosition=bottom;verticalAlign=top;html=1;shape=mxgraph.basic.rect;fillColor2=none;strokeWidth=1;size=20;indent=5;fillColor=none;dashed=1;dashPattern=12 12;" vertex="1" parent="1">
<mxGeometry x="170" y="580" width="410" height="220" as="geometry" />
</mxCell>
<mxCell id="oCZHK8Ia19RPNdtHcHY8-49" value="服务实现" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="279" y="650" width="190" height="120" as="geometry" />
</mxCell>
<mxCell id="oCZHK8Ia19RPNdtHcHY8-50" value="命令队列" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="380" y="620" width="89" height="20" as="geometry" />
</mxCell>
<mxCell id="oCZHK8Ia19RPNdtHcHY8-51" value="响应队列" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="279" y="620" width="91" height="20" as="geometry" />
</mxCell>
<mxCell id="oCZHK8Ia19RPNdtHcHY8-52" value="执行结果 Response" style="shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;" vertex="1" parent="1">
<mxGeometry x="171" y="540" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="oCZHK8Ia19RPNdtHcHY8-53" value="" style="html=1;shadow=0;dashed=0;align=center;verticalAlign=middle;shape=mxgraph.arrows2.arrow;dy=0.78;dx=28.86;direction=south;notch=0;rotation=-180;" vertex="1" parent="1">
<mxGeometry x="331" y="500" width="20" height="120" as="geometry" />
</mxCell>
<mxCell id="oCZHK8Ia19RPNdtHcHY8-54" value="" style="html=1;shadow=0;dashed=0;align=center;verticalAlign=middle;shape=mxgraph.arrows2.arrow;dy=0.78;dx=28.86;direction=south;notch=0;" vertex="1" parent="1">
<mxGeometry x="381" y="500" width="20" height="120" as="geometry" />
</mxCell>
<mxCell id="oCZHK8Ia19RPNdtHcHY8-55" value="后端命令 Command" style="shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;" vertex="1" parent="1">
<mxGeometry x="411" y="540" width="120" height="30" as="geometry" />
</mxCell>
<mxCell id="oCZHK8Ia19RPNdtHcHY8-56" value="&lt;font style=&quot;font-size: 20px;&quot;&gt;视图 View&lt;/font&gt;" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="591" y="180" width="150" height="30" as="geometry" />
</mxCell>
<mxCell id="oCZHK8Ia19RPNdtHcHY8-57" value="&lt;font style=&quot;font-size: 20px;&quot;&gt;状态 State&lt;/font&gt;" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="591" y="425" width="150" height="30" as="geometry" />
</mxCell>
<mxCell id="oCZHK8Ia19RPNdtHcHY8-58" value="&lt;font style=&quot;font-size: 20px;&quot;&gt;服务 Service&lt;/font&gt;" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="591" y="695" width="150" height="30" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
@@ -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;
}
}
@@ -0,0 +1,5 @@
namespace StgSharp.UserInterface
{
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = true)]
public sealed class PanelStateNodeAttribute : Attribute { }
}
@@ -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<object?> ExecuteAsync(
ExecuteCommand command,
CancellationToken cancellationToken = default
);
public abstract ValueTask InitializeAsync(
ServiceLoadCommand command,
CancellationToken cancellationToken = default
);
public abstract ValueTask StopAsync(
CancellationToken cancellationToken = default
);
}
}
@@ -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;
}
}
@@ -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<string, object?> Parameters { get; init; } =
new Dictionary<string, object?>(StringComparer.Ordinal);
public string ServiceName { get; init; }
}
}
@@ -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<string, PanelState> Panels { get; } =
new Dictionary<string, PanelState>(StringComparer.Ordinal);
public IDictionary<string, object?> SharedContext { get; } =
new Dictionary<string, object?>(StringComparer.Ordinal);
public string Name { get; }
}
}
@@ -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<CommandResult> AcceptAsync(
ViewHost source,
StateChangeCommand command,
CancellationToken cancellationToken = default
);
public abstract Task<CommandResult> AcceptAsync(
ViewHost source,
ExecuteCommand command,
CancellationToken cancellationToken = default
);
internal void Attach(
StateHost stateHost
)
{
StateHost = stateHost ?? throw new ArgumentNullException(nameof(stateHost));
}
}
}
@@ -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<CommandResult> 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<CommandResult> 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;
}
}
}
@@ -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; }
}
}
@@ -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<object?> ExecuteAsync(
ExecuteCommand command,
CancellationToken cancellationToken = default
);
public abstract Task<ServiceLoadAccomplishmentEvent> 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));
}
}
}
@@ -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; }
}
}
@@ -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<object?> Parameters { get; init; } = Array.Empty<object?>();
public string OperationName { get; init; }
}
}
@@ -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<CommandResult> PostAsync(
StateChangeCommand command,
CancellationToken cancellationToken = default
)
{
ArgumentNullException.ThrowIfNull(command);
return StateHost.PostAsync(this, command, cancellationToken);
}
public virtual Task<CommandResult> 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;
}
}
}
@@ -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; }
}
}
@@ -0,0 +1,19 @@
using System;
namespace StgSharp.UserInterface
{
public sealed record StateValueCommand<TValue> : StateChangeCommand
{
public StateValueCommand(
string statePath,
TValue value
) : base(statePath)
{
Value = value;
}
public new TValue Value { get; init; }
}
}
@@ -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 决定“怎么生成它”
只要这三个边界不乱,后面的业务逻辑就算加很多,也还会比较稳。
@@ -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<PanelStateIncrementalGeneratorTypeAnalysis>.Empty);
}
HashSet<string> emittedTypes = new(StringComparer.Ordinal);
ImmutableArray<PanelStateIncrementalGeneratorTypeAnalysis>.Builder builder =
ImmutableArray.CreateBuilder<PanelStateIncrementalGeneratorTypeAnalysis>();
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());
}
}
@@ -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` 统一入口,识别类型声明,先完成前两部分分析,再自动进入第三部分派生调度。
@@ -0,0 +1,6 @@
namespace StgSharp.UserInterface.Generator;
internal static class PanelStateIncrementalGeneratorDiagnostics
{
public const string Category = "StgSharp.UserInterface.Generator";
}
@@ -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("// <auto-generated />");
_ = 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));
}
}
}
@@ -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<PanelStateIncrementalGeneratorMarkedMember> MarkedMembers
);
internal readonly record struct PanelStateIncrementalGeneratorInput(
Compilation Compilation,
ImmutableArray<PanelStateIncrementalGeneratorTypeTarget> TypeTargets
);
internal sealed record PanelStateIncrementalGeneratorTypeAnalysis(
PanelStateIncrementalGeneratorTypeTarget Target,
PanelStateSymbolPartAnalysis StateSymbols,
StateChangeBindingPartAnalysis StateChangeBindings,
StateChangeCommandHandlerPartAnalysis StateChangeCommandHandlers
);
internal readonly record struct PanelStateIncrementalGeneratorOutput(
Compilation Compilation,
ImmutableArray<PanelStateIncrementalGeneratorTypeAnalysis> TypeAnalyses
);
internal sealed record PanelStateSymbolPartAnalysis(
string OwnerTypeName,
ImmutableArray<PanelStateIncrementalGeneratorMarkedMember> Members
);
internal sealed record StateChangeBindingPartAnalysis(
string OwnerTypeName,
ImmutableArray<PanelStateIncrementalGeneratorMarkedMember> Members
);
internal sealed record StateChangeCommandHandlerPartAnalysis(
string OwnerTypeName,
ImmutableArray<PanelStateIncrementalGeneratorMarkedMember> Members
);
}
@@ -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<PanelStateIncrementalGeneratorMarkedMember> 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<PanelStateIncrementalGeneratorMarkedMember> CollectMarkedMembers(
INamedTypeSymbol typeSymbol
)
{
ImmutableArray<PanelStateIncrementalGeneratorMarkedMember>.Builder builder =
ImmutableArray.CreateBuilder<PanelStateIncrementalGeneratorMarkedMember>();
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)
};
}
}
}
@@ -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<PanelStateIncrementalGeneratorMarkedMember> members = target.MarkedMembers
.Where(static member => member.AttributeMetadataName == PanelStateIncrementalGeneratorKnownAttributes.StateChangeBindingAttribute)
.ToImmutableArray();
return new StateChangeBindingPartAnalysis(
target.TypeDisplayName,
members);
}
}
@@ -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<PanelStateIncrementalGeneratorMarkedMember> members = MergeMembers(
stateChangeBindings.Members,
stateSymbols.Members);
return new StateChangeCommandHandlerPartAnalysis(
target.TypeDisplayName,
members);
}
private static ImmutableArray<PanelStateIncrementalGeneratorMarkedMember> MergeMembers(
ImmutableArray<PanelStateIncrementalGeneratorMarkedMember> primaryMembers,
ImmutableArray<PanelStateIncrementalGeneratorMarkedMember> secondaryMembers
)
{
if (primaryMembers.IsDefaultOrEmpty && secondaryMembers.IsDefaultOrEmpty)
{
return ImmutableArray<PanelStateIncrementalGeneratorMarkedMember>.Empty;
}
HashSet<string> seen = new(StringComparer.Ordinal);
ImmutableArray<PanelStateIncrementalGeneratorMarkedMember>.Builder builder =
ImmutableArray.CreateBuilder<PanelStateIncrementalGeneratorMarkedMember>();
AddMembers(builder, seen, primaryMembers);
AddMembers(builder, seen, secondaryMembers);
return builder.ToImmutable();
}
private static void AddMembers(
ImmutableArray<PanelStateIncrementalGeneratorMarkedMember>.Builder builder,
HashSet<string> seen,
ImmutableArray<PanelStateIncrementalGeneratorMarkedMember> members
)
{
foreach (PanelStateIncrementalGeneratorMarkedMember member in members)
{
string key = member.Symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
if (!seen.Add(key))
{
continue;
}
builder.Add(member);
}
}
}
@@ -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<PanelStateIncrementalGeneratorMarkedMember> members = target.MarkedMembers
.Where(static member => PanelStateIncrementalGeneratorKnownAttributes.IsStateSymbolAttribute(member.AttributeMetadataName))
.ToImmutableArray();
return new PanelStateSymbolPartAnalysis(
target.TypeDisplayName,
members);
}
}
@@ -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<PanelStateIncrementalGeneratorTypeTarget> 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<ImmutableArray<PanelStateIncrementalGeneratorTypeTarget>> collectedTypeTargets = typeTargets.Collect();
IncrementalValueProvider<PanelStateIncrementalGeneratorInput> generatorInput = context.CompilationProvider
.Combine(collectedTypeTargets)
.Select(
static (pair, _) => new PanelStateIncrementalGeneratorInput(
pair.Left,
pair.Right));
IncrementalValueProvider<PanelStateIncrementalGeneratorOutput> generatorOutput = generatorInput
.Select(
static (input, _) => PanelStateIncrementalGeneratorAnalysis.Analyze(input));
context.RegisterSourceOutput(
generatorOutput,
static (productionContext, source) =>
PanelStateIncrementalGeneratorEmitter.Emit(productionContext, source));
}
}
+5
View File
@@ -0,0 +1,5 @@
global using System;
global using System.Collections.Generic;
global using System.Runtime.CompilerServices;
global using System.Runtime.InteropServices;
@@ -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<int> command
)
{
return string.Equals(command.StatePath, SampleStatePath, StringComparison.Ordinal)
&& current >= 0
&& origin != current;
}
// 实际项目里这整个 StateChangeHandler 都由源生成器产出;
// 这里手写展开,只是为了展示“没有生成器时 VSS 代码长什么样”。
public void ApplySampleStateChangeCommand(
StateValueCommand<int> 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 已禁用。手写版本若还需要状态后置联动,
// 这里只能继续手写内部同步/广播逻辑;正式版本由生成器织入。
}
}
}
@@ -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
{
}
}
-37
View File
@@ -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 { }
}
@@ -1 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk" />
@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<IsRoslynComponent>true</IsRoslynComponent>
<RequiresNativeRuntime>true</RequiresNativeRuntime>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)Generated</CompilerGeneratedFilesOutputPath>
<CommonManagedOutput>$(SourceRoot)StgSharp.Common\bin\$(Platform)\$(Configuration)\$(TargetFramework)\StgSharp.Common.dll</CommonManagedOutput>
<CommonManagedPdb>$(SourceRoot)StgSharp.Common\bin\$(Platform)\$(Configuration)\$(TargetFramework)\StgSharp.Common.pdb</CommonManagedPdb>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\StgSharp.Common\StgSharp.Common.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<None Include="$(OutputPath)$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
<None Include="$(OutputPath)$(AssemblyName).pdb" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" Condition="Exists('$(OutputPath)$(AssemblyName).pdb')" />
<None Include="$(CommonManagedOutput)" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" Condition="Exists('$(CommonManagedOutput)')" />
<None Include="$(CommonManagedPdb)" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" Condition="Exists('$(CommonManagedPdb)')" />
</ItemGroup>
</Project>
+209 -371
View File
@@ -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<Intent> 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<Intent> 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<Intent> ──→ 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<Intent>
↓ 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 层负责承载被调度的具体能力对象
这一定义比旧版本更清楚地表达了唯一中枢、运行时边界和内部层次关系。
+82
View File
@@ -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;
+531
View File
@@ -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<TValue>`;
- 由源生成器直接生成对应的 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` 管理的具体能力对象
这一定义能更清楚地表达所有权、调度边界与运行时结构。
@@ -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
{
}
}
+366
View File
@@ -0,0 +1,366 @@
<mxfile host="Electron" agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/28.2.5 Chrome/138.0.7204.251 Electron/37.6.1 Safari/537.36" version="28.2.5">
<diagram name="第 1 页" id="cbTJhW8NyRPDnIMFj9ZQ">
<mxGraphModel dx="982" dy="90" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" background="#797979" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="25TvGm5YAioSoPTIv9tO-4" value="视图托管接口VH&lt;br&gt;ViewHost" style="rounded=0;whiteSpace=wrap;html=1;verticalAlign=bottom;gradientColor=none;fillColor=#3399FF;fontColor=#000000;strokeColor=none;" vertex="1" parent="1">
<mxGeometry x="-210" y="40" width="430" height="200" as="geometry" />
</mxCell>
<mxCell id="oCZHK8Ia19RPNdtHcHY8-36" value="状态宿主 StateHost" style="verticalLabelPosition=middle;verticalAlign=top;html=1;shape=mxgraph.basic.rect;fillColor2=none;strokeWidth=1;size=0;indent=0;fillColor=#3399FF;labelPosition=center;align=center;strokeColor=none;" parent="1" vertex="1">
<mxGeometry x="-420" y="410" width="440" height="340" as="geometry" />
</mxCell>
<mxCell id="oCZHK8Ia19RPNdtHcHY8-37" value="呈现器VP&lt;br&gt;View Presenter" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="-200" y="50" width="210" height="70" as="geometry" />
</mxCell>
<mxCell id="oCZHK8Ia19RPNdtHcHY8-38" value="视图描述VD&lt;br&gt;View Descriptor" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="-200" y="120" width="210" height="70" as="geometry" />
</mxCell>
<mxCell id="oCZHK8Ia19RPNdtHcHY8-39" value="用户输入 I&lt;br&gt;Input" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="20" y="50" width="190" height="140" as="geometry" />
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-12" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;strokeColor=#66FF66;" edge="1" parent="1" source="25TvGm5YAioSoPTIv9tO-4" target="25TvGm5YAioSoPTIv9tO-10">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="150" y="275" />
<mxPoint x="60" y="275" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-13" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;strokeColor=#66FF66;" edge="1" parent="1" source="25TvGm5YAioSoPTIv9tO-4" target="25TvGm5YAioSoPTIv9tO-11">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="150" y="275" />
<mxPoint x="227" y="275" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-6" value="&lt;h1 style=&quot;margin-top: 0px;&quot;&gt;视图 View&lt;/h1&gt;&lt;p&gt;视图的类型基座是VieHost,一个用户界面应当具有唯一对应的ViewHost。&lt;br&gt;View形式多样,除了图形化操作系统窗体,手柄等HMI也属于视图。&lt;br&gt;跨进程图形界面通过State进程内的Host进行交互&lt;/p&gt;&lt;p&gt;视图本身不处理任何业务逻辑,只将用户的操作转为Command下发给State&lt;/p&gt;&lt;p&gt;在图形化界面中:&lt;br&gt;呈现器VP指OpenGL等底层渲染程序,skia等中层加速库,甚至electron框架&lt;br&gt;&lt;span style=&quot;background-color: transparent;&quot;&gt;视图描述VD指XAML,原生代码,CSS等描述界面上控件布局的代码&lt;/span&gt;&lt;/p&gt;&lt;p&gt;一个程序可以有很多个交互视图,且可以对视图的实现和布局进行丰富地修改&lt;br&gt;只要使用相同的命令,就可以和State进行交互。且设计良好的VSS程序中,&lt;br&gt;视图下线不应当影响State和Service的运行&lt;/p&gt;" style="text;html=1;whiteSpace=wrap;overflow=hidden;rounded=0;fontColor=#000000;" vertex="1" parent="1">
<mxGeometry x="247" y="40" width="460" height="250" as="geometry" />
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-17" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.481;entryY=1.021;entryDx=0;entryDy=0;strokeColor=#FFFF66;entryPerimeter=0;" edge="1" parent="1" source="25TvGm5YAioSoPTIv9tO-7" target="25TvGm5YAioSoPTIv9tO-16">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="-110" y="385" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-32" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;strokeColor=#000000;" edge="1" parent="1" source="25TvGm5YAioSoPTIv9tO-7" target="25TvGm5YAioSoPTIv9tO-33">
<mxGeometry relative="1" as="geometry">
<mxPoint x="-530" y="700" as="targetPoint" />
<Array as="points">
<mxPoint x="50" y="460" />
<mxPoint x="50" y="720" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-57" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=1;entryDx=0;entryDy=0;strokeColor=#FFFF33;" edge="1" parent="1" source="25TvGm5YAioSoPTIv9tO-7" target="25TvGm5YAioSoPTIv9tO-53">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="-310" y="390" />
<mxPoint x="-310" y="390" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-7" value="前端命令调度器 CommandHandler" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="-380" y="441" width="360" height="40" as="geometry" />
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-14" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#66FF66;entryX=0.946;entryY=0;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" source="25TvGm5YAioSoPTIv9tO-10" target="25TvGm5YAioSoPTIv9tO-7">
<mxGeometry relative="1" as="geometry">
<mxPoint x="-40" y="440" as="targetPoint" />
<Array as="points">
<mxPoint x="60" y="390" />
<mxPoint x="-40" y="390" />
<mxPoint x="-40" y="440" />
<mxPoint x="-39" y="440" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-10" value="StateChangeCommand" style="rounded=1;whiteSpace=wrap;html=1;fontColor=#000000;strokeColor=none;fillColor=#66FF66;" vertex="1" parent="1">
<mxGeometry x="-13" y="310" width="146" height="20" as="geometry" />
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-15" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.945;entryY=0.011;entryDx=0;entryDy=0;strokeColor=#33FF33;entryPerimeter=0;" edge="1" parent="1" source="25TvGm5YAioSoPTIv9tO-11" target="25TvGm5YAioSoPTIv9tO-7">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="230" y="390" />
<mxPoint x="-40" y="390" />
<mxPoint x="-40" y="440" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-11" value="CallOperationCommand" style="rounded=1;whiteSpace=wrap;html=1;strokeColor=none;fillColor=#66FF66;fontColor=#000000;" vertex="1" parent="1">
<mxGeometry x="154" y="310" width="146" height="20" as="geometry" />
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-18" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.25;entryY=1;entryDx=0;entryDy=0;strokeColor=#FFFF33;" edge="1" parent="1" source="25TvGm5YAioSoPTIv9tO-16" target="25TvGm5YAioSoPTIv9tO-4">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-16" value="CommandResult" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFFF33;fontColor=#000000;strokeColor=none;" vertex="1" parent="1">
<mxGeometry x="-180" y="310" width="146" height="20" as="geometry" />
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-19" value="&lt;h1 style=&quot;margin-top: 0px;&quot;&gt;前端命令 FrontCommand&lt;/h1&gt;&lt;p&gt;&lt;span style=&quot;background-color: transparent;&quot;&gt;View和State通过前端命令进行交互&lt;br&gt;进程内通信可以直接传递对象&lt;br&gt;跨进程交互可以传递序列化后的json数据包&lt;br&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;State中响应command的结果通过CommandResult&lt;br&gt;数据包返回给View&lt;/p&gt;&lt;p&gt;在需要时,View通过前端命令获取State的快照进行图形化展示&lt;/p&gt;" style="text;html=1;whiteSpace=wrap;overflow=hidden;rounded=0;align=left;fontColor=#000000;" vertex="1" parent="1">
<mxGeometry x="-790" y="250" width="290" height="190" as="geometry" />
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-37" value="&lt;font style=&quot;font-size: 12px; color: rgb(247, 247, 247);&quot;&gt;程序状态集 AppState&lt;/font&gt;" style="rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#000099;dashed=1;dashPattern=12 12;verticalAlign=top;fontSize=9;" vertex="1" parent="1">
<mxGeometry x="-410" y="520" width="420" height="170" as="geometry" />
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-22" value="&lt;font style=&quot;color: rgb(0, 0, 0);&quot;&gt;局部状态 PS&lt;br&gt;PanelState0&lt;/font&gt;" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#66FFFF;fontColor=#ffffff;strokeColor=none;" vertex="1" parent="1">
<mxGeometry x="-400" y="540" width="90" height="140" as="geometry" />
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-23" value="&lt;font style=&quot;color: rgb(0, 0, 0);&quot;&gt;PanelState1&lt;/font&gt;" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#66FFFF;fontColor=#ffffff;strokeColor=none;verticalAlign=top;" vertex="1" parent="1">
<mxGeometry x="-300" y="540" width="300" height="140" as="geometry" />
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-52" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;fontColor=#000000;labelBackgroundColor=none;strokeColor=#000000;" edge="1" parent="1" source="25TvGm5YAioSoPTIv9tO-33">
<mxGeometry relative="1" as="geometry">
<mxPoint x="-380" y="460" as="targetPoint" />
<Array as="points">
<mxPoint x="-450" y="720" />
<mxPoint x="-450" y="460" />
<mxPoint x="-380" y="460" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-70" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;strokeColor=#66FF66;" edge="1" parent="1" source="25TvGm5YAioSoPTIv9tO-33" target="25TvGm5YAioSoPTIv9tO-65">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="-160" y="775" />
<mxPoint x="-126" y="775" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-86" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.657;entryY=0.001;entryDx=0;entryDy=0;entryPerimeter=0;strokeColor=#66FF66;" edge="1" parent="1" source="25TvGm5YAioSoPTIv9tO-33" target="25TvGm5YAioSoPTIv9tO-83">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint y="740" />
<mxPoint y="870" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-88" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.25;entryY=0;entryDx=0;entryDy=0;strokeColor=#66FF66;" edge="1" parent="1" source="25TvGm5YAioSoPTIv9tO-33" target="25TvGm5YAioSoPTIv9tO-79">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="-390" y="740" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-33" value="后端命令调度器 CommandHandler" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="-370" y="700" width="360" height="40" as="geometry" />
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-24" value="状态节点0&lt;br&gt;StateNode0" style="rounded=0;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFE599;fontColor=#000000;" vertex="1" parent="1">
<mxGeometry x="-221" y="560" width="71" height="40" as="geometry" />
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-25" value="状态节点1" style="rounded=0;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFE599;fontColor=#000000;" vertex="1" parent="1">
<mxGeometry x="-150" y="560" width="71" height="40" as="geometry" />
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-21" value="&lt;font style=&quot;color: rgb(0, 0, 0);&quot;&gt;状态变更管线&lt;/font&gt;" style="rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#000099;dashed=1;dashPattern=12 12;verticalAlign=top;fontSize=9;" vertex="1" parent="1">
<mxGeometry x="-294" y="610" width="284" height="70" as="geometry" />
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-44" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;strokeColor=#000000;" edge="1" parent="1" source="25TvGm5YAioSoPTIv9tO-41" target="25TvGm5YAioSoPTIv9tO-26">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="-80" y="650" />
<mxPoint x="-80" y="650" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-41" value="&lt;font style=&quot;font-size: 9px;&quot;&gt;可更新检查&lt;br&gt;UpdateCheck&lt;/font&gt;" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#0050ef;fontColor=#ffffff;strokeColor=#001DBC;" vertex="1" parent="1">
<mxGeometry x="-80" y="635" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-43" value="" style="group" vertex="1" connectable="0" parent="1">
<mxGeometry x="-210" y="630" width="121" height="40" as="geometry" />
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-26" value="状态节点2" style="rounded=0;whiteSpace=wrap;html=1;strokeColor=#000000;fillColor=#FFE599;fontColor=#000000;" vertex="1" parent="25TvGm5YAioSoPTIv9tO-43">
<mxGeometry x="40" width="71" height="40" as="geometry" />
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-42" value="&lt;font style=&quot;font-size: 9px;&quot;&gt;更新&lt;/font&gt;" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#0050ef;fontColor=#ffffff;strokeColor=#001DBC;" vertex="1" parent="25TvGm5YAioSoPTIv9tO-43">
<mxGeometry x="10" width="30" height="40" as="geometry" />
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-48" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.25;entryY=1;entryDx=0;entryDy=0;strokeColor=#000000;" edge="1" parent="1" source="25TvGm5YAioSoPTIv9tO-45" target="25TvGm5YAioSoPTIv9tO-7">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="-256" y="600" />
<mxPoint x="-290" y="600" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-49" value="状态更新回调" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];labelBackgroundColor=none;fontColor=#000000;" vertex="1" connectable="0" parent="25TvGm5YAioSoPTIv9tO-48">
<mxGeometry x="0.6704" y="1" relative="1" as="geometry">
<mxPoint y="-2" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-45" value="&lt;font style=&quot;font-size: 9px;&quot;&gt;状态更新观测器&lt;br&gt;UpdateObserver&lt;/font&gt;" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#0050ef;fontColor=#ffffff;strokeColor=#001DBC;" vertex="1" parent="1">
<mxGeometry x="-291" y="625" width="70" height="50" as="geometry" />
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-47" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;strokeColor=#000000;" edge="1" parent="1" source="25TvGm5YAioSoPTIv9tO-42" target="25TvGm5YAioSoPTIv9tO-45">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="-200" y="650" />
<mxPoint x="-200" y="650" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-50" value="&lt;h1 style=&quot;margin-top: 0px;&quot;&gt;状态 State&lt;/h1&gt;&lt;p&gt;State是VSS架构的核心,用于同步多种不同的视图输入并仲裁其操作&lt;br&gt;&lt;/p&gt;&lt;p&gt;状态层通过状态宿主SH托管,SH应当为单例模式且程序生命周期内唯一,&lt;br&gt;具有多个SH的程序将变为分布式程序,不是VSS架构支持的目标。&lt;/p&gt;&lt;p&gt;整个程序的状态存放在程序状态集AS中,并进一步按照可视化界面和功能组&lt;br&gt;分为不同的局部状态PS,一个状态节点一般为一个属性,PS整体形成向量状态机&lt;/p&gt;&lt;p&gt;前端命令通过状态更新管线进行,状态更新管线使用源生成器自动化构建,不需要手写&lt;br&gt;执行管线会将执行结果返回到前端中。&lt;/p&gt;&lt;p&gt;复杂的,涉及到重型任务的操作将提交给后端命令调度器执行,后端命令调度器将根据&lt;br&gt;当前状态和命令要求,针对每一个Service录制一组指令流下发,并在执行过程中进行同步&lt;/p&gt;" style="text;html=1;whiteSpace=wrap;overflow=hidden;rounded=0;fontColor=#000000;" vertex="1" parent="1">
<mxGeometry x="60" y="475" width="520" height="240" as="geometry" />
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-51" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;strokeColor=#000000;" edge="1" parent="1" source="25TvGm5YAioSoPTIv9tO-7" target="25TvGm5YAioSoPTIv9tO-41">
<mxGeometry relative="1" as="geometry">
<mxPoint x="70" y="850" as="targetPoint" />
<mxPoint x="60" y="590" as="sourcePoint" />
<Array as="points">
<mxPoint x="50" y="460" />
<mxPoint x="50" y="650" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-58" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.25;entryY=0;entryDx=0;entryDy=0;strokeColor=#66FF66;" edge="1" parent="1" source="25TvGm5YAioSoPTIv9tO-53" target="25TvGm5YAioSoPTIv9tO-7">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="-290" y="290" />
<mxPoint x="-290" y="290" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-53" value="ViewHost 1" style="rounded=0;whiteSpace=wrap;html=1;verticalAlign=middle;gradientColor=none;fillColor=#3399FF;fontColor=#000000;strokeColor=none;" vertex="1" parent="1">
<mxGeometry x="-400" y="210" width="180" height="30" as="geometry" />
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-54" value="ViewHost 2" style="rounded=0;whiteSpace=wrap;html=1;verticalAlign=middle;gradientColor=none;fillColor=#3399FF;fontColor=#000000;strokeColor=none;" vertex="1" parent="1">
<mxGeometry x="-577" y="40" width="167" height="200" as="geometry" />
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-55" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#FFFF33;exitX=0;exitY=0;exitDx=0;exitDy=0;" edge="1" parent="1" source="25TvGm5YAioSoPTIv9tO-7">
<mxGeometry relative="1" as="geometry">
<mxPoint x="-355.34000000000003" y="390" as="sourcePoint" />
<mxPoint x="-490" y="240" as="targetPoint" />
<Array as="points">
<mxPoint x="-380" y="330" />
<mxPoint x="-490" y="330" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-59" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.056;entryY=0.015;entryDx=0;entryDy=0;entryPerimeter=0;strokeColor=#66FF66;" edge="1" parent="1" source="25TvGm5YAioSoPTIv9tO-54" target="25TvGm5YAioSoPTIv9tO-7">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="-470" y="310" />
<mxPoint x="-360" y="310" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-64" value="" style="group;fontColor=#000000;" vertex="1" connectable="0" parent="1">
<mxGeometry x="-265" y="850" width="160" height="200" as="geometry" />
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-62" value="" style="group" vertex="1" connectable="0" parent="25TvGm5YAioSoPTIv9tO-64">
<mxGeometry y="17.391304347826086" width="160" height="182.6086956521739" as="geometry" />
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-60" value="服务宿主 ServiceHost" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#66B2FF;" vertex="1" parent="25TvGm5YAioSoPTIv9tO-62">
<mxGeometry width="160" height="23.18840579710145" as="geometry" />
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-61" value="服务功能 ServiceLogic" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#0066CC;fontColor=#ffffff;strokeColor=#F5F5F5;" vertex="1" parent="25TvGm5YAioSoPTIv9tO-62">
<mxGeometry y="23.18840579710145" width="160" height="133.33333333333331" as="geometry" />
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-63" value="服务 Service" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="25TvGm5YAioSoPTIv9tO-62">
<mxGeometry y="156.52173913043478" width="160" height="26.08695652173913" as="geometry" />
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-65" value="ServiceCommand" style="rounded=1;whiteSpace=wrap;html=1;fontColor=#000000;strokeColor=none;fillColor=#66FF66;" vertex="1" parent="1">
<mxGeometry x="-180" y="810" width="107" height="20" as="geometry" />
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-71" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#FFFF33;" edge="1" parent="1" source="25TvGm5YAioSoPTIv9tO-66">
<mxGeometry relative="1" as="geometry">
<mxPoint x="-200" y="740" as="targetPoint" />
<Array as="points">
<mxPoint x="-236" y="775" />
<mxPoint x="-200" y="775" />
<mxPoint x="-200" y="740" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-66" value="CommandResult" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFFF33;fontColor=#000000;strokeColor=none;" vertex="1" parent="1">
<mxGeometry x="-291" y="810" width="109" height="20" as="geometry" />
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-68" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=1;entryDx=0;entryDy=0;strokeColor=#FFFF33;" edge="1" parent="1" source="25TvGm5YAioSoPTIv9tO-60" target="25TvGm5YAioSoPTIv9tO-66">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="-190" y="850" />
<mxPoint x="-236" y="850" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-69" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#66FF66;" edge="1" parent="1" source="25TvGm5YAioSoPTIv9tO-65">
<mxGeometry relative="1" as="geometry">
<mxPoint x="-170" y="870" as="targetPoint" />
<Array as="points">
<mxPoint x="-126" y="850" />
<mxPoint x="-170" y="850" />
<mxPoint x="-170" y="870" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-78" value="" style="group" vertex="1" connectable="0" parent="1">
<mxGeometry x="-420" y="870" width="120" height="180" as="geometry" />
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-79" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#0066CC;fontColor=#ffffff;strokeColor=#F5F5F5;" vertex="1" parent="25TvGm5YAioSoPTIv9tO-78">
<mxGeometry width="119.99999999999999" height="180" as="geometry" />
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-80" value="服务 1&lt;br&gt;Service 1" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="25TvGm5YAioSoPTIv9tO-78">
<mxGeometry y="75" width="119.99999999999999" height="30" as="geometry" />
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-82" value="" style="group" vertex="1" connectable="0" parent="1">
<mxGeometry x="-79" y="870" width="120" height="40" as="geometry" />
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-83" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#0066CC;fontColor=#ffffff;strokeColor=#F5F5F5;" vertex="1" parent="25TvGm5YAioSoPTIv9tO-82">
<mxGeometry width="119.99999999999999" height="40" as="geometry" />
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-84" value="服务 0&lt;br&gt;Service 0" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="25TvGm5YAioSoPTIv9tO-82">
<mxGeometry y="16.666666666666664" width="119.99999999999999" height="6.666666666666666" as="geometry" />
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-85" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#FFFF33;" edge="1" parent="1" source="25TvGm5YAioSoPTIv9tO-83">
<mxGeometry relative="1" as="geometry">
<mxPoint x="-20" y="740" as="targetPoint" />
<Array as="points">
<mxPoint x="-20" y="740" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-87" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#FFFF33;" edge="1" parent="1" source="25TvGm5YAioSoPTIv9tO-79">
<mxGeometry relative="1" as="geometry">
<mxPoint x="-360" y="740" as="targetPoint" />
<Array as="points">
<mxPoint x="-360" y="740" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-89" value="&lt;h1 style=&quot;margin-top: 0px;&quot;&gt;后端命令 ServiceCommand&lt;/h1&gt;&lt;p&gt;&lt;span style=&quot;background-color: transparent;&quot;&gt;State通过后端命令控制Service进行交互&lt;br&gt;进程内通信可以直接传递对象&lt;br&gt;跨进程交互可以传递复杂的配置数据包&lt;br&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;Service按照命令序列执行操作&lt;/p&gt;&lt;p&gt;有专门的Fence命令用以控制多个服务之间的同步&lt;/p&gt;" style="text;html=1;whiteSpace=wrap;overflow=hidden;rounded=0;align=left;fontColor=#000000;" vertex="1" parent="1">
<mxGeometry x="-780" y="740" width="330" height="155" as="geometry" />
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-90" value="跨进程界面&lt;br&gt;ExternalDisplaySurface" style="rounded=0;whiteSpace=wrap;html=1;strokeColor=#FFFF99;fillColor=#FFFF99;fontColor=#000000;" vertex="1" parent="1">
<mxGeometry x="-400" y="40" width="180" height="130" as="geometry" />
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-92" value="" style="shape=flexArrow;endArrow=classic;startArrow=classic;html=1;rounded=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;exitX=0.5;exitY=0;exitDx=0;exitDy=0;endWidth=9.056603773584905;endSize=2.0638888888888887;startWidth=9.865229110512129;startSize=1.2074074074074073;width=2.7777777777777777;" edge="1" parent="1" source="25TvGm5YAioSoPTIv9tO-53" target="25TvGm5YAioSoPTIv9tO-90">
<mxGeometry width="100" height="100" relative="1" as="geometry">
<mxPoint x="-360" y="290" as="sourcePoint" />
<mxPoint x="-260" y="190" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-93" value="json数据包" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];labelBackgroundColor=none;fontColor=#000000;" vertex="1" connectable="0" parent="25TvGm5YAioSoPTIv9tO-92">
<mxGeometry x="-0.2988" relative="1" as="geometry">
<mxPoint y="-6" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-94" value="跨进程服务&lt;br&gt;ExternalService" style="rounded=0;whiteSpace=wrap;html=1;strokeColor=#FFFF99;fillColor=#FFFF99;fontColor=#000000;" vertex="1" parent="1">
<mxGeometry x="-79" y="940" width="120" height="130" as="geometry" />
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-95" value="" style="shape=flexArrow;endArrow=classic;startArrow=classic;html=1;rounded=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;exitX=0.5;exitY=1;exitDx=0;exitDy=0;endWidth=6.633249791144528;endSize=2.0548872180451125;startWidth=6.633249791144528;startSize=2.481203007518797;width=2.7777777777777777;" edge="1" parent="1" target="25TvGm5YAioSoPTIv9tO-94" source="25TvGm5YAioSoPTIv9tO-83">
<mxGeometry width="100" height="100" relative="1" as="geometry">
<mxPoint x="10" y="1100" as="sourcePoint" />
<mxPoint x="60" y="1080" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-96" value="数据包" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];labelBackgroundColor=none;fontColor=#000000;" vertex="1" connectable="0" parent="25TvGm5YAioSoPTIv9tO-95">
<mxGeometry x="-0.2988" relative="1" as="geometry">
<mxPoint x="39" y="-1" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="25TvGm5YAioSoPTIv9tO-97" value="&lt;h1 style=&quot;margin-top: 0px;&quot;&gt;服务 Service&lt;/h1&gt;&lt;p&gt;VSS中重型的任务应当抽象为服务&lt;br&gt;&lt;/p&gt;&lt;p&gt;诸如HPC数学计算,连续的数控操作等都应当各自抽象为一个服务,&lt;br&gt;甚至集群计算中的计算节点也可以抽象为集群管理软件的服务实例&lt;br&gt;一个VSS程序可以有很多个Service。&lt;/p&gt;&lt;p&gt;一个良好的设计的VSS架构程序中,一个服务的失效可能导致程序功能出错,&lt;br&gt;但是不可以影响其他Service实例的工作。&lt;/p&gt;" style="text;html=1;whiteSpace=wrap;overflow=hidden;rounded=0;fontColor=#000000;" vertex="1" parent="1">
<mxGeometry x="70" y="850" width="520" height="240" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>