/// <summary> /// Initializes a new instance. /// </summary> /// <param name="element"></param> /// <param name="invoker"></param> /// <param name="binding"></param> internal UIBinding(XElement element, IInvoker invoker, Binding binding) : this(element, invoker,binding.ModelItem) { Contract.Requires<ArgumentNullException>(element != null); Contract.Requires<ArgumentNullException>(invoker != null); Contract.Requires<ArgumentNullException>(binding != null); this.binding = binding; }
/// <summary> /// Gets the 'origin node-sequence'. /// </summary> /// <param name="insertContext"></param> /// <param name="sequenceBindingNodeSequence"></param> /// <returns></returns> XObject[] GetOriginNodeSequence(EvaluationContext insertContext, XObject[] sequenceBindingNodeSequence) { Contract.Requires<ArgumentNullException>(insertContext != null); Contract.Requires<ArgumentNullException>(sequenceBindingNodeSequence != null); Contract.Ensures(Contract.Result<XObject[]>() != null); XObject[] result = null; // If the origin attribute is not given and the Sequence Binding sequence is empty, then the origin // node-sequence is the empty sequence. if (insertProperties.Origin == null && sequenceBindingNodeSequence.Length == 0) result = new XObject[0]; // Otherwise, if the origin attribute is not given, then the origin node-sequence consists of the last // node of the Sequence Binding node-sequence. else if (insertProperties.Origin == null) result = new[] { sequenceBindingNodeSequence[sequenceBindingNodeSequence.Length - 1] }; // If the origin attribute is given, the origin node-sequence is the result of the evaluation of the origin // attribute in the insert context. else if (insertProperties.Origin != null) result = new Binding(Element, insertContext, insertProperties.Origin).ModelItems .Select(i => i.Xml) .ToArray(); else result = new XObject[0]; // preempt empty result set if (result.Length == 0) return result; // Namespace nodes and root nodes (parents of document elements) are removed from the origin node-sequence. return result .Where(i => i.NodeType != XmlNodeType.Attribute || !((XAttribute)i).IsNamespaceDeclaration) .Where(i => i.Parent != null || i == i.Document.Root) .ToArray(); }
/// <summary> /// Gets the 'insert location node'. /// </summary> /// <param name="insertContext"></param> /// <param name="sequenceBindingNodeSequence"></param> /// <returns></returns> XObject GetInsertLocationNode(EvaluationContext insertContext, XObject[] sequenceBindingNodeSequence) { Contract.Requires<ArgumentNullException>(insertContext != null); Contract.Requires<ArgumentNullException>(sequenceBindingNodeSequence != null); Contract.Ensures(Contract.Result<XObject>() != null); // If the Sequence Binding node-sequence is not specified or empty, the insert location node is the insert // context node. if (sequenceBindingNodeSequence.Length == 0) return insertContext.ModelItem.Xml; // Otherwise, if the at attribute is not given, then the insert location node is the last node of the // Sequence Binding sequence. else if (insertProperties.At == null) return sequenceBindingNodeSequence[sequenceBindingNodeSequence.Length - 1]; // Otherwise, an insert location node is determined from the at attribute as follows: else { // 1. The evaluation context node is the first node in document order from the Sequence Binding // node-sequence, the context size is the size of the Sequence Binding node-sequence, and the context // position is 1. var at = new Binding( Element, new EvaluationContext(ModelItem.Get(sequenceBindingNodeSequence[0]), 1, sequenceBindingNodeSequence.Length), insertProperties.At).Value; // 2. The return value is processed according to the rules of the XPath function round(). For example, // the literal 1.5 becomes 2, and the literal 'string' becomes NaN. double location; if (double.TryParse(at, out location)) location = Math.Round(location, 0, MidpointRounding.AwayFromZero); else location = double.NaN; // 3. If the result is in the range 1 to the Sequence Binding node-sequence size, then the insert // location is equal to the result. If the result is non-positive, then the insert location is 1. // Otherwise, the result is NaN or exceeds the Sequence Binding sequence size, so the insert location // is the Sequence Binding sequence size. if (location <= 0) location = 1; else if (double.IsNaN(location) || location > sequenceBindingNodeSequence.Length) location = sequenceBindingNodeSequence.Length; // 4. The insert location node is the node in the Sequence Binding sequence at the position given by // the insert location. return sequenceBindingNodeSequence[(int)location - 1]; } }
/// <summary> /// The insert context is determined. If the context attribute is not given, the insert context is the /// in-scope evaluation context. Otherwise, the expression provided by the context attribute is evaluated /// using the in-scope evaluation context, and the first-item rule is applied to obtain the insert context. /// The insert action is terminated with no effect if the insert context is the empty sequence. /// </summary> /// <returns></returns> EvaluationContext GetInsertContext() { var insertContext = context.Value.GetInScopeEvaluationContext(); if (commonProperties.Context != null) { var item = new Binding(Element, insertContext, commonProperties.Context).ModelItems.FirstOrDefault(); if (item == null) return null; insertContext = new EvaluationContext(item.Model, item.Instance, item, 1, 1); } return insertContext; }
/// <summary> /// Applies the bindings to the model item. /// </summary> public void Apply() { // TODO this is a poor implementation of nested bind elements var modelItems = GetBoundNodes(); for (int i = 1; i <= modelItems.Length; i++) { var modelItem = modelItems[i - 1]; if (modelItem == null) continue; var state = modelItem.State; if (state == null) continue; if (Type != null) if (state.Type != Type) state.Type = Type; var ec = new EvaluationContext(modelItem.Model, modelItem.Instance, modelItem, i, modelItems.Length); if (!string.IsNullOrWhiteSpace(attributes.ReadOnly)) { var readOnly = ParseBooleanValue(new Binding(Element, ec, attributes.ReadOnly)); if (readOnly != null) state.ReadOnly = readOnly; } if (!string.IsNullOrWhiteSpace(attributes.Required)) { var required = ParseBooleanValue(new Binding(Element, ec, attributes.Required)); if (required != null) state.Required = required; } if (!string.IsNullOrWhiteSpace(attributes.Relevant)) { var relevant = ParseBooleanValue(new Binding(Element, ec, attributes.Relevant)); if (relevant != null && relevant != state.Relevant) { state.Relevant = relevant; Debug.WriteLine("ModelItem relevancy changed: {0}", state.Relevant); } } if (!string.IsNullOrWhiteSpace(attributes.Constraint)) { var constraint = ParseBooleanValue(new Binding(Element, ec, attributes.Constraint)); if (constraint != null) state.Constraint = constraint; } if (!string.IsNullOrWhiteSpace(attributes.Calculate)) { var calculate = new Binding(Element, ec, attributes.Calculate).Value; if (calculate != null) { if (state.ReadOnly == false) state.ReadOnly = true; modelItem.Value = calculate; } } } }
/// <summary> /// Extracts a boolean value from the given binding. /// </summary> /// <param name="binding"></param> /// <returns></returns> bool? ParseBooleanValue(Binding binding) { Contract.Requires<ArgumentNullException>(binding != null); if (binding.Result is bool) return (bool?)binding.Result; else if (binding.Result is bool?) return (bool?)binding.Result; else if (binding.Result is string && !string.IsNullOrWhiteSpace((string)binding.Result)) return bool.Parse((string)binding.Result); else throw new DOMTargetEventException(Element, Events.BindingException, string.Format("{0}", binding.Result)); }
/// <summary> /// Returns the <see cref="EvaluationContext"/> provided by any specified 'context' attribute. /// </summary> /// <returns></returns> internal EvaluationContext GetSpecifiedContextEvaluationContext() { if (Attributes.Context != null) { var context = GetContextForSpecifiedContext(); if (context == null) throw new DOMTargetEventException(Element, Events.BindingException, "Null Context for specified Context."); var binding = new Binding(Element, context, Attributes.Context); if (binding.ModelItem == null) return null; return new EvaluationContext(binding.ModelItem.Model, binding.ModelItem.Instance, binding.ModelItem, 1, 1); } return null; }
public void Invoke() { var deleteContext = GetDeleteContext(); if (deleteContext == null) return; // The Sequence Binding node-sequence is determined. var sequenceBindingNodeSequence = GetSequenceBindingNodeSequence(deleteContext); // The delete action is terminated with no effect if the Sequence Binding is expressed and the Sequence // Binding node-sequence is the empty sequence. if (sequenceBindingNodeSequence != null && sequenceBindingNodeSequence.Length == 0) return; // The behavior of the delete action is undefined if the Sequence Binding node-sequence contains nodes // from more than one instance. if (sequenceBindingNodeSequence != null && sequenceBindingNodeSequence.Select(i => ModelItem.Get(i).Instance).Distinct().Count() > 1) return; // Otherwise, the Sequence Binding is not expressed, so the Sequence Binding node-sequence is set equal to // the delete context node with a position and size of 1. if (sequenceBindingNodeSequence == null) sequenceBindingNodeSequence = new XObject[] { deleteContext.ModelItem.Xml }; // The delete location is determined. If the at attribute is not specified, there is no delete location. // Otherwise, the delete location is determined by evaluating the expression specified by the at attribute // as follows: var deleteLocation = 0d; if (deleteProperties.At != null) { // 1. The evaluation context node is the first node in document order from the Sequence Binding // node-sequence, the context size is the size of the Sequence Binding node-sequence, and the context // position is 1. var at = new Binding( Element, new EvaluationContext(ModelItem.Get(sequenceBindingNodeSequence[0]), 1, sequenceBindingNodeSequence.Length), deleteProperties.At).Value; // 2. The return value is processed according to the rules of the XPath function round(). For example, // the literal 1.5 becomes 2, and the literal 'string' becomes NaN. if (double.TryParse(at, out deleteLocation)) deleteLocation = Math.Round(deleteLocation, 0, MidpointRounding.AwayFromZero); else deleteLocation = double.NaN; // 3. If the result is in the range 1 to the Sequence Binding node-sequence size, then the delete // location is equal to the result. If the result is non-positive, then the delete location is 1. // Otherwise, if the result is NaN or exceeds the Sequence Binding node-sequence size, the delete // location is the Sequence Binding node-sequence size. if (deleteLocation <= 0) deleteLocation = 1; else if (deleteLocation == double.NaN || deleteLocation > sequenceBindingNodeSequence.Length) deleteLocation = sequenceBindingNodeSequence.Length; } // If there is no delete location, each node in the Sequence Binding node-sequence is deleted, except if // the node is a readonly node, a namespace node, a root node, or the root document element of an instance, // then that particular node is not deleted. Otherwise, if there is a delete location, the node at the // delete location in the Sequence Binding node-sequence is deleted, except if the node is the root // document element of an instance or has a readonly parent node, then that node is not deleted. var deleteCount = 0; var deleteNodes = deleteLocation == 0d ? sequenceBindingNodeSequence : new XObject[] { sequenceBindingNodeSequence[(int)deleteLocation - 1] }; foreach (var deleteNode in deleteNodes) { if (ModelItem.Get(deleteNode).ReadOnly) continue; if (deleteNode.NodeType == XmlNodeType.Attribute) if (((XAttribute)deleteNode).IsNamespaceDeclaration) continue; if (deleteNode.Parent == null) continue; if (deleteNode is XAttribute) ((XAttribute)deleteNode).Remove(); else if (deleteNode is XNode) ((XNode)deleteNode).Remove(); else throw new InvalidOperationException(); deleteCount++; } // The delete action is terminated with no effect if no node is deleted. if (deleteCount == 0) return; // 5. The XForms action system's deferred update flags for rebuild, recalculate, revalidate and refresh are // set. deleteContext.Model.State.Rebuild = true; deleteContext.Model.State.Recalculate = true; deleteContext.Model.State.Revalidate = true; deleteContext.Model.State.Refresh = true; // 6. The delete action is successfully completed by dispatching the xforms-delete event with appropriate // context information. deleteContext.Instance.Element.Interface<EventTarget>() .Dispatch(Events.Delete); }
/// <summary> /// The delete context is determined. It is set to the in-scope evaluation context, possibly overridden by the /// context attribute if that attribute is present. The delete action is terminated with no effect if the /// delete context is the empty sequence. /// </summary> /// <returns></returns> EvaluationContext GetDeleteContext() { var deleteContext = resolver.Value.GetInScopeEvaluationContext(); if (commonProperties.Context != null) { var item = new Binding(Element, deleteContext, commonProperties.Context).ModelItems.First(); if (item == null) return null; deleteContext = new EvaluationContext(item.Model, item.Instance, item, 1, 1); } return deleteContext; }
/// <summary> /// Finishes a submission with instance replacement. /// </summary> /// <param name="response"></param> /// <param name="modelItem">Instance data node that was submitted.</param> void FinishWithReplaceInstance(ModelResponse response, ModelItem modelItem) { Contract.Requires<ArgumentNullException>(response != null); Contract.Requires<ArgumentException>(response.Body != null); Contract.Requires<ArgumentNullException>(modelItem != null); // When the attribute is absent, then the default is the instance that contains the submission data. var instance = modelItem != null ? modelItem.Instance : null; // Author-optional attribute specifying the instance to replace when the replace attribute value is // "instance". When the attribute is absent, then the default is the instance that contains the submission // data. An xforms-binding-exception (The xforms-binding-exception Event) occurs if this attribute does not // indicate an instance in the same model as the submission. if (properties.Instance != null) { var instanceElement = Element.ResolveId(properties.Instance); if (instanceElement != null) instance = instanceElement.Interface<Instance>(); } if (instance == null || instance.Element.Parent != Element.Parent) throw new DOMTargetEventException(Element, Events.BindingException, "Submission cannot specify foreign model instance."); var target = instance.State.Document.Root.Annotation<ModelItem>(); if (target == null) throw new InvalidOperationException(); // Author-optional attribute containing an expression that indicates the target node for data replacement. if (properties.TargetRef != null) { // The evaluation context for this attribute is the in-scope evaluation context for the submission // element, except the context node is modified to be the document element of the instance identified // by the instance attribute if present. var ec = new EvaluationContext( ModelItem.Get(context.Value.Context.Instance.State.Document.Root), 1, 1); // If the submission element has a targetref attribute, the attribute value is interpreted as a binding // expression to which the first-item rule is applied to obtain the replacement target node. target = new Binding(Element, ec, properties.TargetRef).ModelItem; } // final check if (target == null) throw new DOMTargetEventException(Element, Events.BindingException, "Null submission replacement target."); // Otherwise, those processing instructions and comments replace any processing instructions and comments // that previously appeared outside of the document element of the instance being replaced. target.Replace(response.Body); }
void OnSubmitImpl() { // The data model is updated based on some of the flags defined for deferred updates. Specifically, if the // deferred update rebuild flag is set for the model containing this submission, then the rebuild operation // is performed without dispatching an event to invoke the operation. Then, if the deferred update // recalculate flag is set for the model containing this submission, then the recalculate operation is // performed without dispatching an event to invoke the operation. This sequence of operations affects the // deferred update behavior by clearing the deferred update flags associated with the operations performed. var model = Element.Ancestors(Constants.XForms_1_0 + "model").First().Interface<Model>(); if (model.State.Rebuild) model.OnRebuild(); if (model.State.Recalculate) model.OnRecalculate(); // If the binding attributes of submission indicate an empty sequence or an item other than an element or // an instance document root node, then submission fails with no-data. Otherwise, the binding attributes of // submission indicate a node of instance data. var modelItems = new Binding(Element, context.Value.Context, properties.Ref).ModelItems; if (modelItems == null || modelItems.Length != 1 || modelItems.Any(i => i.Xml.NodeType != XmlNodeType.Document && i.Xml.NodeType != XmlNodeType.Element)) throw new DOMTargetEventException(Element, Events.SubmitError, new SubmitErrorContextInfo( SubmitErrorErrorType.NoData )); // The indicated node and all nodes for which it is an ancestor are selected. If the attribute relevant is // true, whether by default or declaration, then any selected node which is not relevant as defined in The // relevant Property is deselected (pruned). If all instance nodes are deselected, then submission fails // with no-data. var node = (XNode)new SubmitTransformer(properties.Relevant) .Visit(modelItems[0].Xml); if (node == null) throw new DOMTargetEventException(Element, Events.SubmitError, new SubmitErrorContextInfo( SubmitErrorErrorType.NoData )); // If the attribute validate is true, whether by default or declaration, then all selected instance data // nodes are checked for validity according to the definition in The xforms-revalidate Event (no // notification events are marked for dispatching due to this operation). If any selected instance data // node is found to be invalid, submission fails with validation-error. if (properties.Validate && new ValidationVisitor().Validate(node) == false) throw new DOMTargetEventException(Element, Events.SubmitError, new SubmitErrorContextInfo( SubmitErrorErrorType.ValidationError )); // The submission method is determined. // The submission method may be specified by the method attribute. The submission element can have a child // element named method, which overrides the submission method setting obtained from the method attribute // if both are specified. If more than one method element is given, the first occurrence in document order // must be selected for use. Individually, the method element and the method attribute are not required. // However, one of the two is mandatory as there is no default submission method. var method = GetMethod(); if (method == null) throw new DOMTargetEventException(Element, Events.SubmitError, "Unknown ModelMethod."); // The resource element provides the submission URI, overriding the resource attribute and the action // attribute. If a submission has more than one resource child element, the first resource element child // must be selected for use. Individually, the resource element, the resource attribute and the action // attribute are not required. However, one of the three is mandatory as there is no default submission // resource. var resource = GetResource(); if (resource == null) throw new DOMTargetEventException(Element, Events.SubmitError, new SubmitErrorContextInfo( SubmitErrorErrorType.ResourceError)); // If the serialization attribute value is "none", then the submission data serialization is the empty // string. Otherwise, the event xforms-submit-serialize is dispatched; if the submission-body property // of the event is changed from the initial value of empty string, then the content of the submission-body // property string is used as the submission data serialization. Otherwise, the submission data // serialization consists of a serialization of the selected instance data according to the rules stated // in Serialization. if (properties.Serialization.None) node = null; else { var evt = Element.Interface<EventTarget>().Dispatch(Events.SubmitSerialize, new SubmitSerializeContextInfo()); var ctx = evt.Context as SubmitSerializeContextInfo; if (ctx != null && ctx.SubmissionBody != "") // wrap in XText node = new XText(ctx.SubmissionBody); } // The submission is performed based on the submission headers, submission method, submission resource, and // submission data serialization. The exact rules of submission are based on the URI scheme and the // submission method, as defined in Submission Options. var request = new ModelRequest(resource, (ModelMethod)method); request.MediaType = properties.MediaType; request.Body = node; request.Encoding = properties.Encoding; request.Headers.Add(GetHeaders()); // submit and check for response var response = requestService.Submit(request); if (response == null) throw new DOMTargetEventException(Element, Events.SubmitError, new SubmitErrorContextInfo( SubmitErrorErrorType.ResourceError)); // For error responses, processing depends on the value of the replace attribute on element submission: // all: either the document is replaced with an implementation-specific indication of an error or submission fails with resource-error. // any other value: nothing in the document is replaced, and submission fails with resource-error. if (response.Status == ModelResponseStatus.Error) throw new DOMTargetEventException(Element, Events.SubmitError, new SubmitErrorContextInfo( SubmitErrorErrorType.ResourceError)); // For success responses, if the response does not include a body, submission succeeds. if (response.Body == null) { Element.DispatchEvent(Events.SubmitDone); return; } // handle result based on 'replace' property switch (properties.Replace) { // none: submission succeeds. case SubmissionReplace.None: Element.DispatchEvent(Events.SubmitDone); break; // all: the event xforms-submit-done may be dispatched with appropriate context information, and submit // processing concludes with the entire containing document being replaced with the returned body. case SubmissionReplace.All: throw new NotImplementedException(); // instance: If the body is not of type accepted by the processor, as specified in Creating instance data // from external resources, nothing in the document is replaced and submission fails with resource-error. // Otherwise the body is parsed to give an XPath Data Model according to Creating instance data from // external resources. If the parse fails, then submission fails with parse-error. If the parse succeeds, // then instance data replacement is performed according to Replacing Data with the Submission Response. // If this operation fails, submission fails with target-error. Otherwise, submission succeeds. case SubmissionReplace.Instance: FinishWithReplaceInstance(response, modelItems[0]); break; // text: If the body is neither an XML media type (i.e. with a content type not matching any of the // specifiers in [RFC 3023]) nor a text type (i.e. with a content type not matching text/*), nothing in the // document is replaced and submission fails with resource-error. Otherwise the content replacement is // performed according to Replacing Data with the Submission Response. If this operation fails, then the // submission fails with target-error. Otherwise, submission succeeds. case SubmissionReplace.Text: throw new NotImplementedException(); } }