How to Define Shared Application Blocks for WinForms and Blazor

An application that ships both a desktop and a browser front end usually ends up with two copies of its navigation rules. Each shell decides for itself which screens exist, whether reopening a screen reuses it, and whether a screen with unsaved changes may be left. Sphere10 Framework moves those rules into Sphere10.Framework.Application.UI, a UI-independent model of blocks, screen definitions and a screen host that the WinForms and Blazor adapters both build on.

This article defines a block with two screens, gives each a different lifetime policy, and drives them through the shared ApplicationScreenHost. It covers the block and definition types, the three activation modes, the screen lifecycle hooks and the navigation guard, and it states where the platform adapters take over.

Shared application modules with distinct desktop and browser presentations.
Shared application modules with distinct desktop and browser presentations.
💡 In short. Define application blocks and screen definitions once in Sphere10.Framework.Application.UI, then use the shared ApplicationScreenHost for screen activation modes, lifecycle hooks and navigation guards under both WinForms and Blazor.

1. Declare a block and its screens

A block derives from ApplicationBlockBase, which stores the block's metadata and its screen definitions. A screen implements IApplicationScreen. The interface requires only the two ownership slots, because every lifecycle member has a default implementation.


public sealed class SampleBlock : ApplicationBlockBase {
}

public sealed class ListScreen : IApplicationScreen {
	public IApplicationScreenHost Host { get; set; }

	public IApplicationBlock Block { get; set; }
}

The definitions are then added to the block, and one is chosen as its default.


var block = new SampleBlock { Name = "Sample", Position = 10 };
var list = new ApplicationScreenDefinition(typeof(ListScreen), "List", ScreenActivationMode.SingleInstance);
var editor = new ApplicationScreenDefinition(typeof(EditorScreen), "Editor", ScreenActivationMode.MultiInstance);
block.AddScreenDefinition(list);
block.AddScreenDefinition(editor);
block.DefaultScreenDefinition = list;

Position orders the block among the application's visible blocks. The activation mode is the lifetime policy of the definition.

ScreenActivationMode Behaviour
MultiInstance Every activation creates a new, independent screen. This is the default.
SingleInstance The first activation creates the screen, and later activations select the retained instance.
PermanentSingleton One instance opens automatically, stays open for the block's lifetime and cannot be closed through tab commands.

A definition can also be declared with ScreenKind.Empty, which makes it a tabless placeholder that the host shows only while no normal screen is open.

2. How the shared model is organised

A block is a named group of navigation, such as one section of an application's sidebar. It owns a set of ApplicationScreenDefinition objects, each of which records a screen type, a title, an activation mode and a kind. One of them can be the block's default screen. The definitions are metadata, so declaring them constructs no screen.

The screen host owns everything that happens at run time. It creates screen instances from definitions, retains the ones whose policy says so, tracks the active screen and runs every transition through the leaving screen's guard.

The model lives in the Sphere10.Framework.Application package, which has no dependency on ASP.NET Core, Windows Forms or any other UI framework. The adapters in Sphere10.Framework.Windows.Forms and Sphere10.Framework.Web.AspNetCore.Blazor supply rendering, dispatch and view ownership on top of the same host engine.

Diagram source
classDiagram
    direction TB
    class ApplicationScreenHost {
      +ActiveBlock
      +ActiveScreen
      +OpenScreenAsync()
    }
    class ApplicationBlockBase {
      +Name
      +DefaultScreenDefinition
      +AddScreenDefinition()
    }
    class ApplicationScreenDefinition {
      +ScreenType
      +ActivationMode
      +ScreenKind
    }
    ApplicationScreenHost o-- ApplicationBlockBase : blocks
    ApplicationBlockBase *-- ApplicationScreenDefinition : owns

3. Open screens through the host

The neutral ApplicationScreenHost takes the blocks and a service provider, through which it resolves or creates screen instances. OpenScreenAsync takes a block and one of the definitions that block owns.


using var host = new ApplicationScreenHost(new[] { block }, new EmptyServices());
await host.InitializeAsync();
var firstList = await host.OpenScreenAsync(block, list);
var secondList = await host.OpenScreenAsync(block, list);
var firstEditor = await host.OpenScreenAsync(block, editor);
var secondEditor = (EditorScreen)await host.OpenScreenAsync(block, editor);

Both list calls return the same object, because the definition is SingleInstance and the host retained the first screen. The two editor calls return different objects. Retention is tracked for each block and definition, so two definitions of one screen type can carry different policies.

The host also exposes ActivateBlockAsync(block), which opens the block's default screen, and SelectBlockAsync(block), which browses a block's navigation without leaving the current screen. ActiveScreen and ActiveBlock report the current selection.

4. Guard navigation and react to activation

Every transition follows the same sequence. The host first asks the active screen whether it may be left. If the answer is no, nothing changes and OpenScreenAsync returns null.

Diagram source
---
config:
  sequence:
    actorMargin: 8
    width: 96
    mirrorActors: false
    messageMargin: 28
    noteMargin: 8
---
sequenceDiagram
    participant H as Host
    participant A as Active
    participant T as Target
    Note over H: OpenScreenAsync
    H->>A: CanDeactivateAsync
    alt returns false
        Note over H: returns null
    else returns true
        Note over H: reuse or create
        H->>A: OnDeactivatedAsync
        H->>T: OnActivatedAsync
    end

A screen takes part by overriding the members of IApplicationScreen it cares about.

Member Called
CanDeactivateAsync Before the screen is hidden, closed or navigated away from. Returning false vetoes the transition.
OnDeactivatedAsync After the guard has allowed the transition, before the screen is hidden or removed.
OnActivatedAsync When the screen becomes the active screen.
Host, Block Assigned by the host before activation, and cleared when the screen is removed.

The sample's second screen implements the guard from a flag.


public sealed class EditorScreen : IApplicationScreen {
	public IApplicationScreenHost Host { get; set; }

	public IApplicationBlock Block { get; set; }

	public bool HasUnsavedChanges { get; set; }

	public Task<bool> CanDeactivateAsync(CancellationToken cancellationToken = default) => Task.FromResult(!HasUnsavedChanges);
}


secondEditor.HasUnsavedChanges = true;
var blocked = await host.OpenScreenAsync(block, list) == null;

While the flag is set, the request for the other screen returns null and the guarded screen stays active. Because the guard is asynchronous, a real screen can await a save or discard prompt inside it. The host serializes transitions, so a lifecycle callback must not await another transition on the same host.

5. Bind the correct platform views

The shared layer decides which screens exist, how long they live and when they may be left. It does not turn a WinForms control into a Razor component. Each platform registers its own screen types, which derive from that platform's screen base, and reuses the block structure and activation policies. In an application, blocks are normally declared with the fluent ApplicationPluginBuilder and block builders, which both adapters inherit, and the adapter supplies the host.

ℹ️ The sample is a console program that exercises the neutral ApplicationScreenHost directly. It demonstrates retention, independent instances and the navigation guard. It does not display WinForms controls or render a Blazor page.

Registration-time objects are shared, so they should not capture one user's mutable state. Resolve scoped services when a command executes, and pass identifiers rather than live objects between screens.

6. Get the package and run the sample

Add the package to a .NET 10 project.


dotnet add package Sphere10.Framework.Application --version 3.1.3

Download the sample project. It targets .NET 10 and references the Sphere10.Framework.Application 3.1.3 package.


dotnet run -c Release


Single-instance screen reused: True
Multi-instance screens are independent: True
Guard blocks navigation: True
Navigation resumes when the guard allows it: True

Each line is a check the program makes, and it throws if one fails.


Download the complete NuGet sample project. The archive contains the project, its model files and run instructions. Keep the pinned package version when reproducing the example.

sample.zip
Download the complete worked example and build instructions (ZIP).

The Sphere10 Framework overview describes the rest of the toolkit.

Author: Herman Schoenfeld