GPAL - Generally Positive Automation Library v1.0
GPAL The Fluent Automation LIbrary
Loading...
Searching...
No Matches
Application.cs
1// =============================================================================
2// GPAL - Generally Positive Automation Library
3// Copyright © 2026 Software Decisions, Inc. All rights reserved.
4//
5// This file is part of GPAL.
6// Licensed under the Business Source License 1.1
7//
8// Primary development, architecture, and vision by Michael B. Vederman,
9// CEO of Software Decisions, Inc., Texas.
10//
11// Internal development maintained privately.
12// Public releases appear on GitHub: https://github.com/SoftwareDecisionsInc/GPAL.
13//
14// See LICENSE for full terms, including Additional Use Grant.
15// =============================================================================
16
17//extern alias mUIA;
18using System;
19using System.Collections.Generic;
20using System.Diagnostics;
21using System.IO;
22using System.Linq;
23using System.Text;
24using System.Threading;
25using System.Threading.Tasks;
26using System.Windows.Automation;
29using static GenerallyPositive.Enums;
30// an Application has a HiddenDesktop property now, which hides the static class of the same name, so the
31// class is reached through an alias. The browser side does this for the same reason
33
35{
51 public class Application
53 {
88 public delegate CallIfStatus CallIfDelegate(Application application, List<IGPALAutomationElement> foundElements, List<IGPALAutomationElement> matchedElements, Selector selector, bool matchedAll);
114 public delegate CallIfStatus CallAfterFillInDelegate(Application application, IGPALGrid<string> tokens, int tokenIdx);
115
116 #region <Working Variables>
120 internal ApplicationSettings applicationSettings { get; set; }
121 private dynamic persistenceList;
125 internal UnitOfWork CurrentUOW { get; set; } = null;
126 private UnitOfWork persistentUOW = null;
127 #endregion <Working Variables>
143 public Application ToGPALObject()
144 {
145 return this;
146 }
147
163 internal Application()
164 {
165 CurrentUOW = new UnitOfWork();
166 persistentUOW = new UnitOfWork();
167 persistenceList = new List<UnitOfWork>
168 {
169 persistentUOW
170 };
171 applicationSettings = new ApplicationSettings(this);
172 }
173 #region <Private>
179 private bool CheckWaitFor()
180 {
181 bool matchFound = true;
182 List<GPALAutomationElement> foundElements = null;
183
184 // an earlier action on this same unit of work already waited these selectors in successfully, so
185 // waiting again is redundant - nothing can have moved under us, since any selector added after an
186 // action starts a fresh unit of work. the action still resolves its own elements either way.
187 if (null != CurrentUOW.WaitForSatisfied)
188 {
189 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $"Selectors for this unit of work already found at [{CurrentUOW.WaitForSatisfied:HH:mm:ss.fff}], skipping redundant wait.", this, GPALObjectType.Application);
190 return true;
191 }
192
193 if (WaitTime.Never < CurrentUOW.WaitForInMs)
194 {
195 // we are waiting on selector(s)
196 foundElements = ElementHelper.WaitFor(this, CurrentUOW.WaitForInMs, out matchFound);
197
198 if (0 < foundElements?.Count)
199 CurrentUOW.WaitForSatisfied = DateTime.Now;
200 }
201
202 return matchFound;
203 }
210 private bool InAction(bool checkWaitFor)
211 {
212 ApplicationHelper.TopProcess(this.Process); // ensure the browser window is on top, can use selenium or hardware (mouse click) topping. firefox will only top with hardware
213
214 CurrentUOW.ActionCalled = true;
215
216 SelectorListMaintenanceSweep(CurrentUOW); // purge any nodes marked 'deleteme' before iterating list
217 SelectorListMaintenanceSweep(persistentUOW); // purge any nodes marked 'deleteme' before iterating list
218 CheckPersistentSelectors(); // check if there are persistent selectors to deal with
219
220 // TODO: CAVEAT: WaitFor will call the CallIf handlers, which might result in unexpected behavior?
221 if (true == checkWaitFor)
222 CheckWaitFor();
223
224 // always attempt the gated action, same as the browser side. WaitFor only holds the workflow until
225 // something turns up, it is not a match test - and the action does its own element lookup anyway,
226 // so a genuinely missing element surfaces there where it gets logged and can be debugged, rather
227 // than the action being silently skipped here.
228 return true;
229 }
234 private static void SelectorListMaintenanceSweep(dynamic CurrentUOW)
235 {
236 for (int idx = 0; idx < CurrentUOW.WithSelectorList.Count; idx++)
237 if (true == CurrentUOW.WithSelectorList[idx].DeleteMe)
238 CurrentUOW.WithSelectorList.RemoveAt(idx);
239
240 for (int idx = 0; idx < CurrentUOW.InSelectorList.Count; idx++)
241 if (true == CurrentUOW.InSelectorList[idx].DeleteMe)
242 CurrentUOW.InSelectorList.RemoveAt(idx);
243 }
248 private void CheckPersistentSelectors()
249 {
250 // all we have to do is walk the list, try to find the element and the callif callbacks will do the work
251 if (0 < (persistentUOW.WithSelectorList?.Count ?? 0))
252 foreach (Selector selector in persistentUOW.WithSelectorList)
253 ElementHelper.FindElements(this, persistentUOW, selector, out bool matchedAll, out List<GPALAutomationElement> elems);
254 }
255 #endregion <Private>
256 #region <Application Actions>
261 {
262 get
263 {
264 if (null != Process)
265 ApplicationHelper.ShowWindow(Process.MainWindowHandle, 3); // 1= SW_NORMAL - show normal, 3 = max, 2 = min
266 return this;
267 }
268 }
269
273 {
274 get
275 {
276 ApplicationHelper.ShowWindow(Process.MainWindowHandle, 2); // 1= SW_NORMAL - show normal, 3 = max, 2 = min
277 return this;
278 }
279 }
280
284 {
285 get
286 {
287 ApplicationHelper.ShowWindow(Process.MainWindowHandle, 1); // 1= SW_NORMAL - show normal, 3 = max, 2 = min
288 return this;
289 }
290 }
291
295 {
296 if (true == InAction(true))
297 ApplicationHelper.ResizeWindow(ApplicationSettings.LastWindowHandle, width, height);
298 return this;
299 }
300
304 {
305 if (true == InAction(true))
306 ApplicationHelper.MoveWindow(ApplicationSettings.LastWindowHandle, x, y);
307 return this;
308 }
309
318 {
319 DatabaseHelper.TokenizeDatabase(CurrentUOW, inputDatabase);
320 if (true == InAction(true))
321 ApplicationHelper.FillInWithTokens(this, inputDatabase.Tokens, WriteMode.Append);
322 return this;
323 }
324
333 {
334 ((IGPALFileInternal)inputFile).TokenList = FileHelper.TokenizeFile(CurrentUOW, inputFile);
335
336 if (true == InAction(true))
337 ApplicationHelper.FillInWithTokens(this, ((IGPALFileInternal)inputFile).TokenList, WriteMode.Append);
338
339 return this;
340 }
341
349 public IAllowSelectorActionOrAnySelector AppendFrom(IGPALGrid<string> inputGrid)
350 {
351 if (true == InAction(true))
352 ApplicationHelper.FillInWithTokens(this, inputGrid, WriteMode.Append);
353
354 return this;
355 }
356
363 {
364 if (true == InAction(true))
365 ElementHelper.FillInFrom(this, textToUse, WriteMode.Append);
366 return this;
367 }
368
375 public IAllowAllExceptParameters Open([Directory()] string applicationPath)
376 {
377 IntPtr processHandle = IntPtr.Zero;
378 Process[] processes;
379 StringBuilder parameters = new StringBuilder();
380 bool first = true;
381
382 foreach (string parameter in applicationSettings.Parameters)
383 {
384 if (false == first)
385 parameters.Append(" ");
386 else
387 first = false;
388 parameters.Append(parameter);
389 }
390
391 if (null != Process)
392 {
393 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "Unable to Open/control more than one program. Please use another Application.", this, GPALObjectType.Application);
394 return this;
395 }
396
397 ApplicationPath = applicationPath;
398
399 // a hidden desktop has none of this program's windows on it, so attaching to a copy already
400 // running would attach to one on the desktop the user is working on, which is the one thing the
401 // whole feature is for avoiding
402 if (true == applicationSettings.HiddenDesktop)
403 {
404 applicationSettings.NewInstance = true;
405
406 if (false == Desktops.Ensure(applicationSettings.HiddenDesktopName))
407 {
408 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Windows would not make the desktop [{applicationSettings.HiddenDesktopName}], so [{applicationPath}] runs on the one you are looking at.", this, GPALObjectType.Application);
409
410 applicationSettings.HiddenDesktop = false;
411 }
412 else if (false == applicationSettings.HiddenDesktopReported)
413 {
414 applicationSettings.HiddenDesktopReported = true;
415
416 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"[{applicationPath}] runs on [{applicationSettings.HiddenDesktopName}], where UIAutomation drives it and hardware input cannot reach it. Peek at the desktop to watch it or to use hardware there.", this, GPALObjectType.Application);
417 }
418 }
419
420 // CAVEAT: see if process is open and attach to it? not sure this is what we want
421 // maybe we should just launch a new instance?
422 try
423 {
424 processes = Process.GetProcessesByName(Path.GetFileNameWithoutExtension(applicationPath));
425 bool attached = false;
426
427 // WithNewInstance says launch regardless, for a program that is always running and whose
428 // existing window is not the one the workflow asked for
429 if (false == applicationSettings.NewInstance && null != processes && 0 < processes.Length)
430 {
431 // An already-running instance should have a window quickly; probe each briefly and
432 // skip windowless/background matches rather than blocking for the full timeout.
433 foreach (Process process in processes)
434 {
435 applicationSettings.Process = process;
436 AutomationElement existingRoot = WaitForRootAutomationElement(process, 2000);
437 if (null != existingRoot)
438 {
439 applicationSettings.RootAutomationElement = existingRoot;
440 processHandle = process.MainWindowHandle;
441 attached = true;
442 break;
443 }
444 }
445 }
446
447 if (false == attached)
448 {
449 applicationSettings.Process = true == applicationSettings.HiddenDesktop
450 ? StartOnHiddenDesktop(applicationPath, parameters.ToString())
451 : Process.Start(applicationPath, parameters.ToString());
452
453 // Do not guess with a fixed sleep: poll until the main window exists and its root
454 // automation element is actually retrievable, up to WaitForWindowTimeout seconds.
455 AutomationElement root = WaitForRootAutomationElement(applicationSettings.Process,
456 Math.Max(1, applicationSettings.WaitForWindowTimeout) * 1000);
457
458 if (null != root)
459 {
460 applicationSettings.RootAutomationElement = root;
461 processHandle = applicationSettings.Process.MainWindowHandle;
462 }
463 // a launcher hands its work to something already running and exits, so it never has a window
464 // of its own to take. explorer.exe passes the folder to the shell and quits, and the window
465 // that opens belongs to the shell. Nothing is wrong, but the window has to be found by name
466 else if (true == applicationSettings.Process?.HasExited)
467 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"[{applicationPath}] exited as soon as it started, which is what a launcher does. Use WaitForWindow or WaitForWindowRegex to settle on the window it opened.", this, GPALObjectType.Application);
468 else
469 {
470 string msg = $"Timed out after [{applicationSettings.WaitForWindowTimeout}]s waiting for the main window / root automation element of [{applicationPath}]{(true == applicationSettings.HiddenDesktop ? $" on [{applicationSettings.HiddenDesktopName}]" : string.Empty)}.";
471 GPAL.PublishSimpleEvent(GPALEventType.ERROR, msg, this, GPALObjectType.Application);
472 throw new GPALException(msg);
473 }
474 }
475 }
476 catch (Exception ex)
477 {
478 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Unable to launch [{applicationPath}].", this, GPALObjectType.Application, ex);
479 return this;
480 }
481
482 if (null == applicationSettings.Process)
483 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"Unable to launch [{applicationPath}].", this, GPALObjectType.Application);
484 else
485 {
486 // a launcher that has already exited has no window to bring forward, and asking about one is an
487 // exception rather than an answer. A program on a hidden desktop is not brought forward at all,
488 // since bringing a window forward is the taking of focus the desktop exists to avoid
489 if (false == applicationSettings.Process.HasExited && false == applicationSettings.HiddenDesktop)
490 ApplicationHelper.TopProcess(applicationSettings.Process);
491
492 if (true == applicationSettings.HiddenDesktop && false == applicationSettings.HiddenDesktopChecked)
493 {
494 applicationSettings.HiddenDesktopChecked = true;
495
496 if (false == Desktops.HasWindows(applicationSettings.HiddenDesktopName, 10_000))
497 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Nothing arrived on [{applicationSettings.HiddenDesktopName}], so this program is on the desktop you are working on.", this, GPALObjectType.Application);
498 }
499
500 CurrentUOW.ActionCalled = true;
501 }
502
503 return this;
504 }
505
513 private AutomationElement WaitForRootAutomationElement(Process process, int timeoutMs)
514 {
515 if (null == process)
516 return null;
517
518 Stopwatch stopwatch = Stopwatch.StartNew();
519
520 // Let a GUI app pump its message loop and create its window. This throws immediately for a
521 // process with no message loop (e.g. a console app), so it never wastes the timeout on those.
522 try { process.WaitForInputIdle(timeoutMs); } catch { /* no message loop; fall through to polling */ }
523
524 while (stopwatch.ElapsedMilliseconds < timeoutMs)
525 {
526 try
527 {
528 process.Refresh();
529
530 // a program on a hidden desktop has no window as far as this desktop is concerned, because
531 // MainWindowHandle enumerates the desktop asking rather than the one the window is on
532 IntPtr handle = true == applicationSettings.HiddenDesktop
533 ? Desktops.MainWindowOf(applicationSettings.HiddenDesktopName, process.Id)
534 : process.MainWindowHandle;
535 if (IntPtr.Zero != handle)
536 {
537 AutomationElement root = AutomationElement.FromHandle(handle);
538 if (null != root)
539 return root;
540 }
541 }
542 catch
543 {
544 // Window/handle not ready yet (or transiently invalid); keep polling until timeout.
545 }
546
547 Thread.Sleep(100);
548 }
549
550 return null;
551 }
556 public void Close()
557 {
558 // Process.Close releases our handle to the program. It does not close the program, which is what
559 // this is called for and what it never did. CloseMainWindow asks the window to close the way a
560 // person clicking the X asks, so the application runs whatever it does on the way out. A program
561 // that will not go, because it is asking whether to save or has nothing to ask, is ended instead
562 if (null != Process && false == HasGone(Process))
563 {
564 // CloseMainWindow looks for the window on this desktop and finds nothing when the program is on
565 // a hidden one, so the window there is asked directly and the program still closes itself
566 if (true == ApplicationSettings.HiddenDesktop)
567 Desktops.CloseWindowsOf(ApplicationSettings.HiddenDesktopName, Process.Id);
568 else
569 Process.CloseMainWindow();
570
571 if (false == Process.WaitForExit(CLOSING_MS))
572 {
573 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"[{ApplicationPath}] did not close within [{CLOSING_MS / 1000}]s of being asked, so it is being ended.", this, GPALObjectType.Application);
574
575 Process.Kill();
576 }
577 }
578
579 Process?.Close();
580 Process = null;
581 }
582
583 // how long a program is given to close itself before it is ended
584 private const int CLOSING_MS = 5000;
585
607 private Process StartOnHiddenDesktop(string applicationPath, string parameters)
608 {
610 {
611 cb = (uint)System.Runtime.InteropServices.Marshal.SizeOf<Native.STARTUPINFO>(),
612 lpDesktop = Desktops.StartupNameFor(applicationSettings.HiddenDesktopName)
613 };
614
615 string quoted = "\"" + applicationPath + "\"";
616 string commandLine = true == string.IsNullOrWhiteSpace(parameters) ? quoted : quoted + " " + parameters;
617
618 if (false == Native.CreateProcess(null, commandLine, IntPtr.Zero, IntPtr.Zero, false, 0, IntPtr.Zero,
619 Path.GetDirectoryName(applicationPath), ref startup, out Native.PROCESS_INFORMATION started))
620 throw new System.ComponentModel.Win32Exception(System.Runtime.InteropServices.Marshal.GetLastWin32Error(), $"Could not start [{applicationPath}] on [{applicationSettings.HiddenDesktopName}]");
621
622 Process retVal = Process.GetProcessById((int)started.dwProcessId);
623
624 Native.CloseHandle(started.hProcess);
625 Native.CloseHandle(started.hThread);
626
627 return retVal;
628 }
629
630 private static bool HasGone(Process process)
631 {
632 bool retVal = true;
633
634 try
635 {
636 retVal = process.HasExited;
637 }
638 catch (InvalidOperationException)
639 {
640 // no program was ever attached to this, so there is nothing to close
641 }
642
643 return retVal;
644 }
654 {
655 DatabaseHelper.TokenizeDatabase(CurrentUOW, inputDatabase);
656
657 if (true == InAction(true))
658 ApplicationHelper.FillInWithTokens(this, CurrentUOW.InputDatabase.Tokens, WriteMode.Overwrite);
659
660 return this;
661 }
662
671 {
672 ((IGPALFileInternal)inputFile).TokenList = FileHelper.TokenizeFile(CurrentUOW, inputFile);
673 if (true == InAction(true))
674 ApplicationHelper.FillInWithTokens(this, ((IGPALFileInternal)CurrentUOW.InputFile).TokenList, WriteMode.Overwrite);
675 return this;
676 }
677
685 public IAllowSelectorActionOrAnySelector FillInFrom(IGPALGrid<string> inputGrid)
686 {
687 if (true == InAction(true))
688 ApplicationHelper.FillInWithTokens(this, (IGPALGrid<string>)inputGrid, WriteMode.Overwrite);
689 return this;
690 }
691
698 {
699 if (true == InAction(true))
700 ElementHelper.FillInFrom(this, textToUse, WriteMode.Overwrite);
701 return this;
702 }
703
710 {
711 if (null != selector) WithSelector(selector);
712 if (true == InAction(true))
713 ElementHelper.Focus(this);
714 return this;
715 }
716
724 {
725 if (null != selector) WithSelector(selector);
726 if (true == InAction(true))
727 ElementHelper.Hover(this, CurrentUOW);
728 return this;
729 }
730
738 {
739 if (true == InAction(true))
740 ElementHelper.FillInFrom(this, textToUse, WriteMode.Insert);
741 return this;
742 }
743
752 {
753 DatabaseHelper.TokenizeDatabase(CurrentUOW, inputDatabase);
754 if (true == InAction(true))
755 ApplicationHelper.FillInWithTokens(this, CurrentUOW.InputDatabase.Tokens, WriteMode.Insert);
756 return this;
757 }
758
766 public IAllowSelectorActionOrAnySelector InsertFrom(IGPALGrid<string> inputGrid)
767 {
768 if (true == InAction(true))
769 ApplicationHelper.FillInWithTokens(this, (IGPALGrid<string>)inputGrid, WriteMode.Insert);
770 return this;
771 }
772
781 {
782 ((IGPALFileInternal)inputFile).TokenList = FileHelper.TokenizeFile(CurrentUOW, inputFile);
783 if (true == InAction(true))
784 ApplicationHelper.FillInWithTokens(this, ((IGPALFileInternal)CurrentUOW.InputFile).TokenList, WriteMode.Insert);
785 return this;
786 }
787
794 {
795 if (null != selector) WithSelector(selector);
796 if (true == InAction(true))
797 ElementHelper.LeftClick(this);
798 return this;
799 }
800
805 public IAllowSelectorActionOrAnySelector LeftClick(Enums.ModifierKeys modifierKeys)
806 {
807 if (true == InAction(true))
808 ElementHelper.LeftClick(this, modifierKeys);
809 return this;
810 }
811
817 {
818 if (null != selector) WithSelector(selector);
819 if (true == InAction(true))
820 ElementHelper.GenericClick(this, ClickType.LeftDoubleClick);
821 return this;
822 }
823
829 {
830 if (null != selector) WithSelector(selector);
831 if (true == InAction(true))
832 ElementHelper.GenericClick(this, ClickType.MiddleClick);
833 return this;
834 }
835
841 {
842 if (null != selector) WithSelector(selector);
843 if (true == InAction(true))
844 ElementHelper.MoveTo(this);
845 return this;
846 }
847
868 {
869 return ClickAndAnswer(file);
870 }
871
893 {
894 ClearTheWay(file);
895
896 return ClickAndAnswer(file);
897 }
898
903 private void ClearTheWay(GPALFile file)
904 {
905 foreach (string name in file?.Filenames ?? new List<string>())
906 if (true == System.IO.File.Exists(name))
907 {
908 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Deleting [{name}] before saving over it, so the dialog has nothing to ask about", this, GPALObjectType.Application);
909
910 System.IO.File.Delete(name);
911 }
912 }
913
919 private IAllowSelectorActionOrAnySelector ClickAndAnswer(GPALFile file)
920 {
921 if (true == InAction(true))
922 {
923 ElementHelper.LeftClick(this, ModifierKeys.NONE);
924
925 FileDialogHelper.Answer(this, file, ApplicationSettings.WaitForWindowTimeout);
926 }
927
928 return this;
929 }
930
941 {
942 if (true == InAction(true))
943 FileDialogHelper.Answer(this, file, ApplicationSettings.WaitForWindowTimeout);
944
945 return this;
946 }
947
954 {
955 if (null != selector) WithSelector(selector);
956 if (true == InAction(true))
957 ElementHelper.DragAndDrop(this);
958 return this;
959 }
960
966 public IAllowSelectorActionOrAnySelector DragAndDrop(ModifierKeys modifierKeys)
967 {
968 if (true == InAction(true))
969 ElementHelper.DragAndDrop(this, modifierKeys);
970 return this;
971 }
972
978 public IAllowSelectorActionOrAnySelector PressModifierKey(ModifierKeys modifierKeys)
979 {
980 HardwareHelper.PressModifierKey(modifierKeys);
981 return this;
982 }
983
990 {
991 HardwareHelper.ReleaseModifierKeys(modifierKeys);
992 return this;
993 }
994
1015 {
1016 if (true == InAction(true))
1017 {
1018 HardwareHelper.SendKey(VKCode);
1019
1020 // the keystroke is sent, not taken. Whatever comes next waits for the application to have read
1021 // it, so a chain of sends arrives in the order it was written
1022 ElementHelper.SettleInput(this);
1023 }
1024
1025 return this;
1026 }
1027
1033 {
1034 HardwareHelper.SendString(textToSend, GPAL.TypingDelay);
1035
1036 ElementHelper.SettleInput(this);
1037
1038 return this;
1039 }
1040
1046 {
1047 if (null != selector) WithSelector(selector);
1048 if (true == InAction(true))
1049 ElementHelper.SwitchToTab(this);
1050 return this;
1051 }
1052
1057 public IAllowAllActionAndAnySelector SetRange(double rangeValue)
1058 {
1059 if (true == InAction(true))
1060 ElementHelper.SetRange(this, rangeValue);
1061 return this;
1062 }
1063
1070 {
1071 if (null != selector) WithSelector(selector);
1072 if (true == InAction(true))
1073 ElementHelper.GenericClick(this, ClickType.RightClick);
1074 return this;
1075 }
1076
1086 {
1087 // we are done waiting on any selectors since we performed an action
1088 // so, we just wait/sleep
1089 if (true == CurrentUOW.ActionCalled)
1090 Thread.Sleep(timeoutInTicks);
1091 else
1092 CurrentUOW.WaitForInMs = timeoutInTicks;
1093
1094 return this;
1095 }
1096
1104 {
1105 if (true == InAction(true))
1106 SettleOnWindow(true == ApplicationSettings.HiddenDesktop
1107 ? Desktops.WaitForWindowOn(ApplicationSettings.HiddenDesktopName, title => windowTitle.Equals(title), ApplicationSettings.WaitForWindowTimeout)
1108 : FormHelper.WaitForWindow(windowTitle, ApplicationSettings.WaitForWindowTimeout));
1109 return this;
1110 }
1111
1119 {
1120 if (true == InAction(true))
1121 SettleOnWindow(true == ApplicationSettings.HiddenDesktop
1122 ? Desktops.WaitForWindowOn(ApplicationSettings.HiddenDesktopName, title => 0 < System.Text.RegularExpressions.Regex.Matches(title, windowTitleRegex).Count, ApplicationSettings.WaitForWindowTimeout)
1123 : FormHelper.WaitForWindowRegex(windowTitleRegex, ApplicationSettings.WaitForWindowTimeout));
1124 return this;
1125 }
1126
1138 private void SettleOnWindow(IntPtr windowHandle)
1139 {
1140 ApplicationSettings.LastWindowHandle = windowHandle;
1141
1142 if (IntPtr.Zero != windowHandle)
1143 {
1144 AutomationElement window = AutomationElement.FromHandle(windowHandle);
1145
1146 if (null != window)
1147 ApplicationSettings.RootAutomationElement = window;
1148 }
1149 }
1161 {
1162 if (true == InAction(true))
1163 {
1164 for (; 0 < iterations; iterations--)
1165 ElementHelper.Scroll(this, ScrollAmount.SmallIncrement, ScrollTypes.Increments, ScrollTypes.Horizontal);
1166
1167 for (; 0 > iterations; iterations++)
1168 ElementHelper.Scroll(this, ScrollAmount.SmallDecrement, ScrollTypes.Increments, ScrollTypes.Horizontal);
1169 }
1170
1171 return this;
1172 }
1173
1184 {
1185 if (true == InAction(true))
1186 {
1187 for (; 0 < iterations; iterations--)
1188 ElementHelper.Scroll(this, ScrollAmount.LargeIncrement, ScrollTypes.Increments, ScrollTypes.Horizontal);
1189
1190 for (; 0 > iterations; iterations++)
1191 ElementHelper.Scroll(this, ScrollAmount.LargeDecrement, ScrollTypes.Increments, ScrollTypes.Horizontal);
1192 }
1193
1194 return this;
1195 }
1196
1206 {
1207 if (true == InAction(true))
1208 ElementHelper.Scroll(this, InRange(percent), ScrollTypes.Percent, ScrollTypes.Horizontal);
1209
1210 return this;
1211 }
1212
1222 private int InRange(int percent)
1223 {
1224 int retVal = 0 > percent ? 0 : (100 < percent ? 100 : percent);
1225
1226 if (retVal != percent)
1227 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"A scroll percentage is 0 to 100, so [{percent}] was taken as [{retVal}]", this, GPALObjectType.Application);
1228
1229 return retVal;
1230 }
1242 {
1243 if (true == InAction(true))
1244 {
1245 for (; 0 < iterations; iterations--) // + = down
1246 ElementHelper.Scroll(this, ScrollAmount.SmallIncrement, ScrollTypes.Increments, ScrollTypes.Vertical);
1247
1248 for (; 0 > iterations; iterations++) // - = up
1249 ElementHelper.Scroll(this, ScrollAmount.SmallDecrement, ScrollTypes.Increments, ScrollTypes.Vertical);
1250 }
1251
1252 return this;
1253 }
1254
1265 {
1266 if (true == InAction(true))
1267 {
1268 for (; 0 < iterations; iterations--)
1269 ElementHelper.Scroll(this, ScrollAmount.LargeIncrement, ScrollTypes.Increments, ScrollTypes.Vertical);
1270
1271 for (; 0 > iterations; iterations++)
1272 ElementHelper.Scroll(this, ScrollAmount.LargeDecrement, ScrollTypes.Increments, ScrollTypes.Vertical);
1273 }
1274
1275 return this;
1276 }
1277
1287 {
1288 if (true == InAction(true))
1289 ElementHelper.Scroll(this, InRange(percent), ScrollTypes.Percent, ScrollTypes.Vertical);
1290
1291 return this;
1292 }
1293 #endregion <Application Actions>
1294 #region <Helpers>
1299 public void RemoveCallIfHandlerEverywhere(Application.CallIfDelegate func)
1300 {
1301 CurrentUOW.AppCallIfFound.Remove(func);
1302 CurrentUOW.AppCallIfNotFound.Remove(func);
1303
1304 // TODO: do the same for inselectorlist
1305 foreach (Selector selector in CurrentUOW.WithSelectorList)
1306 {
1307 selector.SelectorSettings.AppCallIfFound.Remove(func);
1308 selector.SelectorSettings.AppCallIfNotFound.Remove(func);
1309 }
1310
1311 foreach (Selector selector in CurrentUOW.InSelectorList)
1312 {
1313 selector.SelectorSettings.AppCallIfFound.Remove(func);
1314 selector.SelectorSettings.AppCallIfNotFound.Remove(func);
1315 }
1316
1317 persistentUOW.AppCallIfFound.Remove(func);
1318 persistentUOW.AppCallIfNotFound.Remove(func);
1319
1320 foreach (Selector selector in persistentUOW.WithSelectorList)
1321 {
1322 selector.SelectorSettings.AppCallIfFound.Remove(func);
1323 selector.SelectorSettings.AppCallIfNotFound.Remove(func);
1324 }
1325 }
1326 #endregion <Helpers>
1327 #region <Application Settings>
1334 {
1335 Name = appName;
1336 return this;
1337 }
1338
1348 {
1349 persistentUOW.AppCallIfFound.Add(persistentCallIfFound);
1350
1351 return this;
1352 }
1353
1363 {
1364 persistentUOW.AppCallIfNotFound.Add(persistentCallIfNotFound);
1365
1366 return this;
1367 }
1368
1378 {
1379 // when would we ever add another persistent uow?
1380 // normal uow is once action is called, any withselector is on the next uow, but persistents don't act like this
1381 if (true == CurrentUOW.ActionCalled)
1382 {
1383 persistentUOW = new UnitOfWork();
1384 persistenceList.Add(persistentUOW);
1385 }
1386
1387 selector.Application = this;
1388 //persistentUOW.SelectorType = SelectorType.WithSelector;
1389 persistentUOW.WithSelectorList.Add(selector);
1390 return this;
1391 }
1392
1401 {
1402 selector.ElementsFoundAndMatchedCount = 0; // in case it is reused
1403 selector.DeleteMe = false; // Remove() latches this on and nothing else clears it, so a reused selector would be swept on its next unit of work
1404 selector.RemovedMethods.Clear(); // a handler that took itself out of play in an earlier workflow is back in play for this one
1405
1406 if (true == CurrentUOW.ActionCalled)
1407 {
1408 CurrentUOW = new UnitOfWork();
1409 }
1410
1411 // clone so if this selector is reused, each UOW list will be unique and we won't change the selector by changing the browser it is associated with
1412 // NOTE: cloning disassociates with actual program selector, so we can't set any queryable properties on Selector like 'ElementsFoundAndMatchedCount'
1413 // ApplicationSettings.MySelector = new Selector(this, selector); // MySelector temp hold for other browser syntax
1414 ApplicationSettings.MySelector = selector;
1415
1416 CurrentUOW.WithSelectorList.Add(ApplicationSettings.MySelector);
1417
1418 return this;
1419 }
1420 // TODO: How to deal with tokens. To consume a row for each row, but to also repeat the same token down the page?
1433 public IAllowAfterAnySelectorExceptWithAll WithAllThatMatch(int rowCount = int.MaxValue)
1434 {
1435 CurrentUOW.WithAllThatMatch = rowCount;
1436 return this;
1437 }
1438
1439 #region <GetGrid / SaveTo / Pagination>
1440 // ─────────────────────────────────────────────────────────────────────────────────────
1441 // Data extraction out of a desktop application, mirroring the Browser GetGrid API.
1442 // A single selector that resolves to a UIA grid/table control is read structurally via
1443 // GridPattern/TablePattern; otherwise each selector is a column and repeating matches are
1444 // the rows (the WithAllThatMatch model). Missing cells use GPAL.ErrorPlaceholder. The
1445 // result lands in CurrentUOW.RetGrid so SaveTo*/pagination reuse the shared, engine-neutral
1446 // helpers (FileHelper.SaveToDelimited, GPAL.Converter) exactly as the Browser side does.
1447 // ─────────────────────────────────────────────────────────────────────────────────────
1454 public IAllowWithHeaderOrFileActions GetGrid(ref IGPALGrid<string> returnGrid)
1455 {
1456 // reading the grid is an action, the same as it is on the browser side, so the unit of work is
1457 // finished with and the next WithSelector starts a new one. GetGridCalled is a separate thing: it
1458 // tells WithHeader that the headers following it belong to this grid, and both it and SaveTo read
1459 // the grid off this same unit of work
1460 CurrentUOW.ActionCalled = true;
1461 CurrentUOW.GetGridCalled = true;
1462 CurrentUOW.RetGrid.Clear();
1463
1464 if (!TryGetGridFromControl())
1465 GetGridFromSelectors();
1466
1467 // Do not take ownership of a grid the caller passed in:
1468 // - null in -> hand them GPAL's internal grid.
1469 // - grid in -> add our rows into THEIR grid and keep their reference so repeated GetGrid calls accumulate.
1470 if (null == returnGrid)
1471 returnGrid = (IGPALGrid<string>)CurrentUOW.RetGrid;
1472 else
1473 returnGrid.Add((IGPALGrid<string>)CurrentUOW.RetGrid);
1474
1475 return this;
1476 }
1477
1478 // Table-native: a single selector that resolves to a control exposing the UIA GridPattern
1479 // is read cell-by-cell via GetItem(row, col); TablePattern column headers become the header
1480 // row. Returns false when this is not a grid control so the selector-column path runs.
1481 private bool TryGetGridFromControl()
1482 {
1483 if (1 != CurrentUOW.WithSelectorList.Count
1484 || SelectorType.Selector != CurrentUOW.WithSelectorList[0].selectorSettings.SelectorType)
1485 return false;
1486
1487 Selector sel = CurrentUOW.WithSelectorList[0];
1488 bool matchedAll; List<GPALAutomationElement> matched;
1489 var found = ElementHelper.FindElements(this, CurrentUOW, sel, out matchedAll, out matched);
1490 AutomationElement ae = (null != found && found.Count > 0) ? found[0].Ae : null;
1491 object gpObj;
1492 if (null == ae || !ae.TryGetCurrentPattern(GridPattern.Pattern, out gpObj))
1493 return false;
1494
1495 GridPattern grid = (GridPattern)gpObj;
1496 int rows = grid.Current.RowCount;
1497 int cols = grid.Current.ColumnCount;
1498
1499 List<string> headers = null;
1500 object tpObj;
1501 if (ae.TryGetCurrentPattern(TablePattern.Pattern, out tpObj))
1502 {
1503 AutomationElement[] colHeaders = ((TablePattern)tpObj).Current.GetColumnHeaders();
1504 if (null != colHeaders && colHeaders.Length > 0)
1505 {
1506 headers = new List<string>();
1507
1508 // Name first, then the text. A column header's name IS its text: Explorer's are
1509 // SplitButtons named Name, Date modified, Type and Size, and they offer a value pattern
1510 // that answers empty, so asking for the text got four blank names
1511 foreach (AutomationElement h in colHeaders)
1512 headers.Add(true == string.IsNullOrWhiteSpace(h.Current.Name) ? (h.GetText() ?? string.Empty) : h.Current.Name);
1513 }
1514 }
1515
1516 // a control can name its columns without offering TablePattern. A list view puts them in a Header
1517 // element with a HeaderItem for each column, which is the shape a person sees at the top of the
1518 // control, and those names are as much the column names as a pattern's would be
1519 if (null == headers)
1520 headers = ColumnHeadersUnder(ae);
1521
1522 int cap = (int.MaxValue == CurrentUOW.WithAllThatMatch)
1523 ? int.MaxValue : CurrentUOW.WithAllThatMatch;
1524
1525 for (int r = 0; r < rows && CurrentUOW.RetGrid.Count() < cap; r++)
1526 {
1527 var row = new List<string>(cols);
1528 for (int c = 0; c < cols; c++)
1529 {
1530 string text;
1531 try
1532 {
1533 AutomationElement cell = grid.GetItem(r, c);
1534 text = (null != cell) ? (cell.GetText() ?? string.Empty) : GPAL.ErrorPlaceholder;
1535 }
1536 catch
1537 {
1538 text = GPAL.ErrorPlaceholder;
1539 }
1540 row.Add(text);
1541 }
1542 CurrentUOW.RetGrid.AddRow(row);
1543 }
1544
1545 if (null != headers && headers.Count > 0)
1546 {
1547 CurrentUOW.ControlHeaderList = headers;
1548
1549 if (null == CurrentUOW.HeaderList || 0 == CurrentUOW.HeaderList.Count)
1550 CurrentUOW.HeaderList = headers;
1551 }
1552
1553 // what the control called its columns, kept on the application so a workflow can read it after the
1554 // grid rather than finding out from the saved file
1555 ColumnNames = null == headers ? new List<string>() : new List<string>(headers);
1556
1557 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"GetGrid: [{sel.Name}] resolved to a UIA grid ([{rows}] x [{cols}]).", this, GPALObjectType.Application);
1558 return true;
1559 }
1560
1572 private static List<string> ColumnHeadersUnder(AutomationElement element)
1573 {
1574 List<string> retVal = null;
1575
1576 AutomationElement header = element.FindFirst(TreeScope.Descendants,
1577 new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Header));
1578
1579 if (null != header)
1580 {
1581 // whatever the header holds, not only HeaderItems. Explorer's columns are SplitButtons, and a
1582 // list view that names its columns names them whatever control type it draws them with
1583 AutomationElementCollection items = header.FindAll(TreeScope.Children, Condition.TrueCondition);
1584
1585 if (0 < items.Count)
1586 {
1587 retVal = new List<string>();
1588
1589 foreach (AutomationElement item in items)
1590 retVal.Add(true == string.IsNullOrWhiteSpace(item.Current.Name) ? (item.GetText() ?? string.Empty) : item.Current.Name);
1591 }
1592 }
1593
1594 return retVal;
1595 }
1596
1597 // Column-per-selector extraction: each selector is a column, repeating matches are the rows.
1598 // Mirrors the Browser column-zip (pad short columns, dedup rows, honor WithAllThatMatch and
1599 // multi-page paging) but reads cells with the UIA .Ae.GetText() the app side already uses.
1600 private void GetGridFromSelectors()
1601 {
1602 var names = new List<string>();
1603 foreach (Selector s in CurrentUOW.WithSelectorList)
1604 names.Add(s.Name);
1605
1606 var seenRows = new HashSet<string>();
1607 bool capReached = false;
1608 int cap = (int.MaxValue == CurrentUOW.WithAllThatMatch)
1609 ? int.MaxValue : CurrentUOW.WithAllThatMatch;
1610
1611 for (int page = 0; page < CurrentUOW.PageCount && !capReached; page++)
1612 {
1613 var columns = new List<List<string>>();
1614 int maxRows = 0;
1615
1616 foreach (Selector sel in CurrentUOW.WithSelectorList)
1617 {
1618 var col = new List<string>();
1619 if (SelectorType.Selector == sel.selectorSettings.SelectorType)
1620 {
1621 bool matchedAll; List<GPALAutomationElement> matched;
1622 var found = ElementHelper.FindElements(this, CurrentUOW, sel, out matchedAll, out matched);
1623 List<GPALAutomationElement> elems =
1624 (!matchedAll && null != matched && matched.Count > 0)
1625 ? matched
1626 : (null != found ? found.ToList() : new List<GPALAutomationElement>());
1627 foreach (GPALAutomationElement e in elems)
1628 col.Add((null != e.Ae) ? (e.Ae.GetText() ?? string.Empty) : GPAL.ErrorPlaceholder);
1629 }
1630 else if (SelectorType.Data == sel.selectorSettings.SelectorType)
1631 col.Add(sel.SelectorPath);
1632 else if (SelectorType.DataFunc == sel.selectorSettings.SelectorType)
1633 col.Add(sel.selectorSettings.DataFunction());
1634
1635 columns.Add(col);
1636 if (col.Count > maxRows) maxRows = col.Count;
1637 }
1638
1639 // pad short columns so every row has a cell per column
1640 for (int i = 0; i < columns.Count; i++)
1641 while (columns[i].Count < maxRows)
1642 columns[i].Add($"{GPAL.ErrorPlaceholder} : {names[i]}");
1643
1644 for (int r = 0; r < maxRows && !capReached; r++)
1645 {
1646 var row = new List<string>(columns.Count);
1647 for (int c = 0; c < columns.Count; c++)
1648 row.Add(columns[c][r]);
1649
1650 string key = string.Join("", row);
1651 if (!seenRows.Add(key))
1652 continue; // duplicate row from a prior page/scroll pass
1653 CurrentUOW.RetGrid.AddRow(row);
1654 if (CurrentUOW.RetGrid.Count() >= cap)
1655 capReached = true;
1656 }
1657
1658 if (!capReached && page < CurrentUOW.PageCount - 1)
1659 if (!ApplicationHelper.GotoNextPage(this))
1660 {
1661 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"GetGrid: no next page after page [{page + 1}].", this, GPALObjectType.Application);
1662 break;
1663 }
1664 }
1665
1666 if (null == CurrentUOW.HeaderList || 0 == CurrentUOW.HeaderList.Count)
1667 CurrentUOW.HeaderList = names;
1668 }
1669
1672 {
1673 if (true == CurrentUOW.GetGridCalled)
1674 {
1675 CurrentUOW.GetGridCalled = false;
1676 CurrentUOW.HeaderList.Clear();
1677 }
1678 CurrentUOW.HeaderList.Add(header);
1679 return this;
1680 }
1681
1689 public IAllowWithHeaderOrFileActions WithGridToSave(IGPALGrid<string> gridToSave)
1690 {
1691 if (true == CurrentUOW.ActionCalled)
1692 CurrentUOW = new UnitOfWork();
1693
1694 CurrentUOW.RetGrid = gridToSave;
1695
1696 return this;
1697 }
1698
1701 {
1702 CurrentUOW.NextPageButton = selector;
1703 return this;
1704 }
1705
1708 {
1709 get
1710 {
1711 CurrentUOW.InfiniteScroll = true;
1712 return this;
1713 }
1714 }
1715
1717 public IAllowGetGridAndFillInFrom WithPages(int numberOfPages)
1718 {
1719 CurrentUOW.PageCount = numberOfPages;
1720 return this;
1721 }
1722
1723 // Resolve the output column names for a grid save, handed to the Converter as column metadata
1724 // (WithColumnNames), NOT injected as a data row. Precedence, highest first, with a short higher list
1725 // topped up from the next level down:
1726 // 1. CurrentUOW.HeaderList - the workflow's .WithHeader columns (override)
1727 // 2. the output GPALFile's ColumnList - the file's own columns (wildcard/.Next replication of the
1728 // first entry happens at expansion time, so entry 0 is representative here)
1729 // 3. selector names - the ultimate global, always defined
1730 // 4. the control's own column names, e.g. a UIA TablePattern's headers
1731 // 5. generated column1..columnN - so the list is never narrower than the widest row, because a
1732 // column with no name must never mean a column with no data
1733 // The result is then made unique: the writer keys cells by column name, so a repeated name would
1734 // collapse those columns into one holding the last value.
1735 // Whether the names are written is governed by the output file's FirstLineIsColumnNames.
1736 private List<string> ResolveHeaderColumns(GPALFile file)
1737 {
1738 List<string> selectorNames = CurrentUOW.WithSelectorList?.Where(s => null != s.Name).Select(s => s.Name).ToList()
1739 ?? new List<string>();
1740
1741 List<string> fileColumns = new List<string>();
1742 IGPALGrid<string> columnList = ((IGPALFileInternal)file).FileSettings.ColumnList;
1743 if (null != columnList && 0 < columnList.Count() && null != columnList[0])
1744 fileColumns = new List<string>(columnList[0]);
1745
1746 List<string> resolved = (null != CurrentUOW.HeaderList && 0 < CurrentUOW.HeaderList.Count)
1747 ? new List<string>(CurrentUOW.HeaderList)
1748 : new List<string>();
1749 for (int i = resolved.Count; i < fileColumns.Count; i++)
1750 resolved.Add(fileColumns[i]);
1751 for (int i = resolved.Count; i < selectorNames.Count; i++)
1752 resolved.Add(selectorNames[i]);
1753
1754 // a name the control gave for itself beats a made up one, and WithHeader clears HeaderList, so the
1755 // control's names are kept apart and read from here
1756 List<string> controlHeaders = CurrentUOW.ControlHeaderList ?? new List<string>();
1757
1758 for (int i = resolved.Count; i < controlHeaders.Count; i++)
1759 resolved.Add(controlHeaders[i]);
1760
1761 // the converter writes the columns it has names for, so a name list shorter than the data would
1762 // drop everything past the end of it. Data is never trimmed to fit the names, the names are made
1763 // up to fit the data, so every cell reaches the file whatever was named
1764 int widest = 0;
1765
1766 if (null != CurrentUOW.RetGrid)
1767 foreach (List<string> row in CurrentUOW.RetGrid)
1768 if (null != row && widest < row.Count)
1769 widest = row.Count;
1770
1771 for (int i = resolved.Count; i < widest; i++)
1772 resolved.Add($"column{i + 1}");
1773
1774 return UniqueColumns(resolved);
1775 }
1776
1789 private static List<string> UniqueColumns(List<string> columns)
1790 {
1791 var used = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
1792 var result = new List<string>();
1793
1794 for (int i = 0; i < columns.Count; i++)
1795 {
1796 string name = true == string.IsNullOrWhiteSpace(columns[i]) ? $"column{i + 1}" : columns[i].Trim();
1797 string candidate = name;
1798 int repeat = 1;
1799
1800 while (false == used.Add(candidate))
1801 {
1802 repeat++;
1803 candidate = $"{name}{repeat}";
1804 }
1805
1806 result.Add(candidate);
1807 }
1808
1809 return result;
1810 }
1811
1817 {
1818 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Saving [{CurrentUOW.RetGrid?.Rows ?? 0}] rows of window data to [{file?.Filename}].", this, GPALObjectType.Application);
1819
1820 var converter = GPAL.Converter.WithInput(CurrentUOW.RetGrid).WithColumnNames(ResolveHeaderColumns(file));
1821 // Naming headers via WithHeader is clear intent to write them; declare it so the output emits a
1822 // header row unless the output file explicitly turns it off (WithFirstLineIsColumnNames(false)).
1823 if (null != CurrentUOW.HeaderList && 0 < CurrentUOW.HeaderList.Count)
1824 converter.WithFirstLineHasColumnNames(true);
1825 converter.SaveTo(file);
1826 return this;
1827 }
1828
1834 {
1835 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"Appending [{CurrentUOW.RetGrid?.Rows ?? 0}] rows of window data to [{file?.Filename}].", this, GPALObjectType.Application);
1836
1837 GPAL.Converter.WithInput(CurrentUOW.RetGrid).WithColumnNames(ResolveHeaderColumns(file)).AppendTo(file);
1838 return this;
1839 }
1840 #endregion <GetGrid / SaveTo / Pagination>
1847 {
1848 ApplicationSettings.WaitForWindowTimeout = waitTimeInSeconds;
1849 return this;
1850 }
1851
1857 {
1858 ApplicationSettings.Parameters.Add(parameter);
1859 return this;
1860 }
1861
1867 {
1868 ApplicationSettings.UseHardware = trueFalse;
1869 return this;
1870 }
1871
1883 {
1884 ApplicationSettings.NewInstance = trueFalse;
1885 return this;
1886 }
1887
1915 {
1916 ApplicationSettings.HiddenDesktop = hiddenDesktop;
1917
1918 // a program that asks for a hidden desktop without saying which is asking for one of its own, so it
1919 // gets a name nothing else will be handed. two programs sharing is what naming one is for
1920 if (true == hiddenDesktop)
1921 ApplicationSettings.HiddenDesktopName = Desktops.NextName();
1922
1923 return this;
1924 }
1925
1942 {
1943 ApplicationSettings.HiddenDesktop = true;
1944
1945 // calling this at all is asking for a hidden desktop. a name that turned out to be nothing is a
1946 // desktop of this program's own, the same as asking without a name. not wanting one is not calling
1947 ApplicationSettings.HiddenDesktopName = true == string.IsNullOrWhiteSpace(desktopName)
1948 ? Desktops.NextName()
1949 : desktopName;
1950
1951 return this;
1952 }
1953
1963 public IAllowAfterAnySelector CallIfFound(Application.CallIfDelegate callIfFoundDelegate)
1964 {
1965 CurrentUOW.AppCallIfFound.Add(callIfFoundDelegate);
1966 return this;
1967 }
1968
1978 public IAllowAfterAnySelector CallIfNotFound(Application.CallIfDelegate callIfNotFoundDelegate)
1979 {
1980 CurrentUOW.AppCallIfNotFound.Add(callIfNotFoundDelegate);
1981 return this;
1982 }
1983
1991 {
1992 CurrentUOW.AppCallAfterFillIn = callAfterFillIn;
1993 return this;
1994 }
1995
2013 public bool IsEndOfPage()
2014 {
2015 bool retVal = true;
2016 ScrollPattern view = ScrollableView();
2017
2018 if (null != view)
2019 {
2020 ScrollPattern.ScrollPatternInformation at = view.Current;
2021
2022 // a control that stops scrolling reports NoScroll, -1, rather than a position
2023 retVal = false == at.VerticallyScrollable || 100 <= Math.Round(at.VerticalScrollPercent);
2024 }
2025
2026 return retVal;
2027 }
2028
2034 internal ScrollPattern ScrollableView()
2035 {
2036 ScrollPattern retVal = null;
2037 AutomationElement root = RootAutomationElement;
2038
2039 if (null != root)
2040 {
2041 AutomationElement focused = AutomationElement.FocusedElement;
2042
2043 if (null != focused && focused.Current.ProcessId != root.Current.ProcessId)
2044 focused = null;
2045
2046 // the raw view, since WinForms leaves whole controls out of the control view
2047 for (AutomationElement at = focused; null != at && null == retVal; at = TreeWalker.RawViewWalker.GetParent(at))
2048 if (true == at.TryGetCurrentPattern(ScrollPattern.Pattern, out object pattern) && true == ((ScrollPattern)pattern).Current.VerticallyScrollable)
2049 retVal = (ScrollPattern)pattern;
2050
2051 if (null == retVal)
2052 {
2053 AutomationElement first = root.FindFirst(TreeScope.Subtree, new AndCondition(
2054 new PropertyCondition(AutomationElement.IsScrollPatternAvailableProperty, true),
2055 new PropertyCondition(ScrollPattern.VerticallyScrollableProperty, true)));
2056
2057 if (null != first)
2058 retVal = (ScrollPattern)first.GetCurrentPattern(ScrollPattern.Pattern);
2059 }
2060 }
2061
2062 return retVal;
2063 }
2064
2065 #region <Getters/Setters>
2070 public IHiddenDesktop HiddenDesktop => true == ApplicationSettings.HiddenDesktop
2071 ? new GPALHiddenDesktop(ApplicationSettings.HiddenDesktopName)
2072 : null;
2073
2078 public IReadOnlyList<string> ColumnNames { get; private set; } = new List<string>();
2086 {
2087 get
2088 {
2089 return applicationSettings.Process;
2090 }
2091 internal set
2092 {
2093 applicationSettings.Process = value;
2094 }
2095 }
2096
2100 {
2101 get
2102 {
2103 return applicationSettings.ManagedRootAutomationElement;
2104 }
2105
2106 internal set
2107 {
2108 applicationSettings.ManagedRootAutomationElement = value;
2109 }
2110 }
2111
2114 public AutomationElement RootAutomationElement
2115 {
2116 get
2117 {
2118 return applicationSettings.RootAutomationElement;
2119 }
2120
2121 internal set
2122 {
2123 applicationSettings.RootAutomationElement = value;
2124 }
2125 }
2126
2132 public string Name
2133 {
2134 get
2135 {
2136 return ApplicationSettings.Name;
2137 }
2138
2139 internal set
2140 {
2141 ApplicationSettings.Name = value;
2142 }
2143 }
2144
2149 public string ApplicationPath
2150 {
2151 get
2152 {
2153 return applicationSettings.ApplicationPath;
2154 }
2155 internal set
2156 {
2157 applicationSettings.ApplicationPath = value;
2158 }
2159 }
2160
2164 internal ApplicationSettings ApplicationSettings
2165 {
2166 get
2167 {
2168 return applicationSettings;
2169 }
2170 }
2171 #endregion <Getters/Setters>
2172 #endregion <Application Settings>
2173 }
2174}
2175
Application object that contains the fluent methods to create your Application workflow....
string ApplicationPath
The path of the program you Opened. This get is intended for debugging only.
IAllowSelectorActionOrAnySelector SendKey(byte VKCode)
Type special characters like Enter, Page Up, Page Down, Tab, etc. Use GPAL.VK constants for ease of ...
IAllowSelectorActionOrAnySelector FillInFrom(GPALFile inputFile)
FillIn data from the file overwriting text in the input Selector(s) defined in the UOW One token (on...
IAllowAfterWaitForAndSizeControl WithWaitForWindowTimeout(int waitTimeInSeconds)
Define the time in seconds to WaitForWindow/Regex.
IAllowApplicationSettingsOrOpen WithParameter(string parameter)
Define the parameters to use in the Open command.
IAllowSelectorActionOrAnySelector ScrollHorizontalPercent(int percent)
Element's horizontal scroll position as a percentage of the total content area within the AutomationE...
IAllowApplicationSettingsOrOpen WithHiddenDesktop(bool hiddenDesktop)
Run the program on a Win32 desktop object of its own, so it has a real window, real rendering and not...
IAllowAllActionAndAnySelector SwitchToTab(Selector selector=null)
Switch to any TabControl defined in the current UOW. This will hardware click the tab control if the ...
IAllowSelectorActionOrAnySelector DragAndDrop(ModifierKeys modifierKeys)
Performs a drag from the source element to the target element, both found via Selector(s) defined in ...
object ManagedRootAutomationElement
The root UI Automation element (AutomationElement) for the managed application's main window.
IAllowApplicationSettingsOrOpen WithName(string appName)
Sets the application's name, used when publishing events.
IAllowWithPagesAndGridActions WithInfiniteScroll
Scroll to load more rows between pages. Not supported for desktop apps (single page).
IAllowSelectorActionOrAnySelector ScrollHorizontalSmall(int iterations=1)
Scroll the element horizontally in a small increment. https://docs.microsoft.com/en-us/windows/win32...
IAllowAfterAnySelector CallIfNotFound(Application.CallIfDelegate callIfNotFoundDelegate)
Add a Unit of Work level CallIfNotFound handler to be called if any defined selectors DO NOT find/mat...
IAllowSelectorActionOrAnySelector DragAndDrop(Selector selector=null)
Performs a drag from the source element to the target element, both found via Selector(s) defined in ...
Application ToGPALObject()
Return a GPAL.Application object so that GPAL.Application does not have to be cast.
IAllowAllActionAndAnySelector SetRange(double rangeValue)
Set the value of a range-based control (e.g. a slider) found via Selector(s) defined in the UOW.
IAllowPersistentCallBack PersistentCallIfFound(CallIfDelegate persistentCallIfFound)
Add a persistent CallIfFound handler to be called if any defined persistent selectors find/match elem...
IAllowAllApplicationAndAllSelector Restore
Restore the application to 'original' size, unminimize.
IAllowSelectorActionOrAnySelector LeftClickAndOpen(GPALFile file)
Clicks the element that raises an Open dialog, waits for the dialog, and answers it with the given fi...
IAllowAfterAnySelector CallIfFound(Application.CallIfDelegate callIfFoundDelegate)
Add a Unit of Work level CallIfFound handler to be called if any defined selectors find/match element...
IAllowSelectorActionOrAnySelector PressModifierKey(ModifierKeys modifierKeys)
Press and hold a modifier key (Alt, Control, Shift) using hardware emulation.
IAllowSelectorActionOrAnySelector SendString(string textToSend)
Send the string using hardware emulation.
IAllowSelectorActionOrAnySelector Hover(Selector selector=null)
Hover the mouse over the current element found via the Selector. Only the first element per Selector ...
IAllowAfterWaitForAndSizeControl WaitForWindowRegex(string windowTitleRegex)
Wait for a Windows window with title that matches the regex pattern to appear on the desktop....
IAllowAfterWaitForAndSizeControl WaitFor(int timeoutInTicks)
Either wait the amount of time specified or wait UP TO the amount of time specified waiting for eleme...
IAllowSelectorActionOrAnySelector ReleaseModifierKey(ModifierKeys modifierKeys)
Release a previously pressed modifier key (Alt, Control, Shift) using hardware emulation.
IAllowSelectorActionOrAnySelector InsertFrom(IGPALGrid< string > inputGrid)
Insert the data from the grid at the beginning of the text in the input Selector(s) defined in the UO...
IAllowAfterAnySelector CallAfterFillIn(CallAfterFillInDelegate callAfterFillIn)
Add a handler to call after a row of tokens is consumed and after all inputs are filled in with data....
IAllowSelectorActionOrAnySelector LeftClick(Enums.ModifierKeys modifierKeys)
Left click the element found for Selector(s) defined in the UOW, optionally emulating pressing a modi...
string Name
A name you provide to be used when publishing events. Use .WithName to provide your own friendly nam...
IAllowSelectorActionOrAnySelector AnswerFileDialog(GPALFile file)
Answers a file dialog that is already on screen with the given file or folder.
IAllowApplicationSettingsOrOpen WithHiddenDesktop(string desktopName)
Run the program on the named Win32 desktop object. Programs naming the same desktop share it,...
IAllowSelectorActionOrAnySelector InsertFrom(GPALDatabase inputDatabase)
Insert the text from the database at the beginning of the input Selector(s) defined in the UOW One t...
IAllowApplicationSettingsOrOpen WithNewInstance(bool trueFalse)
Launch a new copy of the program rather than attaching to one that is already running.
IAllowAfterWaitForAndSizeControl WaitForWindow(string windowTitle)
Wait for a Windows window with title to appear on the desktop. Used in conjunction with ....
IAllowSelectorActionOrAnySelector LeftClickAndSave(GPALFile file)
Clicks the element that raises a Save dialog, waits for the dialog, and answers it with the given fil...
IAllowSelectorActionOrAnySelector FillInFrom(GPALDatabase inputDatabase)
FillIn data from the database overwriting text in the input Selector(s) defined in the UOW One token...
IAllowApplicationSettingsOrOpen WithUseHardware(bool trueFalse)
Force all workflow element interactions to use hardware emulation (UIAutomation is default).
IAllowAllApplicationAndAllSelector MoveWindow(int x, int y)
Move the window last matched by WaitForWindow to the specified screen coordinates.
IAllowSelectorActionOrAnySelector FillInFrom(string textToUse)
FillIn the text from the string overwriting text in the first input in the UOW. Should probably only ...
IReadOnlyList< string > ColumnNames
The column names the last GetGrid read off the control itself, so a workflow can see what it is about...
delegate CallIfStatus CallAfterFillInDelegate(Application application, IGPALGrid< string > tokens, int tokenIdx)
Delegate callback for the CallAfterFillIn EventHandler which will be invoked after each row of tokens...
IAllowSelectorActionOrAnySelector InsertFrom(string textToUse)
Insert the text from the string at the beginning of the first input in the UOW. Should probably only ...
IAllowSelectorActionOrAnySelector Focus(Selector selector=null)
Focus the current element found via the Selector. Only the first element per Selector found will be f...
delegate CallIfStatus CallIfDelegate(Application application, List< IGPALAutomationElement > foundElements, List< IGPALAutomationElement > matchedElements, Selector selector, bool matchedAll)
Delegate callback for the CallIfFound/CallIfNotFound EventHandlers which will be invoked when a selec...
IAllowSelectorActionOrAnySelector LeftClick(Selector selector=null)
Left click the element found for Selector(s) defined in the UOW One left click for the first element...
IAllowSelectorActionOrAnySelector RightClick(Selector selector=null)
Right click the element found for Selector(s) defined in the UOW One right click for the first eleme...
IAllowSelectorActionOrAnySelector MiddleClick(Selector selector=null)
Middle click the element found for Selector(s) defined in the UOW.
IAllowAllApplicationAndAllSelector Minimize
Minimize the application to the taskbar.
IAllowWithHeaderOrFileActions WithGridToSave(IGPALGrid< string > gridToSave)
Supply a GPALGrid (perhaps built elsewhere) to be saved by a following SaveTo*.
IAllowAllActionAndAnySelector AppendTo(GPALFile file)
Append the retrieved grid. The output format and delimiter are determined by the GPALFile (its extens...
IAllowSelectorActionOrAnySelector WithSelector(Selector selector)
Add a selector to the current UOW. Selectors make up a Unit of Work (UOW) to perform actions upon....
IAllowSelectorActionOrAnySelector ScrollVerticalLarge(int iterations=1)
Scroll the element vertically in a large increment. https://docs.microsoft.com/en-us/windows/win32/a...
IAllowAfterAnySelectorExceptWithAll WithAllThatMatch(int rowCount=int.MaxValue)
Indicates the Selectors refer to/match repeating, multiple elements on the page. Use after all your ...
IAllowSelectorActionOrAnySelector FillInFrom(IGPALGrid< string > inputGrid)
FillIn data from the grid overwriting text in the input Selector(s) defined in the UOW One token (on...
IAllowGetGridAndFillInFrom WithPages(int numberOfPages)
Number of result pages to retrieve. Use with WithNextPageButton.
IAllowSelectorActionOrAnySelector ScrollVerticalPercent(int percent)
Element's vertical scroll position as a percentage of the total content area within the AutomationEle...
IAllowSelectorActionOrAnySelector LeftDoubleClick(Selector selector=null)
Left double click the element found for Selector(s) defined in the UOW.
IAllowAllApplicationAndAllSelector ResizeWindow(int width, int height)
Resize the window last matched by WaitForWindow to the specified pixel dimensions.
Process Process
The current Windows process for the program run via Open. Only one program can be Opened/controlled ...
IAllowAllActionAndAnySelector SaveTo(GPALFile file)
Save the retrieved grid. The output format and delimiter are determined by the GPALFile (its extensio...
IAllowSelectorActionOrAnySelector AppendFrom(GPALDatabase inputDatabase)
Append the data from the database to the end of the input Selectors defined in the UOW One token (on...
IAllowSelectorActionOrAnySelector AppendFrom(GPALFile inputFile)
Append the data from the input file to the end of the input Selectors defined in the UOW One token (...
IAllowSelectorActionOrAnySelector AppendFrom(string textToUse)
Append the given text to the first input in the UOW. Should probably only be used with one selector a...
AutomationElement RootAutomationElement
The starting AutomationElement for the application. All xpath searches start from this AutomationElem...
IAllowSelectorActionOrAnySelector InsertFrom(GPALFile inputFile)
Insert the data from the file at the beginning of the text in the input Selector(s) defined in the UO...
IAllowAllExceptParameters Open([Directory()] string applicationPath)
Launch the application specified in the path. Find the root automation element and place into applic...
bool IsEndOfPage()
Whether the application's scrollable view is at the bottom, the way the browser answers it for a page...
IAllowSelectorActionOrAnySelector ScrollHorizontalLarge(int iterations=1)
Scroll the element horizontally in a large increment. https://docs.microsoft.com/en-us/windows/win32...
IAllowPersistence WithPersistentSelector(Selector selector)
Add a persistent selector to the persistent UOW. Persistent selectors are always looked for whenever ...
IAllowSelectorActionOrAnySelector MoveTo(Selector selector=null)
Move the mouse cursor to the element found for Selector(s) defined in the UOW.
IAllowAllApplicationAndAllSelector Maximize
Maximize the application to full-screen.
IAllowPersistentCallBack PersistentCallIfNotFound(CallIfDelegate persistentCallIfNotFound)
Add a persistent CallIfNotFound handler to be called if any defined persistent selectors DO NOT find/...
IAllowSelectorActionOrAnySelector AppendFrom(IGPALGrid< string > inputGrid)
Append the data from the input grid to the end of the input Selectors defined in the UOW One token (...
void RemoveCallIfHandlerEverywhere(Application.CallIfDelegate func)
Helper method for GPAL applications to remove a handler from all UOWs. Envisioned to be used in a per...
IAllowWithHeaderOrFileActions GetGrid(ref IGPALGrid< string > returnGrid)
Retrieve the current selectors' data (or a table/grid control's data) into a GPALGrid....
IHiddenDesktop HiddenDesktop
The hidden desktop this program runs on, or null when it is on the desktop you are looking at....
IAllowWithHeaderOrFileActions WithHeader(string header)
Column header for exported data. Overrides selector names / table headers, in order.
IAllowSelectorActionOrAnySelector ScrollVerticalSmall(int iterations=1)
Scroll the element vertically in a small increment. https://docs.microsoft.com/en-us/windows/win32/a...
IAllowWithPagesAndGridActions WithNextPageButton(Selector selector)
The control to click to load the next page when paging through results.
void Close()
Close/terminate the application. This terminates the fluent interface and nothing can be chained to ...
static void TopProcess(Process process)
Brings the given process's main window to the foreground/top of the z-order, if it is not already.
static void FillInWithTokens(Application.Application application, List< IGPALGrid< string > > tokenList, WriteMode writeMode)
Enter tokens into form controls for each grid of tokens in the list.
File-side plumbing behind the fluent chain: writing a unit of work's data out in a delimited format,...
Definition FileHelper.cs:55
Class to define database usage. Currently only used for input from a table, sql or stored procedure....
IGPALGrid< string > Tokens
The rows/columns returned from the database. Tokens are loaded upon calling any [Append/FillIn/Inser...
Thrown where GPAL deliberately ends the workflow, such as a CallIf handler returning CallIfStatus....
GPAL File object instantied with GPAL.File Used to load tokens into a GPALGrid [rows/columns].
Definition GPALFile.cs:36
Everything starts here, all the GPAL controls and global settings using fluent syntax are here....
Definition GPAL.cs:49
static IAllowConverterInput Converter
New GPAL Convertor.
Definition GPAL.cs:571
static void PublishSimpleEvent(GPALEventType gPALEventType, string msg, dynamic gPALObject=null, Enums.GPALObjectType gPALObjectType=GPALObjectType.None, Exception ex=null)
Publish a message to either the information channel or exception channel (if exception passed in) Pub...
Definition GPAL.cs:2568
GPAL Selector used to locate Application and Browser elements. Instantiated with GPAL....
Definition Selector.cs:56
string SelectorPath
Returns the first defined selector path string.
Definition Selector.cs:766
string Name
The name you gave this selector, or one assigned by GPAL [selector1, selector2...] Used in Informati...
Definition Selector.cs:870
SelectorType SelectorType
Type of selector: A selector for an element, or literal data or a data function for dynamic data not ...
Everything revolves around the Unit of Work. A Unit of Work is defined as one or more selectors betw...
Definition UnitOfWork.cs:39
IAllowSelectorActionOrAnySelector SendString(string textToSend)
Simulates typing via hardware key events, one character at a time. Honors GPAL.TypingDelay (see GPAL....
One Win32 desktop object a browser is running on. Reached from the browser that is on it,...