C# uses the dispatch context for events. Test case:
using System;
using System.Threading;
using System.Threading.Tasks;
public class Program {
static async Task Main() {
await new ExampleAsync().Run();
}
}
class ExampleAsync {
event EventHandler<EventArgs> myEvent;
AsyncLocal<string> myVar = new AsyncLocal<string>();
public async Task Run() {
await Register();
var task = Dispatch();
Console.WriteLine("Setting the myVar to Outer");
myVar.Value = "Outer";
await task;
Console.WriteLine("At the end the AsyncLocal var is set to " + myVar.Value);
}
async Task Register() {
await Task.Delay(100);
myVar.Value = "Register";
myEvent += Handler;
await Task.Delay(100);
}
async Task Dispatch() {
Console.WriteLine("Setting the myVar to Dispatch");
myVar.Value = "Dispatch";
await Task.Delay(100);
Console.WriteLine("Dispatching the event (with myVar set to " + myVar.Value + ")");
myEvent(this, EventArgs.Empty);
await Task.Delay(100);
}
void Handler(object sender, EventArgs args) {
Console.WriteLine("In the event handler the AsyncLocal var is set to " + myVar.Value);
}
}
This code will log
Setting the myVar to Dispatch
Setting the myVar to Outer
Dispatching the event (with myVar set to Dispatch)
In the event handler the AsyncLocal var is set to Dispatch
At the end the AsyncLocal var is set to Outer
You can test it to https://dotnetfiddle.net/. Make sure to set your compiler to Latest and language to C#.
Dart's variant of AsyncContext is called "Zones". They are not as powerful as AsyncContext, but they allow isolating some behavior (such as print
) across a given "async flow".
Dart does not have a built-in events system. However, we can look at other language constructs that support "sending multiple values at different time": streams.
Streams are meant to be "lazy": you only compute values when somebody starts iterating over them, through .forEach
.
You can test Dart's behavior with the following snipped:
import 'dart:async';
void main() {
print("Hello!");
var stream;
runZoned(
() {
var streamController;
streamController = new StreamController<String>.broadcast(
onListen: () {
print("Hi from stream onListen");
streamController.add("val");
}
);
stream = streamController.stream;
},
zoneSpecification: ZoneSpecification(
print: (self, parent, zone, line) async {
parent.print(zone, "ControllerController zone: $line");
},
),
);
runZoned(
() {
stream = stream.map((v) {
print("Hi from stream.map");
return "mapped(" + v + ")";
});
},
zoneSpecification: ZoneSpecification(
print: (self, parent, zone, line) async {
parent.print(zone, "Map zone: $line");
},
),
);
runZoned(
() {
print("Calling .forEach");
stream.forEach((v) {
print("Hi from stream.forEach: " + v);
});
},
zoneSpecification: ZoneSpecification(
print: (self, parent, zone, line) async {
parent.print(zone, "For each zone: $line");
},
),
);
print("Done");
}
It will log
Hello!
For each zone: Calling .forEach
For each zone: Hi from stream onListen
Done
For each zone: Hi from stream.map
For each zone: Hi from stream.forEach: mapped(val)
You can test it at https://dartpad.dev/.
The Dart documentation explains the motivation behind this behavior (https://dart.dev/libraries/async/zones#using-zones-with-streams):
Transformations and other callbacks execute in the zone where the stream is listened to.
This rule follows from the guideline that streams should have no side effect until listened to. A similar situation in synchronous code is the behavior of Iterables, which aren't evaluated until you ask for values.
While Dart itself doesn't have a built-in event system, Flutter (Dart's UI framework, very similar to React) does.
Given that Flutter is abuot building UIs most of its events are always framework-dispatched, however it's possible to programmatically simulate events. This is similar to how, on the web, most click
events are browser-dispatched but you can simulate them with element.click()
.
Flutter uses the dispatch Zone for events triggered programmatically, and it falls back to the Zone where runApp
(the equivalent of react-dom
's render
) for events triggered by the user.
You can verify this with the demo implementation below, running it in https://dartpad.dev/:
runApp
zone_mouseIconPressed
zone.I am not familiar with the Flutter implementation, but the fallback behavior could be explained in two different ways:
runApp
call, which is Flutter's entrypoint.runApp
has basically an "event loop" checking if there is any user interaction, and if there is it's this loop in runApp
that is disaptching the event. Thus, runApp
's Zone is the dispatch Zone.import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'dart:async';
import 'dart:ui';
void main() {
var app = runZoned(() => const MyApp(), zoneValues: {#zoneName: "MyApp()"});
return runZoned(() => runApp(app), zoneValues: {#zoneName: "runApp()"});
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return runZoned(() {
return MaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorSchemeSeed: Colors.blue,
),
home: const MyHomePage(title: 'Zones and events demo'),
);
}, zoneValues: {#zoneName: "MyApp build method"});
}
}
class MyHomePage extends StatefulWidget {
final String title;
const MyHomePage({
super.key,
required this.title,
});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
String _text = "";
GlobalKey _plusButtonKey = GlobalKey();
void _plusPointerDown(e) {
var zoneName = Zone.current[#zoneName];
runZoned(() {
setState(() {
if (zoneName == null) {
_text = "You have pushed + the button from null zone";
} else {
_text = "You have pushed + the button from " + zoneName;
}
});
}, zoneValues: {#zoneName: "_MyHomePageState._plusPointerDown"});
}
void _mouseIconPressed() {
runZoned(() {
var pos = Offset(10, 10);
var box = _plusButtonKey.currentContext!.findRenderObject() as RenderBox;
final result = BoxHitTestResult();
box.hitTest(result, position: pos);
result.path.forEach((element) {
element.target.handleEvent(
PointerDownEvent(
position: box.localToGlobal(pos), kind: PointerDeviceKind.touch),
element,
);
});
}, zoneValues: {#zoneName: "_MyHomePageState._mouseIconPressed"});
}
@override
Widget build(BuildContext context) {
return runZoned(() {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
"Push the + button, or the mouse button to trigger a click on +!"),
Text(_text),
Row(children: [
Listener(
key: _plusButtonKey,
onPointerDown: _plusPointerDown,
child: ElevatedButton(
onPressed: () {},
child: const Icon(Icons.add),
)
),
ElevatedButton(
onPressed: _mouseIconPressed,
child: const Icon(Icons.mouse),
)
])
],
),
),
);
}, zoneValues: {#zoneName: "_MyHomePageState.build"});
}
}