Win10 TabletMode & WinForms.Net Form Event Order
Win10 TabletMode alters the order of Form events in WinForms.Net - this article describes how to correct the event order
Introduction
WinForms.Net has a specific order of events that occurs when a form is created and shown (see this link).
Windows 10 introduced a new UI mode called Tablet Mode which can be toggled on / off by the end user. This new mode forcibly alters the order of these events.
This article explains how to correct the order of events back to the original order.
Background
The altered order of events can cause code that is meant to happen in a specific order to execute out of order resulting in crashes or unexpected behavior.
The original order of events is:
Control.HandleCreated
Control.BindingContextChanged
Form.Load
Control.VisibleChanged
Form.Activated
Form.Shown
The TabletMode
alters the order as such:
Control.HandleCreated
- Form.Activated
- Control.VisibleChanged
Control.BindingContextChanged
Form.Load
Form.Shown
Using the Code
There are three key steps to achieve to correcting the event order:
- Set Windows 10 compatibility in application manifest (necessary to be able to check OS version and get the correct version).
- This step is skipped in this article. Web search for how to add an application manifest to complete this step.
- Detect when the PC is in
TabletMode
. - Prevent the
Activated
&VisibleChanged
event firing out of order.
Step 2: Detect TabletMode
When researching this problem, there were three common suggestions on how to detect tablet mode:
1. UIViewSettings
UIViewSettings.GetForCurrentView().UserInteractionMode =
Windows.UI.ViewManagement.UserInteractionMode.Touch
Pros: Simple & easy line of code
Cons: UWP code. This article is for WinForms.Net and referencing UWP DLLs is not easy.
2. GetSystemMetrics
GetSystemMetrics
is a Windows API that can be called. The specific property suggested is SM_CONVERTIBLESLATEMODE
. IE:
private const int SM_CONVERTIBLESLATEMODE = 0x2003;
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto, EntryPoint = "GetSystemMetrics")]
private static extern int GetSystemMetrics (int nIndex);
Pros: Simple. Declare the function, and call GetSystemMetrics(SM_CONVERTIBLESLTATEMODE)
and compare the return to 0
. If 0
(zero), then Windows is in Tablet Mode.
Cons: Doesn't work on Desktop PCs according to MSDN. TabletMode
can be enabled on any Windows 10 PC that has only 1 active monitor. I tested on a Laptop and it still didn't accurately report the PC being in Tablet Mode.
So this mode is unreliable. It is unlikely a non-tablet PC will be used in Tablet Mode, but since Microsoft allows it, you should guard your program against it.
3. Registry
When TabletMode
is turned on/off, Windows writes a value to HKCU\Software\Microsoft\Windows\CurrentVersion\ImmersiveShell\TabletMode.
private bool GetTabletMode()
{
return (bool)Microsoft.Win32.Registry.GetValue
("HKEY_CURRENT_USER\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\ImmersiveShell", "TabletMode", 0);
}
Private Function GetTabletMode() As Boolean
Return CBool(Microsoft.Win32.Registry.GetValue
("HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\ImmersiveShell", "TabletMode", 0))
End Function
Pros: It's in the HKCU area of the registry so permissions shouldn't be an issue. Retrieving a registry value is easy. Detection of TabletMode
was reliable.
Cons: None(?)
3. Prevent the Activated & VisibleChanged
To prevent these two events from firing out of order, we need to override the OnActivated
and the SetVisibleCore
functions and prevent the base functions from being called in the wrong order.
I found the most effective logic was to detect if we were in TabletMode
inside the SetVisibleCore
- then call the appropriate event handlers in the correct order. A very basic Form1
example is as follows:
using System;
using System.Windows.Forms;
namespace TabletModeFormEventOrderExample
{
public partial class Form1 : Form
{
private bool _loadedEventFired = false;
private bool _onActivatedAllowed = false;
public Form1()
{
InitializeComponent();
this.Load += Form1_Load;
}
private void Form1_Load(object sender, EventArgs e)
{
if (_loadedEventFired) return; // Prevent event from firing twice.
//Do whatever needs to be done in Load event.
_loadedEventFired = true;
}
protected override void OnActivated(EventArgs e)
{
if (GetTabletMode() && !_onActivatedAllowed) return;
base.OnActivated(e);
}
protected override void SetVisibleCore(bool value)
{
// We only need special logic if:
// a. Visible is being set to true and the Loaded event hasn't fired yet.
// b. The OS is Windows 10 or later.
// c. TabletMode is turned on.
if (value && !_loadedEventFired)
{
// Version.Major = 6 unless you set Windows 10 support in application manifest.
if ((Environment.OSVersion.Platform == PlatformID.Win32NT) &&
(Environment.OSVersion.Version.Major >= 10))
{
// Get TabletMode from registry.
if (GetTabletMode())
{
// Handle should already be created, but ensure it is.
if (this.Handle == IntPtr.Zero) this.CreateHandle();
// Ensure BindingContext is properly set and
// fire the BindingContextChanged event.
// This example doesn't include setting a BindingContext.
// Manually fire the load event.
base.OnLoad(EventArgs.Empty);
// Now set the visible core.
base.SetVisibleCore(value);
// Now allow OnActivated to fire.
_onActivatedAllowed = true;
// Calling this.Activate doesn't seem to work
// once we've hijacked the chain of events back from Tablet mode.
// Instead call OnActivated to force it to fire.
if (this.ShowWithoutActivation) OnActivated(EventArgs.Empty);
return;
}
}
}
base.SetVisibleCore(value);
}
private bool GetTabletMode()
{
return (bool)Microsoft.Win32.Registry.GetValue
("HKEY_CURRENT_USER\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\ImmersiveShell",
"TabletMode", 0);
}
}
}
Imports System
Imports System.Windows.Forms
Namespace TabletModeFormEventOrderExample
Public Partial Class Form1
Inherits Form
Private _loadedEventFired As Boolean = False
Private _onActivatedAllowed As Boolean = False
Public Sub New()
InitializeComponent()
Me.Load += AddressOf Form1_Load
End Sub
Private Sub Form1_Load(ByVal sender As Object, ByVal e As EventArgs)
'Prevent event from firing twice.
If _loadedEventFired Then Exit Sub
'Do whatever needs to be done in Load event.
_loadedEventFired = True
End Sub
Protected Overrides Sub OnActivated(ByVal e As EventArgs)
If GetTabletMode() AndAlso Not _onActivatedAllowed Then Exit Sub
MyBase.OnActivated(e)
End Sub
Protected Overrides Sub SetVisibleCore(ByVal value As Boolean)
'We only need special logic if:
' a. Visible is being set to true and the Loaded event hasn't fired yet.
' b. The OS is Windows 10 or later.
' c. TabletMode is turned on.
If value AndAlso Not _loadedEventFired Then
'Version.Major = 6 unless you set Windows 10 support in application manifest.
If (Environment.OSVersion.Platform = PlatformID.Win32NT) _
AndAlso (Environment.OSVersion.Version.Major >= 10) Then
'Get TabletMode from registry.
If GetTabletMode() Then
'Handle should already be created, but ensure it is.
If Me.Handle = IntPtr.Zero Then Me.CreateHandle()
'Ensure BindingContext is properly set and
'fire the BindingContextChanged event.
'This example doesn't include setting a BindingContext.
'Manually fire the load event.
MyBase.OnLoad(EventArgs.Empty)
'Now set the visible core.
MyBase.SetVisibleCore(value)
'Now allow OnActivated to fire.
_onActivatedAllowed = True
'Calling Me.Activate doesn't seem to work
'once we have hijacked the chain of events back from Tablet mode.
'Instead call OnActivated to force it to fire.
If Me.ShowWithoutActivation Then OnActivated(EventArgs.Empty)
Exit Sub
End If
End If
End If
MyBase.SetVisibleCore(value)
End Sub
Private Function GetTabletMode() As Boolean
Return CBool(Microsoft.Win32.Registry.GetValue_
("HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\ImmersiveShell", _
"TabletMode", 0))
End Function
End Class
End Namespace
History
- 7/27/2018: Initial draft