Working with late loading Frames and Script in CCF
What the Heck is that?
When a Browser page that loads multiple levels of frames, or IFrames, with or without Java script attached to those frames is called up in IE, you will get Doc Complete alerts for each of the frames and IFrames loaded into the env.
The problem is that those Frame are not really loaded yet. they are still loading and take a few more cycles to get to a point where you can access them via the DOM. CCF’s WebDDA handles this for the most part however the web application adapters do not.
SO… how do you deal with it in a web application adapter.
The key to dealing with this sort of thing is waiting on the browser to get done loading and running all the initial scripts. in other words .. its all about timing. The catch is that IE doesn't give you a hint when its done, just the doc complete event which gets called when the page is done loading into the DOM, but not when the DOM is done processing the page.
The first thing you are likely to need to do is find the right frame in any given document that has what you are looking for. So to do that we need a few generic functions hunt tags for us.
The goal of these functions is to find the right document ( frame) that has what you are looking for. You don't have to search for what you need exactly, just something on the document you are looking for.
/// <summary>
/// Find the right HTML Doc Containing a given tag
/// </summary>
protected HTMLDocument GetDocumentContainingId(HTMLDocument doc, string id)
{
// Used to check to see if the first field in the search list has been found,
// and if so, what frame it was found in.
bool bFoundRightFrame = false;
// try to find the first Field,
// if the first field is not found. check for sub frames on the page
// if sub frames found check them for the field,
// if its found on a sub frame, set the focus to that frame.
if (doc.getElementById(id) == null)
{
if (doc.frames.length > 0)
{
doc = CheckForValue(doc, id, ref bFoundRightFrame);
}
}
else
{
// Field found.
// current document remains in focus.
bFoundRightFrame = true;
}
if (!bFoundRightFrame)
{
// didn't find the first tag name.. treat as failure and abort sign on process.
System.Diagnostics.Debug.WriteLine( string.Format("Could not find the requested id in {0}", doc.url));
return null;
}
return doc;
}
The CheckForValue is a function I call repeatedly to drill though the sub docs.
/// <summary>
/// Check This frame for the tag.
/// </summary>
private static HTMLDocument CheckForValue(HTMLDocument doc, string id, ref bool bFoundRightFrame)
{
for (int i = 0; i < doc.frames.length; i++)
{
if (bFoundRightFrame)
break;
object iFrameNum = i;
HTMLWindow2Class doc2 = (HTMLWindow2Class)doc.frames.item(ref iFrameNum);
// Check for the field I want in the subframe.
HTMLDocument doc3 = (HTMLDocument)doc2.document;
if (doc3.getElementById(id) != null)
{
// subDoc Found
doc = (HTMLDocument)doc2.document;
bFoundRightFrame = true;
return doc3;
}
if (doc3.frames.length > 0 )
return CheckForValue(doc3, id, ref bFoundRightFrame);
}
return doc;
}
Ok so between those methods we can find the doc’s that got something in it.
Now we need to add some new bits to help us deal with this.
What we are going to do here is to use the DocComplete event to trigger a Threaded Timer that will eventually call our method. We can do this a few times as necessary to deal with late loading. Using the code above to tell us when we can actually access it.
In our web application adapter add as a class var;
// Holds on to the next action to trigger
private Dictionary<string, string> NextActionList = null;
This var is used to hold onto a bit of data to tell us what we are looking for and what action to trigger next.
Ok, so we are going to trigger our initial command based on an Action, Lets say its a Navigate to https://mysite/pg/page1.aspx.
Page1.aspx has several frames and an IFrame, for example we are looking for something called “CreateNote” , which lives in a dynamically loaded sidebar. We also know ( using our DOM inspector tools ) that the sidebar that contains the “CreateNote” Object is called “CommandSideBar” and that its in an IFrame.
What we want to do is get to the “CreateNote” object and click it.
Our First action just does the navigate, and it adds in a hander to the NextActionsList
// In the Web Application Adapter DoAction Method...
if (action.Name.Equals("default", StringComparison.CurrentCultureIgnoreCase))
{
if ((ctxPointer != null) && (ctxPointer.Count > 0))
{
if ( !String.IsNullOrEmpty(ctxPointer["AccountID"]) )
{
// https://mysite/pg/page1.aspx is in the action.url
string sUrl = string.Format("{0}?oId={1}&oType=1&”+ ”security=262167&tabSet=areaService", action.Url , ctxPointer["AccountID"]);
if ( NextActionList == null )
NextActionList = new Dictionary<string,string>();
NextActionList.Add("createnote" , "/page1.aspx");
Browser.Navigate(sUrl);
}
return false;
}
}
That will cause the Browser to head toward Page1.aspx.
In the Doc Complete we do this:
// Check to see if there are pending actions.
if (NextActionList != null)
{
if (NextActionList.Count > 0)
{
foreach (KeyValuePair<string,string> itm in NextActionList)
{
if (urlString.ToLower().Contains(itm.Value.ToLower()))
{
// found an item
NextActionList.Remove(itm.Key);
string sEmptyData = string.Empty;
AdapterFireRequestAction(new RequestActionEventArgs(this.Name,
"processthreadedaction", itm.Key));
}
}
}
}
Here we are looking at what came back to see if the page / URL is in the next Actions List.
if it is we then send an Action back to ourselves with the command as the data. This is simple and straight forward, however you can do some neat stuff with this.
Back in the Action Handler for the adapter.
if ( action.Name.Equals("processthreadedaction" , StringComparision.CurrentCultureIgnoreCase))
{
if ( action.data.Equals("createnote" , StringComparision.CurrentCultureIgnoreCase))
{
System.Threading.Timer tWebPusher = new System.Threading.Timer
(new System.Threading.TimerCallback(RunCreateNote), null,
TimeSpan.FromSeconds(1), TimeSpan.Zero);
return false;
}
}
Now this is sort neat.. We are using the System.Threading.Timer to create an AutoCallBack into a Method we specified without having to do a lot of other work. In this case we are telling it to wait 1 second, then call RunCreateNote.
RunCreateNote Looks like this:
private void RunCreateNote(object oData)
{
// just a pass though,
// you could get other bits from CCF here
// or add data and pass it into the process
CreateNoteProcess();
}
CreateNoteProcess is now where stuff really happens..
private delegate bool CreateNoteProcessDelg();
/// <summary>
/// Hits the Create Note Button if present
/// </summary>
private bool CreateNoteProcess()
{
// switch to main thread if necessary
if (Browser.InvokeRequired)
{
return (bool)Browser.Invoke(new CreateNoteProcessDelg(CreateNoteProcess));
}
else
{
HTMLDocument htmlDoc = Browser.Document as HTMLDocument;
htmlDoc = GetDocumentContainingId(htmlDoc, "CommandSideBar");
if (htmlDoc != null)
{
IHTMLElement crtNote = htmlDoc.getElementById("CreateNote");
if (crtNote != null)
{
IHTMLElementCollection col =
(IHTMLElementCollection)crtNote.children;
if (col != null)
{
foreach (IHTMLElement el in col)
{
if (el is IHTMLButtonElement)
{
el.click();
return true;
}
}
}
}
}
else
{
// Item not found
// You can Recall the Threaded Search again,
// or you can re add something to Next Action
}
}
return false;
}
so here we are making sure we are on the right thread to talk to the browser, Then calling the method “GetDocumentContainingId” which will give us the right frame / doc. Once we have that we execute the action we want to do. in this case we click a button.
If you don't find the root doc ( the frames not loaded or accessible yet ) you can re-fire the Timer Thread command we used to kick the Process off in the DoAction handler for processthreadaction.
This nets out to allowing you to handled a browser page that takes a while to load subpages or scripts. Iv also found that this works well for pages that embed Ajax that are on refresh timers in JavaScript.
Hopefully this will help you though handling these website types in CCF.