|
// 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.
#pragma warning disable 1634, 1691
//
//
// Description:
// 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.
// Spec: Anchoring Namespace Spec.doc
//
using System;
using System.Diagnostics;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;
using System.Windows.Annotations;
using System.Windows.Annotations.Storage;
using System.Windows.Markup;
using System.Windows.Threading;
using System.Xml;
using MS.Utility;
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}"), "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}"), "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>
#pragma warning suppress 7009
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>
#pragma warning suppress 7009
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
}
}
|