I need to open a link in a new tab and get back to the original tab; however, it seems like the WindowHandle is not working properly. The code below only opens a new tab and then loads the link in the original tab without switching to it.
driver.FindElement(By.CssSelector("body")).SendKeys(Keys.Control + "t");
driver.SwitchTo().Window(driver.WindowHandles.Last());
driver.Navigate().GoToUrl("https://bi7-azure.accenture.com/");
driver.SwitchTo().Window(driver.WindowHandles.First());
Alternatively, I read that there is a new feature that does this in a much easier and simple way, but it doesn't work as well (I'm missing a dependency or something), and I don't see a lot of articles about it. It would be great if there's a way I can use this instead, or if not then how can I overcome the issue with WindowHandles. Please note as well that I've already configured IE properly and added the necessary registry edit as per most of the forums on the internet says.
driver.SwitchTo().newWindow(WindowType.TAB);
I know there are duplicate questions for this, but most of them are old so please bear with me.
Try this..
public static string SwitchToTab()
{
var mainHandle = driver.CurrentWindowHandle;
var handles = driver.WindowHandles;
foreach (var handle in handles)
{
if (mainHandle == handle)
{
continue;
}
driver.SwitchTo().Window(handle);
break;
}
var result = Url;
return result;
}
Then...
public static void GoToMainHandle()
{
var handles = driver.WindowHandles;
foreach (var handle in handles)
{
driver.SwitchTo().Window(handle);
break;
}
}
you can also use:
public static void CloseTab()
{
var mainHandle = driver.CurrentWindowHandle;
var handles = driver.WindowHandles;
foreach (var handle in handles)
{
if (mainHandle == handle)
{
continue;
}
driver.SwitchTo().Window(handle);
driver.Close();
driver.SwitchTo().Window(mainHandle);
break;
}
}
You may need to wait for the window to be loaded, before you switch to that window. Here's an extension method, that will do that:
public static SwitchToWindow(this IWebDriver #this, string windowTitle)
{
var tryCount = 0;
while (!#this.WindowHandles.Any(x => #this.SwitchTo().Window(x).Title.StartsWith(windowTitle)))
{
Thread.Sleep(500);
if (tryCount == 60)
throw new NoSuchWindowException($"No window with title {windowTitle} found.");
tryCount=+1;
}
#this.Manage().Window.Maximize();
}
Related
I'm creating a Revit plugin that reads and writes modelinformation to a database, and it all works fine in debug mode, but when I release the project and run Revit with the plugin outside visual studio, I'm getting an error when the plugin tries to read data from the database.
The code runs on DocumenetOpened event and looks like this:
public void application_DocumentOpenedEvent(object sender, DocumentOpenedEventArgs e)
{
UIApplication uiapp = new UIApplication(sender as Autodesk.Revit.ApplicationServices.Application);
Document doc = uiapp.ActiveUIDocument.Document;
//ModelGUID COMMAND
var command = new ModelCheckerCommandExec();
command.Execute(uiapp);
}
It then fails on the following line:
ModelsList = (DatabaseHelper.ReadNonAsync<RevitModel>())
.Where(m => m.ModelGUID == DataStores.ModelData.ModelGUID).ToList();
In this code block that gets executed:
public class ModelCheckerCommandExec : IExternalCommand
{
public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements)
{
return Execute(commandData.Application);
}
public Result Execute(UIApplication uiapp)
{
Document doc = uiapp.ActiveUIDocument.Document;
Transaction trans = new Transaction(doc);
try
{
trans.Start("ModelGUID");
ModelGUIDCommand.GetAndSetGUID(doc);
trans.Commit();
var ModelsList = new List<RevitModel>();
ModelsList = (DatabaseHelper.ReadNonAsync<RevitModel>()).ToList();//.Where(m => m.ModelGUID == DataStores.ModelData.ModelGUID).ToList(); // Read method only finds models the are similar to the DataStore.ModelDate.DBId;
if (ModelsList.Count == 1)
{
trans.Start("DataFromDB");
doc.ProjectInformation.Name = ModelsList[0].ProjectName;
doc.ProjectInformation.Number = ModelsList[0].ModelNumber;
doc.ProjectInformation.Status = ModelsList[0].ModelStatus;
doc.ProjectInformation.IssueDate = ModelsList[0].ProjectIssueDate;
doc.ProjectInformation.ClientName = ModelsList[0].ClientName;
doc.ProjectInformation.Address = ModelsList[0].ProjectAddress;
doc.ProjectInformation.LookupParameter("Cadastral Data").Set(ModelsList[0].ProjectIssueDate);
doc.ProjectInformation.LookupParameter("Current Version").Set(ModelsList[0].CurrentVersion);
doc.ProjectInformation.BuildingName = ModelsList[0].BuildingName;
DataStores.ModelData.ModelManager1 = ModelsList[0].ModelManagerOne;
DataStores.ModelData.ModelManager1Id = ModelsList[0].ModelManagerOneId;
DataStores.ModelData.ModelManager2 = ModelsList[0].ModelManagerTwo;
DataStores.ModelData.ModelManager2Id = ModelsList[0].ModelManagerTwoId;
trans.Commit();
}
return Result.Succeeded;
}
catch (Exception ex)
{
TaskDialog.Show("Error", ex.Message);
return Result.Failed;
}
}
}
The "ReadNonAsync" method is as follows:
public static List<T> ReadNonAsync<T>() where T : IHasId
{
using (var client = new HttpClient())
{
var result = client.GetAsync($"{dbPath}{Properties.Settings.Default.CompanyName}_{typeof(T).Name.ToLower()}.json?access_token={DataStores.IdToken.UserIdToken}").GetAwaiter().GetResult();
var jsonResult = result.Content.ReadAsStringAsync().GetAwaiter().GetResult();
if (result.IsSuccessStatusCode)
{
var objects = JsonConvert.DeserializeObject<Dictionary<string, T>>(jsonResult);
List<T> list = new List<T>();
if (objects != null)
{
foreach (var o in objects)
{
o.Value.Id = o.Key;
list.Add(o.Value);
}
}
return list;
}
else
{
return null;
}
}
}
In the rest of my code I use a async Read method which works, so I'm wondering wether or not that's the issue, but Revit wont let me use an async method inside an Execute method.
How do I debug this issue correctly, and why could there be code working in debug that doesn't work in "live" versions?
I found a solution!
The issue:
The reason for the error was that when I run the software in debug-mode, a file path of "xxx.txt" finds files in the solution folder, but when I run the software "xxx.txt" points to the folder of the software and not the .dll -
So in my case it pointed to "c:\Program Files\Autodesk\Revit\2021".
The fix:
Hard coding the path, or by getting the path of the executing .dll
Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)
Debugging/Troubleshooting:
I found the issue by inserting dialogboxes with errormessages in all my try/catch statements.
I am fetching custom properties from woorkbook using this method:
var wb = Globals.ThisAddIn.Application.Workbooks.Open(file.FullName);
wb.Windows[1].Visible = false;
if (wb != null)
{
foreach (var prop in wb.CustomDocumentProperties)
{
try
{
Console.WriteLine(prop.Name.ToString());
Console.WriteLine(prop.Value.ToString());
Properties.Add(new CustomDocumentProperty { Key = prop.Name.ToString(), Value = prop.Value.ToString() });
}
catch (Exception)
{
Console.WriteLine();
}
}
wb.Close(false);
}
This method works, but the problem is that it is really slow in fetching the properties before executing the loop. So is there any way to speed this up? I have tried to look at other posts on this site, but I havent seen anyone mention this issue. Please let me know if I need to post anymore code. (Properties is a list of a custom class)
I'm using Selenium for retrieve data from this site, and I encountered a little problem when I try to click an element within a foreach.
What I'm trying to do
I'm trying to get the table associated to a specific category of odds, in the link above we have different categories:
As you can see from the image, I clicked on Asian handicap -1.75 and the site has generated a table through javascript, so inside my code I'm trying to get that table finding the corresponding element and clicking it.
Code
Actually I have two methods, the first called GetAsianHandicap which iterate over all categories of odds:
public List<T> GetAsianHandicap(Uri fixtureLink)
{
//Contains all the categories displayed on the page
string[] categories = new string[] { "-1.75", "-1.5", "-1.25", "-1", "-0.75", "-0.5", "-0.25", "0", "+0.25", "+0.5", "+0.75", "+1", "+1.25", "+1.5", "+1.75" };
foreach(string cat in categories)
{
//Get the html of the table for the current category
string html = GetSelector("Asian handicap " + asian);
if(html == string.Empty)
continue;
//other code
}
}
and then the method GetSelector which click on the searched element, this is the design:
public string GetSelector(string selector)
{
//Get the available table container (the category).
var containers = driver.FindElements(By.XPath("//div[#class='table-container']"));
//Store the html to return.
string html = string.Empty;
foreach (IWebElement container in containers)
{
//Container not available for click.
if (container.GetAttribute("style") == "display: none;")
continue;
//Get container header (contains the description).
IWebElement header = container.FindElement(By.XPath(".//div[starts-with(#class, 'table-header')]"));
//Store the table description.
string description = header.FindElement(By.TagName("a")).Text;
//The container contains the searched category
if (description.Trim() == selector)
{
//Get the available links.
var listItems = driver.FindElement(By.Id("odds-data-table")).FindElements(By.TagName("a"));
//Get the element to click.
IWebElement element = listItems.Where(li => li.Text == selector).FirstOrDefault();
//The element exist
if (element != null)
{
//Click on the container for load the table.
element.Click();
//Wait few seconds on ChromeDriver for table loading.
driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(20);
//Get the new html of the page
html = driver.PageSource;
}
return html;
}
return string.Empty;
}
Problem and exception details
When the foreach reach this line:
var listItems = driver.FindElement(By.Id("odds-data-table")).FindElements(By.TagName("a"));
I get this exception:
'OpenQA.Selenium.StaleElementReferenceException' in WebDriver.dll
stale element reference: element is not attached to the page document
Searching for the error means that the html page source was changed, but in this case I store the element to click in a variable and the html itself in another variable, so I can't get rid to patch this issue.
Someone could help me?
Thanks in advance.
I looked at your code and I think you're making it more complicated than it needs to be. I'm assuming you want to scrape the table that is exposed when you click one of the handicap links. Here's some simple code to do this. It dumps the text of the elements which ends up unformatted but you can use this as a starting point and add functionality if you want. I didn't run into any StaleElementExceptions when running this code and I never saw the page refresh so I'm not sure what other people were seeing.
string url = "http://www.oddsportal.com/soccer/europe/champions-league/paok-spartak-moscow-pIXFEt8o/#ah;2";
driver.Url = url;
// get all the (visible) handicap links and click them to open the page and display the table with odds
IReadOnlyCollection<IWebElement> links = driver.FindElements(By.XPath("//a[contains(.,'Asian handicap')]")).Where(e => e.Displayed).ToList();
foreach (var link in links)
{
link.Click();
}
// print all the odds tables
foreach (var item in driver.FindElements(By.XPath("//div[#class='table-container']")))
{
Console.WriteLine(item.Text);
Console.WriteLine("====================================");
}
I would suggest that you spend some more time learning locators. Locators are very powerful and can save you having to stack nested loops looking for one thing... and then children of that thing... and then children of that thing... and so on. The right locator can find all that in one scrape of the page which saves a lot of code and time.
As you mentioned in related Post, this issue is because site executes an auto refresh.
Solution 1:
I would suggest if there is an explicit way to do refresh, perform that refresh on a periodic basis, or (if you are sure, when you need to do refresh).
Solution 2:
Create a Extension method for FindElement and FindElements, so that it try to get element for a given timeout.
public static void FindElement(this IWebDriver driver, By by, int timeout)
{
if(timeout >0)
{
return new WebDriverWait(driver, TimeSpan.FromSeconds(timeout)).Until(ExpectedConditions.ElementToBeClickable(by));
}
return driver.FindElement(by);
}
public static IReadOnlyCollection<IWebElement> FindElements(this IWebDriver driver, By by, int timeout)
{
if(timeout >0)
{
return new WebDriverWait(driver, TimeSpan.FromSeconds(timeout)).Until(ExpectedConditions.PresenceOfAllElementsLocatedBy(by));
}
return driver.FindElements(by);
}
so your code will use these like this:
var listItems = driver.FindElement(By.Id("odds-data-table"), 30).FindElements(By.TagName("a"),30);
Solution 3:
Handle StaleElementException using an Extension Method:
public static void FindElement(this IWebDriver driver, By by, int maxAttempt)
{
for(int attempt =0; attempt <maxAttempt; attempt++)
{
try
{
driver.FindElement(by);
break;
}
catch(StaleElementException)
{
}
}
}
public static IReadOnlyCollection<IWebElement> FindElements(this IWebDriver driver, By by, int maxAttempt)
{
for(int attempt =0; attempt <maxAttempt; attempt++)
{
try
{
driver.FindElements(by);
break;
}
catch(StaleElementException)
{
}
}
}
Your code will use these like this:
var listItems = driver.FindElement(By.Id("odds-data-table"), 2).FindElements(By.TagName("a"),2);
Use this:
string description = header.FindElement(By.XPath("strong/a")).Text;
instead of your:
string description = header.FindElement(By.TagName("a")).Text;
I am writing an application in C# that will run on a users PC and I want to list only the devices that are shown in the Windows "Devices and Printers" control panel, things like monitors, keyboard, mouse, speakers etc.
I can use WMI to extract a list of all devices, but is there a way to only extract only those that are shown in that part of control panel rather than the full list?
I have searched online and found nothing relating to this and I can't even find what is the criteria for a device to appear in that list.
Is it possible to access the list of those devices that are shown in that list or, if not, is there a filter that can be applied to the full list that will only show those devices?
thanks in advance
I do it with p/invoke and COM interop by enumerating the shell items in Microsoft.DevicesAndPrinters and filtering by making sure that PKEY_Devices_CategoryIds contains an item starting with PrintFax.
There's no way to boil the necessary interop definitions down to fit in an answer, but this is the logic I use to enumerate the display name, the DEVMODE name, and images of any size:
public sealed class PrinterInfo
{
public string IdName { get; }
public string DisplayName { get; }
public Bitmap Image { get; }
private PrinterInfo(string idName, string displayName, Bitmap image)
{
IdName = idName;
DisplayName = displayName;
Image = image;
}
public static IReadOnlyList<PrinterInfo> GetInstalledPrinterNamesAndImages(Size imageSize)
{
var r = new List<PrinterInfo>();
using (var folderIdList = CreateDevicesAndPrintersIDL())
{
var folder = GetShellFolder(folderIdList);
var enumerator = folder.EnumObjects(IntPtr.Zero, SHCONTF.NONFOLDERS);
for (;;)
{
// If you request more than are left, actualCount is 0, so we'll do one at a time.
var next = enumerator.Next(1, out var relativeIdList, out var actualCount);
next.ThrowIfError();
if (next == HResult.False || actualCount != 1) break; // printerChild is junk
using (relativeIdList)
using (var absoluteIdList = ILCombine(folderIdList, relativeIdList))
{
var shellItem = GetShellItem(absoluteIdList);
var idName = GetPrinterFriendlyNameIfPrinter(shellItem);
if (idName != null)
r.Add(new PrinterInfo(idName, GetDisplayName(shellItem), GetImage(shellItem, imageSize)));
}
}
}
return r;
}
private static ItemIdListSafeHandle CreateDevicesAndPrintersIDL()
{
SHGetKnownFolderIDList(FOLDERID.ControlPanelFolder, KF_FLAG.DEFAULT, IntPtr.Zero, out var controlPanelIdList).ThrowIfError();
using (controlPanelIdList)
{
GetShellFolder(controlPanelIdList).ParseDisplayName(IntPtr.Zero, null, "::{A8A91A66-3A7D-4424-8D24-04E180695C7A}", IntPtr.Zero, out var childDevicesAndPriversIdList, IntPtr.Zero);
using (childDevicesAndPriversIdList)
return ILCombine(controlPanelIdList, childDevicesAndPriversIdList);
}
}
private static string GetPrinterFriendlyNameIfPrinter(IShellItem2 shellItem)
{
// Devices_PrimaryCategory returns "Printers" for printers and faxes on Windows 10 but "Printers and faxes" on Windows 7.
using (var categoryIds = new PropVariantSafeHandle())
{
shellItem.GetProperty(PKEY.Devices_CategoryIds, categoryIds).ThrowIfError();
if (!categoryIds.ToStringVector().Any(id => id.StartsWith("PrintFax", StringComparison.OrdinalIgnoreCase)))
return null;
}
// The canonical or "friendly name" needed to match the devmode
// https://blogs.msdn.microsoft.com/asklar/2009/10/21/getting-the-printer-friendly-name-from-the-device-center-shell-folder/
// PKEY_Devices_InterfacePaths doesn't seem to ever be found, but PKEY_Devices_FriendlyName works so...
shellItem.GetString(PKEY.Devices_FriendlyName, out var friendlyName).ThrowIfError();
return friendlyName.ReadAndFree();
}
private static string GetDisplayName(IShellItem2 shellItem)
{
return shellItem.GetDisplayName(SIGDN.NORMALDISPLAY).ReadAndFree();
}
private static Bitmap GetImage(IShellItem2 shellItem, Size imageSize)
{
return ((IShellItemImageFactory)shellItem).GetImage(new POINT(imageSize.Width, imageSize.Height), SIIGBF.SIIGBF_BIGGERSIZEOK)
.CopyAndFree(); // Bitmap.FromHbitmap is useless with alpha, so make a copy
}
private static IShellFolder GetShellFolder(ItemIdListSafeHandle itemIdList)
{
SHBindToObject(IntPtr.Zero, itemIdList, null, typeof(IShellFolder).GUID, out var objShellFolder).ThrowIfError();
return (IShellFolder)objShellFolder;
}
private static IShellItem2 GetShellItem(ItemIdListSafeHandle itemIdList)
{
SHCreateItemFromIDList(itemIdList, typeof(IShellItem2).GUID, out var objShellItem).ThrowIfError();
return (IShellItem2)objShellItem;
}
}
(C# 7)
Here's a full demo you can compile: https://github.com/jnm2/example-devices-and-printers/tree/master/src
For a C# 6 version which doesn't require ValueTuple, see https://github.com/jnm2/example-devices-and-printers/tree/master/src-csharp6.
I'm happy to answer any questions.
Is it possible to read the publisher name of the currently running ClickOnce application (the one you set at Project Properties -> Publish -> Options -> Publisher name in Visual Studio)?
The reason why I need it is to run another instance of the currently running application as described in this article and pass parameters to it.
Of course I do know my application's publisher name, but if I hard code it and later on I decide to change my publisher's name I will most likely forget to update this piece of code.
Here is another option. Note that it will only get the publisher name for the currently running application, which is all I need.
I'm not sure if this is the safest way to parse the XML.
public static string GetPublisher()
{
XDocument xDocument;
using (MemoryStream memoryStream = new MemoryStream(AppDomain.CurrentDomain.ActivationContext.DeploymentManifestBytes))
using (XmlTextReader xmlTextReader = new XmlTextReader(memoryStream))
{
xDocument = XDocument.Load(xmlTextReader);
}
var description = xDocument.Root.Elements().Where(e => e.Name.LocalName == "description").First();
var publisher = description.Attributes().Where(a => a.Name.LocalName == "publisher").First();
return publisher.Value;
}
You would think this would be trivial, but I don't see anything in the framework that gives you this info.
If you want a hack, you can get the publisher from the registry.
Disclaimer - Code is ugly and untested...
...
var publisher = GetPublisher("My App Name");
...
public static string GetPublisher(string application)
{
using (var key = Registry.CurrentUser.OpenSubKey(#"Software\Microsoft\Windows\CurrentVersion\Uninstall"))
{
var appKey = key.GetSubKeyNames().FirstOrDefault(x => GetValue(key, x, "DisplayName") == application);
if (appKey == null) { return null; }
return GetValue(key, appKey, "Publisher");
}
}
private static string GetValue(RegistryKey key, string app, string value)
{
using (var subKey = key.OpenSubKey(app))
{
if (!subKey.GetValueNames().Contains(value)) { return null; }
return subKey.GetValue(value).ToString();
}
}
If you find a better solution, please follow-up.
I dont know about ClickOnce, but normally, you can read the assembly-info using the System.Reflection framework:
public string AssemblyCompany
{
get
{
object[] attributes = Assembly.GetExecutingAssembly().GetCustomAttributes(typeof(AssemblyCompanyAttribute), false);
if (attributes.Length == 0)
{
return "";
}
return ((AssemblyCompanyAttribute)attributes[0]).Company;
}
}
Unfortunately, theres no "publisher" custom-attribute, just throwing this out as a possible work-around