File: MS\Internal\Data\XmlBindingWorker.cs
Web Access
Project: src\src\Microsoft.DotNet.Wpf\src\PresentationFramework\PresentationFramework.csproj (PresentationFramework)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
//
// Description: Defines XmlBindingWorker object, workhorse for XML bindings
//
 
using System.Xml;
using System.Xml.XPath;
using System.Collections;
using System.ComponentModel;
using System.Windows;
using System.Windows.Data;
using System.Windows.Controls;      // IGeneratorHost
using System.Windows.Markup;
 
namespace MS.Internal.Data
{
    internal class XmlBindingWorker : BindingWorker, IWeakEventListener
    {
        private enum XPathType : byte { Default, SimpleName, SimpleAttribute }
 
        //------------------------------------------------------
        //
        //  Constructors
        //
        //------------------------------------------------------
 
        internal XmlBindingWorker(ClrBindingWorker worker, bool collectionMode) : base(worker.ParentBindingExpression)
        {
            _hostWorker = worker;
            _xpath = ParentBinding.XPath;
            Debug.Assert(_xpath != null);
 
            // when collectionMode is true, we update the XmlDataCollection for XmlNodeChanges,
            // otherwise, any XmlNodeChange counts as a disastrous change that requires reset.
            _collectionMode = collectionMode;
 
            _xpathType = GetXPathType(_xpath);
 
            // PERF: it is possible to add one more optimization "mode" for the case when
            // we know the host wants to use the CurrentItem (i.e. DrillIn == Always).
            // We could be using SelectSingleNode() instead of SelectNodes(),
            // and then only watch for changes to one node instead of comparing collections.
        }
 
        //------------------------------------------------------
        //
        //  Internal Methods
        //
        //------------------------------------------------------
 
        internal override void AttachDataItem()
        {
            // If there is an XPath, we get a context node for running queries by
            // creating a view from DataItem and using its CurrentItem.
            // If DataItem isn't a valid collection, it's probably an XmlNode,
            // in which case we will try using DataItem directly as the ContextNode
 
            if (XPath.Length > 0)
            {
                CollectionView = DataItem as CollectionView;
 
                if (CollectionView == null && DataItem is ICollection)
                {
                    CollectionView = CollectionViewSource.GetDefaultCollectionView(DataItem, TargetElement);
                }
            }
 
            if (CollectionView != null)
            {
                CurrentChangedEventManager.AddHandler(CollectionView, ParentBindingExpression.OnCurrentChanged);
 
                if (IsReflective)
                {
                    CurrentChangingEventManager.AddHandler(CollectionView, ParentBindingExpression.OnCurrentChanging);
                }
            }
 
            // Set ContextNode and hook events
            UpdateContextNode(true);
        }
 
        internal override void DetachDataItem()
        {
            //UnHook Collection Manager Currency notifications
            if (CollectionView != null)
            {
                CurrentChangedEventManager.RemoveHandler(CollectionView, ParentBindingExpression.OnCurrentChanged);
 
                if (IsReflective)
                {
                    CurrentChangingEventManager.RemoveHandler(CollectionView, ParentBindingExpression.OnCurrentChanging);
                }
            }
 
            // Set ContextNode (this unhooks events first)
            UpdateContextNode(false);
 
            CollectionView = null;
        }
 
        internal override void OnCurrentChanged(ICollectionView collectionView, EventArgs args)
        {
            // There are two possible CurrentChanged events that comes through this event handler.
            // 1. CurrentChanged from DataItem as CollectionView
            // 2. CurrentChanged from QueriedCollection
 
            // only handle changed event from DataItem as CollectionView
            if (collectionView == CollectionView)
            {
                using (ParentBindingExpression.ChangingValue())
                {
                    // This will unhook and hook notifications
                    UpdateContextNode(true);
 
                    // tell host worker to use a new item
                    _hostWorker.UseNewXmlItem(this.RawValue());
                }
            }
        }
 
        internal override object RawValue()
        {
            if (XPath.Length == 0)
            {
                return DataItem;
            }
 
            if (ContextNode == null)    // possibly because currentItem moved off collection
            {
                QueriedCollection = null;
                return null;
            }
 
            XmlNodeList nodes = SelectNodes();
 
            if (nodes == null)
            {
                QueriedCollection = null;
                return DependencyProperty.UnsetValue;
            }
 
            return BuildQueriedCollection(nodes);
        }
 
        internal void ReportBadXPath(TraceEventType traceType)
        {
            if (TraceData.IsEnabled)
            {
                TraceData.TraceAndNotifyWithNoParameters(traceType,
                                    TraceData.BadXPath(
                                        XPath,
                                        IdentifyNode(ContextNode)),
                                    ParentBindingExpression);
            }
        }
 
        //------------------------------------------------------
        //
        //  Private Properties
        //
        //------------------------------------------------------
 
        private XmlDataCollection QueriedCollection
        {
            get { return _queriedCollection; }
            set { _queriedCollection = value; }
        }
 
        private ICollectionView CollectionView
        {
            get { return _collectionView; }
            set { _collectionView = value; }
        }
 
        private XmlNode ContextNode
        {
            get { return _contextNode; }
            set
            {
                if (_contextNode != value && TraceData.IsExtendedTraceEnabled(ParentBindingExpression, TraceDataLevel.ReplaceItem))
                {
                    TraceData.TraceAndNotifyWithNoParameters(TraceEventType.Warning,
                                        TraceData.XmlContextNode(
                                            TraceData.Identify(ParentBindingExpression),
                                            IdentifyNode(value)),
                                        ParentBindingExpression);
                }
 
                _contextNode = value;
            }
        }
 
        private string XPath
        {
            get { return _xpath; }
        }
 
        private XmlNamespaceManager NamespaceManager
        {
            get
            {
                DependencyObject target = TargetElement;
                if (target == null)
                    return null;
 
                XmlNamespaceManager nsMgr = Binding.GetXmlNamespaceManager(target);
 
                if (nsMgr == null)
                {
                    if (XmlDataProvider != null)
                    {
                        nsMgr = XmlDataProvider.XmlNamespaceManager;
                    }
                }
 
                return nsMgr;
            }
        }
 
        // lazy computation of the XmlDataProvider that begat our data.
        // This is used primarily to get the right XmlNamespaceManager for
        // subqueries, sorting, etc.
        private XmlDataProvider XmlDataProvider
        {
            get
            {
                if (_xmlDataProvider == null)
                {
                    XmlDataCollection xdc;
                    ItemsControl ic;
 
                    // if the binding knows its data source and it's the right kind, use it
                    if ((_xmlDataProvider = ParentBindingExpression.DataSource as XmlDataProvider) != null)
                    {
                        // nothing more to do
                    }
 
                    // if the data is an XmlDataCollection, use its provider
                    else if ((xdc = DataItem as XmlDataCollection) != null)
                    {
                        _xmlDataProvider = xdc.ParentXmlDataProvider;
                    }
 
                    // if the data is a view over an XmlDataCollection, use its provider
                    else if (CollectionView != null &&
                            (xdc = CollectionView.SourceCollection as XmlDataCollection) != null)
                    {
                        _xmlDataProvider = xdc.ParentXmlDataProvider;
                    }
 
                    // if the binding is a transient one attached to an ItemsControl,
                    // use the provider for the ItemsSource.  This arises for the "Primary
                    // Text" binding for a ComboBox.
                    else if (TargetProperty == BindingExpressionBase.NoTargetProperty &&
                            (ic = TargetElement as ItemsControl) != null)
                    {
                        object itemsSource = ic.ItemsSource;
                        if ((xdc = itemsSource as XmlDataCollection) == null)
                        {
                            ICollectionView icv = itemsSource as ICollectionView;
                            xdc = ((icv != null) ? icv.SourceCollection : null) as XmlDataCollection;
                        }
 
                        if (xdc != null)
                        {
                            _xmlDataProvider = xdc.ParentXmlDataProvider;
                        }
                    }
 
                    // bindings in DataTemplates are typically bound to a single XmlNode.
                    // Find the governing XmlDataProvider.
                    else
                    {
                        _xmlDataProvider = Helper.XmlDataProviderForElement(TargetElement);
                    }
                }
 
                return _xmlDataProvider;
            }
        }
 
        //------------------------------------------------------
        //
        //  Private Methods
        //
        //------------------------------------------------------
 
        // Recalculate the node to be used as XPath query context;
        // only call this when CurrentItem changes and for Attach/Detach DataItem.
        // If worker was hooked up for notifications, this will UnhookNotifications()
        // before changing ContextNode, and then optionally HookNotifications() after.
        private void UpdateContextNode(bool hookNotifications)
        {
            UnHookNotifications();
 
            if (DataItem == BindingExpressionBase.DisconnectedItem)
            {
                ContextNode = null;
                return;
            }
 
            if (CollectionView != null)
            {
                ContextNode = CollectionView.CurrentItem as XmlNode;
 
                if (ContextNode != CollectionView.CurrentItem && TraceData.IsEnabled)
                {
                    TraceData.TraceAndNotify(TraceEventType.Error, TraceData.XmlBindingToNonXmlCollection, ParentBindingExpression,
                        traceParameters: new object[] { XPath, ParentBindingExpression, DataItem });
                }
            }
            else
            {
                ContextNode = DataItem as XmlNode;
 
                if (ContextNode != DataItem && TraceData.IsEnabled)
                {
                    TraceData.TraceAndNotify(TraceEventType.Error, TraceData.XmlBindingToNonXml, ParentBindingExpression,
                        traceParameters: new object[] { XPath, ParentBindingExpression, DataItem });
                }
            }
 
            if (hookNotifications)
                HookNotifications();
        }
 
        // We hook up only one set of event listeners per document and propagate
        // events to our worker instances.  This is a perf savings because we use
        // a doubly-linked to add/remove workers, whereas delegate add/remove
        // is linear and doesn't scale well for high number of binding workers.
        private void HookNotifications()
        {
            //Hook Xml Node Change Notifications for one way
            //and two way binding
            if (IsDynamic)
            {
                // Check the node on which we would run XPath queries.
                // We can only hook if there is a node.
                if (ContextNode != null)
                {
                    XmlDocument doc = DocumentFor(ContextNode);
                    if (doc != null)
                    {
                        XmlNodeChangedEventManager.AddHandler(doc, OnXmlNodeChanged);
                    }
                }
            }
        }
 
        // see comment on HookNotifications()
        private void UnHookNotifications()
        {
            //Hook Xml Node Change Notifications for one way
            //and two way binding
            if (IsDynamic)
            {
                //this worker might not be hooked, either because
                //the query is empty or because of an invalid query.
                //Only unhook if we were hooked in the first place.
                if (ContextNode != null)
                {
                    XmlDocument doc = DocumentFor(ContextNode);
                    if (doc != null)
                    {
                        XmlNodeChangedEventManager.RemoveHandler(doc, OnXmlNodeChanged);
                    }
                }
            }
        }
 
        private XmlDocument DocumentFor(XmlNode node)
        {
            XmlDocument doc = node.OwnerDocument;
            if (doc == null)
            {
                // this may be a document itself
                doc = node as XmlDocument;
            }
 
            return doc;
        }
 
        XmlDataCollection BuildQueriedCollection(XmlNodeList nodes)
        {
            if (TraceData.IsExtendedTraceEnabled(ParentBindingExpression, TraceDataLevel.GetValue))
            {
                TraceData.TraceAndNotifyWithNoParameters(TraceEventType.Warning,
                                    TraceData.XmlNewCollection(
                                        TraceData.Identify(ParentBindingExpression),
                                        IdentifyNodeList(nodes)),
                                    ParentBindingExpression);
            }
 
            QueriedCollection = new XmlDataCollection(XmlDataProvider);
            QueriedCollection.XmlNamespaceManager = NamespaceManager;
            QueriedCollection.SynchronizeCollection(nodes);
            return QueriedCollection;
        }
 
        bool IWeakEventListener.ReceiveWeakEvent(Type managerType, object sender, EventArgs args)
        {
            return false;   // this method is no longer used (but must remain, for compat)
        }
 
        void OnXmlNodeChanged(object sender, XmlNodeChangedEventArgs e)
        {
            if (TraceData.IsExtendedTraceEnabled(ParentBindingExpression, TraceDataLevel.Events))
            {
                TraceData.TraceAndNotifyWithNoParameters(TraceEventType.Warning,
                                    TraceData.GotEvent(
                                        TraceData.Identify(ParentBindingExpression),
                                        "XmlNodeChanged",
                                        TraceData.Identify(sender)),
                                    ParentBindingExpression);
            }
 
            ProcessXmlNodeChanged(e);
        }
 
        void ProcessXmlNodeChanged(EventArgs args)
        {
            // By the time this worker is notified, its binding's TargetElement may already be gone.
            // We should first check TargetElement to see if this worker still matters. (Fix 1494812)
            DependencyObject target = ParentBindingExpression.TargetElement;
            if (target == null)
                return;
 
            if (IgnoreSourcePropertyChange)
                return;
 
            if (DataItem == BindingExpressionBase.DisconnectedItem)
                return;
 
            // There should never be a change notification when there's no ContextNode.
            // If this Assert ever hits, something is wrong with the logic or ordering of
            // UpdateContextNode(), HookNotifications() and UnHookNotifications().
            Debug.Assert(ContextNode != null);
 
            // ignore changes that cannot possibly affect the value of this XPath
            if (!IsChangeRelevant(args))
                return;
 
            if (XPath.Length == 0)
            {
                // DataItem is being used directly; no need to check queries at all.
                _hostWorker.OnXmlValueChanged();
            }
            else if (QueriedCollection == null)
            {
                // If there was no previous QueryCollection, it's probably because
                // the previous xpath query failed.  Try again now.
                _hostWorker.UseNewXmlItem(this.RawValue());
            }
            else
            {
                // We have a previous query result; run a new query for comparison:
 
                XmlNodeList nodes = SelectNodes();
 
                if (nodes == null)
                {
                    // Node change has caused the new query to fail.
                    QueriedCollection = null;
                    _hostWorker.UseNewXmlItem(DependencyProperty.UnsetValue);
                }
                else if (_collectionMode)
                {
                    if (TraceData.IsExtendedTraceEnabled(ParentBindingExpression, TraceDataLevel.GetValue))
                    {
                        TraceData.TraceAndNotifyWithNoParameters(TraceEventType.Warning,
                                            TraceData.XmlSynchronizeCollection(
                                                TraceData.Identify(ParentBindingExpression),
                                                IdentifyNodeList(nodes)),
                                            ParentBindingExpression);
                    }
 
                    // Any xml change action, doesn't matter if it's an insert,
                    // remove, or change, can result in any number of changes
                    // to the content of the queried collection, so we have to
                    // update the old collection with the new results.
                    QueriedCollection.SynchronizeCollection(nodes);
                }
                // PERF: it is possible to add one more optimization "mode" here for singleMode.
                else if (QueriedCollection.CollectionHasChanged(nodes))
                {
                    // RawValue itself has changed, and we don't know
                    // if the hostWorker is consuming information on the
                    // collection itself or on its CurrentItem, so we just reset.
                    _hostWorker.UseNewXmlItem(BuildQueriedCollection(nodes));
                }
                else
                {
                    // RawValue itself hasn't changed, but its children's content may have.
                    _hostWorker.OnXmlValueChanged();
                }
            }
            GC.KeepAlive(target);   // keep target alive during process xml change (bug 1494812)
        }
 
        private XmlNodeList SelectNodes()
        {
            XmlNamespaceManager nsMgr = NamespaceManager;
            XmlNodeList nodes = null;
            try
            {
                if (nsMgr != null)
                {
                    nodes = ContextNode.SelectNodes(XPath, nsMgr);
                }
                else
                {
                    nodes = ContextNode.SelectNodes(XPath);
                }
            }
            catch (XPathException xe)
            {
                Status = BindingStatusInternal.PathError;
                if (TraceData.IsEnabled)
                {
                    TraceData.TraceAndNotify(TraceEventType.Error, TraceData.CannotGetXmlNodeCollection, ParentBindingExpression,
                        traceParameters: new object[] { (ContextNode != null) ? ContextNode.Name : null, XPath, ParentBindingExpression, xe },
                        eventParameters: new object[] { xe });
                }
            }
 
            if (TraceData.IsExtendedTraceEnabled(ParentBindingExpression, TraceDataLevel.GetValue))
            {
                TraceData.TraceAndNotifyWithNoParameters(TraceEventType.Warning,
                                    TraceData.SelectNodes(
                                        TraceData.Identify(ParentBindingExpression),
                                        IdentifyNode(ContextNode),
                                        TraceData.Identify(XPath),
                                        IdentifyNodeList(nodes)),
                                    ParentBindingExpression);
            }
 
            return nodes;
        }
 
        private string IdentifyNode(XmlNode node)
        {
            if (node == null)
                return "<null>";
 
            return $"{node.GetType().Name} ({node.Name})";
        }
 
        private string IdentifyNodeList(XmlNodeList nodeList)
        {
            if (nodeList == null)
                return "<null>";
 
            return string.Create(TypeConverterHelper.InvariantEnglishUS, $"{nodeList.GetType().Name} (hash={AvTrace.GetHashCodeHelper(nodeList)} Count={nodeList.Count})");
        }
 
        // 90% of the XPaths used in practice are very simple - consisting of a
        // single child or attribute.  For these we can streamline the process of
        // handling change events, since we can ignore many events that cannot
        // possibly affect the value of the XPath.
        private static XPathType GetXPathType(string xpath)
        {
            int n = xpath.Length;
            if (n == 0)
                return XPathType.SimpleName;
 
            // attributes start with '@', followed by a Name
            bool isAttribute = (xpath[0] == '@');
            int index = isAttribute ? 1 : 0;
            if (index >= n)
                return XPathType.Default;
 
            // [XML spec]  Name ::= (Letter | '_' | ':') (NameChar)*
            char c = xpath[index];
            if (!(Char.IsLetter(c) || c == '_' || c == ':'))
                return XPathType.Default;
 
            // [XML spec]  NameChar ::=  Letter | Digit | '.' | '-' | '_' | ':' | CombiningChar | Extender
            // We ignore the last two possibilities to keep the code simple.  They
            // don't arise often in practice.
            for (++index; index < n; ++index)
            {
                c = xpath[index];
                if (!(Char.IsLetterOrDigit(c) || c == '.' || c == '-' || c == '_' || c == ':'))
                    return XPathType.Default;
            }
 
            return isAttribute ? XPathType.SimpleAttribute : XPathType.SimpleName;
        }
 
        // determine if a change can possibly affect the value of the XPath
        private bool IsChangeRelevant(EventArgs rawArgs)
        {
            // if the XPath isn't "simple", any change in the XML tree might
            // affect its value
            if (_xpathType == XPathType.Default)
                return true;
 
            XmlNodeChangedEventArgs args = (XmlNodeChangedEventArgs)rawArgs;
            XmlNode parent = null;
            XmlNode valueNode = null;
 
            switch (args.Action)
            {
                case XmlNodeChangedAction.Insert:
                    parent = args.NewParent;
                    break;
 
                case XmlNodeChangedAction.Remove:
                    parent = args.OldParent;
                    break;
 
                case XmlNodeChangedAction.Change:
                    valueNode = args.Node;
                    break;
            }
 
            if (_collectionMode)
            {
                // only insertions/deletions to the context node are relevant
                return (parent == ContextNode);
            }
            else
            {
                // insertions/deletions to the context node are relevant -
                // the inserted/deleted node might match the XPath
                if (parent == ContextNode)
                    return true;
 
                // also relevant are changes that affect the value of the result
                // node.  This includes value changes directly on the result node,
                // as well as any change to the descendants of the result node.
                XmlNode resultNode = _hostWorker.GetResultNode() as XmlNode;
                if (resultNode == null)
                    return false;
 
                if (valueNode != null)
                    parent = valueNode;
 
                while (parent != null)
                {
                    if (parent == resultNode)
                        return true;
 
                    parent = parent.ParentNode;
                }
 
                return false;
            }
        }
 
        //------------------------------------------------------
        //
        //  Private Fields
        //
        //------------------------------------------------------
 
        private bool _collectionMode;
        private XPathType _xpathType;
        private XmlNode _contextNode;
        private XmlDataCollection _queriedCollection; // new DataCollection
        private ICollectionView _collectionView;
        private XmlDataProvider _xmlDataProvider;
        private ClrBindingWorker _hostWorker;
        private string _xpath;
    }
}