BackgroundWorkerCollection.cs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Collections.ObjectModel;
  4. using System.ComponentModel;
  5. using System.Diagnostics;
  6. using System.Threading;
  7. using Terminal.Gui;
  8. namespace UICatalog.Scenarios;
  9. [ScenarioMetadata ("BackgroundWorker Collection", "A persisting multi Toplevel BackgroundWorker threading")]
  10. [ScenarioCategory ("Threading")]
  11. [ScenarioCategory ("Top Level Windows")]
  12. [ScenarioCategory ("Dialogs")]
  13. [ScenarioCategory ("Controls")]
  14. public class BackgroundWorkerCollection : Scenario
  15. {
  16. public override void Main ()
  17. {
  18. Application.Run<OverlappedMain> ().Dispose ();
  19. #if DEBUG_IDISPOSABLE
  20. if (Application.OverlappedChildren is { })
  21. {
  22. Debug.Assert (Application.OverlappedChildren?.Count == 0);
  23. Debug.Assert (Application.Top == Application.OverlappedTop);
  24. }
  25. #endif
  26. Application.Shutdown ();
  27. }
  28. private class OverlappedMain : Toplevel
  29. {
  30. private readonly MenuBar _menu;
  31. private WorkerApp _workerApp;
  32. public OverlappedMain ()
  33. {
  34. Arrangement = ViewArrangement.Movable;
  35. Data = "OverlappedMain";
  36. IsOverlappedContainer = true;
  37. _workerApp = new WorkerApp { Visible = false };
  38. _workerApp.Border.Thickness = new (0, 1, 0, 0);
  39. _workerApp.Border.LineStyle = LineStyle.Dashed;
  40. _menu = new MenuBar
  41. {
  42. Menus =
  43. [
  44. new MenuBarItem (
  45. "_Options",
  46. new MenuItem []
  47. {
  48. new (
  49. "_Run Worker",
  50. "",
  51. () => _workerApp.RunWorker (),
  52. null,
  53. null,
  54. KeyCode.CtrlMask | KeyCode.R
  55. ),
  56. new (
  57. "_Cancel Worker",
  58. "",
  59. () => _workerApp.CancelWorker (),
  60. null,
  61. null,
  62. KeyCode.CtrlMask | KeyCode.C
  63. ),
  64. null,
  65. new (
  66. "_Quit",
  67. "",
  68. () => Quit (),
  69. null,
  70. null,
  71. (KeyCode)Application.QuitKey
  72. )
  73. }
  74. ),
  75. new MenuBarItem ("_View", new MenuItem [] { }),
  76. new MenuBarItem ("_Window", new MenuItem [] { })
  77. ]
  78. };
  79. ;
  80. _menu.MenuOpening += Menu_MenuOpening;
  81. Add (_menu);
  82. var statusBar = new StatusBar (
  83. new []
  84. {
  85. new StatusItem (Application.QuitKey, $"{Application.QuitKey} to Quit", () => Quit ()),
  86. new StatusItem (
  87. KeyCode.CtrlMask | KeyCode.R,
  88. "~^R~ Run Worker",
  89. () => _workerApp.RunWorker ()
  90. ),
  91. new StatusItem (
  92. KeyCode.CtrlMask | KeyCode.C,
  93. "~^C~ Cancel Worker",
  94. () => _workerApp.CancelWorker ()
  95. )
  96. }
  97. );
  98. Add (statusBar);
  99. Ready += OverlappedMain_Ready;
  100. Activate += OverlappedMain_Activate;
  101. Deactivate += OverlappedMain_Deactivate;
  102. }
  103. private void OverlappedMain_Ready (object sender, EventArgs e)
  104. {
  105. if (_workerApp?.Running == false)
  106. {
  107. Application.Run (_workerApp);
  108. }
  109. }
  110. private void Menu_MenuOpening (object sender, MenuOpeningEventArgs menu)
  111. {
  112. if (menu.CurrentMenu.Title == "_Window")
  113. {
  114. menu.NewMenuBarItem = OpenedWindows ();
  115. }
  116. else if (menu.CurrentMenu.Title == "_View")
  117. {
  118. menu.NewMenuBarItem = View ();
  119. }
  120. }
  121. private MenuBarItem OpenedWindows ()
  122. {
  123. var index = 1;
  124. List<MenuItem> menuItems = new ();
  125. List<Toplevel> sortedChildren = Application.OverlappedChildren;
  126. sortedChildren.Sort (new ToplevelComparer ());
  127. foreach (Toplevel top in sortedChildren)
  128. {
  129. if (top.Data.ToString () == "WorkerApp" && !top.Visible)
  130. {
  131. continue;
  132. }
  133. var item = new MenuItem ();
  134. item.Title = top is Window ? $"{index} {((Window)top).Title}" : $"{index} {top.Data}";
  135. index++;
  136. item.CheckType |= MenuItemCheckStyle.Checked;
  137. string topTitle = top is Window ? ((Window)top).Title : top.Data.ToString ();
  138. string itemTitle = item.Title.Substring (index.ToString ().Length + 1);
  139. if (top == Application.GetTopOverlappedChild () && topTitle == itemTitle)
  140. {
  141. item.Checked = true;
  142. }
  143. else
  144. {
  145. item.Checked = false;
  146. }
  147. item.Action += () => { Application.MoveToOverlappedChild (top); };
  148. menuItems.Add (item);
  149. }
  150. if (menuItems.Count == 0)
  151. {
  152. return new MenuBarItem ("_Window", "", null);
  153. }
  154. return new MenuBarItem ("_Window", new List<MenuItem []> { menuItems.ToArray () });
  155. }
  156. private void OverlappedMain_Activate (object sender, ToplevelEventArgs top)
  157. {
  158. _workerApp?.WriteLog ($"{(top.Toplevel is null ? ((Toplevel)sender).Data : top.Toplevel.Data)} activate.");
  159. }
  160. private void OverlappedMain_Deactivate (object sender, ToplevelEventArgs top)
  161. {
  162. _workerApp?.WriteLog ($"{top.Toplevel.Data} deactivate.");
  163. }
  164. private void Quit () { RequestStop (); }
  165. private MenuBarItem View ()
  166. {
  167. List<MenuItem> menuItems = new ();
  168. var item = new MenuItem { Title = "WorkerApp", CheckType = MenuItemCheckStyle.Checked };
  169. Toplevel top = Application.OverlappedChildren?.Find (x => x.Data.ToString () == "WorkerApp");
  170. if (top != null)
  171. {
  172. item.Checked = top.Visible;
  173. }
  174. item.Action += () =>
  175. {
  176. Toplevel top = Application.OverlappedChildren.Find (x => x.Data.ToString () == "WorkerApp");
  177. item.Checked = top.Visible = (bool)!item.Checked;
  178. if (top.Visible)
  179. {
  180. Application.MoveToOverlappedChild (top);
  181. }
  182. else
  183. {
  184. Application.OverlappedTop.SetNeedsDisplay ();
  185. }
  186. };
  187. menuItems.Add (item);
  188. return new MenuBarItem (
  189. "_View",
  190. new List<MenuItem []> { menuItems.Count == 0 ? new MenuItem [] { } : menuItems.ToArray () }
  191. );
  192. }
  193. /// <inheritdoc />
  194. protected override void Dispose (bool disposing)
  195. {
  196. _workerApp?.Dispose ();
  197. _workerApp = null;
  198. base.Dispose (disposing);
  199. }
  200. }
  201. private class Staging
  202. {
  203. public Staging (DateTime? startStaging, bool completed = false)
  204. {
  205. StartStaging = startStaging;
  206. Completed = completed;
  207. }
  208. public bool Completed { get; }
  209. public DateTime? StartStaging { get; }
  210. }
  211. private class StagingUIController : Window
  212. {
  213. private readonly Button _close;
  214. private readonly Label _label;
  215. private readonly ListView _listView;
  216. private readonly Button _start;
  217. public StagingUIController (Staging staging, ObservableCollection<string> list) : this ()
  218. {
  219. Staging = staging;
  220. _label.Text = "Work list:";
  221. _listView.Enabled = true;
  222. _listView.SetSource (list);
  223. _start.Visible = false;
  224. Id = "";
  225. }
  226. public StagingUIController ()
  227. {
  228. Arrangement = ViewArrangement.Movable;
  229. X = Pos.Center ();
  230. Y = Pos.Center ();
  231. Width = Dim.Percent (85);
  232. Height = Dim.Percent (85);
  233. ColorScheme = Colors.ColorSchemes ["Dialog"];
  234. Title = "Run Worker";
  235. _label = new Label
  236. {
  237. X = Pos.Center (),
  238. Y = 1,
  239. ColorScheme = Colors.ColorSchemes ["Dialog"],
  240. Text = "Press start to do the work or close to quit."
  241. };
  242. Add (_label);
  243. _listView = new ListView { X = 0, Y = 2, Width = Dim.Fill (), Height = Dim.Fill (2), Enabled = false };
  244. Add (_listView);
  245. _start = new Button { Text = "Start", IsDefault = true, ClearOnVisibleFalse = false };
  246. _start.Accept += (s, e) =>
  247. {
  248. Staging = new Staging (DateTime.Now);
  249. RequestStop ();
  250. };
  251. Add (_start);
  252. _close = new Button { Text = "Close" };
  253. _close.Accept += OnReportClosed;
  254. Add (_close);
  255. KeyDown += (s, e) =>
  256. {
  257. if (e == Application.QuitKey)
  258. {
  259. OnReportClosed (this, EventArgs.Empty);
  260. }
  261. };
  262. LayoutStarted += (s, e) =>
  263. {
  264. int btnsWidth = _start.Frame.Width + _close.Frame.Width + 2 - 1;
  265. int shiftLeft = Math.Max ((Viewport.Width - btnsWidth) / 2 - 2, 0);
  266. shiftLeft += _close.Frame.Width + 1;
  267. _close.X = Pos.AnchorEnd (shiftLeft);
  268. _close.Y = Pos.AnchorEnd (1);
  269. shiftLeft += _start.Frame.Width + 1;
  270. _start.X = Pos.AnchorEnd (shiftLeft);
  271. _start.Y = Pos.AnchorEnd (1);
  272. };
  273. }
  274. public Staging Staging { get; private set; }
  275. public event Action<StagingUIController> ReportClosed;
  276. private void OnReportClosed (object sender, EventArgs e)
  277. {
  278. if (Staging?.StartStaging != null)
  279. {
  280. ReportClosed?.Invoke (this);
  281. }
  282. RequestStop ();
  283. }
  284. }
  285. private class WorkerApp : Toplevel
  286. {
  287. private readonly ListView _listLog;
  288. private readonly ObservableCollection<string> _log = [];
  289. private List<StagingUIController> _stagingsUi;
  290. private Dictionary<Staging, BackgroundWorker> _stagingWorkers;
  291. public WorkerApp ()
  292. {
  293. Arrangement = ViewArrangement.Movable;
  294. Data = "WorkerApp";
  295. Title = "Worker collection Log";
  296. Width = Dim.Percent (80);
  297. Height = Dim.Percent (50);
  298. ColorScheme = Colors.ColorSchemes ["Base"];
  299. _listLog = new ListView
  300. {
  301. X = 0,
  302. Y = 0,
  303. Width = Dim.Fill (),
  304. Height = Dim.Fill (),
  305. Source = new ListWrapper<string> (_log)
  306. };
  307. Add (_listLog);
  308. // We don't want WorkerApp to respond to the quitkey
  309. KeyBindings.Remove (Application.QuitKey);
  310. Closing += WorkerApp_Closing;
  311. Closed += WorkerApp_Closed;
  312. }
  313. private void WorkerApp_Closed (object sender, ToplevelEventArgs e)
  314. {
  315. CancelWorker ();
  316. }
  317. private void WorkerApp_Closing (object sender, ToplevelClosingEventArgs e)
  318. {
  319. Toplevel top = Application.OverlappedChildren.Find (x => x.Data.ToString () == "WorkerApp");
  320. if (Visible && top == this)
  321. {
  322. Visible = false;
  323. e.Cancel = true;
  324. Application.OverlappedMoveNext ();
  325. }
  326. }
  327. public void CancelWorker ()
  328. {
  329. if (_stagingWorkers == null || _stagingWorkers.Count == 0)
  330. {
  331. WriteLog ($"Worker is not running at {DateTime.Now}!");
  332. return;
  333. }
  334. foreach (KeyValuePair<Staging, BackgroundWorker> sw in _stagingWorkers)
  335. {
  336. Staging key = sw.Key;
  337. BackgroundWorker value = sw.Value;
  338. if (!key.Completed)
  339. {
  340. value.CancelAsync ();
  341. }
  342. WriteLog (
  343. $"Worker {key.StartStaging}.{key.StartStaging:fff} is canceling at {DateTime.Now}!"
  344. );
  345. _stagingWorkers.Remove (sw.Key);
  346. }
  347. }
  348. public void RunWorker ()
  349. {
  350. var stagingUI = new StagingUIController { Modal = true };
  351. Staging staging = null;
  352. var worker = new BackgroundWorker { WorkerSupportsCancellation = true };
  353. worker.DoWork += (s, e) =>
  354. {
  355. List<string> stageResult = new ();
  356. for (var i = 0; i < 500; i++)
  357. {
  358. stageResult.Add (
  359. $"Worker {i} started at {DateTime.Now}"
  360. );
  361. e.Result = stageResult;
  362. Thread.Sleep (1);
  363. if (worker.CancellationPending)
  364. {
  365. e.Cancel = true;
  366. return;
  367. }
  368. }
  369. };
  370. worker.RunWorkerCompleted += (s, e) =>
  371. {
  372. if (e.Error != null)
  373. {
  374. // Failed
  375. WriteLog (
  376. $"Exception occurred {e.Error.Message} on Worker {staging.StartStaging}.{staging.StartStaging:fff} at {DateTime.Now}"
  377. );
  378. }
  379. else if (e.Cancelled)
  380. {
  381. // Canceled
  382. WriteLog (
  383. $"Worker {staging.StartStaging}.{staging.StartStaging:fff} was canceled at {DateTime.Now}!"
  384. );
  385. }
  386. else
  387. {
  388. // Passed
  389. WriteLog (
  390. $"Worker {staging.StartStaging}.{staging.StartStaging:fff} was completed at {DateTime.Now}."
  391. );
  392. Application.Refresh ();
  393. var stagingUI = new StagingUIController (staging, e.Result as ObservableCollection<string>)
  394. {
  395. Modal = false,
  396. Title =
  397. $"Worker started at {staging.StartStaging}.{staging.StartStaging:fff}",
  398. Data = $"{staging.StartStaging}.{staging.StartStaging:fff}"
  399. };
  400. stagingUI.ReportClosed += StagingUI_ReportClosed;
  401. if (_stagingsUi == null)
  402. {
  403. _stagingsUi = new List<StagingUIController> ();
  404. }
  405. _stagingsUi.Add (stagingUI);
  406. _stagingWorkers.Remove (staging);
  407. #if DEBUG_IDISPOSABLE
  408. if (Application.OverlappedTop is null)
  409. {
  410. stagingUI.Dispose ();
  411. return;
  412. }
  413. #endif
  414. Application.Run (stagingUI);
  415. }
  416. };
  417. Application.Run (stagingUI);
  418. if (stagingUI.Staging != null && stagingUI.Staging.StartStaging != null)
  419. {
  420. staging = new Staging (stagingUI.Staging.StartStaging);
  421. stagingUI.Dispose ();
  422. WriteLog ($"Worker is started at {staging.StartStaging}.{staging.StartStaging:fff}");
  423. if (_stagingWorkers == null)
  424. {
  425. _stagingWorkers = new Dictionary<Staging, BackgroundWorker> ();
  426. }
  427. _stagingWorkers.Add (staging, worker);
  428. worker.RunWorkerAsync ();
  429. }
  430. else
  431. {
  432. stagingUI.Dispose ();
  433. }
  434. }
  435. public void WriteLog (string msg)
  436. {
  437. _log.Add (msg);
  438. _listLog.MoveDown ();
  439. }
  440. private void StagingUI_ReportClosed (StagingUIController obj)
  441. {
  442. WriteLog ($"Report {obj.Staging.StartStaging}.{obj.Staging.StartStaging:fff} closed.");
  443. _stagingsUi.Remove (obj);
  444. obj.Dispose ();
  445. }
  446. }
  447. }