update XML IO pattern.

This commit is contained in:
2026-05-24 14:06:51 +08:00
parent 058003f395
commit d7836db45f
314 changed files with 13376 additions and 573 deletions
@@ -92,24 +92,66 @@
<h2 id="core-components">Core Components</h2>
<h3 id="imakexmlsource-interface">IMakeXmlSource Interface</h3>
<p>The <a class="xref" href="../../api/Hi.Common.XmlUtils.IMakeXmlSource.html">IMakeXmlSource</a> interface defines the contract for objects that can be serialized to XML format. It contains a single method <code>MakeXmlSource</code>.</p>
<h3 id="xfactory-registration">XFactory Registration</h3>
<p>Every class implementing IMakeXmlSource must:</p>
<ol>
<li>Define a static <code>XName</code> property matching the class name.</li>
<li>Register itself in the static constructor using <code>XFactory.Regs.Add</code></li>
<li>Implement XML serialization and deserialization logic</li>
</ol>
<p>The registration delegate signature includes an <code>IProgress&lt;object&gt;</code> parameter for <a href="message-management.html">diagnostic message routing</a>:</p>
<pre><code class="lang-csharp">XFactory.Regs.Add(XName, (xml, baseDirectory, relFile, progress, res)
=&gt; new MyClass(xml, baseDirectory, relFile, progress));
</code></pre>
<p>For example, see <a class="xref" href="../../api/Hi.Milling.Apts.BallApt.html">BallApt</a>:</p>
<pre><code class="lang-csharp" name="XmlRegistration">static BallApt()
<h3 id="xfactory">XFactory</h3>
<p><a class="xref" href="../../api/Hi.Common.XmlUtils.XFactory.html">XFactory</a> is an instance class with a process-wide <code>Default</code> singleton (<code>XFactory.Default</code>). The instance form exists for test isolation and parallel pipelines that need disjoint generator registries; the static <code>Gen&lt;T&gt;</code> / <code>GenByChild&lt;T&gt;</code> / <code>GenByFile&lt;T&gt;</code> entry points always read from <code>Default</code>.</p>
<p>Each instance owns its own <code>Generators</code> dictionary (XML element name → generator delegate). Types add themselves via a <code>Reg(factory)</code> call (see below).</p>
<h3 id="explicit-registration-via-regxfactory-factory--null">Explicit Registration via <code>Reg(XFactory factory = null)</code></h3>
<p>Every class implementing IMakeXmlSource exposes a public static <code>Reg</code> method:</p>
<pre><code class="lang-csharp">public static void Reg(XFactory factory = null)
{
// Register to the &lt;see cref=&quot;XFactory.Default&quot;/&gt;.
XFactory.Generators.Add(XName, (xml,baseDirectory, relFile, progress, res) =&gt; new BallApt(xml));
factory ??= XFactory.Default;
factory.Generators.TryAdd(XName, (xml, baseDirectory, relFile, progress, res)
=&gt; new MyClass(xml, baseDirectory, relFile, progress));
}
</code></pre><h3 id="iprogress-threading">IProgress Threading</h3>
</code></pre>
<p>Key properties:</p>
<ul>
<li><strong>Explicit.</strong> Callers see registration happen — no hidden side effect from accessing a static member or constructing a type.</li>
<li><strong>Idempotent.</strong> Uses <code>TryAdd</code>, so the same <code>Reg</code> may be invoked any number of times from any number of boot paths.</li>
<li><strong>Composable.</strong> Custom factory instances are supported via the optional <code>factory</code> parameter; default usage (<code>MyClass.Reg();</code>) populates <code>XFactory.Default</code>.</li>
</ul>
<p>For example, see <a class="xref" href="../../api/Hi.Milling.Apts.BallApt.html">BallApt</a>:</p>
<pre><code class="lang-csharp" name="XmlRegistration">/// &lt;summary&gt;
/// Registers this type's deserializer with the given &lt;see cref=&quot;XFactory&quot;/&gt;
/// (or &lt;see cref=&quot;XFactory.Default&quot;/&gt; when &lt;paramref name=&quot;factory&quot;/&gt; is
/// &lt;c&gt;null&lt;/c&gt;). Idempotent.
/// &lt;/summary&gt;
public static void Reg(XFactory factory = null)
{
factory ??= XFactory.Default;
factory.Generators.TryAdd(XName, (xml,baseDirectory, relFile, progress, res) =&gt; new BallApt(xml));
}
</code></pre><h3 id="composite-types-chain-regfactory-on-dependents">Composite types chain <code>Reg(factory)</code> on dependents</h3>
<p>When a class deserializes child elements via <code>XFactory.Gen&lt;T&gt;</code> / <code>XFactory.GenByChild&lt;T&gt;</code>, its <code>Reg(factory)</code> must chain <code>Reg(factory)</code> on each concrete child type so the whole dependency graph is reachable from a single root call:</p>
<pre><code class="lang-csharp">public static void Reg(XFactory factory = null)
{
factory ??= XFactory.Default;
DependentA.Reg(factory);
DependentB.Reg(factory);
factory.Generators.TryAdd(XName, (xml, baseDirectory, relFile, progress, res)
=&gt; new MyComposite(xml, baseDirectory, relFile, progress));
}
</code></pre>
<p>For polymorphic deserialization (<code>GenByChild&lt;IInterface&gt;</code>), the composite must chain every concrete implementation that may appear in the XML. The largest composite, <a class="xref" href="../../api/Hi.NcParsers.SoftNcRunner.html">SoftNcRunner</a>, chains roughly 130 dependents (every dependency, initializer, segmenter, syntax, and semantic the NC pipeline may deserialize).</p>
<h3 id="multi-name-registration-legacy-aliases">Multi-name registration (legacy aliases)</h3>
<p>When the XML payload may carry an old element name for backward compatibility, register the current <code>XName</code> first and group legacy aliases under a <code>//legacy aliases</code> comment:</p>
<pre><code class="lang-csharp">public static void Reg(XFactory factory = null)
{
factory ??= XFactory.Default;
XFactory.XGeneratorDelegate gen = (xml, baseDirectory, relFile, progress, res)
=&gt; new MachiningProject(xml, baseDirectory, progress);
factory.Generators.TryAdd(XName, gen);
//legacy aliases
factory.Generators.TryAdd(&quot;MachiningCourse&quot;, gen);
factory.Generators.TryAdd(&quot;MillingCourse&quot;, gen);
}
</code></pre>
<h3 id="iprogress-threading">IProgress Threading</h3>
<p>The <code>IProgress&lt;object&gt;</code> parameter is threaded through the entire deserialization chain. When a class constructor calls <a class="xref" href="../../api/Hi.Common.XmlUtils.XFactory.html">XFactory</a> to deserialize child objects, it passes the same <code>progress</code> instance:</p>
<pre><code class="lang-csharp">public MyClass(XElement src, string baseDirectory, string relFile,
IProgress&lt;object&gt; progress)
@@ -119,6 +161,11 @@
}
</code></pre>
<p>Parsing errors are reported to the caller-provided <code>IProgress&lt;object&gt;</code> handler.</p>
<h2 id="boot-path">Boot path</h2>
<p>An application's entry point (web service <code>Program.cs</code>, WPF <code>App.xaml.cs</code>, test fixture, etc.) must call the appropriate top-level <code>Reg()</code> once at startup, before any project XML is deserialized. For the simulation pipeline this is:</p>
<pre><code class="lang-csharp">LocalProjectService.Reg();
</code></pre>
<p><code>LocalProjectService.Reg()</code> chains <code>MachiningProject.Reg()</code>, which in turn chains every type the simulation pipeline may deserialize. After this single call returns, <code>XFactory.Default.Generators</code> carries the full deserialization graph.</p>
<h2 id="implementation-patterns">Implementation Patterns</h2>
<h3 id="simple-value-objects">Simple Value Objects</h3>
<p>See <a class="xref" href="../../api/Hi.Milling.Apts.BallApt.html">BallApt</a> implementation:</p>
@@ -286,10 +333,12 @@ public XElement MakeXmlSource(string baseDirectory, string relFile, bool exhibit
</code></pre><h2 id="best-practices">Best Practices</h2>
<ol>
<li><strong>XName</strong>: Always define static <code>XName</code> property matching the class name.</li>
<li><strong>Registration</strong>: Register in static constructor using <a class="xref" href="../../api/Hi.Common.XmlUtils.XFactory.html">XFactory</a>.Regs</li>
<li>Call the <code>XName</code> such like <code>_ = CalleeClass.XName;</code> in the caller class static initialization field so that the registration takes effect before calling the Callee construction by <a class="xref" href="../../api/Hi.Common.XmlUtils.XFactory.html">XFactory</a>.</li>
<li><strong>Registration</strong>: Expose <code>public static void Reg(XFactory factory = null)</code>; first line is <code>factory ??= XFactory.Default;</code> then <code>factory.Generators.TryAdd(XName, …)</code>.</li>
<li><strong>Chain dependents</strong>: For every concrete type T that the ctor reads via <code>XFactory.Gen&lt;T&gt;</code> / <code>XFactory.GenByChild&lt;T&gt;</code>, add <code>T.Reg(factory);</code> to the chain. For polymorphic <code>GenByChild&lt;IInterface&gt;</code>, chain every implementation that the XML may carry.</li>
<li><strong>Idempotent</strong>: Use <code>TryAdd</code>, never <code>Add</code>. The same <code>Reg</code> is called from many boot paths.</li>
<li><strong>Progress Threading</strong>: Pass the <code>IProgress&lt;object&gt;</code> parameter through all nested <code>XFactory</code> calls. See <a href="message-management.html">Message Management</a> for the rationale.</li>
<li><strong>Legacy Support</strong>: Maintain backward compatibility when needed</li>
<li><strong>Legacy Support</strong>: Register the canonical <code>XName</code> first, then group aliases under a <code>//legacy aliases</code> comment.</li>
<li><strong>Derived class registration</strong>: When a derived class needs its own <code>Reg</code>, mark it <code>public new static void Reg(XFactory factory = null)</code> so the C# compiler does not warn about hiding the base method.</li>
</ol>
</article>