Aug
16

AllDevelopment.NET
Spurious/Incorrect .NET Form.Activated() Event 2017

Notice: Using the "fix" below can result in the form's Activated event not being fired when it is restored from the taskbar. I guess this isn't a proper solution, after-all. Adding checks for the form being minimised/restored may be a solution but the Form class is firing those events are weird times, so.,,

Notice 2: I've added another version below.

A couple of new categories for this blog: Development and .NET. I'm surprised I haven't written more on programming in general, to be honest. I think I'll have to port across my post from an older blog on the whole BadImageException problem which drove me mad for far too long.

But, let's continue with the subject of this post: the Form.Activated() event incorrectly firing on a Form.

Quite a few years ago I created my own Metro-style form based on the idea of Microsoft's flat-style interface. Metro was approximately a couple of years away from release and there wasn't really any screenshots of what it would look like. I ploughed on with my version - including a break of a year because it took a long time to get it working correctly as working with forms that have their FormBorderStyle set to None in .NET is just outright annoying. This changed in WPF, but we're using WinForms here. #winforms4lyfe

The biggest issue was getting Windows to treat the borderless form as a true window; it just didn't give a crap about it. A quick way to tell if Windows regards a form as a true window is to minimise it to the taskbar. Does it immediately disappear from view? It's not a true window. Does it animate down into the taskbar? It's a true window.

There were two issues remaining (that I remember, anyway): restoring the window from its minimised state causes the window to gain an extra X number of pixels on its height. That's still an issue and seems random as to which form it affects. The fix I put in-place seems to work most of the time, but regardless, the child controls gain extra height with/without the form growing in size. Clearly more work to be done, there.

The second issue which I'm going to talk about here is the form's Activated() event firing when the form hasn't actually been activated. I don't know why this occurs, but it's annoying and obviously unexpected.

It seems somewhat random when it happens, but here's how it generally goes:

While the form is activated, deactivate the form by clicking outside of it and then move the mouse cursor over the form without clicking; chances are the form will receive the Activated() event. Despite this event firing, the form isn't actually activated.

This causes a problem in my particular case because I handle and expose the activation event via a WindowActivityChanged() event, and I also change the form's visual style depending on its active state.

For example, here's a (currently work-in-progress) form that's active:

Active Form

And here's that same form when it has become inactive:

Inactive Form

As you can see, having the form become active when it actually isn't is confusing to the user. And extremely irritating to me.

My first quick go at fixing this seems to work; I'll have to keep testing it over the long term but it seems like it may have solved it. Not a fan that I have to do this in the first place, but what can ya do?

Have this at the first line(s) within the Form_Activated() event [VB]:

' Filter out any spurious activations.
If Not ActiveForm Is Me Then Exit Sub

Here's another version (see notice at top of this post):

If Me.RectangleToScreen(Me.DisplayRectangle).Contains(Cursor.Position) AndAlso Not ActiveForm Is Me Then Exit Sub

ActiveForm is a Read-Only property on the base Form class and contains a reference to the form that is currently active (within our application; it's not system-wide or anything!), so we're checking to see if the active form is actually me (or this in C#) rather than just blindly accepting the fact.

It appears ActiveForm is set before Form_Activated() is fired, so it does what we want.

I'm not sure if it's worth putting a check into the Form_Deactivated() event to ensure that the form is truly deactivated, but I don't recall ever running into that particular problem. I guess time will tell.

Feb
20

AllDevelopment.NET
Running Code on a Self-Contained Timer 2020

A year without a post? Hrm. There is a post dated 2019 but it's in the Draft state; never got around to finishing it... oops.

So here's a little post on running code on a timer; I don't know if C# can do this more elegantly, but I don't think VB can. Hence: this.

There have been far too many times I've wanted to run code that only displays something for a couple of seconds and then hides it. While simple enough, I really don't want to have to create a Timer component, instantiate it, run the code, and then tear it all down again. Just display a thing and then go away - that's all I want!

I couldn't even have a half-solution by instantiating and executing the Timer without having an explicit method for the Timer to call into when the time elapses, such as [note that all code formatting in this post is wonky; a lot of it isn't really fixable]:

Dim timer As New Timer

timer.Start(2000, Sub()
			Console.WriteLine("Runs after 2 seconds!")
		End Sub)

I may make a specific extension method for this sort of thing but that's only tangibly why we're here and so will have to wait for another time.

So, today, I decided to try and get this sorted out. While it would probably have to be a Task, I wasn't entirely sure the best way of going about achomlishing accomplishing this and so I began; after various experiments, here's the result.

#Region " Public: RunAfter "
	''' <summary>Runs the specified <paramref name="action"/> on the UI thread after the specified number of <paramref name="runAfterDuration"/> milliseconds has expired.</summary>
	''' <param name="ctrl">The <see cref="Control"/> thread to run on. If using additional actions (such as <paramref name="additionalEventAction"/>), then any hooked events will be hooked into <c>ctrl</c>.</param>
	''' <param name="runAfterDuration">The number of milliseconds (1000 = 1 second) to wait before running the <paramref name="action"/>.</param>
	''' <param name="action">The code to run.</param>
	''' <param name="additionalHookEvent">Specifies that an event should be hooked for the duration of the task; specify the action associated with the hooked event via <paramref name="additionalEventAction"/>.</param>
	''' <param name="additionalEventAction">The action to perform once the <paramref name="additionalHookEvent"/> has been fired.</param>
	''' <example>
	'''   Me.RunAfter(1000, Sub()
	'''				Me.Text = "Runs on the UI thread"
	'''			End Sub)
	''' </example>
	<Extension>
	Public Sub RunAfter(ctrl As Control, runAfterDuration As Integer, action As Action(Of Task), Optional additionalHookEvent As RunAfterEvent = RunAfterEvent.None, Optional additionalEventAction As Action(Of Task) = Nothing)
		If ctrl Is Nothing Then Throw New ArgumentNullException(NameOf(ctrl), $"{NameOf(ctrl)} cannot be null (Nothing in Visual Basic).")
		If ctrl.IsDisposed Then Throw New ObjectDisposedException(NameOf(ctrl), $"{NameOf(ctrl)} cannot be disposed.")
		If action Is Nothing Then Throw New ArgumentNullException(NameOf(action), $"{NameOf(action)} cannot be null (Nothing in Visual Basic) and must be a method.")
		If Not additionalHookEvent = RunAfterAdditionalEvent.None AndAlso additionalEventAction Is Nothing Then Throw New ArgumentNullException(NameOf(additionalEventAction), $"An {NameOf(additionalHookEvent)} is specified but the {NameOf(additionalEventAction)} is null (Nothing in Visual Basic).")

		' Ensure the duration is within an appropriate integer range.
		If runAfterDuration < 0 Then runAfterDuration = 0
		If runAfterDuration > Integer.MaxValue Then runAfterDuration = Integer.MaxValue

		' This allows us to marshal the invocation to the UI thread
		' so we don't perform a cross-thread execution.
		Dim ts As TaskScheduler = DirectCast(ctrl.Invoke(Function()
						Return TaskScheduler.FromCurrentSynchronizationContext
					End Function), TaskScheduler)

		Dim task = Threading.Tasks.Task.Delay(runAfterDuration)

		' The action will be executed after the initial delay above.
		Dim continuedTask = task.ContinueWith(action, ts)

		If additionalHookEvent = RunAfterEvent.OnClick Then

			' Add the additional specified action to the Control's client event.
			' This won't ever be executed if the user never clicks on the Control.
			AddHandler ctrl.Click, Sub(sender As Object, e As EventArgs)
							ctrl.Invoke(additionalEventAction, continuedTask)
						End Sub

		End If
	End Sub
#End Region

And the enumeration:

#Region " Enum: RunAfterEvent "
	''' <summary>Specifies an event to be hooked.</summary>
	<EditorBrowsable(EditorBrowsableState.Advanced)>
	Public Enum RunAfterEvent As Integer

		''' <summary>No event is hooked.</summary>
		None

		''' <summary>The <c>Click</c> event is hooked.</summary>
		OnClick

	End Enum
#End Region

The formatting is completely off in this post, so it doesn't look like that in actuality. I also have extension methods for certain things (eg. ensuring a value is appropriately clamped between a range) but I've replaced them with "pure code" versions above.

The above allows you to run code after a specific duration has elapsed, and optionally run additional code based on an event that occurs - all without having to create a separate Timer control that needs to be manually instantiated, hooked, and torn down. Again, this appears to be true for VB.

While not posted here, I have another version that uses a TimeSpan instead of a duration (meh; not a major thing) as it's "more appropriate", and a version that has the initial ctrl As Control parameter as item as ToolStripItem to be used with anything that inherits from said ToolStripItem, such as a ToolStripButton.

Use item.GetCurrentParent() with a ToolStripItem as only the parent ToolStrip has the required Invoke() method.

Oh, an example? Right - yep, this is the basic version that only specifies the code that should be run after a duration has elapsed. This hides the (eg.) ToolStripLabel Verified OK! indicator after two seconds and is used to let the user know a thing they validated/verified/whatever is fine.

Me.tbrMain_VerifiedOk.Visible = True

' Hides the success indicator after two seconds.
Me.tbrMain_VerifiedOk.RunAfter(2000, Sub()
						Me.tbrMain_VerifiedOk.Visible = False
				End Sub)

And another version that does the same as the above but also adds code that hides the visual indicator if the user clicks on it (this is a simplified version of code being used in Twitter Delitter).

Me.tbrMain_VerifiedOk.Visible = True

' Hides the success indicator after two seconds, or
' when the user clicks on the indicator itself.
Me.tbrMain_VerifiedOk.RunAfter(2000, Sub()
						Me.tbrMain_VerifiedOk.Visible = False
				End Sub,
				RunAfterEvent.OnClick,
				Sub()
						Me.tbrMain_VerifiedOk.Visible = False
				End Sub)

And that's about it. While countless changes could be made, it would make this post unwieldy; I have a particular loathing for code examples that have extraneous fluff that does nothing but obfuscates what is being shown.

Use this code at your own risk, etc - I (mainly) use VB .NET so you probably shouldn't listen to me in the first place...