UICatalog.cs 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644
  1. global using Attribute = Terminal.Gui.Drawing.Attribute;
  2. global using Color = Terminal.Gui.Drawing.Color;
  3. global using CM = Terminal.Gui.Configuration.ConfigurationManager;
  4. global using Terminal.Gui.App;
  5. global using Terminal.Gui.ViewBase;
  6. global using Terminal.Gui.Drivers;
  7. global using Terminal.Gui.Input;
  8. global using Terminal.Gui.Configuration;
  9. global using Terminal.Gui.Views;
  10. global using Terminal.Gui.Drawing;
  11. global using Terminal.Gui.Text;
  12. global using Terminal.Gui.FileServices;
  13. using System.CommandLine;
  14. using System.CommandLine.Builder;
  15. using System.CommandLine.Parsing;
  16. using System.Data;
  17. using System.Diagnostics;
  18. using System.Diagnostics.CodeAnalysis;
  19. using System.Globalization;
  20. using System.Reflection;
  21. using System.Text;
  22. using System.Text.Json;
  23. using Microsoft.Extensions.Logging;
  24. using Serilog;
  25. using Serilog.Core;
  26. using Serilog.Events;
  27. using Command = Terminal.Gui.Input.Command;
  28. using ILogger = Microsoft.Extensions.Logging.ILogger;
  29. #nullable enable
  30. namespace UICatalog;
  31. /// <summary>
  32. /// UI Catalog is a comprehensive sample library and test app for Terminal.Gui. It provides a simple UI for adding to
  33. /// the
  34. /// catalog of scenarios.
  35. /// </summary>
  36. /// <remarks>
  37. /// <para>UI Catalog attempts to satisfy the following goals:</para>
  38. /// <para>
  39. /// <list type="number">
  40. /// <item>
  41. /// <description>Be an easy-to-use showcase for Terminal.Gui concepts and features.</description>
  42. /// </item>
  43. /// <item>
  44. /// <description>Provide sample code that illustrates how to properly implement said concepts & features.</description>
  45. /// </item>
  46. /// <item>
  47. /// <description>Make it easy for contributors to add additional samples in a structured way.</description>
  48. /// </item>
  49. /// </list>
  50. /// </para>
  51. /// </remarks>
  52. public class UICatalog
  53. {
  54. private static string? _forceDriver = null;
  55. public static string LogFilePath { get; set; } = string.Empty;
  56. public static LoggingLevelSwitch LogLevelSwitch { get; } = new ();
  57. public const string LOGFILE_LOCATION = "logs";
  58. public static UICatalogCommandLineOptions Options { get; set; }
  59. private static int Main (string [] args)
  60. {
  61. Console.OutputEncoding = Encoding.Default;
  62. if (Debugger.IsAttached)
  63. {
  64. CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.GetCultureInfo ("en-US");
  65. }
  66. UICatalogTop.CachedScenarios = Scenario.GetScenarios ();
  67. UICatalogTop.CachedCategories = Scenario.GetAllCategories ();
  68. // Process command line args
  69. // If no driver is provided, the default driver is used.
  70. Option<string> driverOption = new Option<string> ("--driver", "The IConsoleDriver to use.").FromAmong (
  71. Application.GetDriverTypes ().Item2.ToArray ()!
  72. );
  73. driverOption.AddAlias ("-d");
  74. driverOption.AddAlias ("--d");
  75. // Configuration Management
  76. Option<bool> disableConfigManagement = new (
  77. "--disable-cm",
  78. "Indicates Configuration Management should not be enabled. Only `ConfigLocations.HardCoded` settings will be loaded.");
  79. disableConfigManagement.AddAlias ("-dcm");
  80. disableConfigManagement.AddAlias ("--dcm");
  81. Option<bool> benchmarkFlag = new ("--benchmark", "Enables benchmarking. If a Scenario is specified, just that Scenario will be benchmarked.");
  82. benchmarkFlag.AddAlias ("-b");
  83. benchmarkFlag.AddAlias ("--b");
  84. Option<uint> benchmarkTimeout = new (
  85. "--timeout",
  86. () => Scenario.BenchmarkTimeout,
  87. $"The maximum time in milliseconds to run a benchmark for. Default is {Scenario.BenchmarkTimeout}ms.");
  88. benchmarkTimeout.AddAlias ("-t");
  89. benchmarkTimeout.AddAlias ("--t");
  90. Option<string> resultsFile = new ("--file", "The file to save benchmark results to. If not specified, the results will be displayed in a TableView.");
  91. resultsFile.AddAlias ("-f");
  92. resultsFile.AddAlias ("--f");
  93. // what's the app name?
  94. LogFilePath = $"{LOGFILE_LOCATION}/{Assembly.GetExecutingAssembly ().GetName ().Name}";
  95. Option<string> debugLogLevel = new Option<string> ("--debug-log-level", $"The level to use for logging (debug console and {LogFilePath})").FromAmong (
  96. Enum.GetNames<LogLevel> ()
  97. );
  98. debugLogLevel.SetDefaultValue ("Warning");
  99. debugLogLevel.AddAlias ("-dl");
  100. debugLogLevel.AddAlias ("--dl");
  101. Argument<string> scenarioArgument = new Argument<string> (
  102. "scenario",
  103. description:
  104. "The name of the Scenario to run. If not provided, the UI Catalog UI will be shown.",
  105. getDefaultValue: () => "none"
  106. ).FromAmong (
  107. UICatalogTop.CachedScenarios.Select (s => s.GetName ())
  108. .Append ("none")
  109. .ToArray ()
  110. );
  111. var rootCommand = new RootCommand ("A comprehensive sample library and test app for Terminal.Gui")
  112. {
  113. scenarioArgument, debugLogLevel, benchmarkFlag, benchmarkTimeout, resultsFile, driverOption, disableConfigManagement
  114. };
  115. rootCommand.SetHandler (
  116. context =>
  117. {
  118. var options = new UICatalogCommandLineOptions
  119. {
  120. Scenario = context.ParseResult.GetValueForArgument (scenarioArgument),
  121. Driver = context.ParseResult.GetValueForOption (driverOption) ?? string.Empty,
  122. DontEnableConfigurationManagement = context.ParseResult.GetValueForOption (disableConfigManagement),
  123. Benchmark = context.ParseResult.GetValueForOption (benchmarkFlag),
  124. BenchmarkTimeout = context.ParseResult.GetValueForOption (benchmarkTimeout),
  125. ResultsFile = context.ParseResult.GetValueForOption (resultsFile) ?? string.Empty,
  126. DebugLogLevel = context.ParseResult.GetValueForOption (debugLogLevel) ?? "Warning"
  127. /* etc. */
  128. };
  129. // See https://github.com/dotnet/command-line-api/issues/796 for the rationale behind this hackery
  130. Options = options;
  131. }
  132. );
  133. var helpShown = false;
  134. Parser parser = new CommandLineBuilder (rootCommand)
  135. .UseHelp (ctx => helpShown = true)
  136. .Build ();
  137. parser.Invoke (args);
  138. if (helpShown)
  139. {
  140. return 0;
  141. }
  142. Scenario.BenchmarkTimeout = Options.BenchmarkTimeout;
  143. Logging.Logger = CreateLogger ();
  144. UICatalogMain (Options);
  145. return 0;
  146. }
  147. public static LogEventLevel LogLevelToLogEventLevel (LogLevel logLevel)
  148. {
  149. return logLevel switch
  150. {
  151. LogLevel.Trace => LogEventLevel.Verbose,
  152. LogLevel.Debug => LogEventLevel.Debug,
  153. LogLevel.Information => LogEventLevel.Information,
  154. LogLevel.Warning => LogEventLevel.Warning,
  155. LogLevel.Error => LogEventLevel.Error,
  156. LogLevel.Critical => LogEventLevel.Fatal,
  157. LogLevel.None => LogEventLevel.Fatal, // Default to Fatal if None is specified
  158. _ => LogEventLevel.Fatal // Default to Information for any unspecified LogLevel
  159. };
  160. }
  161. private static ILogger CreateLogger ()
  162. {
  163. // Configure Serilog to write logs to a file
  164. LogLevelSwitch.MinimumLevel = LogLevelToLogEventLevel (Enum.Parse<LogLevel> (Options.DebugLogLevel));
  165. Log.Logger = new LoggerConfiguration ()
  166. .MinimumLevel.ControlledBy (LogLevelSwitch)
  167. .Enrich.FromLogContext () // Enables dynamic enrichment
  168. .WriteTo.Debug ()
  169. .WriteTo.File (
  170. LogFilePath,
  171. rollingInterval: RollingInterval.Day,
  172. outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}")
  173. .CreateLogger ();
  174. // Create a logger factory compatible with Microsoft.Extensions.Logging
  175. using ILoggerFactory loggerFactory = LoggerFactory.Create (
  176. builder =>
  177. {
  178. builder
  179. .AddSerilog (dispose: true) // Integrate Serilog with ILogger
  180. .SetMinimumLevel (LogLevel.Trace); // Set minimum log level
  181. });
  182. // Get an ILogger instance
  183. return loggerFactory.CreateLogger ("Global Logger");
  184. }
  185. /// <summary>
  186. /// Shows the UI Catalog selection UI. When the user selects a Scenario to run, the UI Catalog main app UI is
  187. /// killed and the Scenario is run as though it were Application.Top. When the Scenario exits, this function exits.
  188. /// </summary>
  189. /// <returns></returns>
  190. private static Scenario RunUICatalogTopLevel ()
  191. {
  192. // Run UI Catalog UI. When it exits, if _selectedScenario is != null then
  193. // a Scenario was selected. Otherwise, the user wants to quit UI Catalog.
  194. // If the user specified a driver on the command line then use it,
  195. // ignoring Config files.
  196. Application.Init (driverName: _forceDriver);
  197. var top = Application.Run<UICatalogTop> ();
  198. top.Dispose ();
  199. Application.Shutdown ();
  200. VerifyObjectsWereDisposed ();
  201. return UICatalogTop.CachedSelectedScenario!;
  202. }
  203. [SuppressMessage ("Style", "IDE1006:Naming Styles", Justification = "<Pending>")]
  204. private static readonly FileSystemWatcher _currentDirWatcher = new ();
  205. [SuppressMessage ("Style", "IDE1006:Naming Styles", Justification = "<Pending>")]
  206. private static readonly FileSystemWatcher _homeDirWatcher = new ();
  207. private static void StartConfigFileWatcher ()
  208. {
  209. // Set up a file system watcher for `./.tui/`
  210. _currentDirWatcher.NotifyFilter = NotifyFilters.LastWrite;
  211. string assemblyLocation = Assembly.GetExecutingAssembly ().Location;
  212. string tuiDir;
  213. if (!string.IsNullOrEmpty (assemblyLocation))
  214. {
  215. var assemblyFile = new FileInfo (assemblyLocation);
  216. tuiDir = Path.Combine (assemblyFile.Directory!.FullName, ".tui");
  217. }
  218. else
  219. {
  220. tuiDir = Path.Combine (AppContext.BaseDirectory, ".tui");
  221. }
  222. if (!Directory.Exists (tuiDir))
  223. {
  224. Directory.CreateDirectory (tuiDir);
  225. }
  226. _currentDirWatcher.Path = tuiDir;
  227. _currentDirWatcher.Filter = "*config.json";
  228. // Set up a file system watcher for `~/.tui/`
  229. _homeDirWatcher.NotifyFilter = NotifyFilters.LastWrite;
  230. var f = new FileInfo (Environment.GetFolderPath (Environment.SpecialFolder.UserProfile));
  231. tuiDir = Path.Combine (f.FullName, ".tui");
  232. if (!Directory.Exists (tuiDir))
  233. {
  234. Directory.CreateDirectory (tuiDir);
  235. }
  236. _homeDirWatcher.Path = tuiDir;
  237. _homeDirWatcher.Filter = "*config.json";
  238. _currentDirWatcher.Changed += ConfigFileChanged;
  239. //_currentDirWatcher.Created += ConfigFileChanged;
  240. _currentDirWatcher.EnableRaisingEvents = true;
  241. _homeDirWatcher.Changed += ConfigFileChanged;
  242. //_homeDirWatcher.Created += ConfigFileChanged;
  243. _homeDirWatcher.EnableRaisingEvents = true;
  244. }
  245. private static void StopConfigFileWatcher ()
  246. {
  247. _currentDirWatcher.EnableRaisingEvents = false;
  248. _currentDirWatcher.Changed -= ConfigFileChanged;
  249. _currentDirWatcher.Created -= ConfigFileChanged;
  250. _homeDirWatcher.EnableRaisingEvents = false;
  251. _homeDirWatcher.Changed -= ConfigFileChanged;
  252. _homeDirWatcher.Created -= ConfigFileChanged;
  253. }
  254. private static void ConfigFileChanged (object sender, FileSystemEventArgs e)
  255. {
  256. if (Application.Top == null)
  257. {
  258. return;
  259. }
  260. Logging.Debug ($"{e.FullPath} {e.ChangeType} - Loading and Applying");
  261. ConfigurationManager.Load (ConfigLocations.All);
  262. ConfigurationManager.Apply ();
  263. }
  264. private static void UICatalogMain (UICatalogCommandLineOptions options)
  265. {
  266. // By setting _forceDriver we ensure that if the user has specified a driver on the command line, it will be used
  267. // regardless of what's in a config file.
  268. Application.ForceDriver = _forceDriver = options.Driver;
  269. // If a Scenario name has been provided on the commandline
  270. // run it and exit when done.
  271. if (options.Scenario != "none")
  272. {
  273. if (!Options.DontEnableConfigurationManagement)
  274. {
  275. ConfigurationManager.Enable (ConfigLocations.All);
  276. }
  277. int item = UICatalogTop.CachedScenarios!.IndexOf (
  278. UICatalogTop.CachedScenarios!.FirstOrDefault (
  279. s =>
  280. s.GetName ()
  281. .Equals (options.Scenario, StringComparison.OrdinalIgnoreCase)
  282. )!);
  283. UICatalogTop.CachedSelectedScenario = (Scenario)Activator.CreateInstance (UICatalogTop.CachedScenarios [item].GetType ())!;
  284. BenchmarkResults? results = RunScenario (UICatalogTop.CachedSelectedScenario, options.Benchmark);
  285. if (results is { })
  286. {
  287. Console.WriteLine (
  288. JsonSerializer.Serialize (
  289. results,
  290. new JsonSerializerOptions
  291. {
  292. WriteIndented = true
  293. }));
  294. }
  295. VerifyObjectsWereDisposed ();
  296. return;
  297. }
  298. // Benchmark all Scenarios
  299. if (options.Benchmark)
  300. {
  301. BenchmarkAllScenarios ();
  302. return;
  303. }
  304. #if DEBUG_IDISPOSABLE
  305. View.EnableDebugIDisposableAsserts = true;
  306. #endif
  307. if (!Options.DontEnableConfigurationManagement)
  308. {
  309. ConfigurationManager.Enable (ConfigLocations.All);
  310. StartConfigFileWatcher ();
  311. }
  312. while (RunUICatalogTopLevel () is { } scenario)
  313. {
  314. #if DEBUG_IDISPOSABLE
  315. VerifyObjectsWereDisposed ();
  316. // Measure how long it takes for the app to shut down
  317. var sw = new Stopwatch ();
  318. string scenarioName = scenario.GetName ();
  319. Application.InitializedChanged += ApplicationOnInitializedChanged;
  320. #endif
  321. scenario.Main ();
  322. scenario.Dispose ();
  323. // This call to Application.Shutdown brackets the Application.Init call
  324. // made by Scenario.Init() above
  325. // TODO: Throw if shutdown was not called already
  326. Application.Shutdown ();
  327. VerifyObjectsWereDisposed ();
  328. #if DEBUG_IDISPOSABLE
  329. Application.InitializedChanged -= ApplicationOnInitializedChanged;
  330. void ApplicationOnInitializedChanged (object? sender, EventArgs<bool> e)
  331. {
  332. if (e.Value)
  333. {
  334. sw.Start ();
  335. }
  336. else
  337. {
  338. sw.Stop ();
  339. Logging.Trace ($"Shutdown of {scenarioName} Scenario took {sw.ElapsedMilliseconds}ms");
  340. }
  341. }
  342. #endif
  343. }
  344. StopConfigFileWatcher ();
  345. VerifyObjectsWereDisposed ();
  346. }
  347. private static BenchmarkResults? RunScenario (Scenario scenario, bool benchmark)
  348. {
  349. if (benchmark)
  350. {
  351. scenario.StartBenchmark ();
  352. }
  353. Application.Init (driverName: _forceDriver);
  354. if (benchmark)
  355. {
  356. Application.Screen = new (0, 0, 120, 40);
  357. }
  358. scenario.Main ();
  359. BenchmarkResults? results = null;
  360. if (benchmark)
  361. {
  362. results = scenario.EndBenchmark ();
  363. }
  364. scenario.Dispose ();
  365. // TODO: Throw if shutdown was not called already
  366. Application.Shutdown ();
  367. return results;
  368. }
  369. private static void BenchmarkAllScenarios ()
  370. {
  371. List<BenchmarkResults> resultsList = [];
  372. var maxScenarios = 5;
  373. foreach (Scenario s in UICatalogTop.CachedScenarios!)
  374. {
  375. resultsList.Add (RunScenario (s, true)!);
  376. maxScenarios--;
  377. if (maxScenarios == 0)
  378. {
  379. // break;
  380. }
  381. }
  382. if (resultsList.Count <= 0)
  383. {
  384. return;
  385. }
  386. if (!string.IsNullOrEmpty (Options.ResultsFile))
  387. {
  388. string output = JsonSerializer.Serialize (
  389. resultsList,
  390. new JsonSerializerOptions
  391. {
  392. WriteIndented = true
  393. });
  394. using StreamWriter file = File.CreateText (Options.ResultsFile);
  395. file.Write (output);
  396. file.Close ();
  397. return;
  398. }
  399. Application.Init ();
  400. var benchmarkWindow = new Window
  401. {
  402. Title = "Benchmark Results"
  403. };
  404. if (benchmarkWindow.Border is { })
  405. {
  406. benchmarkWindow.Border.Thickness = new (0, 0, 0, 0);
  407. }
  408. TableView resultsTableView = new ()
  409. {
  410. Width = Dim.Fill (),
  411. Height = Dim.Fill ()
  412. };
  413. // TableView provides many options for table headers. For simplicity we turn all
  414. // of these off. By enabling FullRowSelect and turning off headers, TableView looks just
  415. // like a ListView
  416. resultsTableView.FullRowSelect = true;
  417. resultsTableView.Style.ShowHeaders = true;
  418. resultsTableView.Style.ShowHorizontalHeaderOverline = false;
  419. resultsTableView.Style.ShowHorizontalHeaderUnderline = true;
  420. resultsTableView.Style.ShowHorizontalBottomline = false;
  421. resultsTableView.Style.ShowVerticalCellLines = true;
  422. resultsTableView.Style.ShowVerticalHeaderLines = true;
  423. /* By default, TableView lays out columns at render time and only
  424. * measures y rows of data at a time. Where y is the height of the
  425. * console. This is for the following reasons:
  426. *
  427. * - Performance, when tables have a large amount of data
  428. * - Defensive, prevents a single wide cell value pushing other
  429. * columns off-screen (requiring horizontal scrolling
  430. *
  431. * In the case of UICatalog here, such an approach is overkill so
  432. * we just measure all the data ourselves and set the appropriate
  433. * max widths as ColumnStyles
  434. */
  435. //int longestName = _scenarios!.Max (s => s.GetName ().Length);
  436. //resultsTableView.Style.ColumnStyles.Add (
  437. // 0,
  438. // new () { MaxWidth = longestName, MinWidth = longestName, MinAcceptableWidth = longestName }
  439. // );
  440. //resultsTableView.Style.ColumnStyles.Add (1, new () { MaxWidth = 1 });
  441. //resultsTableView.CellActivated += ScenarioView_OpenSelectedItem;
  442. // TableView typically is a grid where nav keys are biased for moving left/right.
  443. resultsTableView.KeyBindings.Remove (Key.Home);
  444. resultsTableView.KeyBindings.Add (Key.Home, Command.Start);
  445. resultsTableView.KeyBindings.Remove (Key.End);
  446. resultsTableView.KeyBindings.Add (Key.End, Command.End);
  447. // Ideally, TableView.MultiSelect = false would turn off any keybindings for
  448. // multi-select options. But it currently does not. UI Catalog uses Ctrl-A for
  449. // a shortcut to About.
  450. resultsTableView.MultiSelect = false;
  451. var dt = new DataTable ();
  452. dt.Columns.Add (new DataColumn ("Scenario", typeof (string)));
  453. dt.Columns.Add (new DataColumn ("Duration", typeof (TimeSpan)));
  454. dt.Columns.Add (new DataColumn ("Refreshed", typeof (int)));
  455. dt.Columns.Add (new DataColumn ("LaidOut", typeof (int)));
  456. dt.Columns.Add (new DataColumn ("ClearedContent", typeof (int)));
  457. dt.Columns.Add (new DataColumn ("DrawComplete", typeof (int)));
  458. dt.Columns.Add (new DataColumn ("Updated", typeof (int)));
  459. dt.Columns.Add (new DataColumn ("Iterations", typeof (int)));
  460. foreach (BenchmarkResults r in resultsList)
  461. {
  462. dt.Rows.Add (
  463. r.Scenario,
  464. r.Duration,
  465. r.RefreshedCount,
  466. r.LaidOutCount,
  467. r.ClearedContentCount,
  468. r.DrawCompleteCount,
  469. r.UpdatedCount,
  470. r.IterationCount
  471. );
  472. }
  473. BenchmarkResults totalRow = new ()
  474. {
  475. Scenario = "TOTAL",
  476. Duration = new (resultsList.Sum (r => r.Duration.Ticks)),
  477. RefreshedCount = resultsList.Sum (r => r.RefreshedCount),
  478. LaidOutCount = resultsList.Sum (r => r.LaidOutCount),
  479. ClearedContentCount = resultsList.Sum (r => r.ClearedContentCount),
  480. DrawCompleteCount = resultsList.Sum (r => r.DrawCompleteCount),
  481. UpdatedCount = resultsList.Sum (r => r.UpdatedCount),
  482. IterationCount = resultsList.Sum (r => r.IterationCount)
  483. };
  484. dt.Rows.Add (
  485. totalRow.Scenario,
  486. totalRow.Duration,
  487. totalRow.RefreshedCount,
  488. totalRow.LaidOutCount,
  489. totalRow.ClearedContentCount,
  490. totalRow.DrawCompleteCount,
  491. totalRow.UpdatedCount,
  492. totalRow.IterationCount
  493. );
  494. dt.DefaultView.Sort = "Duration";
  495. DataTable sortedCopy = dt.DefaultView.ToTable ();
  496. resultsTableView.Table = new DataTableSource (sortedCopy);
  497. benchmarkWindow.Add (resultsTableView);
  498. Application.Run (benchmarkWindow);
  499. benchmarkWindow.Dispose ();
  500. Application.Shutdown ();
  501. }
  502. private static void VerifyObjectsWereDisposed ()
  503. {
  504. #if DEBUG_IDISPOSABLE
  505. if (!View.EnableDebugIDisposableAsserts)
  506. {
  507. View.Instances.Clear ();
  508. RunState.Instances.Clear ();
  509. return;
  510. }
  511. // Validate there are no outstanding View instances
  512. // after a scenario was selected to run. This proves the main UI Catalog
  513. // 'app' closed cleanly.
  514. foreach (View? inst in View.Instances)
  515. {
  516. Debug.Assert (inst.WasDisposed);
  517. }
  518. View.Instances.Clear ();
  519. // Validate there are no outstanding Application.RunState-based instances
  520. // after a scenario was selected to run. This proves the main UI Catalog
  521. // 'app' closed cleanly.
  522. foreach (RunState? inst in RunState.Instances)
  523. {
  524. Debug.Assert (inst.WasDisposed);
  525. }
  526. RunState.Instances.Clear ();
  527. #endif
  528. }
  529. }