| File: MS\Internal\Annotations\Anchoring\DataIdProcessor.cs | Web Access |
| Project: src\wpf\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. using System.ComponentModel; using System.Windows; using System.Windows.Annotations; using System.Xml; namespace MS.Internal.Annotations.Anchoring { /// <summary> /// DataIdProcessor walks the tree and loads annotations based on unique ids /// identified by the DataIdProperty. It loads annotations when it /// reaches a leaf in the tree or when it runs into a node with the /// FetchAnnotationsAsBatch property set to true. /// </summary> internal sealed class DataIdProcessor : SubTreeProcessor { //------------------------------------------------------ // // Constructors // //------------------------------------------------------ #region Constructors /// <summary> /// Creates an instance of DataIdProcessor. /// </summary> /// <param name="manager">the manager that owns this processor</param> /// <exception cref="ArgumentNullException">manager is null</exception> public DataIdProcessor(LocatorManager manager) : base(manager) { } #endregion Constructors //------------------------------------------------------ // // Public Methods // //------------------------------------------------------ #region Public Methods /// <summary> /// If and only if the current node has a DataId set and has FetchAnnotationsAsBatch /// set to true, then all annotations for the subtree rooted at this node are loaded /// at once. /// </summary> /// <param name="node">node to process</param> /// <param name="calledProcessAnnotations">indicates the callback was called by /// this processor</param> /// <returns> /// a list of AttachedAnnotations loaded during the processing of /// the node; can be null if no annotations were loaded /// </returns> /// <exception cref="ArgumentNullException">node is null</exception> public override IList<IAttachedAnnotation> PreProcessNode(DependencyObject node, out bool calledProcessAnnotations) { ArgumentNullException.ThrowIfNull(node); // We get the local value so we can distinguish between the property // being set or not. We don't want to rely on null or String.Empty because // those might have been the values set. object dataId = node.ReadLocalValue(DataIdProcessor.DataIdProperty); bool fetchAsBatch = (bool)node.GetValue(FetchAnnotationsAsBatchProperty); // If the current node has an ID set on it and FetchAnnotationsAsBatch is // set to true, we process this node immediately and return. All its children // will be processed indirectly. if (fetchAsBatch && dataId != DependencyProperty.UnsetValue) { calledProcessAnnotations = true; return Manager.ProcessAnnotations(node); } calledProcessAnnotations = false; return null; } /// <summary> /// This method is called after PreProcessNode and after all the children /// in the subtree have been processed (or skipped if PreProcessNode returns /// true for calledProcessAnnotations). /// If no calls to ProcessAnnotations were made for any portion of the subtree below /// this node, then annotations for this node will be loaded /// </summary> /// <param name="node">the node to process</param> /// <param name="childrenCalledProcessAnnotations">indicates whether calledProcessAnnotations /// was returned as true by any node underneath this node</param> /// <param name="calledProcessAnnotations">indicates whether ProcessAnnotations was called /// by this method</param> /// <returns> /// a list of AttachedAnnotations loaded during the processing of /// the node; can be null if no annotations were loaded /// </returns> public override IList<IAttachedAnnotation> PostProcessNode(DependencyObject node, bool childrenCalledProcessAnnotations, out bool calledProcessAnnotations) { ArgumentNullException.ThrowIfNull(node); // We get the local value so we can distinguish between the property // being set or not. We don't want to rely on null or String.Empty because // those might have been the values set. object dataId = node.ReadLocalValue(DataIdProcessor.DataIdProperty); bool fetchAsBatch = (bool)node.GetValue(FetchAnnotationsAsBatchProperty); // If no children were processed, we try and process this node if (!fetchAsBatch && !childrenCalledProcessAnnotations && dataId != DependencyProperty.UnsetValue) { FrameworkElement nodeParent = null; FrameworkElement feNode = node as FrameworkElement; if (feNode != null) { nodeParent = feNode.Parent as FrameworkElement; } AnnotationService service = AnnotationService.GetService(node); if (service != null && (service.Root == node || (nodeParent != null && service.Root == nodeParent.TemplatedParent))) { calledProcessAnnotations = true; return Manager.ProcessAnnotations(node); } } calledProcessAnnotations = false; return null; } /// <summary> /// Generates a locator part list identifying node. If node has a /// value for DataIdProperty, a locator with a single locator part /// containing the id value is returned. Otherwise null is returned. /// </summary> /// <param name="node">the node to generate a locator for</param> /// <param name="continueGenerating">specifies whether or not generating should /// continue for the rest of the path; always set to true</param> /// <returns>if node has a value for DataIdProperty, a locator with a /// single locator part containing the id value; null otherwise /// </returns> /// <exception cref="ArgumentNullException">node is null</exception> public override ContentLocator GenerateLocator(PathNode node, out bool continueGenerating) { ArgumentNullException.ThrowIfNull(node); continueGenerating = true; ContentLocator locator = null; ContentLocatorPart newLocatorPart = CreateLocatorPart(node.Node); if (newLocatorPart != null) { locator = new ContentLocator(); locator.Parts.Add(newLocatorPart); } return locator; } /// <summary> /// Searches the logical tree for a node matching the values of /// locatorPart. The search begins with startNode. /// </summary> /// <param name="locatorPart">locator part to be matched, must be of the type /// handled by this processor</param> /// <param name="startNode">logical tree node to start search at</param> /// <param name="continueResolving">return flag indicating whether the search /// should continue (presumably because the search was not exhaustive)</param> /// <returns>returns a node that matches the locator part; null if no such /// node is found</returns> /// <exception cref="ArgumentNullException">locatorPart or startNode are /// null</exception> /// <exception cref="ArgumentException">locatorPart is of the incorrect /// type</exception> public override DependencyObject ResolveLocatorPart(ContentLocatorPart locatorPart, DependencyObject startNode, out bool continueResolving) { ArgumentNullException.ThrowIfNull(locatorPart); ArgumentNullException.ThrowIfNull(startNode); if (DataIdElementName != locatorPart.PartType) throw new ArgumentException(SR.Format(SR.IncorrectLocatorPartType, $"{locatorPart.PartType.Namespace}:{locatorPart.PartType.Name}"), nameof(locatorPart)); // Initial value continueResolving = true; // Get the values from the locator part... string id = locatorPart.NameValuePairs[ValueAttributeName]; if (id == null) { throw new ArgumentException(SR.Format(SR.IncorrectLocatorPartType, $"{locatorPart.PartType.Namespace}:{locatorPart.PartType.Name}"), nameof(locatorPart)); } // and from the node to examine. string nodeId = GetNodeId(startNode); if (nodeId != null) { if (nodeId.Equals(id)) { return startNode; } else { // If there was a value and it didn't match, // we shouldn't bother checking the subtree continueResolving = false; } } return null; } /// <summary> /// Returns a list of XmlQualifiedNames representing the /// the locator parts this processor can resolve/generate. /// </summary> public override XmlQualifiedName[] GetLocatorPartTypes() { return (XmlQualifiedName[])LocatorPartTypeNames.Clone(); } #endregion Public Methods //------------------------------------------------------ // // Public Operators // //------------------------------------------------------ //------------------------------------------------------ // // Public Events // //------------------------------------------------------ //------------------------------------------------------ // // Public Properties // //------------------------------------------------------ #region Public Properties /// <summary> /// Id used to register this processor with the LocatorManager. Registration /// is done by the framework and does not need to be repeated. Use this /// string in markup as the value for SubTreeProcessorIdProperty. /// </summary> public const String Id = "Id"; /// <summary> /// Used to specify a unique id for the data represented by a /// logical tree node. Attach this property to the element with a /// unique value. /// </summary> public static readonly DependencyProperty DataIdProperty = DependencyProperty.RegisterAttached( "DataId", typeof(String), typeof(DataIdProcessor), new PropertyMetadata( (string)null, new PropertyChangedCallback(OnDataIdPropertyChanged), new CoerceValueCallback(CoerceDataId))); /// <summary> /// Sets the value of the DataId attached property /// of the LocatorManager class. /// </summary> /// <param name="d">element to which to write the attached property</param> /// <param name="id">the value to set</param> /// <exception cref="ArgumentNullException">d is null</exception> public static void SetDataId(DependencyObject d, String id) { ArgumentNullException.ThrowIfNull(d); d.SetValue(DataIdProperty, id); } /// <summary> /// Gets the value of the DataId attached property /// of the LocatorManager class. /// </summary> /// <param name="d">the object from which to read the attached property</param> /// <returns>the value of the DataId attached property</returns> /// <exception cref="ArgumentNullException">d is null</exception> [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public static String GetDataId(DependencyObject d) { ArgumentNullException.ThrowIfNull(d); return d.GetValue(DataIdProperty) as String; } /// <summary> /// Property that specifies, when set to true on an element, this /// processor should load all annotations for the subtree rooted /// a the element as a batch. /// </summary> public static readonly DependencyProperty FetchAnnotationsAsBatchProperty = DependencyProperty.RegisterAttached( "FetchAnnotationsAsBatch", typeof(bool), typeof(DataIdProcessor), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.Inherits)); /// <summary> /// Sets the value of the FetchAnnotationsAsBatch attached property /// of the LocatorManager class. /// </summary> /// <param name="d">element to which to write the attached property</param> /// <param name="id">the value to set</param> /// <exception cref="ArgumentNullException">d is null</exception> public static void SetFetchAnnotationsAsBatch(DependencyObject d, bool id) { ArgumentNullException.ThrowIfNull(d); d.SetValue(FetchAnnotationsAsBatchProperty, id); } /// <summary> /// Gets the value of the FetchAnnotationsAsBatch attached property /// of the LocatorManager class. /// </summary> /// <param name="d">the object from which to read the attached property</param> /// <returns>the value of the FetchAnnotationsAsBatch attached property</returns> /// <exception cref="ArgumentNullException">d is null</exception> [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public static bool GetFetchAnnotationsAsBatch(DependencyObject d) { ArgumentNullException.ThrowIfNull(d); return (bool)d.GetValue(FetchAnnotationsAsBatchProperty); } #endregion Public Properties //------------------------------------------------------ // // Private Methods // //------------------------------------------------------ #region Private Methods /// <summary> /// Callback triggered when a DataIdProperty value changes. /// If the values are really different we unload and reload annotations /// using the new value. /// </summary> private static void OnDataIdPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { String oldValue = (string)e.OldValue; String newValue = (string)e.NewValue; if (!String.Equals(oldValue, newValue)) { // If we get here the value has changed so we reload annotations AnnotationService service = AnnotationService.GetService(d); if (service != null && service.IsEnabled) { service.UnloadAnnotations(d); service.LoadAnnotations(d); } } } private static object CoerceDataId(DependencyObject d, object value) { string newValue = (string)value; return (newValue != null && newValue.Length == 0) ? null : value; } /// <summary> /// Creates a DataId locator part for node. /// </summary> /// <param name="node">logical tree node for which a locator part will be created</param> /// <returns>a DataId locator part for node, or null if node has no /// value for the DataIdProperty</returns> /// <exception cref="ArgumentNullException">node is null</exception> private ContentLocatorPart CreateLocatorPart(DependencyObject node) { Debug.Assert(node != null, "DependencyObject can not be null"); // Get values from the node string nodeId = GetNodeId(node); if ((nodeId == null) || (nodeId.Length == 0)) return null; ContentLocatorPart part = new ContentLocatorPart(DataIdElementName); part.NameValuePairs.Add(ValueAttributeName, nodeId); return part; } /// <summary> /// Get the value of the DataId dependency property for a /// DependencyObject. /// </summary> /// <param name="d">the object whose DataId value is to be retrieved</param> /// <returns>the object's DataId, if it is set, null otherwise</returns> internal String GetNodeId(DependencyObject d) { Debug.Assert(d != null, "DependencyObject can not be null"); String id = d.GetValue(DataIdProperty) as string; // Return null if the string is empty if (String.IsNullOrEmpty(id)) { id = null; } return id; } #endregion Private Methods //------------------------------------------------------ // // Private Fields // //------------------------------------------------------ #region Private Fields /// <summary> /// The type name of locator parts handled by this handler. /// This is internal and available to the processor that /// is closely aligned with this handler. /// </summary> private static readonly XmlQualifiedName DataIdElementName = new XmlQualifiedName("DataId", AnnotationXmlConstants.Namespaces.BaseSchemaNamespace); //the name of the value attribute private const String ValueAttributeName = "Value"; // ContentLocatorPart types understood by this processor private static readonly XmlQualifiedName[] LocatorPartTypeNames = new XmlQualifiedName[] { DataIdElementName }; #endregion Private Fields } }