public FormMoveQuestion(Office.CustomXMLPart answersPart, List<Word.ContentControl> relevantRepeats, questionnaire questionnaire, string questionID, XPathsPartEntry xppe) { InitializeComponent(); this.textBoxQuestion.Text = questionnaire.getQuestion(questionID).text; this.controlQuestionVaryWhichRepeat1.init(answersPart, relevantRepeats, questionnaire, questionID, xppe); }
public FormRepeat(Office.CustomXMLPart questionsPart, Office.CustomXMLPart answersPart, Model model, Word.ContentControl cc) { InitializeComponent(); this.model = model; this.cc = cc; this.questionsPart = questionsPart; questionnaire = new questionnaire(); questionnaire.Deserialize(questionsPart.XML, out questionnaire); this.answersPart = answersPart; Office.CustomXMLNodes answers = answersPart.SelectNodes("//oda:repeat"); foreach (Office.CustomXMLNode answer in answers) { this.answerID.Add(CustomXMLNodeHelper.getAttribute(answer, "qref")); // ID } // Suggest ID .. the idea is that // the question ID = answer ID. // this.ID = generateId(); xppe = new XPathsPartEntry(model); // List of repeat names, for re-use purposes // (we need a map from repeat name (shown in the UI) to repeat id, // which is xppe.xpathId). // What is it that distinguishes a repeat from any other question? // Answer: The fact that the XPath pointing to it ends with oda:row // Only show repeats which are in scope (since this repeat is not allowed elsewhere) // - if no ancestor repeat, then top level repeats. // - if there is an ancestor repeat, then just those which are direct children of it. Word.ContentControl rptAncestor = RepeatHelper.getYoungestRepeatAncestor(cc); String scope = "/oda:answers"; if (rptAncestor != null) { // find its XPath scope = xppe.getXPathByID( (new TagData(rptAncestor.Tag)).getRepeatID() ).dataBinding.xpath; } repeatNameToIdMap = new Dictionary<string, string>(); foreach (xpathsXpath xpath in xppe.getXPaths().xpath) { if (xpath.dataBinding.xpath.EndsWith("oda:row")) { if (isRepeatInScope(scope, xpath.dataBinding.xpath)) { // the repeat "name" is its question text. // Get that. question q = questionnaire.getQuestion(xpath.questionID); repeatNameToIdMap.Add(q.text, xpath.id); } } } // Populate the comboBox foreach(KeyValuePair<String,String> entry in repeatNameToIdMap) { this.comboBoxRepeatNames.Items.Add(entry.Key); } }
public void init( Office.CustomXMLPart answersPart, List<Word.ContentControl> relevantRepeats, questionnaire questionnaire, string questionID, XPathsPartEntry xppe) { List<Office.CustomXMLNode> commonAncestors =null; foreach (Word.ContentControl repeat in relevantRepeats) { log.Info("considering relevantRepeat cc " + repeat.ID + " " + repeat.Tag); TagData repeatTD = new TagData(repeat.Tag); string repeatXPathID = repeatTD.getRepeatID(); xpathsXpath repeatXP = xppe.getXPathByID(repeatXPathID); Office.CustomXMLNode repeatNode = answersPart.SelectSingleNode(repeatXP.dataBinding.xpath).ParentNode; if (commonAncestors == null) { // First entry, so init // Make a list of the ancestors of the // first repeat. commonAncestors = new List<Microsoft.Office.Core.CustomXMLNode>(); commonAncestors.Add(repeatNode); log.Info("Added to common ancestors " + CustomXMLNodeHelper.getAttribute(repeatNode, "qref")); addAncestors(commonAncestors, repeatNode); } else { // cross off that list anything // which isn't an ancestor of the other repeats. List<Microsoft.Office.Core.CustomXMLNode> whitelist = new List<Microsoft.Office.Core.CustomXMLNode>(); whitelist.Add(repeatNode); addAncestors(whitelist, repeatNode); removeNonCommonAncestor(commonAncestors, whitelist); } if (commonAncestors.Count == 0) break; } if (commonAncestors == null) { commonAncestors = new List<Microsoft.Office.Core.CustomXMLNode>(); } // Is it OK where it is? // Yes - if it is top level log.Debug(questionID + " --> " + xppe.getXPathByQuestionID(questionID).dataBinding.xpath); // eg /oda:answers/oda:repeat[@qref='rpt1']/oda:row[1]/oda:answer[@id='qa_2'] OkAsis = (xppe.getXPathByQuestionID(questionID).dataBinding.xpath.IndexOf("oda:repeat") < 0); Microsoft.Office.Core.CustomXMLNode currentPos = null; // so we can highlight existing choice // Yes - if it is a child of common ancestors if (OkAsis) { log.Debug("its top level"); } else { foreach (Microsoft.Office.Core.CustomXMLNode currentNode in commonAncestors) { Microsoft.Office.Core.CustomXMLNode selection = currentNode.SelectSingleNode("oda:row[1]/oda:answer[@id='" + questionID + "']"); if (selection != null) { log.Debug("found it"); OkAsis = true; currentPos = currentNode; break; } } } // Now make the tree from what is left in commonAncestors root = new TreeNode("Ask only once"); this.treeViewRepeat.Nodes.Add(root); TreeNode thisNode = null; TreeNode previousNode = null; treeViewRepeat.HideSelection = false; // keep selection when focus is lost TreeNode nodeToSelect = null; foreach (Microsoft.Office.Core.CustomXMLNode currentNode in commonAncestors) { // Find the question associated with this repeat string rptQRef = CustomXMLNodeHelper.getAttribute(currentNode, "qref"); //currentNode.Attributes[1].NodeValue; question q = questionnaire.getQuestion(rptQRef); if (q == null) { log.Error("no question with id " + rptQRef); } thisNode = new TreeNode(q.text); thisNode.Tag = rptQRef; if (currentNode == currentPos) { nodeToSelect = thisNode; } if (previousNode == null) { // Check the innermost (may be overridden below by what level user already had, if possible) this.treeViewRepeat.SelectedNode = thisNode; } else { thisNode.Nodes.Add(previousNode); } previousNode = thisNode; } if (thisNode != null) { root.Nodes.Add(thisNode); } treeViewRepeat.ExpandAll(); if (nodeToSelect != null) { this.treeViewRepeat.SelectedNode = nodeToSelect; originalValue = thisNode; } else if (OkAsis) { originalValue = root; this.treeViewRepeat.SelectedNode = root; } }
/// <summary> /// When question is first being added, it is being added to a particular CC, /// so we only need to consider that CC's ancestors. /// /// When a question is being edited, it could be in several content controls, /// so that case is handled by the more generic method. /// </summary> /// <param name="cc"></param> /// <param name="questionnaire"></param> /// <param name="xppe"></param> public void init(Word.ContentControl cc, questionnaire questionnaire, XPathsPartEntry xppe) { root = new TreeNode("Ask only once"); Word.ContentControl currentCC = cc.ParentContentControl; TreeNode thisNode = null; TreeNode previousNode = null; treeViewRepeat.HideSelection = false; // keep selection when focus is lost while (currentCC != null) { if (currentCC.Tag.Contains("od:repeat")) { TagData td = new TagData(currentCC.Tag); string ancestorRepeatXPathID = td.getRepeatID(); // Find associated question xpathsXpath xp = xppe.getXPathByID(ancestorRepeatXPathID); question q = questionnaire.getQuestion(xp.questionID); thisNode = new TreeNode(q.text); thisNode.Tag = ancestorRepeatXPathID; if (previousNode == null) { // Check the innermost treeViewRepeat.SelectedNode = thisNode; } else { thisNode.Nodes.Add(previousNode); } previousNode = thisNode; } currentCC = currentCC.ParentContentControl; } if (thisNode == null) { // Hide the control // this.groupBoxRepeat.Visible = false; root = null; } else { root.Nodes.Add(thisNode); this.treeViewRepeat.Nodes.Add(root); treeViewRepeat.ExpandAll(); } }
/// <summary> /// /// </summary> /// <param name="target"></param> /// <param name="setSourceAttr">When saving a building block, we want to write the ID of the source part</param> /// <param name="setBindingStore">When re-using a building block, storeItemID to write to the xpath; otherwise null</param> /// <param name="overwriteExisting">If re-using a building block back into original source, we want to skip silently. /// When going the other way, we want to overwrite the logic in any existing building block (since /// it may have been updated).</param> public void injectLogic(Model targetModel, bool setSourceAttr, bool setBindingStore, bool overwriteExisting) { //Model targetModel = Model.ModelFactory(target); // XPaths XPathsPartEntry targetXppe = new XPathsPartEntry(targetModel); string sourceAttr = null; if (setSourceAttr) { sourceAttr = srcXPathsPart.Id; } string answersPartStoreID = null; if (setBindingStore) { answersPartStoreID = targetModel.answersPart.Id; } // .. add em foreach (xpathsXpath xp in BBxpaths) { xpathsXpath existing = targetXppe.getXPathByID(xp.id); if (existing == null) { injectLogicXPath(targetXppe, xp, sourceAttr, answersPartStoreID); } else { // Does it come from this doc? //log.Debug("xp.source: " + xp.source); //log.Debug("existing.source: " + existing.source); //log.Debug("targetModel.xpathsPart.Id: " + targetModel.xpathsPart.Id); if (xp.source != null && xp.source.Equals(targetModel.xpathsPart.Id)) { // yes .. if (overwriteExisting) { injectLogicXPath(targetXppe, xp, sourceAttr, answersPartStoreID); } else { continue; } } else if (xp.source != null && existing.source != null && xp.source.Equals(existing.source)) { // It has already been copied in. // so don't do it again, whether we're copying to template // (could go either way, but for now, only update from original source), // or into docx continue; } else { // Yikes! ID collision throw new BuildingBlockLogicException("XPath with ID " + xp.id + " is already present."); } } } // Questions questionnaire targetQuestionnaire = new questionnaire(); questionnaire.Deserialize(targetModel.questionsPart.XML, out targetQuestionnaire); // .. add em foreach (question q in BBquestions) { question existing = targetQuestionnaire.getQuestion(q.id); if (existing == null) { targetQuestionnaire.questions.Add(q); if (setSourceAttr) { q.source = srcQuestionsPart.Id; } } else { // Does it come from this doc? //log.Debug("q.source: " + q.source); //log.Debug("existing.source: " + existing.source); //log.Debug("targetModel.questionsPart.Id: " + targetModel.questionsPart.Id); if (q.source != null && q.source.Equals(targetModel.questionsPart.Id)) { // yes .. if (overwriteExisting) { targetQuestionnaire.questions.Add(q); // this is a HashSet, so we're overrwriting, not adding :-) if (setSourceAttr) { q.source = targetModel.questionsPart.Id; } } else { continue; } } else if (q.source != null && existing.source != null && q.source.Equals(existing.source)) { // It has already been copied in. continue; } else { // Yikes! ID collision throw new BuildingBlockLogicException("Question with ID " + q.id + " is already present."); } } } // Answers answers targetAnswers = new answers(); answers.Deserialize(targetModel.answersPart.XML, out targetAnswers); foreach (answer a in BBanswers) { answer existing = getAnswer(targetAnswers, a.id); if (existing == null) { targetAnswers.Items.Add(a); if (setSourceAttr) { a.source = srcAnswersPart.Id; } } else { // Does it come from this doc? if (a.source != null && a.source.Equals(targetModel.answersPart.Id)) { log.Debug("source is this part"); // yes .. if (overwriteExisting) { log.Debug(".. and overwriting.."); targetAnswers.Items.Add(a); // this is a HashSet, so we're overrwriting, not adding :-) if (setSourceAttr) { a.source = srcAnswersPart.Id; } } else { continue; } } else if (a.source != null && existing.source != null && a.source.Equals(existing.source)) { // It has already been copied in. log.Debug("this logic already present"); continue; } else { // Yikes! ID collision throw new BuildingBlockLogicException("Answer with ID " + a.id + " from different source is already present."); } } } foreach (repeat r in BBrepeats) { repeat existing = getRepeat(targetAnswers, r.qref); if (existing == null) { targetAnswers.Items.Add(r); if (setSourceAttr) { r.source = srcAnswersPart.Id; } } else { // Does it come from this doc? if (r.source != null && r.source.Equals(targetModel.answersPart.Id)) { // yes .. if (overwriteExisting) { targetAnswers.Items.Add(r); // this is a HashSet, so we're overrwriting, not adding :-) if (setSourceAttr) { r.source = srcAnswersPart.Id; } } else { continue; } } else if (r.source != null && existing.source != null && r.source.Equals(existing.source)) { // It has already been copied in. continue; } else { // Yikes! ID collision throw new BuildingBlockLogicException("Answer with ID " + r.qref + " is already present."); } } } // Conditions conditions targetConditions = new conditions(); conditions.Deserialize(targetModel.conditionsPart.XML, out targetConditions); foreach (condition c in BBconditions) { condition existing = getCondition(targetConditions, c.id); if (existing == null) { targetConditions.condition.Add(c); if (setSourceAttr) { c.source = srcConditionsPart.Id; } } else { // Does it come from this doc? if (c.source != null && c.source.Equals(targetModel.conditionsPart.Id)) { // yes .. if (overwriteExisting) { targetConditions.condition.Add(c); // this is a HashSet, so we're overrwriting, not adding :-) if (setSourceAttr) { c.source = targetModel.conditionsPart.Id; } } else { continue; } } else if (c.source != null && existing.source != null && c.source.Equals(existing.source)) { // It has already been copied in. continue; } else { // Yikes! ID collision throw new BuildingBlockLogicException("Condition with ID " + c.id + " is already present."); } } } // .. save: we only save if there have been no ID collisions. // Otherwise, we will have aborted with a BuildingBlockLogicException targetXppe.save(); CustomXmlUtilities.replaceXmlDoc(targetModel.questionsPart, targetQuestionnaire.Serialize()); CustomXmlUtilities.replaceXmlDoc(targetModel.conditionsPart, targetConditions.Serialize()); CustomXmlUtilities.replaceXmlDoc(targetModel.answersPart, targetAnswers.Serialize()); }
public void buttonRepeat_Click(Office.IRibbonControl control) { Word.Document document = Globals.ThisAddIn.Application.ActiveDocument; FabDocxState fabDocxState = (FabDocxState)Globals.ThisAddIn.Application.ActiveDocument.GetVstoObject(Globals.Factory).Tag; Model model = fabDocxState.model; Office.CustomXMLPart answersPart = model.answersPart; //.userParts[0]; // TODO: make this better XPathsPartEntry xppe = new XPathsPartEntry(model); // used to get entries questionnaire questionnaire = new questionnaire(); questionnaire.Deserialize(model.questionsPart.XML, out questionnaire); Microsoft.Office.Interop.Word.Range rng = document.ActiveWindow.Selection.Range; // Are there any content controls in the selection? Word.ContentControls ccs = rng.ContentControls; // First identify nested repeats List<Word.ContentControl> nestedRepeats = new List<Word.ContentControl>(); foreach (Word.ContentControl desc in ccs) { if (desc.Tag.Contains("od:repeat")) { nestedRepeats.Add(desc); } } // questions contains questions wrapped by repeat, // but not nested inside another repeat List<question> questions = new List<question>(); foreach (Word.ContentControl desc in ccs) { if (desc.Tag.Contains("od:repeat")) { continue; // will handle repeats later } // exclude if in nested repeat, since // this question will have previously been dealt with // (ie it already varies with that repeat, or the // use has said they don't want it to) if (isInside(nestedRepeats, desc)) { continue; } //log.Warn("got a desc with tag " + desc.Tag); // Get the tag if (desc.Tag.Contains("od:xpath")) { TagData td = new TagData(desc.Tag); string xpathID = td.getXPathID(); //log.Warn("xpath is " + xpathID); xpathsXpath xp = xppe.getXPathByID(xpathID); log.Warn("qid is " + xp.questionID); question q = questionnaire.getQuestion(xp.questionID); if (q == null) { log.Error("Consistency issue: couldn't find question {0} used in xpath {1}", xp.questionID, xpathID); } else { questions.Add(q); } } else if (desc.Tag.Contains("od:condition")) { // TODO: find questions inside conditions } } if (questions.Count > 0) { // Rule: Only questions which aren't used elsewhere can vary in a repeat. // Check that none of the questions that will be // inside the repeat are also used outside of it. List<question> questionsUsedOutside = new List<question>(); foreach (Word.ContentControl ccx in document.ContentControls) { if (isListed(ccs, ccx)) { // this control is inside the repeat } else { // its outside, so look at its question // TODO: conditions, repeats if (ccx.Tag.Contains("od:xpath")) { TagData td = new TagData(ccx.Tag); string xpathID = td.getXPathID(); //log.Warn("xpath is " + xpathID); xpathsXpath xp = xppe.getXPathByID(xpathID); //log.Warn("qid is " + xp.questionID); question q = questionnaire.getQuestion(xp.questionID); if (q == null) { log.Error("Consistency issue: couldn't find question {0} used in xpath {1}", xp.questionID, xpathID); } else { if (questions.Contains(q)) { questionsUsedOutside.Add(q); } } } } } // foreach // If they are, they can't vary in repeat. Get the user to OK this. if (questionsUsedOutside.Count == 0) { log.Info("None of the questions in wrapping repeat are used elsewhere"); } else { log.Info(questionsUsedOutside.Count + " of the questions in wrapping repeat are used elsewhere"); DialogResult dresult = MessageBox.Show( questionsUsedOutside.Count + " of the questions here are also used elsewhere. If you continue, these won't vary in each repeat.", "Questions used elsewhere", MessageBoxButtons.OKCancel); if (dresult == DialogResult.OK) { // Just remove them from the list foreach (question qx in questionsUsedOutside) { questions.Remove(qx); } } else { log.Info("User cancelled wrapping repeat coz questions used elsewhere"); return; } } } // Create control Word.ContentControl wrappingRepeatCC = null; object oRng = rng; try { fabDocxState.inPlutextAdd = true; wrappingRepeatCC = document.ContentControls.Add(Word.WdContentControlType.wdContentControlRichText, ref oRng); //cc.MultiLine = true; // Causes error for RichText } catch (System.Exception ex) { log.Warn(ex); MessageBox.Show("Selection must be either part of a single paragraph, or one or more whole paragraphs"); fabDocxState.inPlutextAdd = false; return; } FormRepeat formRepeat = new FormRepeat(model.questionsPart, answersPart, model, wrappingRepeatCC); formRepeat.ShowDialog(); string repeatId = formRepeat.ID; formRepeat.Dispose(); // necessary here? shouldn't be.. //answersPart.NamespaceManager.AddNamespace("answers", "http://opendope.org/answers"); // Destination for moves Office.CustomXMLNode destination = answersPart.SelectSingleNode("//oda:repeat[@qref='" + repeatId + "']/oda:row"); if (destination == null) { log.Error("no rpt node " + repeatId); } Dictionary<string, string> xpathChanges = new Dictionary<string, string>(); // Questions, Conditions // ^^^^^^^^^^^^^^^^^^^^^ // If so, the associated questions may need to be moved into the repeat in the answers XML. // Present a table of questions, where the user can say yes/no to each, // then move.. table excludes: // 1. any that are used outside the repeat, since these can't be made to vary (see above) // 2. any that are in a nested repeat if (questions.Count > 0) { FormRepeatWhichVariables formRepeatWhichVariables = new FormRepeatWhichVariables(questions); formRepeatWhichVariables.ShowDialog(); List<question> questionsWhichRepeat = formRepeatWhichVariables.getVars(); formRepeatWhichVariables.Dispose(); log.Info(answersPart.XML); foreach (question q in questionsWhichRepeat) { // Find the relevant answer (by ID) // (easiest to do using XPath on XML document Office.CustomXMLNode node = answersPart.SelectSingleNode("//oda:answer[@id='" + q.id + "']"); if (node == null) { log.Error("no node " + q.id); } string fromXPath = NodeToXPath.getXPath(node); log.Info("from: " + fromXPath); // Move it String nodeXML = node.XML; // No API to add a node! node.ParentNode.RemoveChild(node); destination.AppendChildSubtree(nodeXML); // So we'll have to change its xpath in XPaths part // eg from: // "/oda:answers/oda:answer[@id='qa_2']" // to: // "/oda:answers/oda:repeat[@qref='rpt1"]/oda:row[1]/oda:answer[@id='qa_2']" // // CustomXMLNode's Xpath produces something like: /ns2:answers[1]/ns2:answer[1] // which we don't want string toXPath = NodeToXPath.getXPath(destination.LastChild); log.Info("to: " + toXPath); xpathChanges.Add(fromXPath, toXPath); } } // nested repeats // ^^^^^^^^^^^^^^ // 1. move the nested repeat answer structure // 2. change the xpath for all questions re anything in the nested repeat // Note: if wrapping repeat r0 around r1 which in turn contains r2, // must avoid explicitly processing r2, since doing stuff to r1 here is // enough to take care of r2. // So, step 0. Find top level nested repeats // Already have nestedRepeats list, so just remove from it // those which aren't top level. foreach (Word.ContentControl desc in nestedRepeats) { if (!desc.ParentContentControl.ID.Equals(wrappingRepeatCC.ID) ) { // not top level, so remove nestedRepeats.Remove(desc); } } if (nestedRepeats.Count > 0) { foreach (Word.ContentControl desc in nestedRepeats) { TagData td = new TagData(desc.Tag); string nestedRepeatXPathID = td.getRepeatID(); // Get the XPath, to find the question ID, // which is what we use to find the repeat answer. xpathsXpath xp = xppe.getXPathByID(nestedRepeatXPathID); // 1. move the nested repeat answer structure Office.CustomXMLNode node = answersPart.SelectSingleNode("//oda:repeat[@qref='" + xp.questionID + "']"); if (node == null) { log.Error("no node for nested repeat " + xp.questionID); } string fromXPath = NodeToXPath.getXPath(node); log.Info("from: " + fromXPath); // Move it String nodeXML = node.XML; // No API to add a node! node.ParentNode.RemoveChild(node); destination.AppendChildSubtree(nodeXML); // 2. change the xpath for all questions re anything in the nested repeat // With a bit of luck, this will just work! string toXPath = NodeToXPath.getXPath(destination.LastChild); log.Info("to: " + toXPath); xpathChanges.Add(fromXPath, toXPath); } } // Now do the substitutions in the XPaths part - for all string xpaths = model.xpathsPart.XML; foreach (KeyValuePair<string, string> entry in xpathChanges) { xpaths = xpaths.Replace(entry.Key, entry.Value); } CustomXmlUtilities.replaceXmlDoc(model.xpathsPart, xpaths); //log.Info(model.xpathsPart.XML); //log.Info(answersPart.XML); // Now do the substitutions in the content control databindings // (Added 2012 12 16, since docx4j relies on the databinding element to do its bit) foreach (Word.ContentControl cc in wrappingRepeatCC.Range.ContentControls) //foreach (Word.ContentControl cc in Globals.ThisAddIn.Application.ActiveDocument.ContentControls) { // XMLMapping.IsMapped returns false here, // in cases where it is mapped! So avoid using that. // (Could be a defect elsewhere .. check for usage) if (cc.XMLMapping!=null && cc.XMLMapping.XPath!=null && cc.XMLMapping.PrefixMappings!=null) { foreach (KeyValuePair<string, string> entry in xpathChanges) { //log.Info("Comparing " + cc.XMLMapping.XPath + " with " + entry.Key); if (cc.XMLMapping.XPath.Equals(entry.Key)) { // matched, so replace cc.XMLMapping.SetMapping(entry.Value, cc.XMLMapping.PrefixMappings, answersPart); break; } } } } }