Skip to content

Simplify Invoke and BeginInvoke signature and accept Action #4608

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
RussKie opened this issue Feb 25, 2021 · 16 comments · Fixed by #5043
Closed

Simplify Invoke and BeginInvoke signature and accept Action #4608

RussKie opened this issue Feb 25, 2021 · 16 comments · Fixed by #5043
Labels
api-approved (4) API was approved in API review, it can be implemented

Comments

@RussKie
Copy link
Member

RussKie commented Feb 25, 2021

Proposed API:

Add an overload that takes Action as a parameter for both Invoke(Action method) and BeginInvoke(Action method).
I don't think it is necessary to create overloads for methods that take params object[] args as it is possible to pass necessary parameters via a closure.

+        public IAsyncResult BeginInvoke(Action method)
         public IAsyncResult BeginInvoke(Delegate method)
         public IAsyncResult BeginInvoke(Delegate method, params object[] args)

+        public T Invoke<T>(Func<T> method)
+        public void Invoke(Action method)
         public object Invoke(Delegate method)
         public object Invoke(Delegate method, params object[] args)

Background

There are two golden rules for Windows Forms:

  1. Always interact with UI controls on the same thread as they are created. Interacting with controls from another thread requires InvokeRequired, Invoke, BeginInvoke, etc.
  2. Never execute a long-running piece of code in the UI thread.

Invoke and BeginInvoke take Delegate as their input parameters, which requires rather cumbersome boilerplate code, and frankly look dated by todays standard.

E.g.

private void UpdateUI() 
{
    if (myControl.InvokeRequired) 
    {
        myControl.Invoke(new MethodInvoker(() => { UpdateUI(); })); // <-- this is very cumbersome
    } 
    else
    {
        // we're on UI thread, update myControl
    }
}

⚠️ NB: The discussion of automatically switching to the UI thread is well outside the scope of this proposal. It will come separately.

The cumbersome bit is this:

myControl.Invoke(new MethodInvoker(() => UpdateUI()));
float area = 0;
listView1.Invoke(new Action(() => { area = CalculateArea(); }));

which can also be written as

myControl.Invoke((MethodInvoker)(() => { UpdateUI(); }));
myControl.Invoke((Action)(() => { UpdateUI(); }));

However it can not be written as this:

myControl.Invoke(() => UpdateUI());
// or 
myControl.Invoke(UpdateUI); // slightly inefficient - the delegate is not cached, https://github.com/dotnet/roslyn/issues/5835

despite the fact Action is of a delegate type, just like MethodInvoker Thank you @weltkante for pointing it out.

float area = (float)listView1.Invoke(new Func<float>(() => CalculateArea()));

But now it will be possible to write something like this:

float area1 = listView1.Invoke(() => CalculateArea(width, length));
//
float area2 = listView1.Invoke(CalculateArea);

Perf considerations

The added benefit of the new API is that we will be creating smaller code footprint:

        private void InvokeDelegateFunc(object sender, EventArgs e)
        {
            // current
            float area = (float)listView1.Invoke(new Func<float>(() => CalculateArea()));
        }

        private void InvokeFunc(object sender, EventArgs e)
        {
            // proposed
            float area2 = listView1.Invoke(CalculateArea);
        }
InvokeDelegateFunc
.method private hidebysig 
	instance void InvokeDelegateFunc (
		object sender,
		class [System.Runtime]System.EventArgs e
	) cil managed 
{
	// Method begins at RVA 0x7104
	// Code size 31 (0x1f)
	.maxstack 3
	.locals init (
		[0] float32 area
	)

	// sequence point: (line 193, col 9) to (line 193, col 10) in C:\Development\winforms\src\System.Windows.Forms\tests\IntegrationTests\WinformsControlsTest\ListViewTest.cs
	IL_0000: nop
	// sequence point: (line 194, col 13) to (line 194, col 102) in C:\Development\winforms\src\System.Windows.Forms\tests\IntegrationTests\WinformsControlsTest\ListViewTest.cs
	IL_0001: ldarg.0
	IL_0002: ldfld class [System.Windows.Forms]System.Windows.Forms.ListView WinformsControlsTest.ListViewTest::listView1
	IL_0007: ldarg.0
	IL_0008: ldftn instance float32 WinformsControlsTest.ListViewTest::'<InvokeDelegateFunc>b__5_0'()
	IL_000e: newobj instance void class [System.Runtime]System.Func`1<float32>::.ctor(object, native int)
	IL_0013: callvirt instance object [System.Windows.Forms]System.Windows.Forms.Control::Invoke(class [System.Runtime]System.Delegate)
	IL_0018: unbox.any [System.Runtime]System.Single
	IL_001d: stloc.0
	// sequence point: (line 195, col 9) to (line 195, col 10) in C:\Development\winforms\src\System.Windows.Forms\tests\IntegrationTests\WinformsControlsTest\ListViewTest.cs
	IL_001e: ret
} // end of method ListViewTest::InvokeDelegateFunc

.method private hidebysig 
	instance float32 '<InvokeDelegateFunc>b__5_0' () cil managed 
{
	.custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
		01 00 00 00
	)
	// Method begins at RVA 0x7a7c
	// Code size 7 (0x7)
	.maxstack 8

	// sequence point: (line 194, col 83) to (line 194, col 98) in C:\Development\winforms\src\System.Windows.Forms\tests\IntegrationTests\WinformsControlsTest\ListViewTest.cs
	IL_0000: ldarg.0
	IL_0001: call instance float32 WinformsControlsTest.ListViewTest::CalculateArea()
	IL_0006: ret
} // end of method ListViewTest::'<InvokeDelegateFunc>b__5_0'

vs

InvokeFunc
.method private hidebysig 
	instance void InvokeFunc (
		object sender,
		class [System.Runtime]System.EventArgs e
	) cil managed 
{
	// Method begins at RVA 0x7130
	// Code size 26 (0x1a)
	.maxstack 3
	.locals init (
		[0] float32 area2
	)

	// sequence point: (line 198, col 9) to (line 198, col 10) in C:\Development\winforms\src\System.Windows.Forms\tests\IntegrationTests\WinformsControlsTest\ListViewTest.cs
	IL_0000: nop
	// sequence point: (line 199, col 13) to (line 199, col 59) in C:\Development\winforms\src\System.Windows.Forms\tests\IntegrationTests\WinformsControlsTest\ListViewTest.cs
	IL_0001: ldarg.0
	IL_0002: ldfld class [System.Windows.Forms]System.Windows.Forms.ListView WinformsControlsTest.ListViewTest::listView1
	IL_0007: ldarg.0
	IL_0008: ldftn instance float32 WinformsControlsTest.ListViewTest::CalculateArea()
	IL_000e: newobj instance void class [System.Runtime]System.Func`1<float32>::.ctor(object, native int)
	IL_0013: callvirt instance !!0 [System.Windows.Forms]System.Windows.Forms.Control::Invoke<float32>(class [System.Runtime]System.Func`1<!!0>)
	IL_0018: stloc.0
	// sequence point: (line 200, col 9) to (line 200, col 10) in C:\Development\winforms\src\System.Windows.Forms\tests\IntegrationTests\WinformsControlsTest\ListViewTest.cs
	IL_0019: ret
} // end of method ListViewTest::InvokeFunc

Other considerations

I have checked emitted IL, and it appears to be of the same size, with the only difference use of Action instead of MethodInvoker.

@KlausLoeffelmann has highlighted to me that the issue appears to be C# specific, as VB understand the following right now:

        Friend Sub Test()
            Dim control As New Button()
            control.BeginInvoke(Sub()
                                    MessageBox.Show("Test!")
                                End Sub)
        End Sub

I have verified the proposal does not appear to have any negative impacts on VB.

@RussKie RussKie added the api-suggestion (1) Early API idea and discussion, it is NOT ready for implementation label Feb 25, 2021
@RussKie
Copy link
Member Author

RussKie commented Feb 25, 2021

@MadsTorgersen @jaredpar @KathleenDollard can you see any issues with this change?

@jaredpar
Copy link
Member

LGTM

@kirsan31
Copy link
Contributor

#1347 😁

@tbolon
Copy link
Contributor

tbolon commented Feb 26, 2021

myControl.Invoke(() => { UpdateUI(); });

Will this also works?

myControl.Invoke(UpdateUI);

@weltkante
Copy link
Contributor

Will this also works?

Yes but its currently slightly more inefficient because the delegate is not cached (for historic reasons). There's a roslyn issue for changing this and making it behave like the other delegate syntax variants.

@KlausLoeffelmann
Copy link
Member

@weltkante, do you think if we introduced this, we would break a terribly awful lot of people with regards to what you wrote here?

#1347 (comment)

(And let's all keep in mind, we always said, given the nature of .NET in contrast to Framework, we don't want to, but if the benefit outweighs the risk, we would introduce breaking changes. I'd say it would do here, so I am very much in favor to take this.)

@weltkante
Copy link
Contributor

weltkante commented Feb 26, 2021

do you think if we introduced this, we would break a terribly awful lot of people with regards to what you wrote here?

Not really, no, people who wrote an extension method will compile against the new overload now (has higher priority than the extension method), but I really doubt someone would make an Invoke(Action) extension on Control and give it different behavior than Invoke(Delegate)

I expect existing extension methods are doing the same you are going to do in your overload so that should not be breaking.

Users who don't recompile will still be calling their extension method regardless of what you introduce here, so at most its affecting source code, and you can be expected to update your code when you compile against a new framework version. (Though I don't think its necessary in this case to update anything for most users.)

The comment in the other thread was just saying that having this method inbox is "nice to have" and people are already doing it manually via extension, but it wasn't an argument against introducing it.

@tbolon
Copy link
Contributor

tbolon commented Feb 27, 2021

Will this also works?

Yes but its currently slightly more inefficient because the delegate is not cached (for historic reasons). There's a roslyn issue for changing this and making it behave like the other delegate syntax variants.

Sorry for the digression; but I completely forgot about this, and I remember how I was shocked, because I was always thinking that this form was more efficient that the other one (instead of allocating a delegate, just point directly to the method). Thanks for the reminder 😅

@RussKie
Copy link
Member Author

RussKie commented Mar 1, 2021

Updated with public T Invoke<T>(Func<T> method) overload.

@RussKie RussKie added api-ready-for-review (2) API is ready for formal API review; applied by the issue owner and removed api-suggestion (1) Early API idea and discussion, it is NOT ready for implementation labels Mar 1, 2021
@RussKie RussKie added this to the 6.0 milestone Mar 1, 2021
@weltkante
Copy link
Contributor

weltkante commented Mar 1, 2021

I think you missed that the old Invoke method works with any delegate type, not just with MethodInvoker.

I have no idea how Invoke returning a result is written, it's so cumbersome ...

You'd either cast the boxed value or return it through a variable captured in the closure:

float area = 0;
listView1.Invoke(new Action(() => { area = CalculateArea(); }));
// consume "area"

or

var area = (float)listView1.Invoke(new Func<float>(() => CalculateArea()));

despite the fact Action is of a delegate type, just like MethodInvoker.

MethodInvoker has nothing to do with the original methods, they take any delegate. MethodInvoker is not special in any regards, you could replace it literally with Action:

myControl.Invoke(new Action(() => { UpdateUI(); }));

or

myControl.Invoke((Action)(() => { UpdateUI(); }));

in fact you can pass (almost) arbitrary delegates, taking parameters or returning values. Thats why there is an overload taking a params-array.

For what its worth its not necessary to add an Action<...> overload taking a params array, because it'd be less efficient than the original overload (as mentioned in my previous comment on the other issue). If you are looking for performance you preallocate the delegate without closure and pass arguments explicitly, but if you preallocate the delegate then you can use the existing overload.

@RussKie
Copy link
Member Author

RussKie commented Mar 2, 2021

Thank you @weltkante, I indeed totally overlooked it.
I've update the proposal. I think there are some perf benefits to reap as well.

@terrajobst terrajobst added api-approved (4) API was approved in API review, it can be implemented and removed api-ready-for-review (2) API is ready for formal API review; applied by the issue owner labels Mar 4, 2021
@terrajobst
Copy link
Contributor

terrajobst commented Mar 4, 2021

Video

  • Looks good as proposed
namespace System.Windows.Forms
{
    public partial class Control
    {
        public IAsyncResult BeginInvoke(Action method);
        // public IAsyncResult BeginInvoke(Delegate method);
        // public IAsyncResult BeginInvoke(Delegate method, params object[] args);

        public T Invoke<T>(Func<T> method);
        public void Invoke(Action method);
        // public object Invoke(Delegate method);
        // public object Invoke(Delegate method, params object[] args);
    }
}

@RussKie
Copy link
Member Author

RussKie commented Mar 4, 2021

@weltkante made a great comment in #4631 (comment)

...if you do BeginInvoke behavior of posting you may want to cancel your posted callback before it runs.

This could be addressed by dotnet/runtime#47525 that's looking at adding support for timeout and cancellation token support.

@weltkante
Copy link
Contributor

weltkante commented Mar 4, 2021

This could be addressed by dotnet/runtime#47525 that's looking at adding support for timeout and cancellation token support.

Actually I don't think so, that will only allowing the consumer of the Task to specify when it wants to abort waiting on the Task (to stop waiting when some other condition is met), but the callback will still be running to completion - which for WinForms is especially bad because it will be running to completion on the UI thread. Being able to actually cancel the callback to ensure its never executed if it didn't start yet will not be possible with that API, in order to support that it must be implemented on your side.

WPF has had the ability from the start, their BeginInvoke returns a DispatcherOperation (instead of just an IAsyncResult) and you can call Abort on that.

I mean its not a big deal that WinForms doesn't have it, whenever I need to cancel a BeginInvoke I just define a boolean variable (or CancellationToken) and first thing my callback checks is whether its still supposed to run.

@KathleenDollard
Copy link

I think we should go forward with this. Sorry again for the delay.

@RussKie
Copy link
Member Author

RussKie commented Apr 1, 2021

Setting as up for grabs.

@RussKie RussKie added the help wanted Good issue for external contributors label Apr 1, 2021
@ghost ghost added the 🚧 work in progress Work that is current in progress label Jun 4, 2021
RussKie added a commit to RussKie/winforms that referenced this issue Jun 15, 2021
Add overloads that takes `Action` and `Func<T>` as a parameter:

* `IAsyncResult BeginInvoke(Action method)`
* `void Invoke(Action method)`
* `T Invoke<T>(Func<T> method)`

Closes dotnet#4608
@ghost ghost removed this from the 6.0 milestone Jun 15, 2021
@ghost ghost removed the 🚧 work in progress Work that is current in progress label Jun 15, 2021
@RussKie RussKie removed the help wanted Good issue for external contributors label Jun 15, 2021
@ghost ghost locked as resolved and limited conversation to collaborators Jan 29, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
api-approved (4) API was approved in API review, it can be implemented
Projects
None yet
Development

Successfully merging a pull request may close this issue.

8 participants