Simplify `Invoke` and `BeginInvoke` signature and accept `Action`
See original GitHub issueProposed 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:
- Always interact with UI controls on the same thread as they are created. Interacting with controls from another thread requires
InvokeRequired
,Invoke
,BeginInvoke
, etc. - 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.
Issue Analytics
- State:
- Created 3 years ago
- Reactions:6
- Comments:16 (15 by maintainers)
I think we should go forward with this. Sorry again for the delay.
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 onControl
and give it different behavior thanInvoke(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.