Windows Forms Applications

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Visual Basic 17.13 on .NET 10 (SDK 10.0.402) and was checked on 2026-10-19. Windows Forms runs only on Windows, and the machine used for this page is a Linux container, so the Windows Forms examples were built, not run: each was compiled on the .NET 10 SDK with EnableWindowsTargeting set to true (the SDK then compiles against the Windows desktop reference assemblies on any operating system), with Option Strict On and warnings treated as errors, and none of them was started. That proves that the code compiles and that the compiler messages shown are the real ones; it does not prove how a window looks or behaves, and the descriptions of run-time behaviour come from the Microsoft documentation and from reading the open-source Windows Forms runtime, as cited. Two examples that do not need a window were run for real: the BindingList(Of T) model behind data binding, and the resource lookup behind localization. They are labelled "run". The root namespace of the projects is ShelfDesktop.

Windows Forms is the oldest of the Windows desktop UI frameworks and the one most Visual Basic programmers know first. Mastering VB.NET (Hussain and Hussain) builds its chapters 4 and 11 on it, with the designer, Handles, Anchor and Dock, components such as Timer, ErrorProvider and BindingSource, custom controls and localization; Visual Basic Quickstart Guide (Olmsted) uses a Windows Forms handler for its first event example. Most of that survives on .NET 10 unchanged. What changed is the project format, the start-up code, the designer, a handful of new capabilities and a few removals, and the Visual Basic-specific Application Framework, which is the part with the most surprises and gets most of this page. The same ideas for the other Windows UI technology are on WPF Applications; the C# view of events is on Delegates, Lambdas and Events (C#).

Windows Forms on .NET and on .NET Framework

Windows Forms (WinForms) is a wrapper over the Win32 windowing system: every control is a window handle, and the framework turns Windows messages into .NET events. The code for it was ported to .NET Core 3.0 and is now an open-source part of .NET (the dotnet/winforms repository). It is Windows-only: the project targets a -windows framework moniker and builds against the Windows Desktop targeting pack. The differences from the .NET Framework version, which still ships with Windows and is in maintenance:

Topic .NET Framework 4.8.1 .NET 10

Target

net48 (or v4.8 in an old-style project)

net10.0-windows, with <UseWindowsForms>true</UseWindowsForms>; EnableWindowsTargeting to build on Linux or macOS

Project file

old-style .vbproj with My Project and an AssemblyInfo.vb, or SDK-style

always SDK-style, no My Project folder

Start-up

Application Framework by default: Application.myapp, a generated Main

the SDK template writes its own Sub Main in Program.vb and does not enable the framework (see below)

Designer

in-process designer in Visual Studio

out-of-process designer (Visual Studio 2022 and 2026); controls and component editors that were not yet ported appear over time (some were ported in .NET 10)

Default font

Microsoft Sans Serif 8.25pt

Segoe UI 9pt (since .NET Core 3.1), settable for the whole application from .NET 6

High DPI

opt-in through app.config and a manifest

Application.SetHighDpiMode or ApplyApplicationDefaults, PerMonitorV2 improved in every release

Serialization

the binary object serializer of the .NET Framework is available

removed in .NET 9 (it throws PlatformNotSupportedException); clipboard and drag-and-drop use a safer subset, and the designer was changed accordingly (see Serialization: JSON and XML)

Dialogs

MessageBox, common dialogs

also TaskDialog (.NET 5), ShowDialogAsync (.NET 9, stable in .NET 10)

Threading

BeginInvoke, BackgroundWorker

also Control.InvokeAsync (.NET 9) and, in practice, Async/Await everywhere

Distribution

installed with the OS; ClickOnce, MSI

framework-dependent (needs the Desktop Runtime) or self-contained; MSIX, ClickOnce, MSI (see the deployment page)

Third-party controls and Visual Studio add-ins of the .NET Framework era may have no .NET 10 version, so check each component before a migration; the official upgrade guide is in the references. The features that arrived in each release, with the sources checked on 2026-10-19:

Release What matters to a VB Windows Forms programmer

.NET Core 3.0

Windows Forms and WPF on .NET; Visual Basic was not supported on .NET Core until .NET 5

.NET 5

Visual Basic support for Windows Forms, WPF and class libraries; TaskDialog; the VB template defaults to SystemAware DPI mode

.NET 6

ApplyApplicationDefaults for the VB Application Framework (Font, HighDpiMode, MinimumSplashScreenDisplayTime); the application-wide default font; the new application bootstrap for C# projects; PerMonitorV2 improvements

.NET 7

command binding and a new data-binding engine in preview (EnablePreviewFeatures)

.NET 8

the data-binding engine and Button.Command fully enabled; high DPI improvements

.NET 9

Control.InvokeAsync; Form.ShowAsync, ShowDialogAsync and TaskDialog.ShowDialogAsync as experimental; dark mode (SystemColorMode) as experimental; the binary object serializer removed; the WFO1000 analyzer

.NET 10

async dialogs and dark mode no longer experimental (ApplyApplicationDefaultsEventArgs.ColorMode compiles with no suppression); clipboard redesign with TryGetData(Of T); new analyzers (WFDEV004, WFDEV005, WFDEV006); Form.ScreenCaptureMode

.NET 11 (upcoming)

not generally available at the time of writing (11.0.100 RC 2 was the newest SDK checked); its what’s-new page is a release-candidate document, so verify it after the release

Creating a project: dotnet new winforms

On a Windows machine, dotnet new winforms -lang VB -o ShelfDesktop creates the project. On this Linux container the template files were created but the restore that follows failed with the message that tells you how to compile the project anyway:

$ dotnet new winforms -lang VB -o ShelfDesktop
The template "Windows Forms App" was created successfully.

Processing post-creation actions...
Restoring ShelfDesktop.vbproj:
  Determining projects to restore...
/usr/share/dotnet/sdk/10.0.402/Sdks/Microsoft.NET.Sdk/targets/Microsoft.NET.Sdk.FrameworkReferenceResolution.targets(120,5): error NETSDK1100: To build a project targeting Windows on this operating system, set the EnableWindowsTargeting property to true.
Restore failed.
Post action failed.
Manual instructions: Run 'dotnet restore'


$ dotnet build
/usr/share/dotnet/sdk/10.0.402/Sdks/Microsoft.NET.Sdk/targets/Microsoft.NET.Sdk.FrameworkReferenceResolution.targets(120,5): error NETSDK1100: To build a project targeting Windows on this operating system, set the EnableWindowsTargeting property to true.
/usr/share/dotnet/sdk/10.0.402/Sdks/Microsoft.NET.Sdk/targets/Microsoft.NET.Sdk.FrameworkReferenceResolution.targets(120,5): error NETSDK1100: To build a project targeting Windows on this operating system, set the EnableWindowsTargeting property to true.
    0 Warning(s)
$ dotnet build -p:EnableWindowsTargeting=true
    0 Warning(s)
    0 Error(s)

Source: dotnet new

The generated project (on Windows there is no NETSDK1100; the third command above is the same build with Windows targeting on) has these files, shown exactly as the template wrote them:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net10.0-windows</TargetFramework>
    <RootNamespace>ShelfDesktop</RootNamespace>
    <StartupObject>Sub Main</StartupObject>
    <UseWindowsForms>true</UseWindowsForms>
  </PropertyGroup>

  <ItemGroup>
    <Import Include="System.Data" />
    <Import Include="System.Drawing" />
    <Import Include="System.Windows.Forms" />
  </ItemGroup>

</Project>
Friend Module Program

    <STAThread()>
    Friend Sub Main(args As String())
        Application.SetHighDpiMode(HighDpiMode.SystemAware)
        Application.EnableVisualStyles()
        Application.SetCompatibleTextRenderingDefault(False)
        Application.Run(New Form1)
    End Sub

End Module
Public Class Form1

End Class
<Global.Microsoft.VisualBasic.CompilerServices.DesignerGenerated()>
Partial Class Form1
    Inherits System.Windows.Forms.Form

    'Form overrides dispose to clean up the component list.
    <System.Diagnostics.DebuggerNonUserCode()>
    Protected Overrides Sub Dispose(disposing As Boolean)
        Try
            If disposing AndAlso components IsNot Nothing Then
                components.Dispose()
            End If
        Finally
            MyBase.Dispose(disposing)
        End Try
    End Sub

    'Required by the Windows Form Designer
    Private components As System.ComponentModel.IContainer

    'NOTE: The following procedure is required by the Windows Form Designer
    'It can be modified using the Windows Form Designer.
    'Do not modify it using the code editor.
    <System.Diagnostics.DebuggerStepThrough()>
    Private Sub InitializeComponent()
        components = New System.ComponentModel.Container()
        AutoScaleMode = AutoScaleMode.Font
        ClientSize = New Size(800, 450)
        Text = "Form1"
    End Sub

End Class

Reading the template, in the order of surprise:

  • Program.vb has its own Sub Main. Friend Module Program with <STAThread()>, Application.SetHighDpiMode(HighDpiMode.SystemAware), Application.EnableVisualStyles(), Application.SetCompatibleTextRenderingDefault(False) and Application.Run(New Form1). This is the C# model (Program.cs), not the Visual Basic Application Framework that .NET Framework projects use.

  • <StartupObject>Sub Main</StartupObject> keeps the project on that Sub Main. There is no MyType property, so MyType is Empty and My.Forms, My.Application and the default instances do not exist; Form1.Show() does not compile (BC30469, shown below).

  • Form1.vb is the code-behind, empty, and Form1.Designer.vb is the designer-generated half of the same Partial Class (see the next sections). Form1.Designer.vb is Partial Class Form1 with Inherits System.Windows.Forms.Form (the sections after the next one explain it).

  • <Import Include="System.Windows.Forms" /> (and System.Drawing, System.Data) are project-wide imports: that is why Application, Form and Button need no Imports in any file.

  • ShelfDesktop.vbproj.user (not shown) marks Form1.vb as <SubType>Form</SubType> for the designer.

  • The template turns neither OptionStrict nor TreatWarningsAsErrors on; the examples on this page have <OptionStrict>On</OptionStrict> in the project.

Only the SDK template was checked for this page. Visual Studio’s own Windows Forms App template may differ: the Microsoft blog post cited below describes a Visual Basic project that has the Application Framework switched on and an ApplicationEvents.vb from the start, with the framework as a section of the project properties. To find out which kind of project you have, look for MyType in the project file and for a Sub Main.

Three ways to start a Windows Forms program

The start-up mechanism is chosen by one project property, MyType, plus which Sub Main exists. This is the most important decision in a VB Windows Forms project and it is where most confusion comes from, because the older books and answers assume the Application Framework is always on.

Mode Project Who starts the application What you get

Own Sub Main (the template)

MyType not set (Empty), <StartupObject>Sub Main</StartupObject>

your Sub Main: Application.Run(New Form1)

no My, no default instances, no Application Events; closest to C#

Application Framework

<MyType>WindowsForms</MyType>, no Sub Main

the compiler-generated MyApplication.Main

My.Application, My.Forms, default instances, ApplicationEvents.vb, splash screen, single instance, My.Settings auto-save

Custom Sub Main

<MyType>WindowsFormsWithCustomSubMain</MyType>, your own Sub Main

your Sub Main

My.Forms, default instances and My.Computer exist; the framework’s start-up and events do not run

The compiler messages show how the modes exclude each other. With MyType=WindowsForms the compiler adds a Main of its own, so a project that also has the template’s Sub Main has two:

vbc : error BC30738: 'Sub Main' is declared more than once in 'ShelfDesktop': ShelfDesktop.My.MyApplication.Main(Args As String()), ShelfDesktop.Startup.Main()

while MyType=WindowsFormsWithCustomSubMain with the same code builds without error. Switching a template-generated project to the Application Framework therefore means: set MyType to WindowsForms, delete (or empty) the Sub Main, remove StartupObject, and tell the framework which form to start by overriding OnCreateMainForm. (Visual Studio’s project page does this for you by writing Application.myapp and a generated Application.Designer.vb.)

The designer, Form.vb and Form.Designer.vb

A Windows Forms form is a class split across two files that share one Partial Class:

File Contents

AddBookForm.vb

your code: event handlers, properties, validation. Visual Studio never rewrites it

AddBookForm.Designer.vb

generated by the form designer: the InitializeComponent method that creates the controls and sets their properties, the WithEvents declarations (one per named control), Dispose and the components container. Marked <DesignerGenerated>; do not edit it by hand

AddBookForm.resx

resources that belong to the form: images, and strings and sizes when the form is Localizable (see the localization section)

The designer writes controls as Friend WithEvents fields and the code in InitializeComponent between SuspendLayout and ResumeLayout. A complete designer file for the Add a book dialog of the shelf application, written in the style that Visual Studio produces (a TableLayoutPanel for the fields, a FlowLayoutPanel for the buttons, an ErrorProvider and a ToolTip as components), is:

Option Strict On

<Global.Microsoft.VisualBasic.CompilerServices.DesignerGenerated()>
Partial Class AddBookForm
    Inherits System.Windows.Forms.Form

    'Form overrides dispose to clean up the component list.
    <System.Diagnostics.DebuggerNonUserCode()>
    Protected Overrides Sub Dispose(disposing As Boolean)
        Try
            If disposing AndAlso components IsNot Nothing Then
                components.Dispose()
            End If
        Finally
            MyBase.Dispose(disposing)
        End Try
    End Sub

    Private components As System.ComponentModel.IContainer

    <System.Diagnostics.DebuggerStepThrough()>
    Private Sub InitializeComponent()
        components = New System.ComponentModel.Container()
        LayoutPanel = New System.Windows.Forms.TableLayoutPanel()
        TitleLabel = New System.Windows.Forms.Label()
        TitleTextBox = New System.Windows.Forms.TextBox()
        IsbnLabel = New System.Windows.Forms.Label()
        IsbnTextBox = New System.Windows.Forms.TextBox()
        YearLabel = New System.Windows.Forms.Label()
        YearUpDown = New System.Windows.Forms.NumericUpDown()
        Buttons = New System.Windows.Forms.FlowLayoutPanel()
        OkButton = New System.Windows.Forms.Button()
        DismissButton = New System.Windows.Forms.Button()
        Problems = New System.Windows.Forms.ErrorProvider(components)
        Tips = New System.Windows.Forms.ToolTip(components)
        LayoutPanel.SuspendLayout()
        CType(YearUpDown, System.ComponentModel.ISupportInitialize).BeginInit()
        Buttons.SuspendLayout()
        CType(Problems, System.ComponentModel.ISupportInitialize).BeginInit()
        SuspendLayout()
        '
        'LayoutPanel
        '
        LayoutPanel.ColumnCount = 2
        LayoutPanel.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle())
        LayoutPanel.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100.0F))
        LayoutPanel.Controls.Add(TitleLabel, 0, 0)
        LayoutPanel.Controls.Add(TitleTextBox, 1, 0)
        LayoutPanel.Controls.Add(IsbnLabel, 0, 1)
        LayoutPanel.Controls.Add(IsbnTextBox, 1, 1)
        LayoutPanel.Controls.Add(YearLabel, 0, 2)
        LayoutPanel.Controls.Add(YearUpDown, 1, 2)
        LayoutPanel.Controls.Add(Buttons, 0, 3)
        LayoutPanel.SetColumnSpan(Buttons, 2)
        LayoutPanel.Dock = System.Windows.Forms.DockStyle.Fill
        LayoutPanel.Padding = New System.Windows.Forms.Padding(12)
        LayoutPanel.RowCount = 4
        LayoutPanel.RowStyles.Add(New System.Windows.Forms.RowStyle())
        LayoutPanel.RowStyles.Add(New System.Windows.Forms.RowStyle())
        LayoutPanel.RowStyles.Add(New System.Windows.Forms.RowStyle())
        LayoutPanel.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100.0F))
        '
        'TitleLabel
        '
        TitleLabel.Anchor = System.Windows.Forms.AnchorStyles.Left
        TitleLabel.AutoSize = True
        TitleLabel.Text = "&Title"
        '
        'TitleTextBox
        '
        TitleTextBox.Anchor = System.Windows.Forms.AnchorStyles.Left Or System.Windows.Forms.AnchorStyles.Right
        TitleTextBox.AccessibleName = "Title of the book"
        TitleTextBox.TabIndex = 0
        '
        'IsbnLabel
        '
        IsbnLabel.Anchor = System.Windows.Forms.AnchorStyles.Left
        IsbnLabel.AutoSize = True
        IsbnLabel.Text = "&ISBN-13"
        '
        'IsbnTextBox
        '
        IsbnTextBox.Anchor = System.Windows.Forms.AnchorStyles.Left Or System.Windows.Forms.AnchorStyles.Right
        IsbnTextBox.AccessibleName = "ISBN-13"
        IsbnTextBox.TabIndex = 1
        Tips.SetToolTip(IsbnTextBox, "Thirteen digits; hyphens are allowed")
        '
        'YearLabel
        '
        YearLabel.Anchor = System.Windows.Forms.AnchorStyles.Left
        YearLabel.AutoSize = True
        YearLabel.Text = "&Year"
        '
        'YearUpDown
        '
        YearUpDown.Anchor = System.Windows.Forms.AnchorStyles.Left
        YearUpDown.Maximum = New Decimal(New Integer() {2100, 0, 0, 0})
        YearUpDown.Minimum = New Decimal(New Integer() {1450, 0, 0, 0})
        YearUpDown.TabIndex = 2
        YearUpDown.Value = New Decimal(New Integer() {2000, 0, 0, 0})
        '
        'Buttons
        '
        Buttons.Anchor = System.Windows.Forms.AnchorStyles.Right
        Buttons.AutoSize = True
        Buttons.Controls.Add(OkButton)
        Buttons.Controls.Add(DismissButton)
        Buttons.FlowDirection = System.Windows.Forms.FlowDirection.RightToLeft
        '
        'OkButton
        '
        OkButton.Text = "&Add"
        OkButton.TabIndex = 3
        '
        'DismissButton
        '
        DismissButton.DialogResult = System.Windows.Forms.DialogResult.Cancel
        DismissButton.Text = "Cancel"
        DismissButton.CausesValidation = False
        DismissButton.TabIndex = 4
        '
        'Problems
        '
        Problems.ContainerControl = Me
        '
        'AddBookForm
        '
        AcceptButton = OkButton
        AutoScaleDimensions = New System.Drawing.SizeF(7.0F, 15.0F)
        AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font
        CancelButton = DismissButton
        ClientSize = New System.Drawing.Size(420, 190)
        Controls.Add(LayoutPanel)
        FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog
        MaximizeBox = False
        MinimizeBox = False
        Name = "AddBookForm"
        StartPosition = System.Windows.Forms.FormStartPosition.CenterParent
        Text = "Add a book"
        LayoutPanel.ResumeLayout(False)
        LayoutPanel.PerformLayout()
        CType(YearUpDown, System.ComponentModel.ISupportInitialize).EndInit()
        Buttons.ResumeLayout(False)
        CType(Problems, System.ComponentModel.ISupportInitialize).EndInit()
        ResumeLayout(False)
    End Sub

    Friend WithEvents LayoutPanel As System.Windows.Forms.TableLayoutPanel
    Friend WithEvents TitleLabel As System.Windows.Forms.Label
    Friend WithEvents TitleTextBox As System.Windows.Forms.TextBox
    Friend WithEvents IsbnLabel As System.Windows.Forms.Label
    Friend WithEvents IsbnTextBox As System.Windows.Forms.TextBox
    Friend WithEvents YearLabel As System.Windows.Forms.Label
    Friend WithEvents YearUpDown As System.Windows.Forms.NumericUpDown
    Friend WithEvents Buttons As System.Windows.Forms.FlowLayoutPanel
    Friend WithEvents OkButton As System.Windows.Forms.Button
    Friend WithEvents DismissButton As System.Windows.Forms.Button
    Friend WithEvents Problems As System.Windows.Forms.ErrorProvider
    Friend WithEvents Tips As System.Windows.Forms.ToolTip
End Class

Things to read in this listing, because they are what you meet when you open a real designer file:

  • Friend WithEvents is what lets Handles name a control (below). Every control that you want to handle events of, or touch from code, needs a field; controls with no events can be declared without WithEvents.

  • AutoScaleDimensions and AutoScaleMode = Font make the form scale with the user’s DPI and font. If you change the form’s font after the controls exist, the scale factors may not be what you expect.

  • Anchor = Left Or Right on a text box means "keep the distance to both sides, so stretch". Fields in a TableLayoutPanel cell are anchored, not docked.

  • CancelButton and AcceptButton are form properties, so a button named CancelButton would collide with them: the designer names the second button DismissButton here. VB is case-insensitive, so Cancel and cancel are the same name too.

  • Names that collide with members of Control are the classic designer trap. A field named Layout gets BC40004 because Control has a Layout event:

e8.vb(6,24): warning BC40004: WithEvents variable 'Layout' conflicts with event 'Layout' in the base class 'Control' and should be declared 'Shadows'.
e8.vb(7,24): warning BC40004: WithEvents variable 'Resize' conflicts with event 'Resize' in the base class 'Control' and should be declared 'Shadows'.

The form’s own code is the other half of the class. AddBookForm.vb implements the dialog’s behaviour with Handles clauses and validation events (explained in the next sections):

Option Strict On

Imports System.ComponentModel

Public Class AddBookForm
    ' The result for the caller; Nothing until the user pressed Add with valid input.
    Public ReadOnly Property NewBook As Book

    Public Sub New()
        InitializeComponent()
        AutoValidate = AutoValidate.EnablePreventFocusChange
    End Sub

    Private Sub AddBookForm_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        ActiveControl = TitleTextBox
    End Sub

    ' One handler for two events: the Handles clause takes a comma-separated list.
    Private Sub Fields_TextChanged(sender As Object, e As EventArgs) Handles TitleTextBox.TextChanged, IsbnTextBox.TextChanged
        OkButton.Enabled = TitleTextBox.TextLength > 0
    End Sub

    Private Sub TitleTextBox_Validating(sender As Object, e As CancelEventArgs) Handles TitleTextBox.Validating
        If String.IsNullOrWhiteSpace(TitleTextBox.Text) Then
            Problems.SetError(TitleTextBox, "A title is required.")
            e.Cancel = True
        End If
    End Sub

    Private Sub IsbnTextBox_Validating(sender As Object, e As CancelEventArgs) Handles IsbnTextBox.Validating
        If Not IsbnCheck.IsValid(IsbnTextBox.Text) Then
            Problems.SetError(IsbnTextBox, "Not a valid ISBN-13 (wrong length or check digit).")
            e.Cancel = True
        End If
    End Sub

    ' Validated is raised only after Validating finished without Cancel: clear the error here.
    Private Sub Fields_Validated(sender As Object, e As EventArgs) Handles TitleTextBox.Validated, IsbnTextBox.Validated
        Problems.SetError(DirectCast(sender, Control), "")
    End Sub

    Private Sub OkButton_Click(sender As Object, e As EventArgs) Handles OkButton.Click
        If Not ValidateChildren() Then Return
        _NewBook = New Book With {
            .Title = TitleTextBox.Text.Trim(),
            .Isbn = IsbnTextBox.Text,
            .Year = CInt(YearUpDown.Value)}
        DialogResult = DialogResult.OK
    End Sub
End Class
Option Strict On

Friend Module Startup
    <STAThread>
    Friend Sub Main()
        Application.SetHighDpiMode(HighDpiMode.PerMonitorV2)
        Application.EnableVisualStyles()
        Application.SetCompatibleTextRenderingDefault(False)
        Using dialog As New AddBookForm()
            If dialog.ShowDialog() = DialogResult.OK Then
                MessageBox.Show(dialog.NewBook.ToString(), "Added")
            End If
        End Using
    End Sub
End Module

Built, not run (StartupObject Startup, the root namespace is ShelfDesktop). The first listing needs ShelfDesktop.vbproj with UseWindowsForms and imports of System.Windows.Forms and System.Drawing as shown for the template. Book and IsbnCheck are a trimmed copy of the types of the running shelf scenario (Shelf.Core) with INotifyPropertyChanged, and appear with the data-binding section.

Rules for designer files

  • Do not move code into InitializeComponent: the designer rewrites that method from the control properties and discards what it does not understand. Put start-up code in the constructor after the InitializeComponent() call, or in Load.

  • A form must keep a Public Sub New() (no parameters) that calls InitializeComponent(); the designer instantiates the form with it. In a constructor with parameters (for dependency injection, see below) call InitializeComponent() as well. Forgetting the call is BC30451 when the method name is missing, and a silent empty form when only the call is missing:

e7.vb(7,9): error BC30451: 'InitializeComponent' is not declared. It may be inaccessible due to its protection level.
  • DesignMode is False inside a constructor, so code that must not run at design time (database access, file reads) should not be in the constructor of a control that you drop onto forms.

  • Merge conflicts: the designer file is generated text, and two people editing a form at the same time produce conflicts in it. Keep forms small and put layout in the child controls.

  • Analyzers skip generated code: in a test the same property that is an error in X.vb produced no diagnostic in X.Designer.vb. Code that you write by hand in the other file is analysed, which is the reason for the next section.

WFO1000: properties of controls must say how they are serialized

New in .NET 9 and an error in .NET 10: a Public property of a class that derives from Control (so also Form and UserControl) with a setter must tell the designer whether to serialize it. A form that exposes a property for its caller triggers it:

Option Strict On

Public Class ListForm
    Inherits Form

    Friend WithEvents LayoutPanel As New TableLayoutPanel()
    Friend WithEvents Header As Label
    Public Property Rating As Button
    Public Property Count As Integer
End Class

Friend Module Startup
    <STAThread>
    Friend Sub Main()
    End Sub
End Module
e9.vb(6,23): error WFO1000: Property 'LayoutPanel' does not configure the code serialization for its property content (https://aka.ms/winforms-warnings/wfo1000)
e9.vb(7,23): error WFO1000: Property 'Header' does not configure the code serialization for its property content (https://aka.ms/winforms-warnings/wfo1000)
e9.vb(8,21): error WFO1000: Property 'Rating' does not configure the code serialization for its property content (https://aka.ms/winforms-warnings/wfo1000)
e9.vb(9,21): error WFO1000: Property 'Count' does not configure the code serialization for its property content (https://aka.ms/winforms-warnings/wfo1000)

Note that Friend WithEvents fields count too, because they are compiled to properties (the second line of the output): they are not flagged in a *.Designer.vb file only because the analyzer skips generated code (checked by building the same fields in both file names). The fix is one of DefaultValue, DesignerSerializationVisibility(Hidden) (the usual choice for a property that carries run-time state, as the Browsable(False) plus DesignerSerializationVisibility pair in the IsbnBox control below), or a ShouldSerialize method. ReadOnly properties such as NewBook in AddBookForm are not affected. The documented opt-outs are <NoWarn>$(NoWarn);WFO1000</NoWarn> in the project or dotnet_diagnostic.WFO1000.severity = none in .editorconfig.

Events: Handles and AddHandler

Visual Basic has two ways to attach a handler, and a Windows Forms program uses both.

Aspect Handles clause AddHandler statement

Where the link is made

at compile time, in the declaration of the handler

at run time, when the statement runs

Needs

a WithEvents field (or Me, MyBase)

any object that has the event

Removal

automatic when the WithEvents field is assigned another object (the property setter unhooks the old one)

RemoveHandler, with the same delegate (BC42326 warns for lambdas)

Several events

Handles TitleTextBox.TextChanged, IsbnTextBox.TextChanged

one statement each

Typical use

controls created by the designer

controls created in code, menu items, dynamic lists

The form’s own events

Handles MyBase.Load or Handles Me.Load

AddHandler Load, AddressOf Form_Load

The designer wires a handler with a Handles clause: double-clicking a button in the designer writes Private Sub OkButton_Click(sender As Object, e As EventArgs) Handles OkButton.Click in the code-behind file. (C# writes this.okButton.Click += ... in the designer file instead; see the differences table.) AddBookForm.vb above shows the idioms: one handler for two events with a comma-separated list, Handles MyBase.Load, and Handles IsbnTextBox.Validating. The sender argument is the control that raised the event (use DirectCast(sender, Control), as in Fields_Validated, never sender.Text, which is late binding and BC30574 under Option Strict On).

CatalogForm (below) has no designer file. It builds the controls in the constructor and wires menu items with AddressOf in the constructor arguments (New ToolStripMenuItem("&File", Nothing, AddressOf OpenItem_Click)) and with AddHandler for a non-control event source (_books.ListChanged).

The compiler messages when the pieces do not fit are:

Mistake Compiler message (real)

Handles AddButton.Click where AddButton is declared without WithEvents

BC30506: Handles clause requires a WithEvents variable defined in the containing type or one of its base types.

a misspelt event name, Handles AddButton.Clck

BC30590: Event 'Clck' cannot be found.

a handler that takes e As String

BC31029: Method 'AddButton_Click' cannot handle event 'Click' because they do not have a compatible signature.

Private Async Function Later(...) As Task Handles AddButton.MouseEnter

compiles, with BC42359: The Task returned from this Async Function will be dropped, and any exceptions in it ignored. Consider changing it to an Async Sub so its exceptions are propagated. (an error with warnings as errors); event handlers must be Sub

a missing InitializeComponent()

BC30451: 'InitializeComponent' is not declared.

Dim year As Integer = YearBox.Value

BC30512: Option Strict On disallows implicit conversions from 'Decimal' to 'Integer'.: NumericUpDown.Value is a Decimal, write CInt(YearBox.Value)

Dim choice As String = Choices.SelectedItem

BC30512: ... from 'Object' to 'String'; write Choices.SelectedItem.ToString() or DirectCast

sender.Text = "Clicked"

BC30574: Option Strict On disallows late binding.

The messages in the table are the compiler’s own output for one-class programs (MyType unset, Option Strict On) that contain exactly the mistake in the first column, each built separately. The program for the last three rows and the Async Function handler is:

Option Strict On

Public Class ListForm
    Inherits Form

    Private WithEvents AddButton As New Button()
    Private WithEvents YearBox As New NumericUpDown()
    Private WithEvents Choices As New ComboBox()

    Private Sub AddButton_Click(sender As Object, e As EventArgs) Handles AddButton.Click
        Dim year As Integer = YearBox.Value
        Dim choice As String = Choices.SelectedItem
        sender.Text = "Clicked"
    End Sub

    Private Async Function Later(sender As Object, e As EventArgs) As Task Handles AddButton.MouseEnter
        Await Task.Delay(1)
    End Function
End Class

Friend Module Startup
    <STAThread>
    Friend Sub Main()
    End Sub
End Module
e6.vb(11,31): error BC30512: Option Strict On disallows implicit conversions from 'Decimal' to 'Integer'.
e6.vb(12,32): error BC30512: Option Strict On disallows implicit conversions from 'Object' to 'String'.
e6.vb(13,9): error BC30574: Option Strict On disallows late binding.
e6.vb(16,84): warning BC42359: The Task returned from this Async Function will be dropped, and any exceptions in it ignored. Consider changing it to an Async Sub so its exceptions are propagated.

The first three rows, built one at a time (the name of the source file is the first part of each line):

e2.vb(8,75): error BC30506: Handles clause requires a WithEvents variable defined in the containing type or one of its base types.
e3.vb(8,85): error BC30590: Event 'Clck' cannot be found.
e4.vb(8,82): error BC31029: Method 'AddButton_Click' cannot handle event 'Click' because they do not have a compatible signature.

Option Strict On changes the experience of Windows Forms code more than anything else: ComboBox.SelectedItem is Object, NumericUpDown.Value is Decimal, TextBox.Text is String, DataGridView.CurrentRow.DataBoundItem is Object. The cast is always explicit. This is the cost of Option Strict On and the benefit: the compiler finds the InvalidCastException before the user does. Option Strict Off (the SDK default) compiles the same lines and fails at run time for a bad cell value.

Layout

Windows Forms lays controls out with a few properties of Control and a few container controls.

Property or container Behaviour

Location, Size

absolute position and size (the default for controls dropped from the Toolbox); correct only at 100% scaling and in the designer’s language

Anchor

the sides that stay at a fixed distance from the parent when it resizes; Left Or Right stretches horizontally, None centres

Dock

Top, Bottom, Left, Right, Fill take a whole edge; several docked controls share the remaining space, laid out in z-order: the control that was added last is at the back of Controls and is laid out first

AutoSize, AutoSizeMode

the control grows with its content (labels, buttons, panels)

Margin, Padding

space around the control and space inside it (inside a container, around its content)

MinimumSize, MaximumSize

limits for the container and for stretching

TableLayoutPanel

rows and columns whose sizes are AutoSize, Absolute or Percent; children set Anchor to fill their cell and SetColumnSpan to span; use it for forms with labels and fields

FlowLayoutPanel

children in a row or column with wrapping and FlowDirection; use it for a row of buttons

SplitContainer

two resizable panes; nest one inside another for three-pane layouts

Panel, GroupBox, TabControl

grouping and scrolling (AutoScroll)

CatalogForm below relies on docking order. It adds the grid, with Dock = Fill, first, then the Top text box, the menu (Top), and the status strip (Bottom): because docking runs from the back of the z-order, the status strip and menu claim their edges first, then the text box takes what is left at the top, and the grid, added first and laid out last, fills the rest. Adding the Fill control last would make it cover the others. The documentation says that the z-order affects docking; the rule "add Fill first" is the practical consequence.

Reasons for TableLayoutPanel and FlowLayoutPanel rather than fixed coordinates: they respect DPI scaling and font changes, they adapt to the longer strings of other languages (see localization), and they make the designer files shorter. Three habits: prefer Anchor and Dock over manual Resize handlers; set AutoSize on labels so that long text does not clip; test at 150% and 200% scaling.

Controls, components and dialogs

The Toolbox groups the controls. A short reference of the ones used in business applications, all in System.Windows.Forms:

Group Controls (selection) and notes

Text and labels

Label, LinkLabel, TextBox (MaskedTextBox, RichTextBox), Text property; & in a Label.Text or Button.Text makes the next letter an access key (underlined with Alt; use && for a literal &)

Buttons and choices

Button, CheckBox, RadioButton (group in a GroupBox or Panel), ComboBox, ListBox, CheckedListBox, NumericUpDown, DateTimePicker, MonthCalendar, TrackBar

Lists and data

ListView, TreeView, DataGridView, PropertyGrid, BindingNavigator

Menus and bars

MenuStrip, ToolStrip, StatusStrip, ContextMenuStrip; the .NET Framework controls MainMenu, ContextMenu, StatusBar, ToolBar, DataGrid and Menu are obsolete in .NET 10 (WFDEV006) and cannot be instantiated

Containers

Panel, GroupBox, TabControl, SplitContainer, FlowLayoutPanel, TableLayoutPanel

Components (no UI of their own)

Timer, ToolTip, ErrorProvider, BindingSource, ImageList, NotifyIcon, HelpProvider, BackgroundWorker, FileSystemWatcher

Dialogs

OpenFileDialog, SaveFileDialog, FolderBrowserDialog, ColorDialog, FontDialog, PrintDialog, PageSetupDialog, PrintPreviewDialog, MessageBox, TaskDialog

Web content

WebView2 (the NuGet package Microsoft.Web.WebView2, namespace Microsoft.Web.WebView2.WinForms); the WebBrowser control (Internet Explorer engine) is legacy

Components are classes that implement IComponent and show up in the component tray under the form; the designer creates them like controls, with a components container when they need disposal (New ErrorProvider(components)). Dialogs are components, but OpenFileDialog and friends are normally created in a Using block as in CatalogForm, because they hold native resources.

Dialogs and messages

API Use

MessageBox.Show(Me, text, caption, buttons, icon)

a simple modal question or notice; pass the owner (Me) so that the box is centred and stays on top of the form. MessageBox.Show(...) returns a DialogResult

TaskDialog.ShowDialog(Me, page)

the modern dialog of Windows (since .NET 5): heading, text, buttons, expandable details, a checkbox and a progress bar; build a TaskDialogPage with its Buttons collection

Using d As New OpenFileDialog() …​ d.ShowDialog(Me)

file selection; Filter, FileName, Multiselect, InitialDirectory

form.ShowDialog(Me)

a modal form; returns the DialogResult of the button that closed it (a button’s DialogResult property closes the form and sets the result)

form.Show(Me)

a modeless form; it keeps running beside the owner

MsgBox, InputBox (VB)

Visual Basic runtime wrappers over MessageBox and a small input form; available in Windows Forms projects on .NET (they need Microsoft.VisualBasic.Forms); prefer MessageBox and a real dialog

ShowDialog does not dispose the form: after the user closes a modal dialog the form is only hidden, and its fields still hold their values. Dispose it with a Using block or Dispose() when you created it with New. This is the source of the default-instance pitfall shown below. A related behaviour of validation, and how to keep a dialog closable, is in the validation section.

A complete catalogue form

CatalogForm is a form written entirely in code: a menu, a grid bound to a BindingList(Of Book) through a BindingSource, a text box bound to the current row, a status bar with a progress bar, an Open CSV command with OpenFileDialog, a progress report and cancellation, an error shown with TaskDialog, and a confirmation in OnFormClosing. It is the example for the sections on dialogs, data binding and threads, and it shows the docking order described under layout. It was built, not run.

Option Strict On

Imports System.ComponentModel
Imports System.IO
Imports System.Threading

' A form written entirely in code: no designer file, controls created and wired by hand.
Public Class CatalogForm
    Inherits Form

    Private ReadOnly _books As New BindingList(Of Book)()
    Private ReadOnly _source As New BindingSource()
    Private ReadOnly _grid As New DataGridView()
    Private ReadOnly _status As New StatusStrip()
    Private ReadOnly _statusLabel As New ToolStripStatusLabel("Ready")
    Private ReadOnly _progress As New ToolStripProgressBar() With {.Visible = False}
    Private ReadOnly _titleBox As New TextBox() With {.Dock = DockStyle.Top}
    Private _loading As CancellationTokenSource

    Public Sub New()
        Text = "Shelf catalogue"
        ClientSize = New Size(640, 360)

        ' Menu
        Dim menu As New MenuStrip()
        Dim fileMenu As New ToolStripMenuItem("&File")
        Dim openItem As New ToolStripMenuItem("&Open CSV...", Nothing, AddressOf OpenItem_Click) With {
            .ShortcutKeys = Keys.Control Or Keys.O}
        Dim addItem As New ToolStripMenuItem("&Add book...", Nothing, AddressOf AddItem_Click)
        Dim exitItem As New ToolStripMenuItem("E&xit", Nothing, Sub(s, e) Close())
        fileMenu.DropDownItems.AddRange({openItem, addItem, New ToolStripSeparator(), exitItem})
        menu.Items.Add(fileMenu)
        MainMenuStrip = menu

        ' Status bar
        _status.Items.AddRange({_statusLabel, _progress})

        ' Grid bound through a BindingSource to a BindingList(Of Book)
        _source.DataSource = _books
        _grid.Dock = DockStyle.Fill
        _grid.AutoGenerateColumns = False
        _grid.AllowUserToAddRows = False
        _grid.SelectionMode = DataGridViewSelectionMode.FullRowSelect
        _grid.Columns.Add(New DataGridViewTextBoxColumn() With {.DataPropertyName = "Title", .HeaderText = "Title", .AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill})
        _grid.Columns.Add(New DataGridViewTextBoxColumn() With {.DataPropertyName = "Year", .HeaderText = "Year", .Width = 70})
        _grid.Columns.Add(New DataGridViewTextBoxColumn() With {.DataPropertyName = "Isbn", .HeaderText = "ISBN", .Width = 140})
        _grid.DataSource = _source

        ' A simple-bound TextBox: editing the text edits the current book
        _titleBox.DataBindings.Add("Text", _source, "Title", True, DataSourceUpdateMode.OnPropertyChanged)

        Controls.Add(_grid)
        Controls.Add(_titleBox)
        Controls.Add(menu)
        Controls.Add(_status)
        AddHandler _books.ListChanged, AddressOf Books_ListChanged
    End Sub

    Private Sub Books_ListChanged(sender As Object, e As ListChangedEventArgs)
        _statusLabel.Text = _books.Count.ToString() & " book(s)"
    End Sub

    Private Sub AddItem_Click(sender As Object, e As EventArgs)
        Using dialog As New AddBookForm()
            If dialog.ShowDialog(Me) = DialogResult.OK Then _books.Add(dialog.NewBook)
        End Using
    End Sub

    ' An Async Sub is correct here: an event handler must be a Sub. Catch everything inside it.
    Private Async Sub OpenItem_Click(sender As Object, e As EventArgs)
        Using dialog As New OpenFileDialog() With {.Filter = "CSV files (*.csv)|*.csv|All files (*.*)|*.*"}
            If dialog.ShowDialog(Me) <> DialogResult.OK Then Return
            _loading?.Cancel()
            _loading = New CancellationTokenSource()
            Dim token = _loading.Token
            Dim progress As New Progress(Of Integer)(
                Sub(percent)
                    _progress.Value = percent        ' runs on the UI thread: Progress captured its context
                End Sub)
            _progress.Visible = True
            Try
                Dim rows = Await Task.Run(Function() ReadBooks(dialog.FileName, progress, token), token)
                For Each book In rows
                    _books.Add(book)
                Next
                _statusLabel.Text = "Loaded " & rows.Count.ToString() & " book(s)"
            Catch ex As OperationCanceledException
                _statusLabel.Text = "Cancelled"
            Catch ex As IOException
                Dim page As New TaskDialogPage() With {
                    .Caption = "Shelf",
                    .Heading = "The file could not be read",
                    .Text = ex.Message,
                    .Icon = TaskDialogIcon.Error}
                page.Buttons.Add(TaskDialogButton.OK)
                TaskDialog.ShowDialog(Me, page)
            Finally
                _progress.Visible = False
            End Try
        End Using
    End Sub

    ' Runs on a thread-pool thread: it must not touch any control.
    Private Shared Function ReadBooks(path As String, progress As IProgress(Of Integer), token As CancellationToken) As List(Of Book)
        Dim lines = File.ReadAllLines(path)
        Dim result As New List(Of Book)
        For i = 1 To lines.Length - 1
            token.ThrowIfCancellationRequested()
            Dim f = lines(i).Split(","c)
            If f.Length >= 3 Then
                result.Add(New Book With {.Title = f(0), .Year = Integer.Parse(f(1)), .Isbn = f(2)})
            End If
            progress.Report(CInt(i * 100 \ lines.Length))
        Next
        Return result
    End Function

    Protected Overrides Sub OnFormClosing(e As FormClosingEventArgs)
        If _books.Count > 0 Then
            Dim answer = MessageBox.Show(Me, "Close the catalogue?", "Shelf", MessageBoxButtons.YesNo, MessageBoxIcon.Question)
            If answer = DialogResult.No Then e.Cancel = True
        End If
        MyBase.OnFormClosing(e)
    End Sub
End Class

Points to read in the listing:

  • OnFormClosing is overridden rather than handling FormClosing, and neither OnClosing nor OnClosed is used: from .NET 10 Form.OnClosing, OnClosed and the corresponding events are obsolete (warning WFDEV004).

  • ReadBooks is a Shared Function: it takes the path, a progress reporter and a token, and touches no control. It runs on a pool thread (Task.Run), so the UI thread is free.

  • Progress(Of Integer) captures the SynchronizationContext of the UI thread where it was created; the callback _progress.Value = percent therefore runs on the UI thread even though Report is called from the pool thread.

  • The handler is Async Sub, which is the one legitimate place for Async Sub: an event handler must return Sub. Every exception is caught inside, because one that escapes goes to the synchronization context and is raised on the UI thread (see the threading section).

  • _loading?.Cancel() uses the null-conditional call, and Using dialog As New OpenFileDialog() With {...} uses the object initializer after New.

Validation

Windows Forms validates fields when the focus leaves them. The pieces:

Member Role

Control.Validating (CancelEventArgs)

raised when the control is about to lose focus (and when ValidateChildren runs); set e.Cancel = True to keep the focus

Control.Validated

raised after Validating finished without cancelling; clear the error here

Control.CausesValidation

False on a control (such as a Cancel button) that must be usable while a field is invalid

ContainerControl.AutoValidate

AutoValidate.EnablePreventFocusChange (validation runs when focus leaves, and a failed field keeps the focus), EnableAllowFocusChange (validation runs, but the focus moves on anyway), Disable and Inherit; the documentation says the default differs for each container, so set it explicitly on dialogs

ValidateChildren() and Validate()

validate all (or one) controls now; use them in the OK handler, as AddBookForm.OkButton_Click does

ErrorProvider

a component that shows an icon and a tooltip next to a control (SetError(control, text); an empty text clears it)

IDataErrorInfo

a data-bound model can report errors itself; set ErrorProvider.DataSource to the BindingSource

The AddBookForm listing implements the standard pattern. The title and ISBN fields set the error and Cancel, and Fields_Validated clears it; the OK button calls ValidateChildren(), and sets the dialog’s result only when every field is valid. The Cancel button has CausesValidation = False (in the designer file), otherwise it could not be pressed while an invalid ISBN is in the box.

Pitfalls specific to this model:

  • With EnablePreventFocusChange a field that cancels Validating keeps the focus, and that can make a dialog awkward to dismiss; for dialogs that must always close, set AutoValidate = AutoValidate.EnableAllowFocusChange and validate in the OK handler with ValidateChildren(). AddBookForm sets EnablePreventFocusChange to show the strict behaviour and gives the Cancel button CausesValidation = False so that it can still be pressed.

  • Validating is not raised when you set a field’s Text from code. Call ValidateChildren() (or your own validation method) before using the values.

  • ErrorProvider is not a validator: it displays an error only when your code calls SetError. Do not use it as the only guard for a save.

  • Put the rule (an ISBN-13 check digit in IsbnCheck.IsValid) in a class that does not depend on controls, so that it can be unit-tested and reused (Shelf.Core.Isbn.TryParse in the running scenario).

Data binding

Windows Forms data binding connects a data source to the properties of controls. The types:

Type What it does

BindingList(Of T)

a List-like collection that raises ListChanged when an item is added, removed or (if T implements INotifyPropertyChanged) changed; the source for grids and lists

BindingSource

the component between the data and the controls: tracks the current item (Current, Position), supports ResetBindings, and is how several controls stay in step; adapts a plain List(Of T) to the binding interfaces

INotifyPropertyChanged

the interface a row class implements so that a bound control sees a changed property; raise PropertyChanged from the property setter

DataGridView

the grid: DataSource, AutoGenerateColumns, typed columns (DataGridViewTextBoxColumn with DataPropertyName)

Control.DataBindings.Add(propertyName, dataSource, dataMember, formattingEnabled, updateMode)

simple binding: a control property to a data member; DataSourceUpdateMode.OnPropertyChanged pushes each keystroke instead of waiting for Validated

BindingNavigator

a ToolStrip with first/previous/next/last/add/delete buttons for a BindingSource

The Book class used by every listing implements INotifyPropertyChanged with a small helper. The CallerMemberName attribute works on an Optional parameter in VB, so SetField(_title, value) needs no property name:

Option Strict On

Imports System.ComponentModel
Imports System.Runtime.CompilerServices

' A trimmed copy of the Shelf.Core Book: it raises PropertyChanged, so bound controls update.
Public Class Book
    Implements INotifyPropertyChanged

    Private _title As String = ""
    Private _year As Integer
    Private _isbn As String = ""

    Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged

    Public Property Title As String
        Get
            Return _title
        End Get
        Set(value As String)
            SetField(_title, value)
        End Set
    End Property

    Public Property Year As Integer
        Get
            Return _year
        End Get
        Set(value As Integer)
            SetField(_year, value)
        End Set
    End Property

    Public Property Isbn As String
        Get
            Return _isbn
        End Get
        Set(value As String)
            SetField(_isbn, value)
        End Set
    End Property

    Private Sub SetField(Of T)(ByRef field As T, value As T, <CallerMemberName> Optional name As String = Nothing)
        If EqualityComparer(Of T).Default.Equals(field, value) Then Return
        field = value
        RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(name))
    End Sub

    Public Overrides Function ToString() As String
        Return $"{Title} ({Year})"
    End Function
End Class

Public Module IsbnCheck
    ' ISBN-13 check digit, as in Shelf.Core.Isbn.
    Public Function IsValid(text As String) As Boolean
        Dim digits = New String(If(text, "").Where(AddressOf Char.IsDigit).ToArray())
        If digits.Length <> 13 Then Return False
        Dim sum = 0
        For i = 0 To 12
            Dim d = AscW(digits(i)) - AscW("0"c)
            sum += If(i Mod 2 = 0, d, d * 3)
        Next
        Return sum Mod 10 = 0
    End Function
End Module

CatalogForm creates _source.DataSource = _books and _grid.DataSource = _source; adding to _books adds a grid row, and editing the bound TextBox edits the current book’s Title, because Book raises PropertyChanged.

The model part runs anywhere

BindingList(Of T) is in the base class library, so the model side of a bound form can be exercised in a console program. This one (run) listens to ListChanged, shows that a changed property of an item raises ItemChanged, adds the missing sorting (BindingList(Of T) does not sort by itself, so a DataGridView column header does nothing without it) and shows how to add many rows with one notification:

Option Strict On

Imports System.ComponentModel

' BindingList(Of T) is the data source WinForms controls understand. It lives in the base class
' library, so the model part of a bound form can be tested in a console program.
Public Class SortableBindingList(Of T)
    Inherits BindingList(Of T)

    Private _sortProperty As PropertyDescriptor
    Private _sortDirection As ListSortDirection
    Private _isSorted As Boolean

    Protected Overrides ReadOnly Property SupportsSortingCore As Boolean
        Get
            Return True
        End Get
    End Property

    Protected Overrides ReadOnly Property IsSortedCore As Boolean
        Get
            Return _isSorted
        End Get
    End Property

    Protected Overrides ReadOnly Property SortPropertyCore As PropertyDescriptor
        Get
            Return _sortProperty
        End Get
    End Property

    Protected Overrides ReadOnly Property SortDirectionCore As ListSortDirection
        Get
            Return _sortDirection
        End Get
    End Property

    Protected Overrides Sub ApplySortCore(prop As PropertyDescriptor, direction As ListSortDirection)
        Dim backing = DirectCast(Items, List(Of T))
        Dim byValue = Comparer(Of Object).Default
        backing.Sort(Function(a, b)
                       Dim result = byValue.Compare(prop.GetValue(a), prop.GetValue(b))
                       Return If(direction = ListSortDirection.Ascending, result, -result)
                   End Function)
        _sortProperty = prop
        _sortDirection = direction
        _isSorted = True
        OnListChanged(New ListChangedEventArgs(ListChangedType.Reset, -1))
    End Sub

    Protected Overrides Sub RemoveSortCore()
        _isSorted = False
        _sortProperty = Nothing
    End Sub
End Class

Module BindingListDemo
    Sub Main()
        Dim books As New SortableBindingList(Of Book)()
        AddHandler books.ListChanged,
            Sub(sender As Object, e As ListChangedEventArgs)
                Dim detail = If(e.PropertyDescriptor IsNot Nothing, " property=" & e.PropertyDescriptor.Name, "")
                Console.WriteLine($"  ListChanged: {e.ListChangedType} index={e.NewIndex}{detail}")
            End Sub

        Console.WriteLine("Add two books")
        books.Add(New Book With {.Title = "Neuromancer", .Year = 1984})
        books.Add(New Book With {.Title = "Dune", .Year = 1965})

        Console.WriteLine("Change a property (Book raises PropertyChanged)")
        books(1).Year = 1966

        Console.WriteLine("Sort by Title, the way a DataGridView column header would")
        Dim props = TypeDescriptor.GetProperties(GetType(Book))
        DirectCast(books, IBindingList).ApplySort(props("Title"), ListSortDirection.Ascending)
        For Each b In books
            Console.WriteLine("  " & b.ToString())
        Next

        Console.WriteLine("Add many books without raising an event per row")
        books.RaiseListChangedEvents = False
        For i = 1 To 100
            books.Add(New Book With {.Title = "Bulk " & i.ToString(), .Year = 2000})
        Next
        books.RaiseListChangedEvents = True
        books.ResetBindings()
        Console.WriteLine("Count = " & books.Count.ToString())
    End Sub
End Module
Add two books
  ListChanged: ItemAdded index=0
  ListChanged: ItemAdded index=1
Change a property (Book raises PropertyChanged)
  ListChanged: ItemChanged index=1 property=Year
Sort by Title, the way a DataGridView column header would
  ListChanged: Reset index=-1
  Dune (1966)
  Neuromancer (1984)
Add many books without raising an event per row
  ListChanged: Reset index=-1
Count = 102

What the output shows and what to remember:

  • Each Add raises ItemAdded with the index; changing a property of an item raises ItemChanged with the property descriptor, because Book implements INotifyPropertyChanged. A class that does not implement it gives no ItemChanged, and the grid will not repaint a cell that changed from code.

  • Sorting is the job of the list. ApplySortCore sorted Book objects by Title with a Comparer(Of Object), then raised a single Reset. BindingSource.Sort and clicking a column header both reach this method; a plain BindingList(Of T) ignores them. BindingSource.Filter works only when the underlying list implements IBindingListView, which BindingList(Of T) does not, so filter by building a new list.

  • RaiseListChangedEvents = False around a bulk load is much cheaper than one ListChanged per row; finish with ResetBindings(), which raised one Reset.

  • Local names that equal members are a case-insensitivity trap in this kind of class: Items is the inherited collection, so Dim items = ... Items fails (BC30980) and Dim comparer hides the Comparer type (BC32045). The listing uses backing and byValue.

Commands and the newer binding engine

NET 8 added a binding engine modelled on WPF, with Button.Command bound to an ICommand. It works from VB without the designer:
Option Strict On

Imports System.Windows.Input

' The data-binding and command engine that arrived in .NET 8: a button runs an ICommand.
Public Class RelayCommand
    Implements ICommand

    Private ReadOnly _execute As Action
    Private ReadOnly _canExecute As Func(Of Boolean)

    Public Sub New(execute As Action, Optional canExecute As Func(Of Boolean) = Nothing)
        _execute = execute
        _canExecute = canExecute
    End Sub

    Public Event CanExecuteChanged As EventHandler Implements ICommand.CanExecuteChanged

    Public Function CanExecute(parameter As Object) As Boolean Implements ICommand.CanExecute
        Return _canExecute Is Nothing OrElse _canExecute()
    End Function

    Public Sub Execute(parameter As Object) Implements ICommand.Execute
        _execute()
    End Sub

    Public Sub RaiseCanExecuteChanged()
        RaiseEvent CanExecuteChanged(Me, EventArgs.Empty)
    End Sub
End Class

Public Class CommandForm
    Inherits Form

    Private ReadOnly _books As New List(Of Book)
    Private ReadOnly _removeCommand As RelayCommand
    Private ReadOnly _removeButton As New Button() With {.Text = "Remove", .Dock = DockStyle.Top}
    Private ReadOnly _addButton As New Button() With {.Text = "Add", .Dock = DockStyle.Top}

    Public Sub New()
        _removeCommand = New RelayCommand(AddressOf RemoveLast, Function() _books.Count > 0)
        _removeButton.Command = _removeCommand       ' Enabled follows CanExecute
        _addButton.Command = New RelayCommand(
            Sub()
                _books.Add(New Book With {.Title = "New book"})
                _removeCommand.RaiseCanExecuteChanged()
            End Sub)
        Controls.Add(_removeButton)
        Controls.Add(_addButton)
    End Sub

    Private Sub RemoveLast()
        _books.RemoveAt(_books.Count - 1)
        _removeCommand.RaiseCanExecuteChanged()
    End Sub

    ' ShowDialogAsync (.NET 9; no longer experimental in .NET 10) awaits a modal dialog
    ' without blocking inside the handler's caller.
    Public Async Function AskAsync() As Task(Of Boolean)
        Using dialog As New AddBookForm()
            Dim result = Await dialog.ShowDialogAsync(Me)
            Return result = DialogResult.OK
        End Using
    End Function
End Class

Built, not run. RelayCommand is the usual few-line ICommand; _removeButton.Command = _removeCommand makes the button’s Enabled follow CanExecute, and RaiseCanExecuteChanged tells it to ask again. AskAsync uses ShowDialogAsync, which is not experimental in .NET 10 (it needed WFO5002 to be suppressed in .NET 9). The older and larger part of Windows Forms binding (BindingSource, DataBindings.Add) is still the way most business forms work. For databases and Entity Framework see Data Access: ADO.NET and EF Core; Microsoft’s post on the Application Framework adds that Entity Framework Core is not natively supported for VB by Microsoft, so designer-level code generation for VB data sources is left to the community.

Threads and asynchronous event handlers

A Windows Forms control belongs to the thread that created its window handle, and almost always that is the single UI thread that runs the message loop (Application.Run). Touching a control from another thread is a bug that the runtime reports with an InvalidOperationException whose message is "Cross-thread operation not valid. Control accessed from a thread other than the thread it was created on." The documentation says that this exception always occurs under the Visual Studio debugger and might occur at run time (Control.CheckForIllegalCrossThreadCalls can switch it off, which hides the bug but does not fix it). The cure is to run UI code on the UI thread. The ways to do it, from the most to the least recommended today:

Technique Notes

Await in an event handler

the continuation runs on the UI thread again, because the handler started there and Windows Forms installs a WindowsFormsSynchronizationContext

Progress(Of T)

created on the UI thread, it calls your callback on the UI thread from Report

Control.InvokeAsync (.NET 9)

from any thread, returns a Task that completes when the UI work has run; has overloads for synchronous and asynchronous callbacks

Control.Invoke(Sub() ...)

synchronous: blocks the calling thread until the UI thread ran the delegate; InvokeRequired tells whether you are on another thread

Control.BeginInvoke(...)

asynchronous, fire-and-forget on the UI thread

BackgroundWorker

the .NET 2.0 component with DoWork/ProgressChanged/RunWorkerCompleted; it still works and is still in the Toolbox, but Task.Run plus Progress(Of T) replaces it

Option Strict On

Imports System.Threading

Public Class CrossThreadForm
    Inherits Form

    Private ReadOnly _label As New Label() With {.Dock = DockStyle.Fill, .TextAlign = ContentAlignment.MiddleCenter}
    Private ReadOnly _panel As New FlowLayoutPanel() With {.Dock = DockStyle.Bottom, .AutoSize = True}
    Private ReadOnly _wrong As New Button() With {.Text = "Wrong", .AutoSize = True}
    Private ReadOnly _invoke As New Button() With {.Text = "Invoke", .AutoSize = True}
    Private ReadOnly _async As New Button() With {.Text = "Await", .AutoSize = True}

    Public Sub New()
        _panel.Controls.AddRange({_wrong, _invoke, _async})
        Controls.Add(_label)
        Controls.Add(_panel)
        AddHandler _wrong.Click, AddressOf Wrong_Click
        AddHandler _invoke.Click, AddressOf Invoke_Click
        AddHandler _async.Click, AddressOf Async_Click
    End Sub

    ' Wrong: a worker thread touches a control. At run time WinForms throws InvalidOperationException
    ' ("Cross-thread operation not valid") on the worker thread, and the process ends if nothing catches it.
    Private Sub Wrong_Click(sender As Object, e As EventArgs)
        Dim worker As New Thread(Sub() _label.Text = "from a worker thread")
        worker.Start()
    End Sub

    ' Right, explicit: marshal the call to the UI thread. InvokeRequired is False on the UI thread itself.
    Private Sub Invoke_Click(sender As Object, e As EventArgs)
        Dim worker As New Thread(
            Sub()
                Dim text = "computed on thread " & Environment.CurrentManagedThreadId.ToString()
                If _label.InvokeRequired Then
                    _label.Invoke(Sub() _label.Text = text)
                Else
                    _label.Text = text
                End If
            End Sub)
        worker.Start()
    End Sub

    ' Right, usual today: Await resumes on the UI thread because the click handler started there.
    Private Async Sub Async_Click(sender As Object, e As EventArgs)
        _async.Enabled = False
        Try
            Dim text = Await Task.Run(Function() "computed on thread " & Environment.CurrentManagedThreadId.ToString())
            _label.Text = text
        Finally
            _async.Enabled = True
        End Try
    End Sub

    ' .NET 9 and later: Control.InvokeAsync hops to the UI thread from any thread and returns a Task.
    Public Function ShowFromAnyThreadAsync(text As String) As Task
        Return _label.InvokeAsync(Sub() _label.Text = text)
    End Function
End Class

Built, not run. Wrong_Click is the bug (do not ship it; the exception is raised on the worker thread); Invoke_Click and Async_Click are the two usual cures and ShowFromAnyThreadAsync is the .NET 9 helper. Async_Click leaves a Try/Finally around the awaited call so that the button is re-enabled whatever happens.

Rules and pitfalls:

  • Async Sub event handlers are the correct form; but an exception that escapes one is posted to the synchronization context, so it reaches Application.ThreadException (or the Application Framework’s UnhandledException event) instead of the Try of the caller. Catch inside.

  • After Await x.ConfigureAwait(False) the continuation runs on a pool thread, so touching a control after it is the cross-thread bug again. Do not use ConfigureAwait(False) in event handlers; use it in library code that has no UI.

  • Never block the UI thread: task.Result, task.Wait() and Thread.Sleep in a handler freeze the window, and .Result on a task whose continuation needs the UI thread deadlocks. Await instead.

  • Re-entrancy: while an Await is pending the user can click again. Disable the button or cancel the earlier operation (CatalogForm does the second).

  • Application.SetUnhandledExceptionMode, Application.ThreadException and AppDomain.CurrentDomain.UnhandledException are the three places to log a crash; with the Application Framework the UnhandledException event covers the UI thread (see below).

  • Form.ShowAsync and ShowDialogAsync (stable in .NET 10) are for code that is itself asynchronous; they do not change how modal dialogs behave.

For Await, Task.Run and cancellation in general see Async and Await and Threads, Parallelism and Synchronization.

The Visual Basic Application Framework

The Application Framework is a Visual Basic layer over Windows Forms that handles start-up and shutdown for you. You switch it on with <MyType>WindowsForms</MyType>. The compiler then adds a hidden MyApplication class that derives from Microsoft.VisualBasic.ApplicationServices.WindowsFormsApplicationBase, generates Main, and generates the My objects (see The My Namespace). The sequence, read from the open-source WindowsFormsApplicationBase, is:

Start-up of a Windows Forms application with the Application Framework: the generated Main, the single-instance check, OnInitialize with ApplyApplicationDefaults, OnStartup with Startup, OnRun with OnCreateMainForm and the message loop, and OnShutdown with Shutdown, with the Application Events that are raised at each step

What the compiler generates

A decompile (ILSpy) of the project in the next section shows exactly what the hidden class contains. This is an excerpt of the real output, with the compiler’s boilerplate removed (Main, the constructor that your Handles Me.Startup lines became, and the override that selects the main form):

internal class MyApplication : WindowsFormsApplicationBase
{
	[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
	[STAThread]
	[DebuggerHidden]
	[EditorBrowsable(EditorBrowsableState.Advanced)]
	internal static void Main(string[] Args)
	{
		try
		{
			Application.SetCompatibleTextRenderingDefault(WindowsFormsApplicationBase.UseCompatibleTextRendering);
		}
		finally
		{
		}
		((WindowsFormsApplicationBase)MyProject.Application).Run(Args);
	}

	public MyApplication()
		: base(AuthenticationMode.Windows)
	{
		this.ApplyApplicationDefaults += MyApplication_ApplyApplicationDefaults;
		this.Startup += MyApplication_Startup;
		this.StartupNextInstance += MyApplication_StartupNextInstance;
		this.UnhandledException += MyApplication_UnhandledException;
		this.NetworkAvailabilityChanged += MyApplication_NetworkAvailabilityChanged;
		this.Shutdown += MyApplication_Shutdown;
		this.IsSingleInstance = true;
		this.EnableVisualStyles = true;
		this.SaveMySettingsOnExit = false;
		this.ShutdownStyle = ShutdownMode.AfterMainFormCloses;
	}

	protected override void OnCreateMainForm()
	{
		this.MainForm = MyProject.Forms.MainForm;
	}
}

Source: WindowsFormsApplicationBase.vb (read for this page)

Three facts follow from it. Handles Me.Startup in a Partial Class MyApplication is turned into AddHandler calls in the constructor, the same as Handles anywhere else. The generated Main is [STAThread] and calls Run(Args); there is nothing you can put into Main, which is why the events exist. And the options you set (IsSingleInstance, EnableVisualStyles, ShutdownStyle) are plain properties set in the constructor.

Project, options and main form

The project file for the Application Framework has MyType and no StartupObject:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net10.0-windows</TargetFramework>
    <UseWindowsForms>true</UseWindowsForms>
    <EnableWindowsTargeting>true</EnableWindowsTargeting>
    <RootNamespace>ShelfDesktop</RootNamespace>
    <MyType>WindowsForms</MyType>
    <OptionStrict>On</OptionStrict>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
    <NuGetAudit>false</NuGetAudit>
  </PropertyGroup>
  <ItemGroup>
    <Import Include="System.Data" />
    <Import Include="System.Drawing" />
    <Import Include="System.Windows.Forms" />
  </ItemGroup>
</Project>

In Visual Studio, the Application page of the project properties edits an XML file My Project\Application.myapp and a custom tool regenerates Application.Designer.vb from it (the file is hidden until you choose Show All Files). The fields of Application.myapp that the Microsoft blog post documents are MySubMain (the framework is on), MainForm, SingleInstance, ShutdownMode (0 when the main form closes, 1 when the last form closes), EnableVisualStyles, AuthenticationMode (0 application-defined, 1 Windows), SaveMySettingsOnExit, HighDpiMode and, when you pick one, SplashScreen. Application.Designer.vb turns those into the constructor options and the OnCreateMainForm override. The two files below do the same by hand, so the example needs no designer; Visual Studio’s generated file does the same job (its exact text was not checked here):

Option Strict On

Imports Microsoft.VisualBasic.ApplicationServices

' What Visual Studio's Application.Designer.vb contains, written by hand:
' the Application Framework options and the main form.
Namespace My
    Partial Friend Class MyApplication
        Public Sub New()
            MyBase.New(AuthenticationMode.Windows)
            IsSingleInstance = True
            EnableVisualStyles = True
            SaveMySettingsOnExit = False
            ShutdownStyle = ShutdownMode.AfterMainFormCloses
        End Sub

        Protected Overrides Sub OnCreateMainForm()
            MainForm = My.Forms.MainForm
        End Sub

        Protected Overrides Sub OnCreateSplashScreen()
            SplashScreen = My.Forms.SplashForm
        End Sub
    End Class
End Namespace

This is also the whole of what has to exist: with the framework on and no OnCreateMainForm, the framework has no form to run and raises NoStartupFormException. That is the most common mistake after switching a template project to the framework (no MainForm setting means no override). Application.myapp is the input of a Visual Studio custom tool that regenerates Application.Designer.vb when the file is saved; the compiler builds the generated .vb file, not the XML. A project that you edit outside Visual Studio therefore needs the generated (or hand-written) code in the repository.

Option (protected property of MyApplication) Effect

IsSingleInstance

when True, a second start hands its command line to the first instance and ends; the first gets StartupNextInstance. The implementation uses a named pipe; a failure to reach the first instance raises CantStartSingleInstanceException

EnableVisualStyles

calls Application.EnableVisualStyles() when start-up begins

SaveMySettingsOnExit

calls My.Settings.Save() after the main form closes (see the My page; there is no settings file in a new project)

ShutdownStyle

AfterMainFormCloses (the default) or AfterAllFormsClose

MinimumSplashScreenDisplayTime

milliseconds the splash stays visible, even if the main form is ready sooner

SplashScreen (set in OnCreateSplashScreen)

a form shown on its own thread at the end of OnInitialize, after ApplyApplicationDefaults; hidden when the main form’s Load event has run and the minimum time is over; /nosplash and -nosplash on the command line skip it

MainForm (set in OnCreateMainForm)

the form that Application.Run runs; the program ends when it closes (with AfterMainFormCloses)

Constructor argument AuthenticationMode

Windows sets My.User from the Windows identity, ApplicationDefined leaves it to you

Application Events

You handle the events in ApplicationEvents.vb. In a .NET project it exists from the start when Visual Studio creates the framework project; in a hand-made project it is the second partial class of MyApplication. All six events are in this file:

Option Strict On

Imports Microsoft.VisualBasic.ApplicationServices

Namespace My
    ' MyApplication is a partial class: this file holds the handlers (the Application Events).
    Partial Friend Class MyApplication

        ' 1. Before anything is shown. Fonts, DPI mode and splash time are set here.
        Private Sub MyApplication_ApplyApplicationDefaults(sender As Object, e As ApplyApplicationDefaultsEventArgs) Handles Me.ApplyApplicationDefaults
            e.Font = New Font("Segoe UI", 9.0F)
            e.HighDpiMode = HighDpiMode.PerMonitorV2
            e.MinimumSplashScreenDisplayTime = 1500
        End Sub

        ' 2. The splash screen is already visible; the main form does not exist yet.
        Private Sub MyApplication_Startup(sender As Object, e As StartupEventArgs) Handles Me.Startup
            Log.WriteEntry("Startup with " & e.CommandLine.Count.ToString() & " argument(s)")
            If e.CommandLine.Contains("--version") Then
                MessageBox.Show(Info.Version.ToString(), Info.Title)
                e.Cancel = True            ' stop: no main form, Shutdown is not raised
            End If
        End Sub

        ' 3. A second copy was started while the first is running (IsSingleInstance = True).
        Private Sub MyApplication_StartupNextInstance(sender As Object, e As StartupNextInstanceEventArgs) Handles Me.StartupNextInstance
            e.BringToForeground = True
            Log.WriteEntry("Second instance with " & e.CommandLine.Count.ToString() & " argument(s)")
        End Sub

        ' Raised for exceptions that reach the top of the UI thread's message loop.
        Private Sub MyApplication_UnhandledException(sender As Object, e As UnhandledExceptionEventArgs) Handles Me.UnhandledException
            Log.WriteException(e.Exception, TraceEventType.Error, "Unhandled")
            MessageBox.Show(e.Exception.Message, Info.Title, MessageBoxButtons.OK, MessageBoxIcon.Error)
            e.ExitApplication = False      ' keep running
        End Sub

        Private Sub MyApplication_NetworkAvailabilityChanged(sender As Object, e As Microsoft.VisualBasic.Devices.NetworkAvailableEventArgs) Handles Me.NetworkAvailabilityChanged
            Log.WriteEntry("Network available: " & e.IsNetworkAvailable.ToString())
        End Sub

        ' 4. After the main form closed (and before the process ends).
        Private Sub MyApplication_Shutdown(sender As Object, e As EventArgs) Handles Me.Shutdown
            Log.WriteEntry("Shutdown")
        End Sub
    End Class
End Namespace

The events, in the order in which they can occur:

Event When it is raised and what to do with it

ApplyApplicationDefaults

before anything is shown, from OnInitialize (.NET 6 and later); the arguments carry Font, HighDpiMode, ColorMode and MinimumSplashScreenDisplayTime: set them here. This is the only place where HighDpiMode and the application font can be set from code in this mode. (Newer .NET versions add more members to ApplyApplicationDefaultsEventArgs; on .NET 10 the four named here compile and FormRevealMode and VisualStylesMode do not exist, BC30456)

Startup

after the framework is initialised, before the main form is created; e.CommandLine has the arguments, e.Cancel = True ends the program without running the main form (and Shutdown is not raised)

StartupNextInstance

raised in the first process when a second one is started (needs IsSingleInstance); e.CommandLine carries the second’s arguments, e.BringToForeground = True activates the first instance

UnhandledException

an exception escaped a handler on the UI thread; e.Exception, e.ExitApplication. If no handler is written the exception propagates. Not raised for exceptions on other threads, and, per the framework’s source, not when a debugger is attached (the debugger sees the exception first)

NetworkAvailabilityChanged

the network connection went up or down (e.IsNetworkAvailable); the listener starts in OnStartup

Shutdown

after the main form closed, before the process ends; not raised if the program terminates abnormally or after Startup was cancelled

Behaviour that is worth knowing before debugging a framework application:

  • Startup runs before any form exists, so there is no owner for a dialog (MessageBox.Show(text) without an owner is what the --version branch above does) and e.Cancel = True is the way to handle command-line-only operations. Initialising the Generic Host there is the subject of the host section below.

  • The main form runs inside Application.Run. When you write an UnhandledException handler, the framework hooks Application.ThreadException and raises its own event for what arrives there; without a handler, Windows Forms' own thread-exception behaviour applies. Pick one place to log.

  • The splash screen is shown on another thread: the framework calls Application.Run for it from a pool thread, so that it keeps painting while the main form is created (this is in the source of ShowSplashScreen). Keep the splash form trivial, with no data access, and do not touch the main form’s controls from it.

  • The framework starts a network listener in OnStartup; you only see its effect when you handle NetworkAvailabilityChanged.

  • My.Application.CommandLineArgs, Info, Log and OpenForms are available on the generated class from any code; see the My page for the table.

  • The old StartupForm setting, Application.myapp edited by hand in a text editor and Application.EnableVisualStyles calls in Sub Main are .NET Framework habits; on .NET 10 the options are properties of MyApplication.

Why is it still there? It belongs to the Visual Basic application model that came with My, and with it a VB project has no visible program entry, so the events are the hooks. C# never had that layer: its template shows Main and ApplicationConfiguration.Initialize() in plain sight. The blog post that announced the .NET version says that Visual Basic and the Application Framework "continue to be first class citizens" of Windows Forms and that the team plans to keep modernizing around it without breaking existing code. Treat that as support for existing projects: for a new project the template’s own Sub Main is simpler and nothing in it is hidden, while the framework is the right choice when you migrate a .NET Framework application that already has ApplicationEvents.vb, My.Settings and default instances.

Default form instances

With the Application Framework (or WindowsFormsWithCustomSubMain) on, every Form class in the project gets a default instance: the class name can be used as if it were an object. AddBookForm.ShowDialog(Me) in MainForm does not create anything with New. The decompiled My.MyProject.MyForms class (an excerpt for one form, from the same ILSpy run) shows how it works:

internal sealed class MyForms   // nested in the hidden module My.MyProject
{
	[ThreadStatic]
	private static Hashtable m_FormBeingCreated;

	public MainForm m_MainForm;

	public MainForm MainForm
	{
		get
		{
			m_MainForm = Create__Instance__(m_MainForm);
			return m_MainForm;
		}
		set
		{
			if (value != m_MainForm)
			{
				if (value != null)
				{
					throw new ArgumentException("Property can only be set to Nothing");
				}
				Dispose__Instance__(ref m_MainForm);
			}
		}
	}

	// the same property exists for every other Form class of the project (AddBookForm, SplashForm)

	private static T Create__Instance__<T>(T Instance) where T : Form, new()
	{
		if (Instance == null || ((Control)Instance).IsDisposed)
		{
			if (m_FormBeingCreated != null)
			{
				if (m_FormBeingCreated.ContainsKey(typeof(T)))
				{
					throw new InvalidOperationException(Utils.GetResourceString("WinForms_RecursiveFormCreate"));
				}
			}
			else
			{
				m_FormBeingCreated = new Hashtable();
			}
			m_FormBeingCreated.Add(typeof(T), null);
			try
			{
				return new T();
			}
			// catch (TargetInvocationException): rethrown as InvalidOperationException with the inner message
			finally
			{
				m_FormBeingCreated.Remove(typeof(T));
			}
		}
		return Instance;
	}

	private void Dispose__Instance__<T>(ref T instance) where T : Form
	{
		((Component)instance/*cast due to constrained. prefix*/).Dispose();
		instance = default;
	}
}

The class MainForm and the expression MainForm (in My.Forms) are therefore different things; the compiler picks the property when the name is used where an instance is needed (MainForm.Show(), MainForm.Text). My.Forms.MainForm is the same object as MainForm. MyProject.Forms itself is a per-thread object (ThreadSafeObjectProvider; see the My page), so each thread has its own default instances.

Behaviour (read from the generated code) Consequence

The getter creates the form with New when the field is Nothing or the form is disposed

closing a modeless form and then writing Form2.Show() silently makes a new form; state is lost

ShowDialog hides but does not dispose, so the instance survives

a second AddBookForm.ShowDialog() shows the old text and selection unless you reset them; or dispose it with My.Forms.AddBookForm = Nothing

The setter accepts only Nothing (else ArgumentException), and disposes the form

My.Forms.AddBookForm = Nothing is the explicit "dispose and forget"

Creation is guarded by a per-thread Hashtable

a form that creates itself through its default instance in its constructor throws InvalidOperationException (recursive form creation) instead of overflowing the stack

Create__Instance__ calls New T()

the form needs an accessible parameterless constructor; constructor injection needs the DI pattern below, not default instances

MyForms is per thread

touching Form2 from a background thread creates a second Form2 on that thread, so a handler on the pool thread that "updates `Form2.Label1`" updates the wrong form

The form class name is also a type

inside a form’s own code Form2.Text means the default instance, while Me.Text means this instance: with two instances open the two lines change different forms

Without the framework, the same call is a compile error, which is how most people discover that the template is not the framework:

Option Strict On

Public Class Form1
    Inherits Form
End Class

Friend Module Startup
    <STAThread>
    Friend Sub Main()
        Form1.Show()
    End Sub
End Module
e1.vb(10,9): error BC30469: Reference to a non-shared member requires an object reference.

BC30469: Reference to a non-shared member requires an object reference. is the compiler saying that Form1 here is the class, Show is an instance member, and there is no default instance to bridge them. The ways out are Dim f As New Form1() : f.Show() (an explicit instance, which is the only way in C#) or setting MyType.

Recommendation: use default instances in code that is already written with them and never in new code. Create forms with New, keep the reference, dispose modal forms in a Using block, and pass the data a form needs through its constructor or a service. Default instances make forms look like global variables, hide lifetime, and conflict with dependency injection and unit tests. Microsoft’s My.Forms page presents them as a convenience for rapid application development.

Custom controls and UserControls

There are three ways to build a reusable piece of UI:

Kind Base class Use when

UserControl

UserControl

you compose existing controls into one unit with a designer surface (a form fragment); the quickest, and what the winformscontrollib template starts as

Inherited control

TextBox, Button, …​

you want an existing control with a small change

Custom control

Control

the control draws itself (OnPaint) and handles its own mouse and keyboard input

Component

Component

no UI, only designer support (like Timer)

dotnet new winformscontrollib -lang VB (a "Windows Forms Control Library") and dotnet new winformslib -lang VB create the library projects for them. The two listings below are in one file. RatingControl is a custom control that draws stars, raises RatingChanged, is selectable by Tab and operable by the arrow keys, and reports itself to assistive technology as a slider; IsbnBox is a UserControl-style composite of a text box and a mark. Built, not run:

Option Strict On

Imports System.ComponentModel
Imports System.Drawing.Drawing2D

' A custom control: draws itself, raises an event, takes keyboard and mouse input.
<DefaultEvent("RatingChanged")>
Public Class RatingControl
    Inherits Control

    Private _rating As Integer
    Private _maximum As Integer = 5

    <Category("Behavior"), Description("The current rating."), DefaultValue(0)>
    Public Property Rating As Integer
        Get
            Return _rating
        End Get
        Set(value As Integer)
            value = Math.Clamp(value, 0, _maximum)
            If value = _rating Then Return
            _rating = value
            Invalidate()
            OnRatingChanged(EventArgs.Empty)
        End Set
    End Property

    <Category("Behavior"), Description("How many stars are shown."), DefaultValue(5)>
    Public Property Maximum As Integer
        Get
            Return _maximum
        End Get
        Set(value As Integer)
            _maximum = Math.Max(1, value)
            Rating = Math.Min(_rating, _maximum)
            Invalidate()
        End Set
    End Property

    <Category("Property Changed"), Description("Raised when Rating changes.")>
    Public Event RatingChanged As EventHandler

    Protected Overridable Sub OnRatingChanged(e As EventArgs)
        RaiseEvent RatingChanged(Me, e)
    End Sub

    Public Sub New()
        DoubleBuffered = True
        SetStyle(ControlStyles.Selectable Or ControlStyles.UserPaint Or ControlStyles.ResizeRedraw, True)
        TabStop = True
        AccessibleRole = AccessibleRole.Slider
        Size = New Size(130, 28)
    End Sub

    Protected Overrides Sub OnPaint(e As PaintEventArgs)
        MyBase.OnPaint(e)
        e.Graphics.SmoothingMode = SmoothingMode.AntiAlias
        Dim cell = ClientSize.Width / _maximum
        For i = 0 To _maximum - 1
            Dim box As New RectangleF(CSng(i * cell) + 2, 2, CSng(cell) - 4, ClientSize.Height - 4)
            Using brush As New SolidBrush(If(i < _rating, Color.Goldenrod, Color.LightGray))
                e.Graphics.FillEllipse(brush, box)
            End Using
        Next
        If Focused AndAlso ShowFocusCues Then
            ControlPaint.DrawFocusRectangle(e.Graphics, ClientRectangle)
        End If
    End Sub

    Protected Overrides Sub OnMouseDown(e As MouseEventArgs)
        MyBase.OnMouseDown(e)
        Focus()
        Rating = CInt(Math.Ceiling(e.X / (ClientSize.Width / _maximum)))
    End Sub

    Protected Overrides Function IsInputKey(keyData As Keys) As Boolean
        Return keyData = Keys.Left OrElse keyData = Keys.Right OrElse MyBase.IsInputKey(keyData)
    End Function

    Protected Overrides Sub OnKeyDown(e As KeyEventArgs)
        MyBase.OnKeyDown(e)
        If e.KeyCode = Keys.Right Then Rating += 1
        If e.KeyCode = Keys.Left Then Rating -= 1
    End Sub

    Protected Overrides Sub OnGotFocus(e As EventArgs)
        MyBase.OnGotFocus(e)
        Invalidate()
    End Sub

    Protected Overrides Sub OnLostFocus(e As EventArgs)
        MyBase.OnLostFocus(e)
        Invalidate()
    End Sub
End Class

' A UserControl: several existing controls composed into one reusable unit.
Public Class IsbnBox
    Inherits UserControl

    Private ReadOnly _text As New TextBox() With {.Dock = DockStyle.Fill}
    Private ReadOnly _mark As New Label() With {.Dock = DockStyle.Right, .Width = 24, .TextAlign = ContentAlignment.MiddleCenter}

    Public Event IsValidChanged As EventHandler

    Public Sub New()
        Controls.Add(_text)
        Controls.Add(_mark)
        Height = _text.PreferredHeight
        AddHandler _text.TextChanged, AddressOf Text_TextChanged
    End Sub

    <Browsable(True), DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)>
    Public Overrides Property Text As String
        Get
            Return _text.Text
        End Get
        Set(value As String)
            _text.Text = value
        End Set
    End Property

    <Browsable(False)>
    Public ReadOnly Property IsValid As Boolean
        Get
            Return IsbnCheck.IsValid(_text.Text)
        End Get
    End Property

    Private Sub Text_TextChanged(sender As Object, e As EventArgs)
        _mark.Text = If(IsValid, ChrW(&H2713), "")
        RaiseEvent IsValidChanged(Me, EventArgs.Empty)
    End Sub
End Class

Rules shown in the listing:

  • Mark the public properties for the designer: Category, Description, DefaultValue (also the way out of WFO1000) and DefaultEvent for the double-click handler. A property that is run-time state gets DesignerSerializationVisibility(Hidden) or Browsable(False), as IsValid does.

  • DoubleBuffered = True and ControlStyles.UserPaint Or ControlStyles.ResizeRedraw stop flicker and repaint on resize; invalidate (Invalidate()) when state changes, and never paint outside OnPaint.

  • ControlStyles.Selectable and TabStop make a custom control reachable by keyboard; IsInputKey tells the framework that the arrow keys are input rather than navigation.

  • AccessibleRole sets what Narrator and other tools announce; see the accessibility section.

  • Dispose the GDI+ objects you create in OnPaint (Using brush As New SolidBrush(...)); the e.Graphics object is not yours to dispose.

  • Custom drawing is also done by owner-draw properties on lists and grids (DrawMode, CellPainting); the documentation’s custom painting page covers both.

The Generic Host in a Windows Forms application

Windows Forms does not integrate the Generic Host (dependency injection, configuration, logging) but works with it; the Windows Forms documentation shows both a C# and a VB way, and the VB way for an Application Framework project is to use the framework’s events. Startup builds and starts the host, Shutdown stops it, and OnCreateMainForm resolves the main form from the container instead of using the default instance. The example needs the package Microsoft.Extensions.Hosting (PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0") and was built, not run:

Option Strict On

Imports Microsoft.Extensions.DependencyInjection
Imports Microsoft.Extensions.Hosting
Imports Microsoft.VisualBasic.ApplicationServices

' The Generic Host inside the Application Framework: build it in Startup, stop it in Shutdown,
' and resolve the main form from the container instead of using the default form instance.
Namespace My
    Partial Friend Class MyApplication
        Private Shared _host As IHost

        Friend Shared ReadOnly Property Services As IServiceProvider
            Get
                Return _host.Services
            End Get
        End Property

        Private Sub MyApplication_Startup(sender As Object, e As StartupEventArgs) Handles Me.Startup
            Dim builder = Host.CreateApplicationBuilder(e.CommandLine.ToArray())
            builder.Services.AddSingleton(Of ICatalogService, CatalogService)()
            builder.Services.AddTransient(Of MainForm)()
            _host = builder.Build()
            _host.Start()
        End Sub

        Private Sub MyApplication_Shutdown(sender As Object, e As EventArgs) Handles Me.Shutdown
            _host.StopAsync().GetAwaiter().GetResult()
            _host.Dispose()
        End Sub

        Protected Overrides Sub OnCreateMainForm()
            MainForm = Services.GetRequiredService(Of MainForm)()
        End Sub
    End Class
End Namespace
Option Strict On

Imports Microsoft.Extensions.Logging

Public Class MainForm
    Inherits Form

    Private ReadOnly _logger As ILogger(Of MainForm)
    Private ReadOnly _catalog As ICatalogService

    ' Constructor injection into a form: the designer needs a parameterless constructor too
    ' (it is used only at design time), but the container uses this one.
    Public Sub New(catalog As ICatalogService, logger As ILogger(Of MainForm))
        _catalog = catalog
        _logger = logger
        Text = "Shelf (" & _catalog.Count().ToString() & " books)"
        _logger.LogInformation("Main form created")
    End Sub

    Public Sub New()
        InitializeDesignTime()
    End Sub

    Private Sub InitializeDesignTime()
        Text = "Shelf (design time)"
    End Sub
End Class
Option Strict On

Public Interface ICatalogService
    Function Count() As Integer
End Interface

Public Class CatalogService
    Implements ICatalogService

    Public Function Count() As Integer Implements ICatalogService.Count
        Return 3
    End Function
End Class

Points to take: the project must not select a main form any other way (Visual Studio’s Startup form setting writes a MainForm entry that generates a competing OnCreateMainForm, so clear it); a form that wants injected services needs a constructor with parameters and the parameterless one for the designer; the host runs IHostedService`s beside the UI thread, so they must not touch controls; and `host.StopAsync().GetAwaiter().GetResult() in Shutdown is the one place where blocking is acceptable.

Without the Application Framework, the same thing is written in your own Sub Main, as in the C# model. This Startup module (alternative to the earlier one; select it with StartupObject) builds the host, applies the application settings, starts the host and runs the form resolved from DI:

Option Strict On

Imports Microsoft.Extensions.DependencyInjection
Imports Microsoft.Extensions.Hosting

' The Generic Host in a WinForms program: forms and services come from the container.
Friend Module HostStartup
    <STAThread>
    Friend Sub Main(args As String())
        Dim builder = Host.CreateApplicationBuilder(args)
        builder.Services.AddTransient(Of CatalogForm)()
        builder.Services.AddTransient(Of AddBookForm)()

        Using host = builder.Build()
            Application.SetHighDpiMode(HighDpiMode.PerMonitorV2)
            Application.EnableVisualStyles()
            Application.SetCompatibleTextRenderingDefault(False)
            host.Start()
            Try
                Application.Run(host.Services.GetRequiredService(Of CatalogForm)())
            Finally
                host.StopAsync().GetAwaiter().GetResult()
            End Try
        End Using
    End Sub
End Module

High DPI, dark mode and the default font

Windows scales applications on screens with more than 96 dots per inch. A Windows Forms application on .NET chooses how:

HighDpiMode Behaviour (summary of the documentation)

DpiUnaware

the window is always 100% and the system bitmap-stretches it: blurry text on high-resolution screens

DpiUnawareGdiScaled

like unaware, with better GDI and GDI+ quality; not for custom OnPaint with double buffering

SystemAware

one scale factor for the whole session, from the primary monitor; blurry when the window moves to a monitor with another scale. The VB template uses it

PerMonitor

each monitor has its own scale; not optimised by Windows Forms (use V2)

PerMonitorV2

the mode most Windows Forms controls are optimised for: per-monitor scale, DpiChanged, DpiChangedBeforeParent and DpiChangedAfterParent events; the right choice for new applications

Three places can set it:

  • in the Application Framework: e.HighDpiMode = HighDpiMode.PerMonitorV2 in ApplyApplicationDefaults (as the listing above does) or the HighDpiMode field of Application.myapp;

  • in your own Sub Main: Application.SetHighDpiMode(HighDpiMode.PerMonitorV2) before the first form is created (Startup.vb above does it);

  • in an application manifest: this is the approach for older Windows Forms apps, and it overrides settings elsewhere (the documentation calls it no longer recommended for .NET Framework). The file below was built into a project with <ApplicationManifest>app.manifest</ApplicationManifest>:

<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
  <assemblyIdentity version="1.0.0.0" name="ShelfDesktop.app"/>
  <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
    <application>
      <!-- Windows 10 and Windows 11 -->
      <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
    </application>
  </compatibility>
  <application xmlns="urn:schemas-microsoft-com:asm.v3">
    <windowsSettings>
      <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
    </windowsSettings>
  </application>
</assembly>

AutoScaleMode (Font or Dpi) and AutoScaleDimensions on every form, written by the designer, are what scale the controls; fixed pixel sizes in code (Size = New Size(130, 28)) do not scale, so compute sizes from DeviceDpi or use layout containers. Test at 100%, 150% and 200% on a real machine and on a monitor with a different scale: that cannot be checked in a build.

Default font. Since .NET Core 3.1 the font is Segoe UI 9pt, not Microsoft Sans Serif 8.25pt, so forms designed on .NET Framework look different when migrated. From .NET 6 ApplyApplicationDefaults (e.Font = New Font("Segoe UI", 9.0F)) or Application.SetDefaultFont sets it for the application.

Dark mode. SystemColorMode has Classic (light, the default), System (follow Windows) and Dark. In an Application Framework project set e.ColorMode; in your own Sub Main call Application.SetColorMode. It was experimental in .NET 9 and is a supported feature in .NET 10 (this page’s e.ColorMode = ... compiled with no experimental diagnostic on the .NET 10 SDK); owner-drawn controls must read SystemColors instead of hard-coding colours, or they stay light in a dark application.

Accessibility

Windows Forms exposes controls to UI Automation through their AccessibilityObject; screen readers such as Narrator, NVDA and JAWS read the properties below. The Microsoft documentation reports accessibility work in every release from .NET 5 to .NET 7 and improved NVDA support in .NET 10.

Member Use

AccessibleName

the name announced for a control without a caption (a text box next to a label gets the label’s text through its TabIndex order, a text box alone needs AccessibleName; see the designer listing)

AccessibleDescription

a longer explanation

AccessibleRole

what the control is (Slider for RatingControl); the default role is right for standard controls

TabIndex, TabStop

keyboard order; labels immediately before their field in the tab order give the field its name

& access keys in Text

keyboard shortcuts (Alt plus the underlined letter); the label’s UseMnemonic moves focus to the next control in the tab order

ShowFocusCues, ControlPaint.DrawFocusRectangle

custom controls must draw the focus

System colours and high-contrast themes

use SystemColors, not fixed colours

The AddBookForm designer listing sets AccessibleName on both text boxes, & access keys on the labels and the Add button, and a tab order of 0 to 4. Accessibility is partly checked at run time (Narrator, Accessibility Insights for Windows, UI Automation inspectors), which could not be done here.

Localization

A Windows Forms form is localizable by setting Localizable = True in the designer. The designer then writes the text, size and layout of each control into resource files named after the culture, and InitializeComponent reads them with a ComponentResourceManager:

File Contents

AddBookForm.resx

the default (neutral) resources: the strings and sizes of the controls

AddBookForm.de.resx, AddBookForm.es.resx

the translated values, one per culture (de-AT falls back to de, then to neutral)

satellite assemblies

MSBuild compiles each culture’s .resx into a satellite assembly de\<assembly name>.resources.dll next to the program

The lookup is the same one that an application uses for its own strings (My.Resources or a ResourceManager), and it can be shown without a window. This is the same mechanism in a console program (run): a neutral Strings.resx and two translations, the fallback chain, the separate roles of CurrentUICulture (which language) and CurrentCulture (how numbers and dates are written):

<?xml version="1.0" encoding="utf-8"?>
<root>
  <resheader name="resmimetype"><value>text/microsoft-resx</value></resheader>
  <resheader name="version"><value>2.0</value></resheader>
  <resheader name="reader"><value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value></resheader>
  <resheader name="writer"><value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value></resheader>
  <data name="AddBookTitle" xml:space="preserve"><value>Add a book</value></data>
  <data name="OkButton" xml:space="preserve"><value>&amp;Add</value></data>
  <data name="BookCount" xml:space="preserve"><value>{0} book(s) in the catalogue</value></data>
</root>

The two other resource files, Strings.de.resx and Strings.es.resx, have the same keys with translated values (Buch hinzufügen, &Hinzufügen, {0} Buch/Bücher im Katalog for German; Añadir un libro, &Añadir and {0} libro(s) en el catálogo for Spanish).

Option Strict On

Imports System.Globalization
Imports System.Reflection
Imports System.Resources

' The lookup WinForms uses for a Localizable form, done by hand: one .resx per culture,
' satellite assemblies built by MSBuild, and a fallback chain from specific to neutral.
Module Program
    Sub Main()
        Dim manager As New ResourceManager("ShelfApp.Strings", Assembly.GetExecutingAssembly())
        For Each name In {"en", "de", "es", "de-AT", "fr"}
            Dim culture = CultureInfo.GetCultureInfo(name)
            Console.WriteLine($"{name,-6} {manager.GetString("AddBookTitle", culture),-18} {manager.GetString("OkButton", culture),-12} " &
                              String.Format(culture, manager.GetString("BookCount", culture), 1234.5))
        Next

        ' The UI culture picks the language of resources; the culture (CurrentCulture) formats numbers and dates.
        CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo("de")
        CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("de")
        Console.WriteLine(manager.GetString("AddBookTitle") & " / " & (1234.5).ToString("N1"))
        Console.WriteLine("Satellite assembly folders: " &
            String.Join(", ", IO.Directory.GetDirectories(AppContext.BaseDirectory).Select(AddressOf IO.Path.GetFileName).OrderBy(Function(n) n)))
    End Sub
End Module
en     Add a book         &Add         1234.5 book(s) in the catalogue
de     Buch hinzufügen    &Hinzufügen  1234,5 Buch/Bücher im Katalog
es     Añadir un libro    &Añadir      1234,5 libro(s) en el catálogo
de-AT  Buch hinzufügen    &Hinzufügen  1234,5 Buch/Bücher im Katalog
fr     Add a book         &Add         1234,5 book(s) in the catalogue
Buch hinzufügen / 1.234,5
Satellite assembly folders: de, es

Read the output as the contract:

  • de and es find their own file; de-AT has none and falls back to de; fr has none and falls back to the neutral resources (English). The runtime walks from the specific culture to the parent culture to the invariant culture.

  • Number formatting follows CurrentCulture, not the language of the resource: the line 1234,5 book(s) in the catalogue for fr is French number formatting with the English text.

  • MSBuild created the satellite assemblies in de and es folders (the last line of the output).

  • For a form, set CultureInfo.CurrentUICulture before the form is created (before InitializeComponent runs), because ApplyResources is called once from the constructor; changing the culture afterwards does not re-localize an open form unless you re-apply the resources.

For layouts that must survive longer text, use AutoSize and the layout panels, and leave room in fixed-size controls; for right-to-left languages set RightToLeft and RightToLeftLayout on the form (and MessageBoxOptions.RtlReading and RightAlign on message boxes). The Label access keys are part of the resource (the & in &Hinzufügen above), so a translator must choose a free letter for each form.

C# differences

Topic Visual Basic C#

Start-up

template: Sub Main in Program.vb; optional Application Framework with MyApplication, OnCreateMainForm, Application Events

Program.cs with ApplicationConfiguration.Initialize() (it reads ApplicationVisualStyles, ApplicationHighDpiMode, ApplicationDefaultFont and related properties from the .csproj) and Application.Run(new Form1())

Application settings (DPI, font)

ApplyApplicationDefaults event or Application.myapp

project properties in the .csproj

Event wiring

Handles clauses and WithEvents fields, in the code-behind; AddHandler for dynamic wiring

control.Click += handler; in the designer file

Default instances

Form2.Show() and My.Forms.Form2 with the framework

not available: always new Form2().Show()

Designer file

Form1.Designer.vb, Friend WithEvents fields, <DesignerGenerated>

Form1.Designer.cs, private fields, #region Windows Form Designer generated code

sender

DirectCast(sender, Button)

(Button)sender or pattern matching sender is Button b

Case

Okbutton and OkButton are the same name; a control named Layout clashes with the event (BC40004)

case-sensitive

Strictness

Option Strict Off allows sender.Text and implicit Decimal to Integer

always strict

Properties on controls

WFO1000 for public settable properties

the same analyzer, same fixes

Async handlers

Private Async Sub Button1_Click(...) Handles Button1.Click

private async void Button1_Click(object sender, EventArgs e)

My

My.Settings, My.Resources, My.Application, My.Computer, My.Forms

none (use Properties.Settings, Properties.Resources, Application)

For the C# platform pages see C# and .NET.

Where the books are outdated

Book habit Today on .NET 10

"Windows Forms App (.NET Framework)" template, Form1.vb plus Application.myapp created by default

dotnet new winforms -lang VB creates Program.vb with its own Sub Main and no Application Framework; switch the framework on with MyType

Form1.Show() and Form2.ShowDialog() as the way to open forms

works only with MyType=WindowsForms or WindowsFormsWithCustomSubMain; otherwise BC30469. Prefer Dim f As New Form2()

"WinForms is cross-platform" (a claim in Mastering VB.NET about Windows Forms on other systems)

Windows Forms is Windows-only; net10.0-windows and the Windows Desktop runtime are required (NETSDK1100 on other systems without EnableWindowsTargeting, and then you can build but not run)

BackgroundWorker as the way to run long operations

still available; Task.Run with Progress(Of T) and Async/Await, Control.InvokeAsync in .NET 9

MsgBox and InputBox as the dialogs of a Visual Basic application

work in Windows Forms projects (via Microsoft.VisualBasic.Forms); MessageBox and TaskDialog are the .NET APIs

The MainMenu, ContextMenu, StatusBar, ToolBar and DataGrid controls

obsolete (WFDEV006) in .NET 10 and not instantiable; MenuStrip, ContextMenuStrip, StatusStrip, ToolStrip and DataGridView

Application.EnableVisualStyles() in Sub Main or Startup form in the project page

framework option EnableVisualStyles (and EnableVisualStyles = True in the constructor); in your own Sub Main still the call

the binary object serializer to move objects through the clipboard or to store them

removed in .NET 9; the .NET 10 clipboard methods TryGetData(Of T) replace the obsolete GetData (WFDEV005)

ClickOnce, "ensure the .NET Framework is installed", Microsoft Store UWP (a page in Mastering VB.NET)

framework-dependent deployment with the Desktop Runtime, self-contained, MSIX or MSI; see the deployment page

WPF "runs on the web" (XBAP)

XBAP is gone; WPF is a desktop technology (see the WPF page)

Error, warning and diagnostics reference

Diagnostic Meaning (messages are the compiler’s, shown above)

BC30469

Reference to a non-shared member requires an object reference. — a form class name used as an instance without the Application Framework (no default instances)

BC30738

'Sub Main' is declared more than once ... — MyType=WindowsForms plus your own Sub Main

BC30506

Handles clause requires a WithEvents variable ... — the control is not declared WithEvents

BC30590

Event 'Clck' cannot be found. — misspelt event in a Handles clause

BC31029

Method 'AddButton_Click' cannot handle event 'Click' because they do not have a compatible signature.

BC40004

WithEvents variable 'Layout' conflicts with event 'Layout' in the base class 'Control' and should be declared 'Shadows'. (a warning; an error under TreatWarningsAsErrors)

BC42359

The Task returned from this Async Function will be dropped ... — an Async Function used as a handler

BC30512, BC30574

Option Strict On disallows implicit conversions and late binding: NumericUpDown.Value, SelectedItem, sender.Text

WFO1000

Property '...' does not configure the code serialization for its property content (an error in .NET 10)

WFDEV004, WFDEV005, WFDEV006

warnings in .NET 10: Form.OnClosing and OnClosed, the GetData clipboard methods, and the .NET Framework controls ContextMenu, DataGrid, MainMenu, Menu, StatusBar and ToolBar are obsolete

NETSDK1100

To build a project targeting Windows on this operating system, set the EnableWindowsTargeting property to true.

NoStartupFormException (run time)

the Application Framework found no MainForm: OnCreateMainForm missing

CantStartSingleInstanceException (run time)

a second instance could not reach the first

InvalidOperationException cross-thread (run time)

a control was used from a thread that does not own it

Pitfalls checklist

  • Decide the start-up mode first: Sub Main (template), WindowsForms (framework) or WindowsFormsWithCustomSubMain; the three are not interchangeable, and MyType=WindowsForms with a Sub Main is BC30738.

  • A project with the framework needs an OnCreateMainForm override; dotnet build does not read Application.myapp.

  • Do not edit InitializeComponent by hand; name controls so that they do not collide with members of Control (Layout, Resize, Text, Name) and with CancelButton and AcceptButton.

  • Declare controls WithEvents for Handles; handlers are Sub, never Async Function.

  • ShowDialog does not dispose; dispose modal forms, or avoid default instances.

  • Never touch a control from another thread; do not use ConfigureAwait(False) in handlers; do not block with .Result or .Wait().

  • Validating is not raised by code that sets Text; call ValidateChildren() and give the Cancel button CausesValidation = False.

  • Public settable properties on forms and controls need DefaultValue or DesignerSerializationVisibility (WFO1000).

  • Give controls AccessibleName where there is no label, check the tab order, and use SystemColors.

  • Set HighDpiMode (and the font) before the first form is created; test at 150% and 200% scaling.

  • Windows Forms is Windows-only: a Linux or macOS build with EnableWindowsTargeting compiles but cannot start the program.