UICatalog.cs 27 KB

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