Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Win10 TabletMode & WinForms.Net Form Event Order

0.00/5 (No votes)
28 Jul 2018 1  
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 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:

  1. Control.HandleCreated
  2. Control.BindingContextChanged
  3. Form.Load
  4. Control.VisibleChanged
  5. Form.Activated
  6. Form.Shown

The TabletMode alters the order as such:

  1. Control.HandleCreated
  2. Form.Activated
  3. Control.VisibleChanged
  4. Control.BindingContextChanged
  5. Form.Load
  6. Form.Shown

Using the code

There are three key steps to achieve to correcting the event order:

  1. 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.
  2. Detect when the PC is in TabletMode.
  3. 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; // 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've 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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here