public void updateQuestionsPart(question q) { questionnaire questionnaire = new questionnaire(); questionnaire.Deserialize(questionsPart.XML, out questionnaire); questionnaire.questions.Add(q); // Save it in docx string result = questionnaire.Serialize(); log.Info(result); CustomXmlUtilities.replaceXmlDoc(questionsPart, result); }
public FormAnswerOrder(Office.CustomXMLPart answersPart, Office.CustomXMLPart questionsPart) { InitializeComponent(); // To populate the tree view, we need to traverse // the answers. We could do this at the DOM level, // or using our answers object. // Best to use our answers object. this.answersPart = answersPart; answersObj = new answers(); OpenDope_AnswerFormat.answers.Deserialize(answersPart.XML, out answersObj); // We want to show the question text in the tree view questionnaire = new questionnaire(); questionnaire.Deserialize(questionsPart.XML, out questionnaire); ImageList TreeviewIL = new ImageList(); TreeviewIL.Images.Add(System.Drawing.Image.FromStream( System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream("OpenDope_AnswerFormat.folder.png"))); TreeviewIL.Images.Add(System.Drawing.Image.FromStream( System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream("OpenDope_AnswerFormat.Icons.LogicTree.variable_chevron.png"))); TreeviewIL.Images.Add(System.Drawing.Image.FromStream( System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream("OpenDope_AnswerFormat.Icons.LogicTree.repeat.png"))); this.treeView1.ImageList = TreeviewIL; addNodes(answersObj.Items, root); this.treeView1.Nodes.Add(root); root.ImageIndex = 0; root.SelectedImageIndex = 0; treeView1.ExpandAll(); }
private void init1() { FabDocxState fabDocxState = (FabDocxState)Globals.ThisAddIn.Application.ActiveDocument.GetVstoObject(Globals.Factory).Tag; this.model = fabDocxState.model; xppe = new XPathsPartEntry(model); this.questionsPart = model.questionsPart; questionnaire = new questionnaire(); questionnaire.Deserialize(questionsPart.XML, out questionnaire); this.answersPart = model.answersPart; }
/// <summary> // RULE: A variable cc can copied wherever. // If its repeat ancestors change, its "vary in repeat" // will need to change (to lowest common denominator). // If this cc is not in any repeat, // make the answer top level and we're done // Otherwise, could assume its position is OK wrt // existing repeats. // So if anything, we just need to move it // up the tree until // we reach a node which contains this additional repeat. // But we'd like this code to // be used for both moves and copy. (Move case // needs this constraint relaxed) // So our algorithm finds viable positions // (ie ancestors common to all // repeats). // That node and higher are candidates for new position // Ask user to choose. /// </summary> protected void handleXPath(string xpathID, bool dontAskIfOkAsis) { xpathsXpath xp = xppe.getXPathByID(xpathID); string questionID = xp.questionID; // If this cc is not in any repeat, // make the answer top level and we're done // Otherwise, we can assume its position is OK wrt // existing repeats. // So if anything, we just need to move it // up the tree until // we reach a node which contains this additional repeat. // But for the variable move case (handled in MoveHandler) // an existing repeat will no longer be a constraint. // So better to just have a single algorithm which // finds viable positions (ie ancestors common to all // repeats). // 2 algorithms for doing this. // The first: Make a list of the ancestors of the // first repeat. Then cross off that list anything // which isn't an ancestor of the other repeats. // The second: Get XPath for each repeat. // Find the shortest XPath. // Then find the shortest substring common to each // repeat. Then make a list out of that. // I like the first better. // Find all cc's in which this question is used. QuestionHelper qh = new QuestionHelper(xppe, cpe); List<Word.ContentControl> thisQuestionControls = qh.getControlsUsingQuestion(questionID, xpathID); // For each such cc, find closest repeat ancestor cc (if any). // With each such repeat, do the above algorithm. // Find all cc's in which this question is used. List<Word.ContentControl> relevantRepeats = new List<Word.ContentControl>(); foreach (Word.ContentControl ccx in thisQuestionControls) { Word.ContentControl rpt = RepeatHelper.getYoungestRepeatAncestor(ccx); if (rpt == null) { // make the answer top level and we're done. // That means moving it in AF, and XPaths part. log.Info("question " + questionID + " used at top level, so moving it there."); NodeMover nm = new NodeMover(); nm.Move(xp.dataBinding.xpath, "/oda:answers"); nm.adjustBinding(thisQuestionControls, "/oda:answers", questionID); return; } else { relevantRepeats.Add(rpt); } } Office.CustomXMLPart answersPart = model.answersPart; // userParts[0]; // TODO: make this better // That node and higher are candidates for new position // Ask user to choose, or cancel (and remove this cc, // but what about any impending child add events?? Maybe // need a cancel state) this.questionsPart = model.questionsPart; questionnaire = new questionnaire(); questionnaire.Deserialize(questionsPart.XML, out questionnaire); FormMoveQuestion formMoveQuestion = new FormMoveQuestion(answersPart, relevantRepeats, questionnaire, questionID, xppe); if (dontAskIfOkAsis && formMoveQuestion.OkAsis() ) { // Do nothing } else { formMoveQuestion.ShowDialog(); formMoveQuestion.moveIfNecessary(questionID, xp, answersPart); //string varyInRepeat = formMoveQuestion.getVaryingRepeat(); //if (varyInRepeat == null) //{ // // make the answer top level and we're done. // NodeMover nm = new NodeMover(); // nm.Move(xp.dataBinding.xpath, "/oda:answers"); // nm.adjustBinding(thisQuestionControls, "/oda:answers", questionID); //} //else //{ // // Move it to the selected repeat // // get the node corresponding to the repeat's row // Office.CustomXMLNode node = answersPart.SelectSingleNode("//oda:repeat[@qref='" + varyInRepeat + "']/oda:row[1]"); // if (node == null) // { // log.Error("no node for nested repeat " + varyInRepeat); // } // string toRepeat = NodeToXPath.getXPath(node); // NodeMover nm = new NodeMover(); // nm.Move(xp.dataBinding.xpath, toRepeat); // nm.adjustBinding(thisQuestionControls, toRepeat, questionID); //} } formMoveQuestion.Dispose(); }
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 FormCondition(Word.ContentControl cc, ConditionsPartEntry cpe, condition existingCondition) { InitializeComponent(); // NET 4 way; see http://msdn.microsoft.com/en-us/library/microsoft.office.tools.word.extensions.aspx FabDocxState fabDocxState = (FabDocxState)Globals.Factory.GetVstoObject(Globals.ThisAddIn.Application.ActiveDocument).Tag; // NET 3.5 way, which requires using Microsoft.Office.Tools.Word.Extensions //FabDocxState fabDocxState = (FabDocxState)Globals.ThisAddIn.Application.ActiveDocument.GetVstoObject(Globals.Factory).Tag; this.model = fabDocxState.model; xppe = new XPathsPartEntry(model); this.cc = cc; this.cpe = cpe; this.existingCondition = existingCondition; this.questionsPart = model.questionsPart; questionnaire = new questionnaire(); questionnaire.Deserialize(questionsPart.XML, out questionnaire); questionListHelper = new Helpers.QuestionListHelperForConditionsForm(model, xppe, questionnaire, cc); questionListHelper.listBoxTypeFilter = listBoxTypeFilter; questionListHelper.listBoxQuestions = listBoxQuestions; questionListHelper.checkBoxScope = checkBoxScope; questionListHelper.comboBoxValues = comboBoxValues; questionListHelper.listBoxPredicate = listBoxPredicate; this.listBoxQuestions.SelectedIndexChanged += new System.EventHandler(questionListHelper.listBoxQuestions_SelectedIndexChanged); this.listBoxTypeFilter.SelectedIndexChanged += new System.EventHandler(questionListHelper.listBoxTypeFilter_SelectedIndexChanged); question existingQuestion = null; string matchResponse = null; if (existingCondition != null) { // Use the question associated with it, to pre-select // the correct entries in the dialog. // Re-label the window, so user can see what the condition was about this.Text = "Editing Condition: " + cc.Title; //List<xpathsXpath> xpaths = ConditionsPartEntry.getXPathsUsedInCondition(existingCondition, xppe); List<xpathsXpath> xpaths = new List<xpathsXpath>(); existingCondition.listXPaths(xpaths, cpe.conditions, xppe.getXPaths()); if (xpaths.Count > 1) { // TODO: use complex conditions editor } xpathsXpath xpathObj = xpaths[0]; String xpathVal = xpathObj.dataBinding.xpath; if (xpathVal.StartsWith("/")) { // simple //System.out.println("question " + xpathObj.getQuestionID() // + " is in use via boolean condition " + conditionId); existingQuestion = this.questionnaire.getQuestion(xpathObj.questionID); matchResponse = xpathVal; } else if (xpathVal.Contains("position")) { // TODO } else { //System.out.println(xpathVal); String qid = xpathVal.Substring( xpathVal.LastIndexOf("@id") + 5); // System.out.println("Got qid: " + qid); qid = qid.Substring(0, qid.IndexOf("'")); // System.out.println("Got qid: " + qid); //System.out.println("question " + qid // + " is in use via condition " + conditionId); existingQuestion = this.questionnaire.getQuestion(qid); matchResponse = xpathVal; } } questionListHelper.populateTypeFilter(true); if (existingQuestion == null) { // for init, populate with all questions questionListHelper.populateQuestions(null); } else { // Just show the existing question listBoxQuestions.Items.Add(existingQuestion); } if (this.listBoxQuestions.Items.Count == 0) // Never happens if in a repeat, and nor do we want it to, since user might just want to use "repeat pos" stuff { // Try including out of scope this.checkBoxScope.Checked = true; questionListHelper.populateQuestions(null); if (this.listBoxQuestions.Items.Count == 0) { MessageBox.Show("You can't define a condition until you have set up at least one question. Let's do that now. "); FormQA formQA = new FormQA(cc, false); formQA.ShowDialog(); formQA.Dispose(); // Refresh these xppe = new XPathsPartEntry(model); questionnaire.Deserialize(questionsPart.XML, out questionnaire); questionListHelper.filterAction(); return; } } // value question q; if (existingQuestion == null) { // for init, populate with all questions q = (question)this.listBoxQuestions.Items[0]; } else { q = existingQuestion; } this.listBoxQuestions.SelectedItem = q; if (q.response.Item is responseFixed) { questionListHelper.populateValues((responseFixed)q.response.Item, matchResponse); } // predicate = questionListHelper.populatePredicates(q); // TODO: set this correctly in editing mode }
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; } } } } }