Skip to content

Commit

Permalink
Add check for UI thread when using UI controls.
Browse files Browse the repository at this point in the history
  • Loading branch information
cwensley committed Sep 29, 2021
1 parent dee24bc commit b0a24f8
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 3 deletions.
90 changes: 88 additions & 2 deletions src/Eto/Forms/Application.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,65 @@
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace Eto.Forms
{
/// <summary>
/// UI Thread check mode when <see cref="Application.EnsureUIThread"/> is called.
/// </summary>
public enum UIThreadCheckMode
{
/// <summary>
/// Do no checking
/// </summary>
None,
/// <summary>
/// Emit a warning with Trace.WriteLine()
/// </summary>
Warning,
/// <summary>
/// Throw an exception
/// </summary>
Error
}

/// <summary>
/// Exception thrown when a control method is accessed in a non-UI thread using <see cref="Application.EnsureUIThread"/>.
/// </summary>
#if NETSTANDARD2_0_OR_GREATER
[System.Serializable]
#endif
public class UIThreadAccessException : System.Exception
{
/// <summary>
/// Initializes a new instance of the UIThreadAccessException class
/// </summary>
public UIThreadAccessException() { }
/// <summary>
/// Initializes a new instance of the UIThreadAccessException class with the specified message
/// </summary>
/// <param name="message">Message for the exception</param>
public UIThreadAccessException(string message) : base(message) { }
/// <summary>
/// Initializes a new instance of the UIThreadAccessException class with the specified message and inner exception
/// </summary>
/// <param name="message">Message for the exception</param>
/// <param name="inner">Inner exception</param>
public UIThreadAccessException(string message, System.Exception inner) : base(message, inner) { }
#if NETSTANDARD2_0_OR_GREATER
/// <summary>
/// Initializes a new instance of the UIThreadAccessException class from serialization
/// </summary>
/// <param name="info">Serialization info</param>
/// <param name="context">Streaming context</param>
protected UIThreadAccessException(
System.Runtime.Serialization.SerializationInfo info,
System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
#endif
}

/// <summary>
/// Starting point for any UI application
/// </summary>
Expand All @@ -17,6 +72,9 @@ namespace Eto.Forms
[Handler(typeof(Application.IHandler))]
public class Application : Widget
{
#if NETSTANDARD2_0_OR_GREATER
Thread mainThread;
#endif
LocalizeEventArgs localizeArgs;
readonly object localizeLock = new object();
static readonly object ApplicationKey = new object();
Expand All @@ -28,7 +86,7 @@ public class Application : Widget
public static Application Instance
{
get
{
{
var platform = Platform.Instance;
return platform != null ? platform.GetSharedProperty<Application>(ApplicationKey, () => null) : null;
}
Expand Down Expand Up @@ -241,6 +299,9 @@ public Application(Platform platform)
: this(InitializePlatform(platform))
{
Instance = this;
#if NETSTANDARD2_0_OR_GREATER
mainThread = System.Threading.Thread.CurrentThread;
#endif
}

Application(InitHelper init)
Expand All @@ -262,6 +323,31 @@ static InitHelper InitializePlatform(Platform platform)
return null;
}


/// <summary>
/// Gets or sets the UI thread check mode which can be used to troubleshoot or ensure that all UI code is executed in the UI thread.
/// </summary>
/// <value>The current thread check mode</value>
[DefaultValue(UIThreadCheckMode.Warning)]
public UIThreadCheckMode UIThreadCheckMode { get; set; } = System.Diagnostics.Debugger.IsAttached ? UIThreadCheckMode.Error : UIThreadCheckMode.Warning;

/// <summary>
/// Ensures the current thread is the main/UI thread
/// </summary>
public void EnsureUIThread()
{
#if NETSTANDARD2_0_OR_GREATER
if (UIThreadCheckMode == UIThreadCheckMode.None)
return;
if (mainThread == Thread.CurrentThread)
return;
if (UIThreadCheckMode == UIThreadCheckMode.Warning)
System.Diagnostics.Trace.WriteLine("Warning: Accessing UI object from a non-UI thread. UI objects can only be used from the main thread.");
else if (UIThreadCheckMode == UIThreadCheckMode.Error)
throw new UIThreadAccessException();
#endif
}

/// <summary>
/// Runs the application and begins the main loop.
/// </summary>
Expand Down Expand Up @@ -610,7 +696,7 @@ public void OnNotificationActivated(Application widget, NotificationEventArgs e)
/// Gets a value indicating whether the application supports the <see cref="Quit"/> operation.
/// </summary>
/// <value><c>true</c> if quit is supported; otherwise, <c>false</c>.</value>
bool QuitIsSupported { get ; }
bool QuitIsSupported { get; }

/// <summary>
/// Gets the common modifier for shortcuts.
Expand Down
13 changes: 12 additions & 1 deletion src/Eto/Forms/Controls/Control.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,18 @@ namespace Eto.Forms
[sc.TypeConverter(typeof(ControlConverter))]
public partial class Control : BindableWidget, IMouseInputSource, IKeyboardInputSource, ICallbackSource
{
new IHandler Handler => (IHandler)base.Handler;
/// <summary>
/// Gets the handler for the widget, ensuring the current thread is the UI thread
/// </summary>
/// <value>The handler object for this control</value>
protected new IHandler Handler
{
get
{
Application.Instance?.EnsureUIThread();
return (IHandler)base.Handler;
}
}

/// <summary>
/// Gets a value indicating that the control is loaded onto a form, that is it has been created, added to a parent, and shown
Expand Down
1 change: 1 addition & 0 deletions src/Eto/Forms/MessageBox.cs
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ public static DialogResult Show(Control parent, string text, MessageBoxButtons b
/// <param name="defaultButton">Button to set focus to by default</param>
public static DialogResult Show(Control parent, string text, string caption, MessageBoxButtons buttons, MessageBoxType type = MessageBoxType.Information, MessageBoxDefaultButton defaultButton = MessageBoxDefaultButton.Default)
{
Application.Instance.EnsureUIThread();
var mb = Platform.Instance.Create<IHandler>();
mb.Text = text;
mb.Caption = caption;
Expand Down
1 change: 1 addition & 0 deletions test/Eto.Test/TestApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public TestApplication(Platform platform)
: base(platform)
{
TestAssemblies = DefaultTestAssemblies().ToList();
UIThreadCheckMode = UIThreadCheckMode.Error;
this.Name = "Test Application";
this.Style = "application";

Expand Down
18 changes: 18 additions & 0 deletions test/Eto.Test/UnitTests/Forms/ApplicationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,5 +89,23 @@ public void RunIterationShouldAllowBlocking(int delay)

Assert.IsTrue(stopClicked, "#1 - Must press the stop button to close the form");
}

[Test]
public void EnsureUIThreadShouldThrow()
{
Form form = null;
TextBox textBox = null;
var oldMode = Application.Instance.UIThreadCheckMode;
Application.Instance.UIThreadCheckMode = UIThreadCheckMode.Error;
Invoke(() => {
textBox = new TextBox();
form = new Form();
});

Assert.Throws<UIThreadAccessException>(() => textBox.Text = "hello", "#1");
Assert.Throws<UIThreadAccessException>(() => form.Bounds = new Rectangle(0, 0, 100, 100), "#2");

Application.Instance.UIThreadCheckMode = oldMode;
}
}
}

0 comments on commit b0a24f8

Please sign in to comment.