Ridiculous Javascript/HTML and infopath stuff



  • So I have a coworker who needs to work with a particular sharepoint form very regularly. Much of this data is repeated because they are filing things like org changes.

    Due to politics, and general technology implementations, this is a completely manual process right now, and I REALLY feel it shouldn't be.

    So I'm trying to investigate how to post information to an infopath form, and I was able to create a local template that fit the needs -- except when it posts, you have to manually open the infopath form, it won't open from the sharepoint page (the version doesn't match, even though I did some version hacking to make it the same version.)

    So I decided to try another approach, I have a c# app that has a web browser control, and that web browser control loads the form.

    This particular form has a 'select type of request' field dropdown which shows/hides information based on what is selected.

    So here's what I ended up with to manually select the dropdown field and force the javascript to perform the page refresh:

    private static void Getdata(WebBrowser browser)
            {
                
                const string target = "http://target.target/Forms/template.xsn&OpenIn=browser";
                var request = (HttpWebRequest)WebRequest.Create(target);
                request.Credentials = CredentialCache.DefaultCredentials;
                
                browser.Navigate(target);
                browser.DocumentCompleted += HandleData;
                Debug.Print(@"Done");
            }
    
    private static void HandleData(object sender, EventArgs e)
            {
                var b = (WebBrowser) sender;
                b.Document.GetElementById("V1_I1_S3_I1_D1").Children[1].SetAttribute("selected", "selected"); //New Employee Provisioning Request
                b.Document.GetElementById("V1_I1_S3_I1_D1").Children[1].SetAttribute("value", "1");
                b.Document.GetElementById("V1_I1_S3_I1_D1").InvokeMember("onchange"); //Force form switch
    
                b.Document.GetElementById("V1_I1_S4_I1_T1").SetAttribute("value", "Test Name");
    }
    
    

    The problem is once it switches the page, and I attempt to set the 'FirstName' value, I get system.nullreference exceptions as if it can't see the new field.

    There's also this disgustingly gigantic javascript object which contaminates every field,

    "First Name" input box

    <input type="text" onfocus="return (TextBox.OnFocus(this, event));" onblur="return (TextBox.OnBlur(this, event));" onclick="return (TextBox.OnClick(this, event));" onkeypress="return (TextBox.OnKeyPress(this, event));" onmousedown="return (TextBox.OnMouseDown(this, event));" onmouseup="return (TextBox.OnMouseUp(this, event));" onpropertychange="return (TextBox.OnPropertyChange(this, event));" oninput="return (TextBox.OnInput(this, event));" onmouseover="return (TextBox.OnMouseOver(this, event));" onmouseout="return (TextBox.OnMouseOut(this, event));" id="V1_I1_S4_I1_T1" scriptclass="TextBox" class="n_ fk_ fl_ " wrapped="true" direction="ltr" viewdatanode="7" tabindex="1" title="" value="" style="position: relative">
    

    Snipped javascript object

    var g_objCurrentFormData = [[],[1,[[[2,[0,["","","",-1],[[3,["",["","","",-1],[],true,false,false],-1,false,false],[4,["Request",["Request","","",-1],[],false,true,false],-1,false,false],[5,[[[6,[0,["","","",-1],[[7,["Select...",["Select...","","",1], !!!!!!!SNIP!!!!
    
    ,[215,["FirstName",["FirstName","","",-1],[],false,false,false],-1,false,false]],false],-1,false,false]
    

    I'm not sure if the above is directly related, or if it's just pulling from my profile information.

    All I want to do is pre-populate the first name field and I can extrapolate that via copy pasta to the rest of the fields I need to pre-populate!

    I'm sorry you had to see this. I understand if you hate me forever for the blight I've bestowed upon your soul.

    I'm also open to alternatives, jquery, access apps, actual integration of infopath forms (without being able to upload templates due to politics)

    I'm listening!

    (Why am I here instead of SO? I tried using SO answers. They didn't work.)

    I've also tried .innertext on that element.



  • I think you're overthinking.

    InfoPath forms are just XML files, saved into some folder inside Sharepoint. Get access to the folder, then write a quick and dirty C# app that opens it as a network folder, goes through all of the XMLs and does whatever with the data.

    EDIT: Nevermind I misread your question. You're trying to auto-fill the InfoPath forms. I'd recommend a browser add-in, but Chrome recently made it impossible to distribute one without using their web store, and Firefox seems to have given up entirely on their Jetpack tool.



  • Yeah, I already considered the browser app route, unfortunately our work also locks these types of things out in such a way you can't even go to the chrome app store (proxy blocks it)

    The c# app is actually just me managing the IE browser since it at least plays decently well with the form. (I'm pretending the C# app is a plugin - and I swear, I'm not above sendkeys at this point.)

    Maybe it's just trying to execute the second bit too quickly, is there a way to force it to wait for the javascript to complete the page update? (NOT document complete, but javascript complete)



  • You can just thread.sleep. I doubt there's an intelligent way to do it.



  • That's what I was afraid of. I just started working on the backgroundworker... lol



  • Well you're talking less than 250ms, unless there's a network interaction. I'd just temporarily disable your GUI during the sleep(). BackgroundWorker, while technically correct, is probably overkill for an operation like this.



  • I wish I knew javascript better, I feel like this should be significantly easy to automate and I'm just doing it quite wrong.

    You are right though, I was making it too complicated.

    I'm just going to make it button based, with instructions "Click this button and wait for it to load, then click this button"

    This will still result in the desired speedup, without taking a long time for me to implement.

    So still good advice @blakeyrat.



  • So apparently on said form, I can set all the items to default expected values and check checkboxes... but if you click on a checkbox that changes the show/hide stuff, it wipes out all of the selected data that I prepopulate.

    What the flying fuck.



  • Does it do a postback?



  • Every element appears to have all On(xxx) hooked to events, and I think it does do a postback. Any idea how to simulate sending the correct postback to the form using the webbrowser control? When I use the invokemethod("click") it's not actually highlighting the checkbox, that visual change only appears when I set the attribute to checked.

    Fiddler indicates:

    /Postback.FormServer.aspx
    Requests/Forms/template.xsn;http://url/template.xsn;;http://url.... (repeats like 5 times with some encoded data at the very end



  • Postbacks are a bitch. I'm not sure why invokemethod isn't working, fixing that would be your best bet.



  • Discourse keeps giving me a 503 error when trying to post the stuff in the link, @PJH


  • Sorry pastebin is blocked by my work's filter (for... reasons? It's listed as "file sharing", weird.)



  • Hah, the irony, pastebin.ca is blocked at mine.

    I actually got the click to work (I think) by loading the template directly instead of through the form server. I'm going to play with this a bit and see if it works.

    Thanks for taking time out and reading this garbage crazy shit. Just trying to make a coworkers life easier.


  • Discourse touched me in a no-no place

    @Matches said:

    Discourse keeps giving me a 503 error when trying to post the stuff in the link, @PJH

    No repro...

    http://what.thedailywtf.com/t/code-test/2037



  • Can you do a directed reply @blakeyrat using the attached stuff?


  • Discourse touched me in a no-no place

    <input onclick="return (CheckBox.OnClick(this, event));" onfocus="return (CheckBox.OnFocus(this, event));" onblur="return (CheckBox.OnBlur(this, event));" onmouseover="return (CheckBox.OnMouseOver(this, event));" onmouseout="return (CheckBox.OnMouseOut(this, event));" id="V1_I1_S13_I1_C1" scriptclass="CheckBox" class="l_ fj_ " wrapped="true" direction="ltr" viewdatanode="56" tabindex="0" title="" checked="" type="checkbox">
    

    Which I'm trying to call via

    SharepointRequest.Document.GetElementById("V1_I1_S13_I1_C1").InvokeMember("click");
    


  • I seem to remember that IE had a lot of issues with sendEvent (which is what I assume InvokeMember is doing behind-the-scenes there...), but I'm sure those have been fixed by now. http://stackoverflow.com/questions/19044659/c-sharp-webbrowser-control-form-submit-not-working-using-invokememberclick This SO page implies it works correctly in IE10, at least.

    Sorry I think you've reached the limit of my knowledge on this.



  • Which is useful, because our work runs IE8 :D



  • That honestly could be your problem. Do you have a way to test on a current IE?



  • No, but even if I could, it would still not be useful because my coworker doesn't have priv to update IE due to security restrictions (non admin)

    But I think I've got it as good as it's going to get... the question is whether or not it will work when submitted. (the local form looks correct, but the submit button is a javascript monster too)



  • In case anybody stumbles across this in the future, I think I've determined the best* method to create a front end auto populating tool for infopath forms served over sharepoint.

    YMMV and if you have access to the site to create an actual template, or access to infopath query language, or access to.. well, you get the idea. But if your requirement is 'I want to have an autopopulate form, and politics prevents it' then this is what I used.

    private void button5_Click(object sender, EventArgs e)
            {
                var worker = new BackgroundWorker();
                worker.DoWork += Populate;
                worker.RunWorkerAsync();
            }
    

    Note: In the below, the V1_I1... stuff is the auto generated infopath form item element id.

    private void Populate(object sender, DoWorkEventArgs e)
            {
                this.Invoke((MethodInvoker)(() => SharepointRequest.Document.GetElementById("V1_I1_S13_I1_C1").InvokeMember("click")));
                Thread.Sleep(500);
                this.Invoke((MethodInvoker)(() => SharepointRequest.Document.GetElementById("V1_I1_S15_I1_C8").InvokeMember("click")));   
            }
    

    All items should call this type of method as well, as infopath hijacks all field elements. (Basically, required fields don't get updated until you call the onblur, from what I've seen)

    Maybe it will help someone in the future. I hope not. I hope I'm the only one who has to deal with this type of garbage setup.



  • @Matches said:

    I wish I knew javascript better, I feel like this should be significantly easy to automate and I'm just doing it quite wrong.

    You are right though, I was making it too complicated.

    I'm just going to make it button based, with instructions "Click this button and wait for it to load, then click this button"


    Could you do this by a bookmarklet (rather than a browser plug-in)?

    (A bookmarklet is basically a URL starting with "javascript:" as the protocol and some JS-Code following. Make sure your JS-Code is URL-escaped. For longer scripts, exceeding the range of a URL, the common pattern is to inject a script tag that would load and execute an external script. Add this to your browser's bookmarks bar and fly ...)

    Pattern (injecting an external script)

    A) Bookmarklet

    "javascript:" + escape(
    
      (function() { // create and call an anonymous function (private scope)
         var url='path to my script (may be on local drive)';
         // create a script tag and set the src-attribute
         var s=document.createElement('script');
         s.src=url;
         // inject it into the page's header
         document.getElementsByTagName('head')[0].appendChild(s);
      })();
    
    );
    

    This would result in a (single line) string like

    javascript:(function(){var%20url='mypath',s=document.createElement ('script');s.src=url;document.getElementsByTagName('head')[0].appendChild(s);})();
    

    In your "real script" you could prompt for a name and populate the desired form fields (in case the ids are unknown or dynamically generated, refer to them by index):

    B) The script

    // put it in a function expression again to not mess with the global name space
    (function() {
      // get the name
      var name = window.prompt('Name:');
      if (!name) return; // return on cancel or empty
      // get the desired fields, define them per index in an array
      // (please mind that any form element counts as an input,
      //  there might also be hidden ones)
      var toPopulate = [ 0, 4, 5, 9 ];
      var elements = document.getElementsByTagName('input');
      // loop over them and set the value
      for (var i = 0; i < toPopulate.length; i++) {
        var element = elements[toPopulate[i]];
        if (element && element.type == 'text')  {
          element.value = name;
          // optionally:
          // you might have to trigger a handler in order to keep things in sync
          // here first focus the element, then fire a blur event
          // (this would trigger JS-stuff that would be called by an onblur-handler)
          if (element.focus) {
            element.focus();
            if (document.createEvent) {
              // standard browser (most compatible approach)
              var evt = document.createEvent('UIEvent');
              evt.initEvent( 'blur', false, false );
              element.dispatchEvent(evt);
            }
            else if (document.createEventObject) {
              // IE (pre 9)
              var evt = document.createEventObject();
              element.fireEvent('onblur', evt);
            }
          }
        }
      }
    })();
    

    Now, you could load the form, click your bookmarklet and enter the name in the prompt in order to pre-populate the form ...
    (If you meet some troubles adding the bookmarklet, you could put the bookmarklet-string in the href-attribute of a link, load this into your browser and drag the link to the bookmark bar.)



  • Timely advice.



  • Unfortunately even though the data is on the form, it still didn't work. Apparently even though the required star went away I missed a magic set for the textbox fields, and I have no idea which event is the issue. So ultimately this whole thing was a failure and a waste of time that I can't dedicate any more to.

    Sucks, really. I still blame infopath for using at least an equal amount of js as discourse, without the json.


  • ♿ (Parody)

    @blakeyrat said:

    Timely advice.

    Discourse coldmap strikes again?



  • @Matches said:

    Unfortunately even though the data is on the form, it still didn't work. Apparently even though the required star went away I missed a magic set for the textbox fields, and I have no idea which event is the issue. So ultimately this whole thing was a failure and a waste of time that I can't dedicate any more to.

    Sucks, really. I still blame infopath for using at least an equal amount of js as discourse, without the json.

    So, if you would have nerves for a last try, here's a version emulating the whole editing process.
    (Steps are executed procedurally in a timeout queue, so there will be first a focus event, then the value changes, then there will be the blur event, for each element after the other.)

    (function() {
      // returns a function to emulate editing
      function edit(element, value) {
         return function() { element.value = value; };
      }
      // returns a function to dispatch an event
      function dispatch(element, etype) {
         if (document.createEvent) {
            // standard browser (most compatible approach)
            return function() {
               var evt = window.document.createEvent('UIEvent');
               evt.initEvent( etype, false, false );
               element.dispatchEvent(evt);
            };
         }
         else if (document.createEventObject) {
            // IE (pre 9)
            return function() {
               var evt = window.document.createEventObject();
               element.fireEvent('on'+etype, evt);
            };
         }
      }
      // a timeout counter
      var delay = 10;
      // get the name
      var name = window.prompt('Name:');
      if (!name) return; // return on cancel or empty
      // get the desired fields, define them per index in an array
      var toPopulate = [ 0, 4, 5, 9 ];
      var elements = document.getElementsByTagName('input');
      // loop over them and set the value
      for (var i = 0; i < toPopulate.length; i++) {
         var element = elements[toPopulate[i]];
         if (element && element.type == 'text')  {
            setTimeout( dispatch(element, 'focus'), delay++ );
            setTimeout( edit(element, name), delay++ );
            // setTimeout( dispatch(element, 'propertychange'), delay++ );
            setTimeout( dispatch(element, 'blur'), delay++ );
         }
      }
    })();
    

    Fell free to experiment by adding or discarding any steps/events.
    (This should be the equivalent to tabbing through the form fields and editing the fields manually.)


Log in to reply