title: Adding nodes to xml with DOM in inno Setup - strange problems
body: <p>Very strange problem: I use the DOM to edit an xml file (a .exe.config file for an app that needs to interact with ours), but seeing as I have to bulk-add several similar sections, I made a function to insert the whole needed block.</p>
  
  <p>Calling this function once works perfectly. Calling it again with different parameters just afterwards gives an exception (see explanation below the code).</p>
  
  <p>The code:</p>
  
  <pre><code>// Split a string into an array using passed delimeter
  procedure Explode(var Dest: TArrayOfString; Text: String; Separator: String);
  var
    i: Integer;
  begin
    i := 0;
    repeat
      SetArrayLength(Dest, i+1);
      if Pos(Separator,Text) &gt; 0 then
      begin
        Dest[i] := Copy(Text, 1, Pos(Separator, Text)-1);
        Text := Copy(Text, Pos(Separator,Text) + Length(Separator), Length(Text));
        i := i + 1;
      end
      else
      begin
        Dest[i] := Text;
        Text := '';
      end;
    until Length(Text)=0;
  end;
  
  // Ensures an XPath exists, creating nodes if needed
  function EnsureXPath(const XmlDoc: Variant; XPath: string): Variant;
  var
    PathParts: TArrayOfString;
    TestNode, CurrentNode, NewNode: Variant;
    NodeList: Variant;
    i, j: Integer;
    found: Boolean;
  begin
    CurrentNode:=XMLDoc.documentElement;
    Explode(PathParts, XPath, '/');
  
    MsgBox('Array length: ' + IntToStr(GetArrayLength(PathParts)), mbInformation, MB_OK);
    for i := 0 to GetArrayLength(PathParts) - 1 do
    begin
      MsgBox('Current path part:'#13#10 + '''' + pathparts[i] + '''', mbInformation, MB_OK);
      if pathparts[i] &lt;&gt; '' then
      begin
        //MsgBox('Current node:'#13#10 + '''' + CurrentNode.NodeName + '''' + #13#10'Current path part:'#13#10 + '''' + PathParts[i] + '''' + #13#10'List length: ' + IntToStr(NodeList.Length), mbInformation, MB_OK);
        MsgBox('Current node:'#13#10 + '''' + CurrentNode.NodeName + '''', mbInformation, MB_OK);
        MsgBox('Current path part:'#13#10 + '''' + PathParts[i] + '''', mbInformation, MB_OK);
        NodeList:= CurrentNode.childNodes;
        MsgBox('List length: ' + IntToStr(NodeList.Length), mbInformation, MB_OK);
        found:=false;
        for j := 0 to NodeList.Length - 1 do
        begin
          TestNode:=NodeList.Item[j]
          if (TestNode.NodeName = pathparts[i]) then
          begin
            currentNode:= TestNode;
            found:=true;
          end;
        end;      
        if (not found) then
        begin
          newNode := XMLDoc.createElement(PathParts[i]);
          currentNode.appendChild(newNode);
          currentNode:=currentNode.lastChild;
        end;
      end;
    end;
    Result:=currentNode;
    MsgBox('Last node:'#13#10 + '''' + CurrentNode.NodeName + '''', mbInformation, MB_OK);
  end;
  
  // Seeks out a node, returning the node in "resultnode", and whether it was found as Result.
  function SeekNode(const ParentNode: Variant; var resultnode: Variant; subNodePath, attrName, attrValue :String; IsFirstCall: Boolean): Boolean;
  var
    NodesList: Variant;
    AttrNode: Variant;
    AttrList: Variant;
    Attr: Variant;
    PathParts, NewPathParts: TArrayOfString;
    i, j, truelength: Integer;
    currentPath, remainderPath: String;
    CallAgain,callResult: Boolean;
  begin
    Explode(PathParts, subNodePath, '/');
    truelength:=GetArrayLength(PathParts);
    for i:=0 to GetArrayLength(PathParts) -1 do
    begin 
      if PathParts[i] = '' then
        truelength:=truelength-1;
    end;
    if (truelength &lt;&gt; GetArrayLength(PathParts)) then
    begin
      SetArrayLength(NewPathParts, truelength);
      truelength:=0;
      for i:=0 to GetArrayLength(PathParts) -1 do
      begin 
        if PathParts[i] &lt;&gt; '' then
        begin
          NewPathParts[truelength] := PathParts[i];
          truelength:=truelength+1;
        end;
      end;
    end
    else
      NewPathParts:=PathParts;
  
    CallAgain:=GetArrayLength(NewPathParts)&gt;1;
    currentPath:=NewPathParts[0];
    remainderPath:='';
    for i:=1 to GetArrayLength(NewPathParts) -1 do
    begin
      if (remainderPath &lt;&gt; '') then
        remainderPath:=remainderPath + '/';
      remainderPath:=remainderPath + NewPathParts[i];
    end;
    NodesList:=ParentNode.childNodes;
    //MsgBox('Node count for ' + currentPath + ':'#13#10 + '''' + IntToStr(NodesList.length) + '''', mbInformation, MB_OK);
    Result:=false;
    for i := 0 to NodesList.length - 1 do
    begin
      attrNode := NodesList.Item[i];
      //MsgBox('Current node:'#13#10 + '''' + attrNode.NodeName  + ''''#13#10'Current path:'#13#10+ '''' + currentPath  + '''', mbInformation, MB_OK);
      if (attrNode.NodeName = currentPath) then
      begin
        if CallAgain then
        begin
          //MsgBox('Remainder of path:'#13#10 + '''' + remainderPath  + '''', mbInformation, MB_OK);
          callResult:=SeekNode(attrNode, resultnode, remainderPath, attrName, attrValue, false);
          if callResult then
          begin
            Result:=true;
            if IsfirstCall then
              resultnode:=attrNode;
            exit;
          end;
        end
        else
        begin
          AttrList:=attrNode.Attributes;
          //MsgBox('Node:'#13#10 + '''' + attrNode.NodeName + '''' + #13#10'Attributes count:'#13#10 + '''' + IntToStr(AttrList.Length) + '''', mbInformation, MB_OK);
          for j := 0 to AttrList.length - 1 do
          begin
            Attr:= AttrList.Item[j];
            //MsgBox('Node:'#13#10'''' + attrNode.NodeName + ''''#13#10'Attribute:'#13#10'''' + Attr.NodeName + ''''#13#10'Value:'#13#10'''' + Attr.NodeValue + ''''#13#10'To find:'#13#10'''' + AttrValue + '''', mbInformation, MB_OK);
            if (Attr.NodeName = attrName) then
            begin
              if (Attr.NodeValue = attrValue) then
              begin
                //MsgBox('Attribute found.', mbInformation, MB_OK);
                resultnode:=attrNode;
                Result:=true;
                Exit;
              end
              else
              begin
                Result:=false;
                Exit;
              end;
            end;
          end;
        end;
      end;
    end;
  end;
  
  // Use of SeekNode: Remove node
  function removeNode(const ParentNode: Variant; subNodePath, attrName, attrValue :String): Boolean;
  var
    resultNode: Variant;
  begin
    Result:=SeekNode(ParentNode, resultNode, subNodePath, attrName, attrValue, true);
    if (Result) then
      ParentNode.removeChild(resultNode);
  end;
  
  // Use of SeekNode: test node existence
  function hasNode(const ParentNode: Variant; subNodePath, attrName, attrValue :String): Boolean;
  var
    resultNode: Variant;
  begin
    Result:=SeekNode(ParentNode, resultNode, subNodePath, attrName, attrValue, true);
  end;
  
  // Adds a single assembly binding block into the xml
  procedure AddAssemblyBinding(const XmlDoc: Variant; const ParentNode: Variant; aiName, aiCulture, aiKey, brOld, brNew, cbVer, cbHref: String);
  var
    dependentAssemblyNode: Variant;
    assemblyIdentityNode: Variant;
    bindingRedirectNode: Variant;
    codeBaseNode: Variant;
    publisherPolicyNode: Variant;
  begin
  //        &lt;assemblyIdentity name="ECompas.Runtime" culture="" publicKeyToken="f27ad8cb97726f87" /&gt;
  //        &lt;bindingRedirect oldVersion="3.0.1.0 - 3.0.1.133" newVersion="3.0.1.133" /&gt;
  //        &lt;codeBase version="3.0.1.133" href="[TARGETDIR]Ecompas.Runtime.dll" /&gt;
  //        &lt;publisherPolicy apply="no"/&gt;
  
    dependentAssemblyNode:= XMLDoc.createElement('dependentAssembly');
  
    assemblyIdentityNode:= XMLDoc.createElement('assemblyIdentity');
    assemblyIdentityNode.setAttribute('name', aiName);
    assemblyIdentityNode.setAttribute('culture', aiCulture);
    assemblyIdentityNode.setAttribute('publicKeyToken', aiKey);
    dependentAssemblyNode.appendChild(assemblyIdentityNode);
  
    if ((brOld &lt;&gt; '') and (brNew &lt;&gt; '')) then
    begin
      bindingRedirectNode:= XMLDoc.createElement('bindingRedirect');
      bindingRedirectNode.setAttribute('oldVersion', brOld);
      bindingRedirectNode.setAttribute('newVersion', brNew);
      dependentAssemblyNode.appendChild(bindingRedirectNode);
    end;
  
    codeBaseNode:= XMLDoc.createElement('codeBase');
    codeBaseNode.setAttribute('version', cbVer);
    codeBaseNode.setAttribute('href', cbHref);
    dependentAssemblyNode.appendChild(codeBaseNode);
  
    publisherPolicyNode:= XMLDoc.createElement('publisherPolicy');
    publisherPolicyNode.setAttribute('apply', 'no');
    dependentAssemblyNode.appendChild(publisherPolicyNode);
  
    // Doesn't work? No idea why it gives it an xmlns while its parent already has one.
    //dependentAssemblyNode.RemoveAttribute('xmlns');
  
    // It seems the actual variables of the nodes are somehow lost after adding
    // them to a parent - so add everything to them in advance!
    ParentNode.appendChild(dependentAssemblyNode);
  end;
  
  function UpdateConfig(const AFileName, Appdir: string; delete:Boolean): boolean;
  var
    XMLDoc: Variant;
    RootNode, MainNode, AddNode: Variant;
    bECompasRuntime, bECompasMetamodel, bECompasDatabaseMS:  Boolean;
  
  begin
    try
      XMLDoc := CreateOleObject('MSXML2.DOMDocument');
    except
      RaiseException('MSXML is required to complete the post-installation process.'#13#10#13#10'(Error ''' + GetExceptionMessage + ''' occurred)');
    end;  
  
    XMLDoc.async := False;
    XMLDoc.resolveExternals := False;
    XMLDoc.load(AFileName);
  
    if XMLDoc.parseError.errorCode &lt;&gt; 0 then
    begin
      MsgBox('XML processing error:'#13#10 + XMLDoc.parseError.reason, mbInformation, MB_OK);
      Result:=False;
      exit;
    end;
    XMLDoc.setProperty('SelectionLanguage', 'XPath');
  
    RootNode:=XMLDoc.documentElement;
    if (RootNode.nodeName &lt;&gt; 'configuration') then
    begin
      MsgBox('XML processing error:'#13#10'Root element ''configuration'' not found.', mbInformation, MB_OK);
      Result:=False;
      exit;
    end;
    MainNode:=EnsureXPath(XMLDoc, 'runtime/assemblyBinding');
  
    bECompasRuntime := HasNode(MainNode,'dependentAssembly/assemblyIdentity','name','ECompas.Runtime');
    bECompasMetamodel := HasNode(MainNode,'dependentAssembly/assemblyIdentity','name','ECompas.Metamodel');
    bECompasDatabaseMS := HasNode(MainNode,'dependentAssembly/assemblyIdentity','name','ECompas.Database.MS');
  
    if (not delete) then
    begin
  
      if not bECompasRuntime then
        AddAssemblyBinding(XMLDoc, MainNode, 'ECompas.Runtime', '', 'f27ad8cb97726f87', '3.0.1.0 - 3.0.1.133', '3.0.1.133', '3.0.1.133', Appdir + '\Ecompas.Runtime.dll');
      if not bECompasMetamodel then
        AddAssemblyBinding(XMLDoc, MainNode, 'ECompas.Metamodel', '', 'f27ad8cb97726f87', '3.0.1.0 - 3.0.1.133', '3.0.1.133', '3.0.1.133', Appdir + '\Ecompas.Metamodel.dll');
      if not bECompasDatabaseMS then
        AddAssemblyBinding(XMLDoc, MainNode, 'ECompas.Database.MS', '', 'f27ad8cb97726f87', '3.0.1.0 - 3.0.1.133', '3.0.1.133', '3.0.1.133', Appdir + '\Ecompas.Database.MS.dll');
    end
    else
    begin
      removeNode(MainNode,'dependentAssembly/assemblyIdentity','name','ECompas.Runtime');
      removeNode(MainNode,'dependentAssembly/assemblyIdentity','name','ECompas.Metamodel');
      removeNode(MainNode,'dependentAssembly/assemblyIdentity','name','ECompas.Database.MS');
    end;
  
    MainNode:=EnsureXPath(XMLDoc, 'appSettings');
    if (not delete) then
    begin
      //&lt;add key="logdir" value=".\log" /&gt;
      if (not HasNode(MainNode,'add','key','logdir')) then
      begin
        AddNode:= XMLDoc.createElement('add');
        AddNode.setAttribute('key', 'logdir');
        AddNode.setAttribute('value', '.\log');
        MainNode.appendChild(AddNode);
      end;
    end
    else
    begin
      removeNode(MainNode,'add','key','logdir');
    end;
  
    XMLDoc.Save(AFileName); 
    Result:=true;
  end;
  </code></pre>
  
  <p>Originally, the <code>UpdateConfig</code> function was done like this:</p>
  
  <pre><code>if (not HasNode(MainNode,'dependentAssembly/assemblyIdentity','name','ECompas.Runtime')) then
    AddAssemblyBinding(XMLDoc, MainNode, 'ECompas.Runtime', '', 'f27ad8cb97726f87', '3.0.1.0 - 3.0.1.133', '3.0.1.133', '3.0.1.133', Appdir + '\Ecompas.Runtime.dll');
  if (not HasNode(MainNode,'dependentAssembly/assemblyIdentity','name','ECompas.Metamodel')) then
    AddAssemblyBinding(XMLDoc, MainNode, 'ECompas.Metamodel', '', 'f27ad8cb97726f87', '3.0.1.0 - 3.0.1.133', '3.0.1.133', '3.0.1.133', Appdir + '\Ecompas.Metamodel.dll');
  if (not HasNode(MainNode,'dependentAssembly/assemblyIdentity','name','ECompas.Database.MS')) then
    AddAssemblyBinding(XMLDoc, MainNode, 'ECompas.Database.MS', '', 'f27ad8cb97726f87', '3.0.1.0 - 3.0.1.133', '3.0.1.133', '3.0.1.133', Appdir + '\Ecompas.Database.MS.dll');
  </code></pre>
  
  <p>This code ran fine the first time, but the second time, it gave the aforementioned "<strong>Unknown Method</strong>" error on <code>setAttribute</code> in <code>AddAssemblyBinding</code>. It got more bizarre... when I removed the three lines setting the attributes to assemblyIdentityNode, the rest of the code DID run fine for the other nodes.</p>
  
  <p>The only thing I could imagine is related is that these are the nodes I query in the <code>HasNode</code> function to see if the block already exists. Can DOM not handle querying through unsaved changes? So I edited the code to do the existence checks in advance and store the result in Booleans, because I thought maybe the problem was seeking the nodes on a modified tree. But now it gives an error about trying to nest a node under itself or its own child (<strong>"msxml3.dll: Inserting a Node or its ancestor under itself is not allowed"</strong>), on the <code>dependentAssemblyNode.appendChild(bindingRedirectNode);</code> line. Neither of these errors makes any sense whatsoever.</p>
  
  <p>I seem to get loads more like it. <code>EnsureXPath</code>, when used a second time in a situation where it had to add nodes, also gave the illegal nesting error. I get the feeling that somehow, the object mysteriously becomes null somewhere, and that that null is seen as the root node in functions handling node objects.</p>
  
  <p>Does anyone have any clue what may be causing this behaviour?</p>
  
  <p>The XML I'm editing typically looks like this:</p>
  
  <pre><code>&lt;?xml version="1.0" encoding="utf-8"?&gt;
  &lt;configuration&gt;
      &lt;runtime&gt;
          &lt;assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"&gt;
              &lt;dependentAssembly&gt;
                  &lt;assemblyIdentity name="AppString1" publicKeyToken="43265234666" culture="neutral"/&gt;
                  &lt;bindingRedirect oldVersion="1.0.0.0-1.1.99.99" newVersion="1.2.0.0"/&gt;
              &lt;/dependentAssembly&gt;
              &lt;dependentAssembly&gt;
                  &lt;assemblyIdentity name="AppString2" publicKeyToken="43265234666" culture="neutral"/&gt;
                  &lt;bindingRedirect oldVersion="1.0.0.0-1.1.99.99" newVersion="1.2.0.0"/&gt;
              &lt;/dependentAssembly&gt;
          &lt;/assemblyBinding&gt;
      &lt;/runtime&gt;
  &lt;/configuration&gt;
  </code></pre>
  
  <p>(with some more of these dependentAssembly sections... but that hardly matters)</p>