/// <summary> // RULE: A repeat can be moved wherever under // the same repeat ancestor cc. // It can be moved elsewhere, provided it is // the only cc using that repeat. (change // AF and XPaths accordingly). // In principle, all n x cc using that repeat // could legally be moved to the same destination, // but we have no way to facilitate this! /// </summary> private void handleRepeat(Word.ContentControl copiedCC) { td = new TagData(copiedCC.Tag); string repeatID = td.getRepeatID(); suppressDescendantEvents(copiedCC); // Look in the answer file to find this repeats // closest ancestor repeat (if any). // It is enough to look at the XPath, so get that. // (Actually, either way you get a qref, which is // NOT a repeat id. ) string xpathCurrent = xppe.getXPathByID(repeatID).dataBinding.xpath; // something like /oda:answers/oda:repeat[@qref='rpt1']/oda:row[1]/oda:repeat[@qref='rpt2']" int index = xpathCurrent.LastIndexOf("oda:repeat"); if (index < 0) { log.Error("Couldn't find repeat in " + xpathCurrent); } string xpathSubstring = xpathCurrent.Substring(0, index); log.Debug("xpath substring: " + xpathSubstring); //eg /oda:answers/oda:repeat[@qref='rpt1']/oda:row[1]/ // OK, find last remaining qref int qrefIndex = xpathSubstring.LastIndexOf("qref"); Word.ContentControl rptAncestor = RepeatHelper.getYoungestRepeatAncestor(copiedCC); if (qrefIndex < 0) { // No repeat ancestor, so this repeat cc can be moved anywhere log.Debug("No repeat ancestor, so this repeat cc can be moved anywhere"); // Has it been moved into a repeat? if (rptAncestor == null) { // Destination has no repeat ancestor, // so nothing to do return; } // Now change AF and XPath structure. string destRptID = (new TagData(rptAncestor.Tag)).getRepeatID(); string destRptXPath = xppe.getXPathByID(destRptID).dataBinding.xpath; //Office.CustomXMLPart answersPart = model.userParts[0]; // TODO: make this better //Office.CustomXMLNode node = answersPart.SelectSingleNode(destRptXPath); //if (node == null) //{ // log.Error("no answer for repeat " + destRptXPath); //} //string toRepeat = NodeToXPath.getXPath(node); NodeMover nm = new NodeMover(); nm.Move(xpathCurrent, destRptXPath); return; // TODO: Need to figure out whether to do this just for // the repeat (and let descendants take care of themselves), // or do it for all (and suppress action on descendant event) // NodeMover does it for all XPaths, so unless that is changed, we're looking at suppression! // - suppression is difficult? // - easy to do just a single xpath, but what about answers? we move these as a tree!! leave desc behind? // BIG QUESTION: are there things we need to do to the descendants in response to their events, // or does this repeat logic take care of everything? // What about in the copy case? } else { // Had a repeat ancestor, with ancestorRepeatID string ancestorQref = xpathSubstring.Substring(qrefIndex + 6); //log.Debug("ancestorQref: " + ancestorQref); // rpt1']/oda:row[1]/ ancestorQref = ancestorQref.Substring(0, ancestorQref.IndexOf("'")); log.Debug("ancestorQref: " + ancestorQref); string ancestorRepeatID = xppe.getXPathByQuestionID(ancestorQref).id; log.Debug("Had a repeat ancestor, with ancestorRepeatID: " + ancestorRepeatID); List<Word.ContentControl> controlsThisRepeat = getRepeatCCsUsingRepeatID(copiedCC, repeatID); // Find the new repeat ancestor, if any if (rptAncestor == null) { // It can be moved elsewhere, provided it is // the only cc using that repeat // Is it the only repeat cc using this repeat id? if (controlsThisRepeat.Count == 0) // since this cc excluded from count { // Yes NodeMover nm = new NodeMover(); nm.Move(xpathCurrent, "/oda:answers"); } else { // This is a problem MessageBox.Show("Your move includes a repeat content control which can't go here. Removing that repeat."); removeButKeepContents(copiedCC); // and enable descendant events fabDocxState.suppressEventsForSdtID.Clear(); return; // ie there were >1 cc using this repeat ID, and we just removed one of them! } } else { string destRptID = (new TagData(rptAncestor.Tag)).getRepeatID(); string destRptXPath = xppe.getXPathByID(destRptID).dataBinding.xpath; if (destRptID.Equals(ancestorRepeatID)) { // OK, no change in ancestor. // Nothing to do. return; } else if (controlsThisRepeat.Count == 1) { // OK, only used once NodeMover nm = new NodeMover(); nm.Move(xpathCurrent, destRptXPath); } else { // Not allowed MessageBox.Show("Your move includes a repeat content control which can't go here. Removing that repeat."); removeButKeepContents(copiedCC); // and enable descendant events fabDocxState.suppressEventsForSdtID.Clear(); return; // ie there were >1 cc using this repeat ID, and we just removed one of them! } } } }
protected void populateConditions() { // Get list of repeat ancestors List<String> repeatXPaths = new List<String>(); Word.ContentControl currentCC = cc.ParentContentControl; while (currentCC != null) { if (currentCC.Tag.Contains("od:repeat")) { TagData td = new TagData(currentCC.Tag); string rXPathID = td.getRepeatID(); xpathsXpath xp = xppe.getXPathByID(rXPathID); repeatXPaths.Add(xp.dataBinding.xpath); } currentCC = currentCC.ParentContentControl; } foreach (condition c in fcb.conditions.condition) { log.Debug(c.id); // Limit to what is in scope. A condition is // in scope of it does not use a question which varies // in some repeat which is out of scope. List<xpathsXpath> xpathsUsedInCondition = new List<xpathsXpath>(); c.listXPaths(xpathsUsedInCondition, fcb.conditions, xppe.xpaths); //foreach(xpathsXpath xp in theList) { // log.Info("condition " + c.id + " uses " + xp.dataBinding.xpath); //} if (isConditionInScope(repeatXPaths, xpathsUsedInCondition)) { listBoxQuestions.Items.Add(c); } else { log.Warn("Condition " + c.id + " is out of scope"); } } if (this.listBoxQuestions.Items.Count > 0) { object o = this.listBoxQuestions.Items[0]; this.listBoxQuestions.Value = o; // Freeze predicate and value columns since these aren't relevant freezeAsNoQuestions(); } else { // None of this type string message = "No conditions available for re-use"; this.listBoxQuestions.Items.Add(message); this.listBoxQuestions.Value = message; freezeAsNoQuestions(); } }
/// <summary> /// Handle Word's OnEnter event for content controls, to set the selection in the pane (if the option is set). /// </summary> /// <param name="ccEntered">A ContentControl object specifying the control that was entered.</param> private void doc_ContentControlOnEnter(Word.ContentControl ccEntered) { log.Debug("Document.ContentControlOnEnter fired."); if (ccEntered.XMLMapping.IsMapped) { log.Debug("control mapped to " + ccEntered.XMLMapping.XPath); if (ccEntered.XMLMapping.CustomXMLNode != null) { m_cmTaskPane.RefreshControls(Controls.ControlMain.ChangeReason.OnEnter, null, null, null, ccEntered.XMLMapping.CustomXMLNode, null); } else { // Not mapped to anything; probably because the part was replaced? log.Debug(".. but XMLMapping.CustomXMLNode is null"); m_cmTaskPane.controlTreeView.DeselectNode(); m_cmTaskPane.WarnViaProperties(ccEntered.XMLMapping.XPath); } } else if (ccEntered.Tag!=null) { TagData td = new TagData(ccEntered.Tag); string xpathid = td.getXPathID(); if (xpathid==null) { xpathid = td.getRepeatID(); } if (xpathid == null) { // Visually indicate in the task pane that we're no longer in a mapped control m_cmTaskPane.controlTreeView.DeselectNode(); m_cmTaskPane.PropertiesClear(); } else { log.Debug("control mapped via tag to " + xpathid); // Repeats, escaped XHTML; we don't show anything for conditions XPathsPartEntry xppe = new XPathsPartEntry(m_cmTaskPane.model); xpathsXpath xx = xppe.getXPathByID(xpathid); Office.CustomXMLNode customXMLNode = null; if (xx != null) { log.Debug(xx.dataBinding.xpath); customXMLNode = m_currentPart.SelectSingleNode(xx.dataBinding.xpath); } if (customXMLNode == null) { // Not mapped to anything; probably because the part was replaced? m_cmTaskPane.controlTreeView.DeselectNode(); if (xx == null) { log.Error("Couldn't find xpath for " + xpathid); m_cmTaskPane.WarnViaProperties("Missing"); } else { log.Warn("Couldn't find target node for " + xx.dataBinding.xpath); m_cmTaskPane.WarnViaProperties(xx.dataBinding.xpath); } } else { m_cmTaskPane.RefreshControls(Controls.ControlMain.ChangeReason.OnEnter, null, null, null, customXMLNode, null); } } } Ribbon.buttonEditEnabled = true; Ribbon.buttonDeleteEnabled = true; Ribbon.myInvalidate(); }
///// <summary> ///// Generate a simple ID ///// </summary> ///// <returns></returns> //private string generateId() //{ // int i = this.answerID.Count; // do // { // i++; // } while (this.answerID.Contains("rpt" + i)); // return "rpt" + i; //} private void buttonNext_Click(object sender, EventArgs e) { int min = 1; int defautlVal = 2; int max = 4; int step = 1; //log.Info(".Text:" + answerID.Text); //log.Info(".SelectedText:" + answerID.SelectedText); //log.Info(".textBoxQuestionText:" + textBoxQuestionText.Text); if (//string.IsNullOrWhiteSpace(this.answerID.Text) || string.IsNullOrWhiteSpace(this.textBoxQuestionText.Text)) { Mbox.ShowSimpleMsgBoxError("Required data missing!"); DialogResult = DialogResult.None; // or use on closing event; see http://stackoverflow.com/questions/2499644/preventing-a-dialog-from-closing-in-the-buttons-click-event-handler return; } String repeatName = this.textBoxQuestionText.Text.Trim(); try { string foo = repeatNameToIdMap[repeatName]; Mbox.ShowSimpleMsgBoxError("You already have a repeat named '" + repeatName + "' . Click the 're-use' tab to re-use it, or choose another name."); DialogResult = DialogResult.None; return; } catch (KeyNotFoundException knfe) { // Good } // Basic data validation try { min = int.Parse(this.textBoxMin.Text); defautlVal = int.Parse(this.textBoxDefault.Text); max = int.Parse(this.textBoxMax.Text); step = int.Parse(this.textBoxRangeStep.Text); } catch (Exception) { log.Warn("Repeat range val didn't parse to int properly"); } if (min < 0) min = 0; if (min > 20) min = 20; if (max < min) max = min + 4; if (max > 30) max = 30; int av = (int)Math.Round((min + max) / 2.0); if (defautlVal < min || defautlVal > max) defautlVal = av; if (step > av) step = 1; TagData td; // Work out where to put this repeat // It will just go in /answers, unless it // has a repeat ancestor. // So look for one... Word.ContentControl repeatAncestor = null; Word.ContentControl currentCC = cc.ParentContentControl; while (repeatAncestor == null && currentCC != null) { if (currentCC.Tag.Contains("od:repeat")) { repeatAncestor = currentCC; break; } currentCC = currentCC.ParentContentControl; } // Generate a nice ID // .. first we need a list of existing IDs List<string> reserved = new List<string>(); foreach (question qx in questionnaire.questions) { reserved.Add(qx.id); } ID = IdHelper.SuggestID(repeatName, reserved) + "_" + IdHelper.GenerateShortID(2); string xpath; if (repeatAncestor == null) { // create it in /answers Office.CustomXMLNode node = answersPart.SelectSingleNode("/oda:answers"); //string xml = "<repeat qref=\"" + ID + "\" xmlns=\"http://opendope.org/answers\"><row/></repeat>"; string xml = "<oda:repeat qref=\"" + ID + "\" xmlns:oda=\"http://opendope.org/answers\" ><oda:row/></oda:repeat>"; node.AppendChildSubtree(xml); xpath = "/oda:answers/oda:repeat[@qref='" + ID + "']/oda:row"; // avoid trailing [1] } else { td = new TagData(repeatAncestor.Tag); string ancestorRepeatXPathID = td.getRepeatID(); // Get the XPath, to find the question ID, // which is what we use to find the repeat answer. xpathsXpath xp = xppe.getXPathByID(ancestorRepeatXPathID); Office.CustomXMLNode node = answersPart.SelectSingleNode("//oda:repeat[@qref='" + xp.questionID + "']/oda:row"); string parentXPath = NodeToXPath.getXPath(node); //string xml = "<repeat qref=\"" + ID + "\" xmlns=\"http://opendope.org/answers\"><row/></repeat>"; string xml = "<oda:repeat qref=\"" + ID + "\" xmlns:oda=\"http://opendope.org/answers\" ><oda:row/></oda:repeat>"; node.AppendChildSubtree(xml); xpath = parentXPath + "/oda:repeat[@qref='" + ID + "']/oda:row"; // avoid trailing [1] } log.Info(answersPart.XML); // Question q = new question(); //q.id = this.answerID.Text; // not SelectedText q.id = ID; q.text = repeatName; if (!string.IsNullOrWhiteSpace(this.textBoxHelp.Text)) { q.help = this.textBoxHelp.Text; } if (!string.IsNullOrWhiteSpace(this.textBoxHint.Text)) { q.hint = this.textBoxHint.Text; } if (this.isAppearanceCompact()) { q.appearance = appearanceType.compact; q.appearanceSpecified = true; } else { q.appearanceSpecified = false; } questionnaire.questions.Add(q); // Bind to answer XPath xpathsXpath xpathEntry = xppe.setup("", answersPart.Id, xpath, null, false); xpathEntry.questionID = q.id; xpathEntry.dataBinding.prefixMappings = "xmlns:oda='http://opendope.org/answers'"; xpathEntry.type = "nonNegativeInteger"; xppe.save(); // Repeat td = new TagData(""); td.set("od:repeat", xppe.xpathId); cc.Tag = td.asQueryString(); // At this point, answer should be present. Sanity check! cc.Title = "REPEAT " + repeatName; cc.SetPlaceholderText(null, null, "Repeating content goes here."); //Not for wdContentControlRichText! //cc.XMLMapping.SetMapping(xpath, null, model.userParts[0]); // Responses response responses = q.response; if (responses == null) { responses = new response(); q.response = responses; } // MCQ: display response form responseFixed responseFixed = new responseFixed(); responses.Item = responseFixed; //for (int i = min; i<=max; i=i+step) //{ // responseFixedItem item = new OpenDoPEModel.responseFixedItem(); // item.value = ""+i; // item.label = "" + i; // responseFixed.item.Add(item); //} responseFixed.canSelectMany = false; // Finally, add to part updateQuestionsPart(); this.Close(); }
public void populateQuestions(string type) { log.Debug("populateQuestions of type " + type); listBoxQuestions.Value = null; listBoxQuestions.Items.Clear(); if (type != null && type.Equals("repeats")) { populateRepeats(); return; } if (type != null && type.Equals("conditions")) { populateConditions(); return; } HashSet<question> questions = null; // Find questions which are in scope: questions = new HashSet<question>(); // Add their questions. Could do this by finding // the repeat answer via XPath, then getting // variables in it, but in this case its easier // just to string match in the XPaths part. xpaths xpaths = xppe.xpaths; // Get list of repeat ancestors List<Word.ContentControl> relevantRepeats = new List<Word.ContentControl>(); Word.ContentControl currentCC = cc.ParentContentControl; while (currentCC != null) { if (currentCC.Tag.Contains("od:repeat")) { relevantRepeats.Add(currentCC); } currentCC = currentCC.ParentContentControl; } foreach (Word.ContentControl rcc in relevantRepeats) { TagData td = new TagData(rcc.Tag); string rXPathID = td.getRepeatID(); xpathsXpath xp = xppe.getXPathByID(rXPathID); string rXPath = xp.dataBinding.xpath; int rXPathLength = rXPath.Length; // we want xpaths containing this xpath, // and no extra repeat foreach (xpathsXpath xx in xpaths.xpath) { if (xx.questionID != null) // includes repeats. Shouldn't if can add/remove row on XForm? { string thisXPath = xx.dataBinding.xpath; if (thisXPath.Contains(rXPath)) { if (thisXPath.LastIndexOf("oda:repeat") < rXPathLength) { questions.Add( questionnaire.getQuestion(xx.questionID)); } } } } } // Finally, add top level questions foreach (xpathsXpath xx in xpaths.xpath) { if (xx.questionID != null) { string thisXPath = xx.dataBinding.xpath; if (thisXPath.IndexOf("oda:repeat") < 0) { questions.Add( questionnaire.getQuestion(xx.questionID)); } } } if (questions.Count == 0) { // None of any type string message = "No questions in scope!"; this.listBoxQuestions.Items.Add(message); this.listBoxQuestions.Value = message; freezeAsNoQuestions(); } else { // filter and add foreach (question q in questions) { xpathsXpath xpath = xppe.getXPathByQuestionID(q.id); if (xpath.dataBinding.xpath.EndsWith("oda:row")) { // Ignore raw repeats } else if (type == null || type.Equals(ALL_QUESTION_TYPES)) { // all questions listBoxQuestions.Items.Add(q); } else { if (xpath.type != null && xpath.type.Equals(type)) { listBoxQuestions.Items.Add(q); log.Debug("Added to listbox " + q.id); } } } if (this.listBoxQuestions.Items.Count > 0) { object o = this.listBoxQuestions.Items[0]; this.listBoxQuestions.Value = o; if (o is question) { currentQuestion = (question)this.listBoxQuestions.Items[0]; populateValues(augmentResponses(currentQuestion), null); } else { currentQuestion = null; } populatePredicates(o); } else { // None of this type string message = "No questions of type " + type; this.listBoxQuestions.Items.Add(message); this.listBoxQuestions.Value = message; freezeAsNoQuestions(); } } this.listBoxQuestions.Items.Add(NEW_QUESTION); //if (this.listBoxQuestions.Items.Count == 2) //{ // this.listBoxQuestions.Value = NEW_QUESTION; //} }
public Word.ContentControl getRepeatAncestor(Word.ContentControl cc) { Word.ContentControl repeatAncestor = null; if (root != null && !treeViewRepeat.SelectedNode.Equals(root)) // this varies with some repeat { // Which one is checked? string variesInRepeatId = (string)treeViewRepeat.SelectedNode.Tag; Word.ContentControl currentCC = cc.ParentContentControl; while (repeatAncestor == null && currentCC != null) { if (currentCC.Tag.Contains("od:repeat")) { TagData td = new TagData(currentCC.Tag); string variesXPathID = td.getRepeatID(); if (variesXPathID.Equals(variesInRepeatId)) { return currentCC; } } currentCC = currentCC.ParentContentControl; } } return null; }
public void handle(Word.ContentControl copiedCC) { if (copiedCC.Tag == null) { // Its just some content control we don't care about fabDocxState.registerKnownSdt(copiedCC); return; // this is OK, since if it contains a repeat or something, that event will fire } log.Info("copy handler invoked on CC: " + copiedCC.Tag); if (copiedCC.Tag.Contains("od:xpath")) { td = new TagData(copiedCC.Tag); string xpathID = td.getXPathID(); // Check it is known to this docx if (xppe.getXPathByID(xpathID) == null) { removeButKeepContents(copiedCC); return; } // RULE: A variable cc can copied wherever. // If its repeat ancestors change, its "vary in repeat" // will need to change (to lowest common denominator). handleXPath(td.getXPathID(), true); } else if (copiedCC.Tag.Contains("od:repeat")) { td = new TagData(copiedCC.Tag); string xpathID = td.getRepeatID(); // Check it is known to this docx if (xppe.getXPathByID(xpathID) == null) { removeButKeepContents(copiedCC); return; } // RULE: A repeat can only be copied if destination // has same repeat ancestors (in which case no change // to answer file is required). handleRepeat(copiedCC); } else if (copiedCC.Tag.Contains("od:condition")) { // Find child CC Microsoft.Office.Interop.Word.Range rng = copiedCC.Range; Word.ContentControls ccs = rng.ContentControls; foreach (Word.ContentControl desc in ccs) { if (desc.ParentContentControl.ID.Equals(copiedCC.ID)) { handle(desc); } } } else { // Its just some content control we don't care about fabDocxState.registerKnownSdt(copiedCC); return; } }
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(); } }
public void identifyLogic(Word.Range range) { foreach (Word.ContentControl cc in range.ContentControls) { if (cc.Tag == null) { // Its just some content control we don't care about continue; } TagData td = new TagData(cc.Tag); if (cc.Tag.Contains("od:xpath")) { xpathsXpath xp = srcXppe.getXPathByID( td.getXPathID() ); identifyLogicInXPath(xp, range, cc); } else if (cc.Tag.Contains("od:repeat")) { testOldestRepeatIncluded(range, cc); xpathsXpath xp = srcXppe.getXPathByID(td.getRepeatID()); // TODO: Clone, so we can write source to just the clone //xp.Serialize //xpathsXpath.Deserialize // TODO consider cloning issue further. I've implemented it // below for xpaths, but since we're throwing the objects // away, it doesn't matter that we're altering them? BBxpaths.Add(xp); string questionID = xp.questionID; question q = srcQuestionnaire.getQuestion(questionID); BBquestions.Add(q); // Answer - just need to do this for the outermost repeat Word.ContentControl oldestRepeat = RepeatHelper.getOldestRepeatAncestor(cc); if (oldestRepeat.ID.Equals(cc.ID)) { foreach (object o in srcAnswers.Items) { if (o is repeat) { repeat a = (repeat)o; if (a.qref.Equals(questionID)) // question ID == answer ID { BBrepeats.Add(a); log.Debug("Added outermost repeat!"); break; } } } } } else if (cc.Tag.Contains("od:condition")) { // Add the condition condition c = srcCpe.getConditionByID(td.getConditionID()); BBconditions.Add(c); // Find and add questions and xpaths used within it //List<xpathsXpath> xpaths = ConditionsPartEntry.getXPathsUsedInCondition(c, srcXppe); List<xpathsXpath> xpaths = new List<xpathsXpath>(); c.listXPaths(xpaths, srcCpe.conditions, srcXppe.getXPaths()); foreach (xpathsXpath xp in xpaths) { identifyLogicInXPath(xp, range, cc); } } } }
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; } } } } }
public void populateQuestions(string type) { listBoxQuestions.Items.Clear(); if (type != null && type.Equals(REPEAT_COUNT)) { populateRepeats(); return; } if (type != null && type.Equals(REPEAT_POS)) { // This special type does not relate to any specific repeat question return; } HashSet<question> questions = null; bool limitScope = !checkBoxScope.Checked; if (limitScope) { // Find questions which are in scope: questions = new HashSet<question>(); // Add their questions. Could do this by finding // the repeat answer via XPath, then getting // variables in it, but in this case its easier // just to string match in the XPaths part. xpaths xpaths = xppe.xpaths; // Get list of repeat ancestors List<Word.ContentControl> relevantRepeats = new List<Word.ContentControl>(); Word.ContentControl currentCC = cc.ParentContentControl; while (currentCC != null) { if (currentCC.Tag.Contains("od:repeat")) { relevantRepeats.Add(currentCC); } currentCC = currentCC.ParentContentControl; } foreach (Word.ContentControl rcc in relevantRepeats) { TagData td = new TagData(rcc.Tag); string rXPathID = td.getRepeatID(); xpathsXpath xp = xppe.getXPathByID(rXPathID); string rXPath = xp.dataBinding.xpath; int rXPathLength = rXPath.Length; // we want xpaths containing this xpath, // and no extra repeat foreach (xpathsXpath xx in xpaths.xpath) { if (xx.questionID != null) // includes repeats. Shouldn't if can add/remove row on XForm? { string thisXPath = xx.dataBinding.xpath; if (thisXPath.Contains(rXPath)) { if (thisXPath.LastIndexOf("oda:repeat") < rXPathLength) { questions.Add( questionnaire.getQuestion(xx.questionID)); } } } } } // Finally, add top level questions foreach (xpathsXpath xx in xpaths.xpath) { if (xx.questionID != null) { string thisXPath = xx.dataBinding.xpath; if (thisXPath.IndexOf("oda:repeat") < 0) { questions.Add( questionnaire.getQuestion(xx.questionID)); } } } } else { questions = questionnaire.questions; } foreach (question q in questions) { if (type == null) // || type.Equals("ALL")) { // all questions listBoxQuestions.Items.Add(q); } else { xpathsXpath xpath = xppe.getXPathByQuestionID(q.id); if (xpath.type != null) { if (type.Equals("card number")) { type = "card-number"; // the real date type name } if (type.Equals("number")) { // special case if (xpath.type.Equals("decimal") || xpath.type.Contains("Integer")) { listBoxQuestions.Items.Add(q); log.Debug("Added to listbox " + q.id); } } else { if (xpath.type.Equals(type)) { listBoxQuestions.Items.Add(q); log.Debug("Added to listbox " + q.id); } } } } } }