Skip to content

Create the windows of the samples from the container instead of with new() - #870

Merged
romanett merged 5 commits into
masterfrom
romanett/di-refactoring-all-classes-d251b7
Sep 4, 2026
Merged

romanett merged 5 commits into
masterfrom
romanett/di-refactoring-all-classes-d251b7

Conversation

@romanett

@romanett romanett commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Closes #813.

The problem

A sample form was created by the container, but everything it opened was not. A control
called new SomeDlg() and then handed the dialog what it needed through the arguments of
ShowDialog:

string endpointUrl = new DiscoverServerDlg().ShowDialog(Configuration, hostName, m_telemetry);

The dependencies of a window were therefore the business of whoever opened it, which is why
ITelemetryContext, ApplicationConfiguration and ILogger were threaded through nearly
every show method in the repository - 212 construction sites across 185 form and control
classes.

What this does

Samples/WinForms.Common (Opc.Ua.Samples.WinForms) carries the plumbing:
IWindowFactory.Create<TWindow>() builds a window out of the container - what the caller
passes is matched to the constructor by type, the rest is resolved as services - and hands
itself to what it created, so a dialog can open the next one.

// DiscoverServerDlg(ITelemetryContext telemetry)
string endpointUrl = Windows.Create<DiscoverServerDlg>().ShowDialog(Configuration, hostName);

The Windows Forms designer writes new SomeCtrl() into the generated code of a form, so a
control cannot take the factory through its constructor. SampleForm and
SampleUserControl expose it as Windows, and the form the container created hands it down
its control tree once; a control parented later finds it by walking up. A control on neither
says so rather than failing with a null reference. See
the README.

With that in place:

  • Opc.Ua.ClientControls, Opc.Ua.SampleControls, Opc.Ua.ServerControls, the global
    discovery client controls and every sample and Workshop application
    create their windows
    through the factory, and their dialogs take the telemetry, the configuration and their
    logger through their constructors.
  • ServerForm has one container constructor plus ServerFormOptions, which replaces
    ServerForm.Create(IServiceProvider) and the two hand written provider factories the
    reference server and the global discovery server had. The 18 Workshop servers are plain
    SampleWinFormsHost.Run<ServerForm> now.
  • ClientForm and SampleClientForm lost the ServiceMessageContext and the master
    form from their constructors - the first is derived from the configuration, the second is
    what OpenForm sets on the window it opens - so Client.Net4 has no form factory either.
  • The message box the stack asks its questions through was a class copied into the client
    controls and the server controls and installed by hand in 45 entry points. It is a
    registration, and the entry point helper installs it before the host starts.
  • The reference client got a composition root (AddReferenceClient()), so the listener
    its Server menu arms comes from the container rather than out of the window's own fields.
  • ~100 CA2000 suppressions whose justification was the undisposed new SomeDlg() are
    gone with it.

Two windows are deliberately still constructed by hand: ExceptionDlg, which reports
failures of the sample including a failure to build the container, and the console samples'
own message dialog.

Testing

All five tiers pass, on the tree as it stands:

Tier Result
0 - Configuration 237 passed
1 - Server smoke 121 passed
1.5 - Node managers 178 passed, 4 skipped
1.7 - Client models 420 passed, 4 skipped
2 - Client smoke 65 passed

Tier 2 now builds its forms through the factory too, so what it covers is the wiring of the
samples rather than a second copy of it written in the test; a test which assembles a form
out of single controls hands them a factory through the new TestWindowFactory.

dotnet build of the solution and of all four .sln files is clean, with the same warning
set as master.

🤖 Generated with Claude Code

romanett and others added 5 commits September 4, 2026 20:44
…gs with

The forms of the samples were created by the container, but everything they
opened was not: a control called `new SomeDlg()` and then handed the dialog the
telemetry, the configuration and whatever else it needed through the arguments
of ShowDialog. The dependencies of a window were therefore the business of
whoever happened to open it.

Opc.Ua.Samples.WinForms carries the plumbing: `IWindowFactory` creates a window
from the container - constructor parameters the caller does not pass are
resolved as services - and hands itself to what it created, so a dialog can open
the next one. `SampleForm` and `SampleUserControl` expose that factory as
`Windows`; the Windows Forms designer writes a parameterless constructor call
for the controls of a form, so they are handed the factory by the form the
container created rather than taking it themselves.

With that in place the client controls, the sample controls and the server
controls take their telemetry through their constructors and the show methods
lost the parameter. `ServerForm` gained a single container constructor and
`ServerFormOptions`, which replaces `ServerForm.Create` and the two hand written
provider factories the reference server and the global discovery server had.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…he container

The dialogs of the global discovery client took their telemetry and their logger
as arguments of ShowDialog and were created with new(); they now take them
through their constructors and come out of the window factory. The sample client
form and the client form it is built on lost the ServiceMessageContext and the
master form from their constructors - the first is derived from the
configuration, the second is what OpenForm sets on the window it opens - so the
container can create them and Program.cs no longer has a factory of its own.

The message box the stack asks its questions through was a class copied into the
client controls and the server controls and installed by hand in 45 entry
points. It is a registration now, and the entry point helper hands it to the
application instance before the host starts.

The Windows Forms tests build their forms and dialogs through the same factory,
so what they cover is the wiring of the samples and not a second copy of it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The workshop clients kept the last hand rolled window construction: a sample
dialog was newed up and handed the telemetry through ShowDialog. They take it
through their constructors now and come out of the window factory, which leaves
the client controls, the sample controls, the server controls, the global
discovery client and the workshop clients all creating their windows the same
way.

A control which is not below a window the container created now says so -
WindowServices.RequireWindows - instead of failing with a null reference on the
first dialog it opens. The three tier 2 tests which assemble a form out of single
controls hand them a factory of their own through TestWindowFactory, so what they
drive is the wiring of the sample rather than an exception.

All five test tiers pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…built

The reference client had no composition root, so the listener its Server menu
arms was built out of the fields of the window. AddReferenceClient() registers
it, the window asks the container for one per wait, and tier 2 covers that
registration like every other client's.

The four Windows Forms solutions carry the new library, and the documentation
says what it is for: a README next to it on the factory and on why controls are
handed it rather than taking it, the hosting README on what Run<TMainForm> now
does, and the testing notes on why a tier 2 form has to come out of the factory.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Around a hundred CA2000 suppressions justified a dialog which was newed up and
never disposed. The window is created by the container now, the analyser does
not flag those lines, and the justification ("ownership is transferred to the
control owner") no longer describes what happens - so they go.

With every entry point on Run<TMainForm>, the overload which took a factory
delegate has no caller left and is private. The four form classes the earlier
commits missed - the reference client window, the sample server window and its
diagnostics control, and the reference server's event log dialog - are on the
sample base classes like the rest, and ExceptionDlg says why it is the one
window the container does not create.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@romanett
romanett merged commit 96d183c into master Sep 4, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Use Dependency Injection to construct Forms instead of using new() for dependencies

1 participant