From c8bcabcca26e4b8b439ae9e6a1ffef9c94e0dbee Mon Sep 17 00:00:00 2001 From: TheSevenPens <98374109+TheSevenPens@users.noreply.github.com> Date: Sat, 9 May 2026 14:59:52 -0700 Subject: [PATCH 1/3] refactor(scripting): route Loaders and *Dimensions through facade methods (#182 Phase B) Adds facade methods on `Client` for the two operations that previously required reaching past the `VisioScripting.Client` facade, and internalizes the now-redundant types/methods. Implements Phase B of the hybrid public-API decision in #156. Public-API additions on `VisioScripting.Client`: - `Client.Model.LoadDirectedGraphFromXml(XDocument)` -> `DirectedGraphDocument` - `Client.Model.LoadOrgChartFromXml(XDocument)` -> `OrgChartDocument` - `Client.Page.GetPageDimensions(TargetPages)` -> `List` - `Client.Selection.GetShapeDimensions(TargetShapes)` -> `List` Internalized (breaking for direct consumers; data-carrier classes stay public): - `VisioScripting.Loaders.DirectedGraphDocumentLoader`, `OrgChartDocumentLoader` -> `internal class` - `VisioScripting.Models.PageDimensions.Get_PageDimensions` -> `internal static` - `VisioScripting.Models.ShapeDimensions.Get_ShapeDimensions` -> `internal static` Also drops the unused `LoadFromXml(Client, string filename)` overloads on both loaders (each was an internal convenience that loaded the file and deferred to the `XDocument` overload; no external caller invoked them). Migrations: - 3 cmdlets: `ImportVisioModel`, `MeasureVisioPage`, `MeasureVisioShape`. - 4 test calls in `VTest.Models`: 3 in `DirectedGraphDrawModelTests`, 1 in `OrgChartDrawModelTests`. Build clean. All 230 tests passing across the four test projects, run-time ~2 minutes, zero Visio orphan processes. Refs #156, closes Phase B of #182. Phase C (enforcement layer: `Helpers/*` internalization, `InternalsVisibleTo`, `EditorBrowsable`, ADR, CLAUDE.md note) is next. Co-Authored-By: Claude Opus 4.7 (1M context) --- NuGet/CHANGELOG.md | 14 +++++++++++++ .../DirectedGraphDrawModelTests.cs | 6 +++--- .../VTest.Models/OrgChartDrawModelTests.cs | 2 +- .../Commands/VisioModel/ImportVisioModel.cs | 7 +++---- .../Commands/VisioPage/MeasureVisioPage.cs | 13 ++---------- .../Commands/VisioShape/MeasureVisioShape.cs | 21 +++---------------- .../VisioScripting/Commands/ModelCommands.cs | 11 ++++++++++ .../VisioScripting/Commands/PageCommands.cs | 12 +++++++++++ .../Commands/SelectionCommands.cs | 14 +++++++++++++ .../Loaders/DirectedGraphDocumentLoader.cs | 8 +------ .../Loaders/OrgChartDocumentLoader.cs | 8 +------ .../VisioScripting/Models/PageDimensions.cs | 2 +- .../VisioScripting/Models/ShapeDimensions.cs | 2 +- 13 files changed, 67 insertions(+), 53 deletions(-) diff --git a/NuGet/CHANGELOG.md b/NuGet/CHANGELOG.md index 517e11bfe..9d2bf0602 100644 --- a/NuGet/CHANGELOG.md +++ b/NuGet/CHANGELOG.md @@ -12,6 +12,20 @@ The format follows [Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/ ## [Unreleased] +### Added +- New facade methods on `VisioScripting.Client` for operations that previously required reaching past the facade. These are the canonical entry points going forward and align with the hybrid public-API contract decided in [#156](https://github.com/saveenr/VisioAutomation/issues/156): + - `Client.Model.LoadDirectedGraphFromXml(XDocument)` returns a `DirectedGraphDocument` (replaces `DirectedGraphDocumentLoader.LoadFromXml`). + - `Client.Model.LoadOrgChartFromXml(XDocument)` returns an `OrgChartDocument` (replaces `OrgChartDocumentLoader.LoadFromXml`). + - `Client.Page.GetPageDimensions(TargetPages)` returns `List` (replaces the static `PageDimensions.Get_PageDimensions`). + - `Client.Selection.GetShapeDimensions(TargetShapes)` returns `List` (replaces the static `ShapeDimensions.Get_ShapeDimensions`). + + Part of [#182](https://github.com/saveenr/VisioAutomation/issues/182). + +### Removed +- `VisioScripting.Loaders.DirectedGraphDocumentLoader` and `VisioScripting.Loaders.OrgChartDocumentLoader` are now `internal`. Direct consumers should switch to `Client.Model.LoadDirectedGraphFromXml` / `Client.Model.LoadOrgChartFromXml`. Part of [#182](https://github.com/saveenr/VisioAutomation/issues/182). +- Static `Get_PageDimensions` / `Get_ShapeDimensions` factory methods on `VisioScripting.Models.PageDimensions` / `Models.ShapeDimensions` are now `internal`. Use `Client.Page.GetPageDimensions` / `Client.Selection.GetShapeDimensions` instead. The data-carrier types themselves remain `public` (they are return values). Part of [#182](https://github.com/saveenr/VisioAutomation/issues/182). +- Unused `LoadFromXml(Client, string filename)` overloads on both `DirectedGraphDocumentLoader` and `OrgChartDocumentLoader`. Each was an internal convenience that loaded the XML file and deferred to the `XDocument` overload; no external caller ever invoked them. Part of [#182](https://github.com/saveenr/VisioAutomation/issues/182). + ### Fixed - `MsaglRenderer` now honors `Node.Size` when `Node.Cells` is also set on a directed-graph node. Previously the `Cells` assignment overwrote the entire `shape_node.Cells` object, silently dropping the `XFormWidth` / `XFormHeight` populated from `Size`, so the rendered shape came out at the master's default size instead of the requested `Size`. Now the user's `Cells` is merged onto the existing cells via `ApplyFormulasTo`, which preserves Size-derived width/height. Closes [#82](https://github.com/saveenr/VisioAutomation/issues/82). diff --git a/VisioAutomation_2010/VTest.Models/DirectedGraphDrawModelTests.cs b/VisioAutomation_2010/VTest.Models/DirectedGraphDrawModelTests.cs index c973ebaae..9c8b7ddc2 100644 --- a/VisioAutomation_2010/VTest.Models/DirectedGraphDrawModelTests.cs +++ b/VisioAutomation_2010/VTest.Models/DirectedGraphDrawModelTests.cs @@ -300,7 +300,7 @@ public void Loader_RootElement_WrongNameThrows() var dg_xml = SXL.XDocument.Parse(xml); var client = this.GetScriptingClient(); MUT.Assert.ThrowsExactly( - () => VisioScripting.Loaders.DirectedGraphDocumentLoader.LoadFromXml(client, dg_xml)); + () => client.Model.LoadDirectedGraphFromXml(dg_xml)); } [MUT.TestMethod] @@ -364,13 +364,13 @@ private VA.Models.Layouts.DirectedGraph.DirectedGraphDocument load_two_node_grap extra_attrs ?? ""); var dg_xml = SXL.XDocument.Parse(xml); var client = this.GetScriptingClient(); - return VisioScripting.Loaders.DirectedGraphDocumentLoader.LoadFromXml(client, dg_xml); + return client.Model.LoadDirectedGraphFromXml(dg_xml); } private void draw_directed_graph(VisioScripting.Client client, string dg_text) { var dg_xml = SXL.XDocument.Parse(dg_text); - var dgdoc = VisioScripting.Loaders.DirectedGraphDocumentLoader.LoadFromXml(client, dg_xml); + var dgdoc = client.Model.LoadDirectedGraphFromXml(dg_xml); // TODO: Investigate if this this special case for Visio 2013 can be removed // this is a temporary fix to handle the fact that server_u.vss in Visio 2013 doesn't result in server_u.vssx diff --git a/VisioAutomation_2010/VTest.Models/OrgChartDrawModelTests.cs b/VisioAutomation_2010/VTest.Models/OrgChartDrawModelTests.cs index 162fe0613..8becbd126 100644 --- a/VisioAutomation_2010/VTest.Models/OrgChartDrawModelTests.cs +++ b/VisioAutomation_2010/VTest.Models/OrgChartDrawModelTests.cs @@ -29,7 +29,7 @@ public void RenderOrgChart_FromSampleData_DoesNotThrow() private void draw_org_chart(VisioScripting.Client client, string text) { var xmldoc = SXL.XDocument.Parse(text); - var orgchart = VisioScripting.Loaders.OrgChartDocumentLoader.LoadFromXml(client, xmldoc); + var orgchart = client.Model.LoadOrgChartFromXml(xmldoc); client.Model.DrawOrgChart(VisioScripting.TargetPage.Auto, orgchart); } diff --git a/VisioAutomation_2010/VisioPowerShell/Commands/VisioModel/ImportVisioModel.cs b/VisioAutomation_2010/VisioPowerShell/Commands/VisioModel/ImportVisioModel.cs index b672f7829..73dbfffe4 100644 --- a/VisioAutomation_2010/VisioPowerShell/Commands/VisioModel/ImportVisioModel.cs +++ b/VisioAutomation_2010/VisioPowerShell/Commands/VisioModel/ImportVisioModel.cs @@ -1,4 +1,3 @@ -using VisioScripting.Loaders; using SXL = System.Xml.Linq; using SMA = System.Management.Automation; @@ -26,13 +25,13 @@ protected override void ProcessRecord() if (root.Name == "directedgraph") { this.WriteVerbose("Loading Directed Graph"); - var dgdoc = DirectedGraphDocumentLoader.LoadFromXml(this.Client, xmldoc); - this.WriteObject(dgdoc); + var dgdoc = this.Client.Model.LoadDirectedGraphFromXml(xmldoc); + this.WriteObject(dgdoc); } else if (root.Name == "orgchart") { this.WriteVerbose("Loading as Org Chart"); - var orgchartdoc = OrgChartDocumentLoader.LoadFromXml(this.Client, xmldoc); + var orgchartdoc = this.Client.Model.LoadOrgChartFromXml(xmldoc); this.WriteObject(orgchartdoc); } else diff --git a/VisioAutomation_2010/VisioPowerShell/Commands/VisioPage/MeasureVisioPage.cs b/VisioAutomation_2010/VisioPowerShell/Commands/VisioPage/MeasureVisioPage.cs index 56fcdb011..46bd807e5 100644 --- a/VisioAutomation_2010/VisioPowerShell/Commands/VisioPage/MeasureVisioPage.cs +++ b/VisioAutomation_2010/VisioPowerShell/Commands/VisioPage/MeasureVisioPage.cs @@ -17,18 +17,9 @@ public class MeasureVisioPage : VisioCmdlet protected override void ProcessRecord() { - - var targetpages = new VisioScripting.TargetPages(this.Page).ResolveToPages(this.Client); - - if (targetpages.Pages.Count < 1) - { - return; - } - - var list_pagedim = VisioScripting.Models.PageDimensions.Get_PageDimensions(targetpages.Pages); - + var targetpages = new VisioScripting.TargetPages(this.Page); + var list_pagedim = this.Client.Page.GetPageDimensions(targetpages); this.WriteObject(list_pagedim,true); - } } } diff --git a/VisioAutomation_2010/VisioPowerShell/Commands/VisioShape/MeasureVisioShape.cs b/VisioAutomation_2010/VisioPowerShell/Commands/VisioShape/MeasureVisioShape.cs index d5d214494..f9ec23785 100644 --- a/VisioAutomation_2010/VisioPowerShell/Commands/VisioShape/MeasureVisioShape.cs +++ b/VisioAutomation_2010/VisioPowerShell/Commands/VisioShape/MeasureVisioShape.cs @@ -1,4 +1,3 @@ -using System.Linq; using SMA = System.Management.Automation; using IVisio = Microsoft.Office.Interop.Visio; @@ -7,29 +6,15 @@ namespace VisioPowerShell.Commands.VisioShape [SMA.Cmdlet(SMA.VerbsDiagnostic.Measure, Nouns.VisioShape)] public class MeasureVisioShape: VisioCmdlet { - // CONTEXT:SHAPES + // CONTEXT:SHAPES [SMA.Parameter(Mandatory = false)] public IVisio.Shape[] Shape; protected override void ProcessRecord() { - - var targetshapes = new VisioScripting.TargetShapes(this.Shape).ResolveToShapes(this.Client); - - if (targetshapes.Shapes.Count < 1) - { - return; - } - - - var shapeids = VisioAutomation.Core.ShapeIDPairs.FromShapes(targetshapes.Shapes).Select(i => i.ShapeID).ToList(); - var page = targetshapes.Shapes[0].ContainingPage; - var list_shapedim = VisioScripting.Models.ShapeDimensions.Get_ShapeDimensions(page, shapeids); - + var targetshapes = new VisioScripting.TargetShapes(this.Shape); + var list_shapedim = this.Client.Selection.GetShapeDimensions(targetshapes); this.WriteObject(list_shapedim,true); - } - - } } \ No newline at end of file diff --git a/VisioAutomation_2010/VisioScripting/Commands/ModelCommands.cs b/VisioAutomation_2010/VisioScripting/Commands/ModelCommands.cs index 32f674d7d..82e0c0d29 100644 --- a/VisioAutomation_2010/VisioScripting/Commands/ModelCommands.cs +++ b/VisioAutomation_2010/VisioScripting/Commands/ModelCommands.cs @@ -4,6 +4,7 @@ using ORG = VisioAutomation.Models.Documents.OrgCharts; using GRAPH = VisioAutomation.Models.Layouts.DirectedGraph; using GRID = VisioAutomation.Models.Layouts.Grid; +using SXL = System.Xml.Linq; namespace VisioScripting.Commands { @@ -192,5 +193,15 @@ public void DrawDirectedGraphDocument(GRAPH.DirectedGraphDocument dgdoc, GRAPH.D this._client.Output.WriteVerbose("Finished rendering all pages"); this._client.Output.WriteVerbose("Finished rendering directed graph."); } + + public GRAPH.DirectedGraphDocument LoadDirectedGraphFromXml(SXL.XDocument xmldoc) + { + return Loaders.DirectedGraphDocumentLoader.LoadFromXml(this._client, xmldoc); + } + + public ORG.OrgChartDocument LoadOrgChartFromXml(SXL.XDocument xmldoc) + { + return Loaders.OrgChartDocumentLoader.LoadFromXml(this._client, xmldoc); + } } } \ No newline at end of file diff --git a/VisioAutomation_2010/VisioScripting/Commands/PageCommands.cs b/VisioAutomation_2010/VisioScripting/Commands/PageCommands.cs index 1544bdc11..48d3f00b2 100644 --- a/VisioAutomation_2010/VisioScripting/Commands/PageCommands.cs +++ b/VisioAutomation_2010/VisioScripting/Commands/PageCommands.cs @@ -60,6 +60,18 @@ public void DeletePages(TargetPages targetpages, bool renumber) return sizes; } + public List GetPageDimensions(TargetPages targetpages) + { + targetpages = targetpages.ResolveToPages(this._client); + + if (targetpages.Pages.Count < 1) + { + return new List(0); + } + + return Models.PageDimensions.Get_PageDimensions(targetpages.Pages); + } + public IVisio.Page NewPage(VisioScripting.TargetDocument targetdoc, VisioAutomation.Core.Size? size, bool isbackgroundpage) { targetdoc = targetdoc.ResolveToDocument(this._client); diff --git a/VisioAutomation_2010/VisioScripting/Commands/SelectionCommands.cs b/VisioAutomation_2010/VisioScripting/Commands/SelectionCommands.cs index d7f6d11a5..3ddf696eb 100644 --- a/VisioAutomation_2010/VisioScripting/Commands/SelectionCommands.cs +++ b/VisioAutomation_2010/VisioScripting/Commands/SelectionCommands.cs @@ -267,5 +267,19 @@ public bool ContainsShapes(TargetSelection targetselection, int min_items) bool v = num_selected >= min_items; return v; } + + public List GetShapeDimensions(TargetShapes targetshapes) + { + targetshapes = targetshapes.ResolveToShapes(this._client); + + if (targetshapes.Shapes.Count < 1) + { + return new List(0); + } + + var shapeids = VisioAutomation.Core.ShapeIDPairs.FromShapes(targetshapes.Shapes).Select(i => i.ShapeID).ToList(); + var page = targetshapes.Shapes[0].ContainingPage; + return Models.ShapeDimensions.Get_ShapeDimensions(page, shapeids); + } } } \ No newline at end of file diff --git a/VisioAutomation_2010/VisioScripting/Loaders/DirectedGraphDocumentLoader.cs b/VisioAutomation_2010/VisioScripting/Loaders/DirectedGraphDocumentLoader.cs index 89744d433..62ae54bf5 100644 --- a/VisioAutomation_2010/VisioScripting/Loaders/DirectedGraphDocumentLoader.cs +++ b/VisioAutomation_2010/VisioScripting/Loaders/DirectedGraphDocumentLoader.cs @@ -8,7 +8,7 @@ namespace VisioScripting.Loaders { - public class DirectedGraphDocumentLoader + internal class DirectedGraphDocumentLoader { private class BuilderError { @@ -46,12 +46,6 @@ public static BuilderError InvalidToNode(int pagenum, string conid, string toid) } } - public static DirectedGraphDocument LoadFromXml(Client client, string filename) - { - var xmldoc = SXL.XDocument.Load(filename); - return DirectedGraphDocumentLoader.LoadFromXml(client, xmldoc); - } - private class PageData { public VisioAutomation.Models.ConnectorType ConnectorType; diff --git a/VisioAutomation_2010/VisioScripting/Loaders/OrgChartDocumentLoader.cs b/VisioAutomation_2010/VisioScripting/Loaders/OrgChartDocumentLoader.cs index 8ddbe54c2..84d311567 100644 --- a/VisioAutomation_2010/VisioScripting/Loaders/OrgChartDocumentLoader.cs +++ b/VisioAutomation_2010/VisioScripting/Loaders/OrgChartDocumentLoader.cs @@ -5,14 +5,8 @@ namespace VisioScripting.Loaders { - public class OrgChartDocumentLoader + internal class OrgChartDocumentLoader { - public static VAORGCHART.OrgChartDocument LoadFromXml(Client client, string filename) - { - var xdoc = SXL.XDocument.Load(filename); - return OrgChartDocumentLoader.LoadFromXml(client, xdoc); - } - public static VAORGCHART.OrgChartDocument LoadFromXml(Client client, SXL.XDocument xdoc) { var root = xdoc.Root; diff --git a/VisioAutomation_2010/VisioScripting/Models/PageDimensions.cs b/VisioAutomation_2010/VisioScripting/Models/PageDimensions.cs index c280ae42d..9e0683298 100644 --- a/VisioAutomation_2010/VisioScripting/Models/PageDimensions.cs +++ b/VisioAutomation_2010/VisioScripting/Models/PageDimensions.cs @@ -17,7 +17,7 @@ public class PageDimensions public double PrintBottomMargin; - public static List Get_PageDimensions(IList pages) + internal static List Get_PageDimensions(IList pages) { var list_pagedim = new List(pages.Count); diff --git a/VisioAutomation_2010/VisioScripting/Models/ShapeDimensions.cs b/VisioAutomation_2010/VisioScripting/Models/ShapeDimensions.cs index 3f4b9ca1c..477f2292f 100644 --- a/VisioAutomation_2010/VisioScripting/Models/ShapeDimensions.cs +++ b/VisioAutomation_2010/VisioScripting/Models/ShapeDimensions.cs @@ -22,7 +22,7 @@ public class ShapeDimensions public double OneDEndY; - public static List Get_ShapeDimensions(IVisio.Page page, List shapeids) + internal static List Get_ShapeDimensions(IVisio.Page page, List shapeids) { var query = new VASS.Query.CellQuery(); From 30dd4dff3aad7395e7e005c0b8ffe720e6d60010 Mon Sep 17 00:00:00 2001 From: TheSevenPens <98374109+TheSevenPens@users.noreply.github.com> Date: Sat, 9 May 2026 15:08:39 -0700 Subject: [PATCH 2/3] refactor(scripting): enforcement layer for hybrid public-API contract (#182 Phase C) Applies the layered enforcement strategy from #156 Q2: every type in `VisioScripting` that can be `internal` is `internal`, with one `InternalsVisibleTo` exception for `VTest`. Closes Phase C of #182. Internalized (the remaining public-but-off-contract surface): - `VisioScripting.Helpers.WildcardHelper`, `InteropHelper`, `SelectionHelper` -> `internal static`. The other three helpers (`ArrangeHelper`, `ReflectionHelper`, `TextHelper`) were already internal. - `VisioScripting.CommandTarget` -> `internal class`. - `VisioScripting.CommandTargetFlags` -> `internal enum`. - `VisioScripting.Client.GetCommandTarget(CommandTargetFlags)` -> `internal` method. `InternalsVisibleTo`: - `VTest` is the only consumer that reaches into `VisioScripting` internals (exclusively `WildcardHelper.GetRegexForWildcardPattern`, a unit test of the helper itself). Adding `[InternalsVisibleTo("VTest")]` in `VisioScripting/Properties/AssemblyInfo.cs`. None of `VisioPowerShell`, `VTest.Models`, `VTest.Scripting`, or `VTest.PowerShell` need internal access; if a future test or cmdlet does, add a project-specific InternalsVisibleTo line at that time rather than blanket-granting. Models sweep produced no changes today: `DgShapeInfo` and `DgConnectorInfo` were already `internal`; the other Models types either appear in public method signatures (including methods that are dead but staying public until CY27 per #183) or are orphan candidates that fall under the same dead-surface deferral. Documentation: - New ADR at `docs/decisions/visioscripting-public-api.md` capturing the hybrid contract: what's public-stable, what's internal-mutable, the layered enforcement strategy, and the conditions under which to revisit. - `CLAUDE.md` per-commit conventions section gains a one-bullet pointer at the ADR with the actionable summary: renaming/removing `Client` or `*Commands` public methods is breaking; touching `Helpers/`, `Loaders/`, `CommandTarget` etc. is not. Build clean. All 230 tests pass. After Phases B + C, the `VisioScripting` boundary now matches the #156 contract. The boundary is enforced by the C# compiler (mostly via the `internal` keyword); the ADR + CLAUDE.md make the contract reviewable without needing to reconstruct it from the type-system signals alone. Phase A (~36 dead-method removal, deferred to CY27 in #183) is the remaining cleanup. The doc-write under #131 can now proceed against the locked surface; #184 (dead-method-stance sub-question) needs to be answered before the per-page templates start. Refs #156, closes Phase C of #182. Co-Authored-By: Claude Opus 4.7 (1M context) --- CLAUDE.md | 1 + NuGet/CHANGELOG.md | 1 + VisioAutomation_2010/VisioScripting/Client.cs | 2 +- .../VisioScripting/CommandTarget.cs | 2 +- .../VisioScripting/CommandTargetFlags.cs | 2 +- .../VisioScripting/Helpers/InteropHelper.cs | 2 +- .../VisioScripting/Helpers/SelectionHelper.cs | 2 +- .../VisioScripting/Helpers/WildcardHelper.cs | 2 +- .../VisioScripting/Properties/AssemblyInfo.cs | 8 +- docs/decisions/visioscripting-public-api.md | 82 +++++++++++++++++++ 10 files changed, 97 insertions(+), 7 deletions(-) create mode 100644 docs/decisions/visioscripting-public-api.md diff --git a/CLAUDE.md b/CLAUDE.md index 2c3e05935..975947c9b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -44,6 +44,7 @@ All test projects exercise real Visio COM calls. There is no mock/fake layer (in ## Per-commit conventions - **Changelogs:** when a change is consumer-visible (public API, behavior, supported runtime, dependencies) add an entry to the matching `[Unreleased]` section of [`NuGet/CHANGELOG.md`](NuGet/CHANGELOG.md) or [`VisioAutomation_2010/VisioPowerShell/CHANGELOG.md`](VisioAutomation_2010/VisioPowerShell/CHANGELOG.md) in the **same commit**. Pure internal / build / docs changes don't need entries. +- **`VisioScripting` public-API contract:** the `Client` facade and the public method signatures on each `*Commands` class are public-stable per [`docs/decisions/visioscripting-public-api.md`](docs/decisions/visioscripting-public-api.md). **Renaming or removing** a public method on `Client` or any `*Commands` class is a **breaking change** — treat it accordingly. Adding methods is non-breaking. The plumbing (`Helpers/`, `Loaders/`, `CommandTarget`, `CommandTargetFlags`, `Get_*Dimensions` static factories) is `internal` and free to change without notice. When the line is unclear, the ADR has the authoritative table. - **PowerShell loader scripts** in `VisioAutomation_2010/VisioPowerShell/`: `Load*` does in-session imports; `Install*` does persistent installs to the user's PS modules folder; `*.ISE.ps1` is the ISE-launched variant. See the folder's [README.md](VisioAutomation_2010/VisioPowerShell/README.md). ## Tooling notes diff --git a/NuGet/CHANGELOG.md b/NuGet/CHANGELOG.md index 9d2bf0602..dce9010e5 100644 --- a/NuGet/CHANGELOG.md +++ b/NuGet/CHANGELOG.md @@ -25,6 +25,7 @@ The format follows [Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/ - `VisioScripting.Loaders.DirectedGraphDocumentLoader` and `VisioScripting.Loaders.OrgChartDocumentLoader` are now `internal`. Direct consumers should switch to `Client.Model.LoadDirectedGraphFromXml` / `Client.Model.LoadOrgChartFromXml`. Part of [#182](https://github.com/saveenr/VisioAutomation/issues/182). - Static `Get_PageDimensions` / `Get_ShapeDimensions` factory methods on `VisioScripting.Models.PageDimensions` / `Models.ShapeDimensions` are now `internal`. Use `Client.Page.GetPageDimensions` / `Client.Selection.GetShapeDimensions` instead. The data-carrier types themselves remain `public` (they are return values). Part of [#182](https://github.com/saveenr/VisioAutomation/issues/182). - Unused `LoadFromXml(Client, string filename)` overloads on both `DirectedGraphDocumentLoader` and `OrgChartDocumentLoader`. Each was an internal convenience that loaded the XML file and deferred to the `XDocument` overload; no external caller ever invoked them. Part of [#182](https://github.com/saveenr/VisioAutomation/issues/182). +- The plumbing types `VisioScripting.CommandTarget`, `VisioScripting.CommandTargetFlags`, and `Client.GetCommandTarget(CommandTargetFlags)` are now `internal`. The remaining `public` helpers in `VisioScripting.Helpers` (`WildcardHelper`, `InteropHelper`, `SelectionHelper`) are also now `internal`. None of these appear in any documented `*Commands` method signature; they are implementation detail of the `Client` facade. The new public-API contract is captured in [`docs/decisions/visioscripting-public-api.md`](https://github.com/saveenr/VisioAutomation/blob/master/docs/decisions/visioscripting-public-api.md). Phase C of [#182](https://github.com/saveenr/VisioAutomation/issues/182). ### Fixed - `MsaglRenderer` now honors `Node.Size` when `Node.Cells` is also set on a directed-graph node. Previously the `Cells` assignment overwrote the entire `shape_node.Cells` object, silently dropping the `XFormWidth` / `XFormHeight` populated from `Size`, so the rendered shape came out at the master's default size instead of the requested `Size`. Now the user's `Cells` is merged onto the existing cells via `ApplyFormulasTo`, which preserves Size-derived width/height. Closes [#82](https://github.com/saveenr/VisioAutomation/issues/82). diff --git a/VisioAutomation_2010/VisioScripting/Client.cs b/VisioAutomation_2010/VisioScripting/Client.cs index c7e1d19ea..5a1525aa6 100644 --- a/VisioAutomation_2010/VisioScripting/Client.cs +++ b/VisioAutomation_2010/VisioScripting/Client.cs @@ -74,7 +74,7 @@ public ClientContext ClientContext get { return this._client_context; } } - public CommandTarget GetCommandTarget(CommandTargetFlags flags) + internal CommandTarget GetCommandTarget(CommandTargetFlags flags) { var command_target = new CommandTarget(this, flags); return command_target; diff --git a/VisioAutomation_2010/VisioScripting/CommandTarget.cs b/VisioAutomation_2010/VisioScripting/CommandTarget.cs index 9ea320aa0..47d2fe4a1 100644 --- a/VisioAutomation_2010/VisioScripting/CommandTarget.cs +++ b/VisioAutomation_2010/VisioScripting/CommandTarget.cs @@ -3,7 +3,7 @@ namespace VisioScripting { - public class CommandTarget + internal class CommandTarget { private readonly Client _client; public IVisio.Application Application { get; private set; } diff --git a/VisioAutomation_2010/VisioScripting/CommandTargetFlags.cs b/VisioAutomation_2010/VisioScripting/CommandTargetFlags.cs index dd6f2fde5..423621d10 100644 --- a/VisioAutomation_2010/VisioScripting/CommandTargetFlags.cs +++ b/VisioAutomation_2010/VisioScripting/CommandTargetFlags.cs @@ -1,7 +1,7 @@ namespace VisioScripting { [System.Flags] - public enum CommandTargetFlags + internal enum CommandTargetFlags { RequireApplication, RequireDocument, diff --git a/VisioAutomation_2010/VisioScripting/Helpers/InteropHelper.cs b/VisioAutomation_2010/VisioScripting/Helpers/InteropHelper.cs index cb3f87b9a..524265baf 100644 --- a/VisioAutomation_2010/VisioScripting/Helpers/InteropHelper.cs +++ b/VisioAutomation_2010/VisioScripting/Helpers/InteropHelper.cs @@ -4,7 +4,7 @@ namespace VisioScripting.Helpers { - public static class InteropHelper + internal static class InteropHelper { private static bool _static_initialized = false; private static Dictionary _static_g_name_to_enum; diff --git a/VisioAutomation_2010/VisioScripting/Helpers/SelectionHelper.cs b/VisioAutomation_2010/VisioScripting/Helpers/SelectionHelper.cs index c39ea70e0..da80b048d 100644 --- a/VisioAutomation_2010/VisioScripting/Helpers/SelectionHelper.cs +++ b/VisioAutomation_2010/VisioScripting/Helpers/SelectionHelper.cs @@ -5,7 +5,7 @@ namespace VisioScripting.Helpers { - public static class SelectionHelper + internal static class SelectionHelper { public static List GetSelectedShapes(IVisio.Selection selection) { diff --git a/VisioAutomation_2010/VisioScripting/Helpers/WildcardHelper.cs b/VisioAutomation_2010/VisioScripting/Helpers/WildcardHelper.cs index c89db3c5f..13f3227bd 100644 --- a/VisioAutomation_2010/VisioScripting/Helpers/WildcardHelper.cs +++ b/VisioAutomation_2010/VisioScripting/Helpers/WildcardHelper.cs @@ -3,7 +3,7 @@ namespace VisioScripting.Helpers { - public static class WildcardHelper + internal static class WildcardHelper { public static bool NullOrStar(string s) { diff --git a/VisioAutomation_2010/VisioScripting/Properties/AssemblyInfo.cs b/VisioAutomation_2010/VisioScripting/Properties/AssemblyInfo.cs index 27b5a32e7..98adf08dc 100644 --- a/VisioAutomation_2010/VisioScripting/Properties/AssemblyInfo.cs +++ b/VisioAutomation_2010/VisioScripting/Properties/AssemblyInfo.cs @@ -1,7 +1,13 @@ using System.Reflection; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -// General Information about an assembly is controlled through the following +// VTest exercises a couple of VisioScripting helpers (e.g. WildcardHelper) +// directly. The helpers are not part of the hybrid public-API contract +// (see docs/decisions/visioscripting-public-api.md) and stay `internal`. +[assembly: InternalsVisibleTo("VTest")] + +// General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. [assembly: AssemblyTitle("VisioScripting")] diff --git a/docs/decisions/visioscripting-public-api.md b/docs/decisions/visioscripting-public-api.md new file mode 100644 index 000000000..be6f4a9c8 --- /dev/null +++ b/docs/decisions/visioscripting-public-api.md @@ -0,0 +1,82 @@ +# `VisioScripting.Client` is a hybrid public API + +**Status:** Accepted (codified 2026-05-09; see [#156](https://github.com/saveenr/VisioAutomation/issues/156) for the decision thread) + +## Context + +`VisioScripting` is the higher-level facade between the low-level `VisioAutomation` library and the `VisioPowerShell` cmdlets. The entry point is `VisioScripting.Client`, which exposes 25 group properties (`Application`, `Arrange`, `Connection`, `ConnectionPoint`, `Container`, `Control`, `CustomProperty`, `Developer`, `Document`, `Draw`, `Export`, `Grouping`, `Hyperlink`, `Layer`, `Lock`, `Master`, `Model`, `Output`, `Page`, `Selection`, `ShapeSheet`, `Text`, `Undo`, `UserDefinedCell`, `View`). Each property returns a `*Commands` instance whose public methods do the actual work. + +The repo's [`readme.md`](../../readme.md) opens its C# quick-start with `new VisioScripting.Client(app)`, so direct .NET consumers are not hypothetical — but the type was previously undocumented on the gitbook and treated as project-internal in practice. We had to choose: + +1. **Fully public.** Document the entire surface (every method on every `*Commands`, plus `Helpers/`, `Loaders/`, `CommandTarget`, etc.) and treat all of it as a public contract under SemVer. +2. **Fully internal.** Mark the namespace as not-for-direct-use, fix the readme quick-start to use `VisioAutomation` directly, close out [#131](https://github.com/saveenr/VisioAutomation/issues/131) won't-fix. +3. **Hybrid.** Public-stable for the `Client` facade itself; implementation detail for the plumbing. + +The pre-decision usage audit found that `Client..` accounts for ~76% of method usage across cmdlets and tests. Two cmdlets bypassed the facade (the `Loaders` reach-in and the `*Dimensions.Get_*` reach-in), but both were small and migrating them was straightforward. + +## Decision + +Hybrid (option 3). + +## The contract + +### Public-stable (documented; breaking changes treated as breaking) + +- `VisioScripting.Client` — the type, both constructors, and the 25 group properties. +- The public method signatures on each `*Commands` class. Adding methods is non-breaking; renaming or removing methods is breaking. +- `Target*` types at the namespace root (`TargetDocument`, `TargetDocuments`, `TargetPage`, `TargetPages`, `TargetSelection`, `TargetShapes`, `TargetWindow`). Forced — they appear in `*Commands` method signatures. +- `VisioScripting.Models.*` types referenced from public signatures (enums like `PageOrientation`, `ZoomToObject`, `ShapeSelectionOperation`; data carriers like `PageDimensions`, `ShapeDimensions`, `ShapeSheetReader`, `ShapeSheetWriter`). +- `ClientContext` and `DefaultClientContext`. The `Client(app, ClientContext)` overload exists so an embedding host (today `VisioPowerShell`'s [`VisioPsClientContext`](../../VisioAutomation_2010/VisioPowerShell/VisioPsClientContext.cs)) can plug in its own `Output` plumbing; subclassing has to remain a supported pattern. + +### Internal-mutable (not part of contract; free to change) + +- The `*Commands` classes as **constructible types**. The classes themselves are necessarily `public` (they appear as the return type of `Client.Page`, `Client.ShapeSheet`, etc.), but their constructors are `internal`. Consumers obtain instances via `Client.`, never `new`. +- `VisioScripting.Helpers/*` — `WildcardHelper`, `InteropHelper`, `SelectionHelper`, `ArrangeHelper`, `ReflectionHelper`, `TextHelper`. Pure utilities. +- `VisioScripting.Loaders/*` — `DirectedGraphDocumentLoader`, `OrgChartDocumentLoader`. Reached only through `Client.Model.LoadDirectedGraphFromXml(...)` / `Client.Model.LoadOrgChartFromXml(...)`. +- `CommandTarget`, `CommandTargetFlags`. Used inside `*Commands` method bodies to validate preconditions; never appear in a public signature. +- `Client.GetCommandTarget(flags)` — helper for `*Commands` implementations. +- `Models.*` types not appearing in any public signature (today: `DgShapeInfo`, `DgConnectorInfo`, both already `internal`). +- The static factory methods on data-carrier types: `Models.PageDimensions.Get_PageDimensions(...)`, `Models.ShapeDimensions.Get_ShapeDimensions(...)`. The data classes themselves stay `public` (return values); the factories are `internal`. + +## Enforcement + +Layered, weakest to strongest: + +1. **C# `internal` keyword.** The default mechanism. Every type that can be `internal` is `internal`. Same-assembly callers (the `*Commands` classes calling `Helpers/`, `*Dimensions.Get_*`, `CommandTarget`) keep working. +2. **`[InternalsVisibleTo("VTest")]`** on the `VisioScripting` assembly ([`Properties/AssemblyInfo.cs`](../../VisioAutomation_2010/VisioScripting/Properties/AssemblyInfo.cs)). `VTest` exercises `WildcardHelper.GetRegexForWildcardPattern` directly — the test is testing the helper itself, not its consumers. None of `VisioPowerShell`, `VTest.Models`, `VTest.Scripting`, or `VTest.PowerShell` need internal access today; they consume `VisioScripting` exclusively through public surface. If a future test or cmdlet needs to reach in, add an `InternalsVisibleTo` line for the specific project rather than blanket-granting access. +3. **`[EditorBrowsable(EditorBrowsableState.Never)]` + XML doc comments.** Held in reserve for any type that has to stay `public` for type-system reasons but isn't part of the contract. None today after the Phase B + C work, but the mechanism is documented here so future code knows to use it. + +The `Microsoft.CodeAnalysis.PublicApiAnalyzers` package (`PublicAPI.Shipped.txt` / `PublicAPI.Unshipped.txt`) was deferred. It's the strongest enforcement (a build error when the shipped surface drifts unintentionally), but it's another moving part. Revisit if drift becomes a real problem. + +## Consequences + +### Documentation + +- The doc-write under [#131](https://github.com/saveenr/VisioAutomation/issues/131) covers the public surface listed above and only that surface. The pre-decision estimate was ~152 method signatures across the 25 `*Commands` classes; after the Phase B facade additions in [#182](https://github.com/saveenr/VisioAutomation/issues/182) it's ~156. Plus `Client` itself, the 7 `Target*` types, `ClientContext`/`DefaultClientContext`, and the `Models.*` types referenced from public signatures. +- The ~36 dead methods identified in the audit (zero external callers as of 2026-05-09) are part of the locked surface; their removal is deferred to CY27 in [#183](https://github.com/saveenr/VisioAutomation/issues/183) per the project's "improve before audience-reducing changes" guiding principle. The open sub-question of whether to flag them as removal candidates in their gitbook pages is tracked in [#184](https://github.com/saveenr/VisioAutomation/issues/184). + +### Code review + +- Removing or renaming a public method on a `*Commands` class is a breaking change. Reviewers and PR authors need to recognize this. +- Adding methods is not breaking. Same for adding properties to `Client`. +- Touching anything in `Helpers/`, `Loaders/`, `CommandTarget`, `CommandTargetFlags`, or the `Get_*Dimensions` static methods is not a public-API change. + +### Embedding hosts + +- A future host besides `VisioPowerShell` (a VS extension, a different shell, an in-process consumer) can subclass `ClientContext` to plug in its own `Output` routing. The subclass-friendly contract on `ClientContext` is part of the public commitment. + +## Cross-references + +- [#156](https://github.com/saveenr/VisioAutomation/issues/156) — the decision thread, including the four-question Q1–Q4 walkthrough that produced this contract. +- [#182](https://github.com/saveenr/VisioAutomation/issues/182) — pre-lock cleanup (Phases B + C). Phase B added the four facade methods; Phase C applied the enforcement layer that this ADR records. +- [#183](https://github.com/saveenr/VisioAutomation/issues/183) — CY27 dead-method removal (Phase A). Cites this ADR. +- [#184](https://github.com/saveenr/VisioAutomation/issues/184) — the open dead-method-stance sub-question. +- [#131](https://github.com/saveenr/VisioAutomation/issues/131) — the doc-write for the public surface. +- [`CLAUDE.md`](../../CLAUDE.md) — per-commit conventions section carries the reviewer-facing summary of "what's a breaking change in `VisioScripting`." +- [`VisioAutomation_2010/VisioScripting/README.md`](../../VisioAutomation_2010/VisioScripting/README.md) — in-source overview of the facade structure. + +## Reconsider when + +- **A second embedding host appears that needs a different boundary.** Today's only host is `VisioPowerShell`, which lives in this repo. A VS extension or out-of-repo consumer could surface needs that the current line doesn't serve. +- **The dead-surface backlog ([#183](https://github.com/saveenr/VisioAutomation/issues/183)) ships and the line shifts.** Phase A in CY27 will likely shrink the public surface enough to revisit whether the hybrid mechanism is still the right one, or whether a tighter "fully public for what's left" stance becomes affordable. +- **Drift becomes a real problem.** If the contract erodes via inadvertent breaking changes that slip past code review, escalate to the `PublicAPI.Shipped.txt` analyzer (deferred above) or move the off-contract types to a `VisioScripting.Internal.*` sub-namespace as a more visible signal. From b727324b8d7a62a5429a58ecf4772540a2f05442 Mon Sep 17 00:00:00 2001 From: TheSevenPens <98374109+TheSevenPens@users.noreply.github.com> Date: Sat, 9 May 2026 15:42:21 -0700 Subject: [PATCH 3/3] docs(adr): record #184 dead-method-stance decision (Option A) The hybrid public-API ADR previously noted "open sub-question" for whether documented-but-dead methods get a "candidate for CY27 removal" remark on their gitbook pages. #184 resolved this: yes, Option A. Per-method "Remarks" notes go on the page; codebase gets [Obsolete] one minor release before CY27 deletion; both come out together when #183 executes. Closes #184. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/decisions/visioscripting-public-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/decisions/visioscripting-public-api.md b/docs/decisions/visioscripting-public-api.md index be6f4a9c8..fa919cd10 100644 --- a/docs/decisions/visioscripting-public-api.md +++ b/docs/decisions/visioscripting-public-api.md @@ -53,7 +53,7 @@ The `Microsoft.CodeAnalysis.PublicApiAnalyzers` package (`PublicAPI.Shipped.txt` ### Documentation - The doc-write under [#131](https://github.com/saveenr/VisioAutomation/issues/131) covers the public surface listed above and only that surface. The pre-decision estimate was ~152 method signatures across the 25 `*Commands` classes; after the Phase B facade additions in [#182](https://github.com/saveenr/VisioAutomation/issues/182) it's ~156. Plus `Client` itself, the 7 `Target*` types, `ClientContext`/`DefaultClientContext`, and the `Models.*` types referenced from public signatures. -- The ~36 dead methods identified in the audit (zero external callers as of 2026-05-09) are part of the locked surface; their removal is deferred to CY27 in [#183](https://github.com/saveenr/VisioAutomation/issues/183) per the project's "improve before audience-reducing changes" guiding principle. The open sub-question of whether to flag them as removal candidates in their gitbook pages is tracked in [#184](https://github.com/saveenr/VisioAutomation/issues/184). +- The ~36 dead methods identified in the audit (zero external callers as of 2026-05-09) are part of the locked surface; their removal is deferred to CY27 in [#183](https://github.com/saveenr/VisioAutomation/issues/183) per the project's "improve before audience-reducing changes" guiding principle. Per [#184](https://github.com/saveenr/VisioAutomation/issues/184), each dead method's gitbook page carries a "Remarks: candidate for CY27 removal (see [#183](https://github.com/saveenr/VisioAutomation/issues/183))" note. When CY27 deletion is approached, the codebase first gets `[Obsolete("Will be removed in version X")]` markers on those methods (one minor release before deletion); the gitbook page strengthens its remark accordingly. Final removal in CY27 takes both the method and its gitbook page out together. ### Code review