Introduction
WinForms.Net has a specific order of events that occurs when a form is created and shown (see https://docs.microsoft.com/en-us/dotnet/framework/winforms/order-of-events-in-windows-forms).
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 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 was 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 (https://msdn.microsoft.com/en-us/library/windows/desktop/ms724385(v=vs.85).aspx). 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;
_loadedEventFired = true;
}
protected override void OnActivated(EventArgs e)
{
if (GetTabletMode() && !_onActivatedAllowed) return;
base.OnActivated(e);
}
protected override void SetVisibleCore(bool value)
{
if (value && !_loadedEventFired)
{
if ((Environment.OSVersion.Platform == PlatformID.Win32NT) && (Environment.OSVersion.Version.Major >= 10))
{
if (GetTabletMode())
{
if (this.Handle == IntPtr.Zero) this.CreateHandle();
base.OnLoad(EventArgs.Empty);
base.SetVisibleCore(value);
_onActivatedAllowed = true;
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)
If _loadedEventFired Then Exit Sub
_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)
If value AndAlso Not _loadedEventFired Then
If (Environment.OSVersion.Platform = PlatformID.Win32NT) AndAlso (Environment.OSVersion.Version.Major >= 10) Then
If GetTabletMode() Then
If Me.Handle = IntPtr.Zero Then Me.CreateHandle()
MyBase.OnLoad(EventArgs.Empty)
MyBase.SetVisibleCore(value)
_onActivatedAllowed = True
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.