|
// 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:
// The main entry-point to the Anchoring namespace. LocatorManager is the
// controller for the Anchoring algorithms. Most of the work is delegated
// to processors. LocatorManager maintains a registry of processors.
// Spec: Anchoring Namespace Spec.doc
//
//
using System.Collections;
using System.ComponentModel;
using System.Windows;
using System.Windows.Annotations;
using System.Windows.Annotations.Storage;
using System.Windows.Documents;
using System.Windows.Threading;
using System.Xml;
namespace MS.Internal.Annotations.Anchoring
{
/// <summary>
/// The main entry-point to the Anchoring namespace. LocatorManager is the
/// controller for the Anchoring algorithms. Most of the work is delegated
/// to processors. LocatorManager maintains a registry of processors.
/// </summary>
sealed internal class LocatorManager : DispatcherObject
{
//------------------------------------------------------
//
// Constructors
//
//------------------------------------------------------
#region Constructors
/// <summary>
/// Create an instance of LocatorManager. It manages the
/// processors that are used to create and resolve locators.
/// </summary>
public LocatorManager() : this(null)
{
}
/// <summary>
/// Create an instance of LocatorManager with an optional specific store.
/// It manages the processors that are used to create and resolve locators.
/// If a store is passed in, its used to query for annotations. Otherwise
/// the service is looked for and the service's store is used.
/// </summary>
/// <param name="store">optional, store to use for query of annotations</param>
public LocatorManager(AnnotationStore store)
{
_locatorPartHandlers = new Hashtable();
_subtreeProcessors = new Hashtable();
_selectionProcessors = new Hashtable();
RegisterSubTreeProcessor(new DataIdProcessor(this), DataIdProcessor.Id);
RegisterSubTreeProcessor(new FixedPageProcessor(this), FixedPageProcessor.Id);
TreeNodeSelectionProcessor nodeProcessor = new TreeNodeSelectionProcessor();
RegisterSelectionProcessor(nodeProcessor, typeof(FrameworkElement));
RegisterSelectionProcessor(nodeProcessor, typeof(FrameworkContentElement));
TextSelectionProcessor textProcessor = new TextSelectionProcessor();
RegisterSelectionProcessor(textProcessor, typeof(TextRange));
RegisterSelectionProcessor(textProcessor, typeof(TextAnchor));
_internalStore = store;
}
#endregion Constructors
//------------------------------------------------------
//
// Public Methods
//
//------------------------------------------------------
#region Public Methods
#region Processor Caches
/// <summary>
/// Registers a subtree processor. The processorId is the string to be
/// used as the value of the SubTreeProcessorIdProperty for this processor.
/// This call overrides any previous registrations for processorId or for
/// the locator part types recognized by processor.
/// </summary>
/// <param name="processor">instance to be registered</param>
/// <param name="processorId">string id used to specify this processor as
/// the SubTreeProcessorIdProperty value</param>
/// <exception cref="ArgumentNullException">if the processor or processorId are null</exception>
public void RegisterSubTreeProcessor(SubTreeProcessor processor, String processorId)
{
VerifyAccess();
ArgumentNullException.ThrowIfNull(processor);
ArgumentNullException.ThrowIfNull(processorId);
XmlQualifiedName[] locatorPartTypes = processor.GetLocatorPartTypes();
_subtreeProcessors[processorId] = processor;
if (locatorPartTypes != null)
{
foreach (XmlQualifiedName typeName in locatorPartTypes)
{
_locatorPartHandlers[typeName] = processor;
}
}
}
/// <summary>
/// Returns the subtree processor set on this tree node to be used to
/// process the subtree rooted at this node. The subtree processor
/// returned is not the one used to process this node - only its
/// subtree. If no subtree processor is specified on this node
/// or any of its ancestors in the tree, an instance of the DataIdProcessor
/// is returned.
/// </summary>
/// <param name="node">tree node we are retrieving subtree processor for</param>
/// <returns>a subtree processor specified by the SubTreeProcessorIdProperty
/// on this node, or an instance of the DataIdProcessor if none is specified</returns>
/// <exception cref="ArgumentNullException">node is null</exception>
public SubTreeProcessor GetSubTreeProcessor(DependencyObject node)
{
VerifyAccess();
ArgumentNullException.ThrowIfNull(node);
// This property should contain one or more (comma-delimited)
// registered string IDs for subtree processors.
string processorString = node.GetValue(SubTreeProcessorIdProperty) as string;
if (!String.IsNullOrEmpty(processorString))
{
SubTreeProcessor processor = (SubTreeProcessor)_subtreeProcessors[processorString];
if (processor != null)
return processor;
else
throw new ArgumentException(SR.Format(SR.InvalidSubTreeProcessor, processorString));
}
else
{
return _subtreeProcessors[DataIdProcessor.Id] as SubTreeProcessor;
}
}
/// <summary>
/// Returns the subtree processor registered to handle the locator
/// part's type. If none is registered, null is returned.
/// </summary>
/// <param name="locatorPart">locator part for which a subtree processor
/// is being retreived</param>
/// <returns>a subtree processor registered to handle the locator part's
/// type; null if no such processor is registred</returns>
/// <exception cref="ArgumentNullException">locatorPart is null</exception>
public SubTreeProcessor GetSubTreeProcessorForLocatorPart(ContentLocatorPart locatorPart)
{
VerifyAccess();
ArgumentNullException.ThrowIfNull(locatorPart);
return _locatorPartHandlers[locatorPart.PartType] as SubTreeProcessor;
}
/// <summary>
/// Registers a selection processor for a selection Type.
/// This call overrides any previous registrations for the Type.
/// The processor also provides an array of locator part types it knows how
/// to handle. If a processor provides a locator part type that has been
/// provided by a previously registered processor, the last processor to be
/// registered overrides all others.
/// </summary>
/// <remark>
/// GetSelectionProcessor(Type) looks for a processor registered for
/// the Type or any of its base Types. Interfaces are not taken into
/// account. Do not register a selection processor for an interface
/// Type.
/// </remark>
/// <param name="processor">instance to be registered</param>
/// <param name="selectionType">Type of selection processed by this processor</param>
/// <exception cref="ArgumentNullException">processor or selectionType is null</exception>
public void RegisterSelectionProcessor(SelectionProcessor processor, Type selectionType)
{
VerifyAccess();
ArgumentNullException.ThrowIfNull(processor);
ArgumentNullException.ThrowIfNull(selectionType);
XmlQualifiedName[] locatorPartTypes = processor.GetLocatorPartTypes();
_selectionProcessors[selectionType] = processor;
if (locatorPartTypes != null)
{
foreach (XmlQualifiedName type in locatorPartTypes)
{
_locatorPartHandlers[type] = processor;
}
}
}
/// <summary>
/// Returns the selection processor for selections of the specified
/// Type. If no processor is registered for the specified Type, the
/// Type's base Type (as defined by Type.BaseType) is checked, and so
/// on. Interfaces implemented by the Type or any of its base Types
/// are not taken into account. If no processor is registered for Type
/// or any of its base Types, null is returned.
/// </summary>
/// <param name="selectionType">the Type of selection for which a handler
/// is requested</param>
/// <returns>the selection processor for the specified Type or one of its
/// base Types; null if no processor has been registered for this Type or
/// any of its base Types</returns>
/// <exception cref="ArgumentNullException">selectionType is null</exception>
public SelectionProcessor GetSelectionProcessor(Type selectionType)
{
VerifyAccess();
ArgumentNullException.ThrowIfNull(selectionType);
SelectionProcessor processor = null;
// We keep looking until we find a processor or
// there are no more base types to check for
do
{
processor = _selectionProcessors[selectionType] as SelectionProcessor;
selectionType = selectionType.BaseType;
}
while (processor == null && selectionType != null);
return processor;
}
/// <summary>
/// Returns the selection processor registered to handle the locator
/// part's type. If none is registered, null is returned.
/// </summary>
/// <param name="locatorPart">locator part for which a selection processor is
/// being retreived</param>
/// <returns>the selection processor for the locatorPart's type; null if no
/// processor has been registerd for that type</returns>
/// <exception cref="ArgumentNullException">locatorPart is null</exception>
public SelectionProcessor GetSelectionProcessorForLocatorPart(ContentLocatorPart locatorPart)
{
VerifyAccess();
ArgumentNullException.ThrowIfNull(locatorPart);
return _locatorPartHandlers[locatorPart.PartType] as SelectionProcessor;
}
#endregion Processor Caches
/// <summary>
/// Called by processors when they've encountered content in the tree
/// that should have annotations processed for it. This is a low-level
/// method that will get called several times during the algorithm for
/// loading annotations.
/// </summary>
/// <param name="node">the tree node that needs to be processed</param>
/// <returns>list of IAttachedAnnotations that were loaded for 'node';
/// the list will never be null but may be empty</returns>
/// <exception cref="ArgumentNullException">node is null</exception>
/// <exception cref="SystemException">no AnnotationStore is available from
/// the element tree</exception>
public IList<IAttachedAnnotation> ProcessAnnotations(DependencyObject node)
{
VerifyAccess();
ArgumentNullException.ThrowIfNull(node);
IList<IAttachedAnnotation> attachedAnnotations = new List<IAttachedAnnotation>();
IList<ContentLocatorBase> locators = GenerateLocators(node);
if (locators.Count > 0)
{
AnnotationStore store = null;
if (_internalStore != null)
{
store = _internalStore;
}
else
{
AnnotationService service = AnnotationService.GetService(node);
if (service == null || !service.IsEnabled)
{
throw new InvalidOperationException(SR.AnnotationServiceNotEnabled);
}
store = service.Store;
}
// LocatorBases for a single node should always be Locators
ContentLocator[] lists = new ContentLocator[locators.Count];
locators.CopyTo(lists, 0);
IList<Annotation> annotations = store.GetAnnotations(lists[0]);
foreach (ContentLocator locator in locators)
{
if (locator.Parts[locator.Parts.Count - 1].NameValuePairs.ContainsKey(TextSelectionProcessor.IncludeOverlaps))
{
locator.Parts.RemoveAt(locator.Parts.Count - 1);
}
}
foreach (Annotation annotation in annotations)
{
foreach (AnnotationResource anchor in annotation.Anchors)
{
foreach (ContentLocatorBase locator in anchor.ContentLocators)
{
AttachmentLevel attachmentLevel;
object attachedAnchor = FindAttachedAnchor(node, lists, locator, out attachmentLevel);
if (attachmentLevel != AttachmentLevel.Unresolved)
{
Debug.Assert(attachedAnchor != null, "AttachedAnchor cannot be null if attachmentLevel is not Unresolved.");
attachedAnnotations.Add(new AttachedAnnotation(this, annotation, anchor, attachedAnchor, attachmentLevel));
// Only process one locator per resource
break;
}
}
}
}
}
return attachedAnnotations;
}
/// <summary>
/// Generates zero or more Locators that map to the passed in
/// anchor. The Locators are generated using the
/// SubTreeProcessorIdProperty values set on the tree containing
/// the anchor and the processors registered.
/// </summary>
/// <param name="selection">the anchor to generate Locators for</param>
/// <returns>an array of Locators; will never return null but may return
/// an empty list</returns>
/// <exception cref="ArgumentNullException">if selection is null</exception>
/// <exception cref="ArgumentException">if no processor is registered for
/// selection's Type</exception>
public IList<ContentLocatorBase> GenerateLocators(Object selection)
{
VerifyAccess();
ArgumentNullException.ThrowIfNull(selection);
ICollection nodes = null;
SelectionProcessor selProcessor = GetSelectionProcessor(selection.GetType());
if (selProcessor != null)
{
nodes = (ICollection)selProcessor.GetSelectedNodes(selection);
}
else
{
throw new ArgumentException("Unsupported Selection", "selection");
}
IList<ContentLocatorBase> returnLocators = null;
PathNode pathRoot = PathNode.BuildPathForElements(nodes);
if (pathRoot != null)
{
SubTreeProcessor processor = GetSubTreeProcessor(pathRoot.Node);
Debug.Assert(processor != null, "SubtreeProcessor can not be null");
returnLocators = GenerateLocators(processor, pathRoot, selection);
}
// We never return null. A misbehaved processor might return null so we fix it up.
if (returnLocators == null)
returnLocators = new List<ContentLocatorBase>(0);
return returnLocators;
}
/// <summary>
/// Produces an anchor spanning the content specified by 'locator'.
/// This method traverses the tree and, using the registered
/// processors, resolves the locator in the current element tree.
/// </summary>
/// <param name="locator">the locator to be resolved</param>
/// <param name="offset">the index of the locator part to begin resolution with, ignored for
/// LocatorGroups which always start with the first locator part</param>
/// <param name="startNode">the tree node to start the resolution from</param>
/// <param name="attachmentLevel">type of the returned anchor</param>
/// <returns>an anchor that spans the content specified by locator; may return
/// null if the locator could not be resolved (in which case type is set to
/// AttachmentLevel.Unresolved</returns>
/// <exception cref="ArgumentNullException">locator or startNode are null</exception>
/// <exception cref="ArgumentException">offset is negative or greater than
/// locator.Count - 1</exception>
public Object ResolveLocator(ContentLocatorBase locator, int offset, DependencyObject startNode, out AttachmentLevel attachmentLevel)
{
VerifyAccess();
ArgumentNullException.ThrowIfNull(locator);
ArgumentNullException.ThrowIfNull(startNode);
// Offset need only be checked for Locators
ContentLocator realLocator = locator as ContentLocator;
if (realLocator != null)
{
ArgumentOutOfRangeException.ThrowIfNegative(offset);
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(offset, realLocator.Parts.Count);
}
return InternalResolveLocator(locator, offset, startNode, false /*skipStartNode*/, out attachmentLevel);
}
#endregion Public Methods
//------------------------------------------------------
//
// Public Operators
//
//------------------------------------------------------
//------------------------------------------------------
//
// Public Events
//
//------------------------------------------------------
//------------------------------------------------------
//
// Public Properties
//
//------------------------------------------------------
#region Public Properties
/// <summary>
/// DependencyProperty used to specify the processor to use for a
/// given subtree. When set on a node, all nodes below it will be
/// processed with the specified processor, unless overriden. Setting
/// this property after annotations have been loaded will have no effect
/// on existing annotations. If you want to change how the tree is
/// processed, you should set this property and call LoadAnnotations/
/// UnloadAnnotations on the service.
/// </summary>
#pragma warning suppress 7009
public static readonly DependencyProperty SubTreeProcessorIdProperty = DependencyProperty.RegisterAttached(
"SubTreeProcessorId",
typeof(string),
typeof(LocatorManager),
new FrameworkPropertyMetadata(string.Empty, FrameworkPropertyMetadataOptions.Inherits | FrameworkPropertyMetadataOptions.OverridesInheritanceBehavior));
/// <summary>
/// Sets the value of the SubTreeProcessorId 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 SetSubTreeProcessorId(DependencyObject d, String id)
{
ArgumentNullException.ThrowIfNull(d);
//d will check the context
d.SetValue(SubTreeProcessorIdProperty, id);
}
/// <summary>
/// Gets the value of the SubTreeProcessorId 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 SubTreeProcessorId attached property</returns>
/// <exception cref="ArgumentNullException">d is null</exception>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public static String GetSubTreeProcessorId(DependencyObject d)
{
ArgumentNullException.ThrowIfNull(d);
//d will check the context
return d.GetValue(SubTreeProcessorIdProperty) as String;
}
#endregion Public Properties
//------------------------------------------------------
//
// Internal Methods
//
//------------------------------------------------------
#region Internal Methods
/// <summary>
/// Traverse the element tree starting at subtree and load
/// annotations for that subtree.
/// </summary>
/// <param name="subTree">root of the subtree for which to load annotations</param>
/// <returns>list of IAttachedAnnotations that were loaded for 'node';
/// the list will never be null but may be empty</returns>
/// <exception cref="ArgumentNullException">subtree is null</exception>
internal IList<IAttachedAnnotation> ProcessSubTree(DependencyObject subTree)
{
ArgumentNullException.ThrowIfNull(subTree);
ProcessingTreeState data = new ProcessingTreeState();
PrePostDescendentsWalker<ProcessingTreeState> walker = new PrePostDescendentsWalker<ProcessingTreeState>(TreeWalkPriority.VisualTree, PreVisit, PostVisit, data);
walker.StartWalk(subTree);
return data.AttachedAnnotations;
}
/// <summary>
/// Searches the element subtree for a node that maps to the
/// passed in ContentLocatorBase.
/// Note: For LocatorGroups the startNode, offset, and prefixes
/// are ignored. Due to the nature of LocatorGroups, we always
/// start searching at the node with the service enabled, with
/// the first locator part, ignoring all prefixes.
/// </summary>
/// <param name="startNode">the root of the subtree to search</param>
/// <param name="prefixes">locators for the root of the subtree</param>
/// <param name="locator">the ContentLocatorBase we are resolving to an anchor</param>
/// <param name="attachmentLevel">type of the anchor returned</param>
/// <returns>the anchor for the passed in ContentLocatorBase; will return null if no match can be found</returns>
/// <exception cref="ArgumentNullException">startNode or locator is null</exception>
internal Object FindAttachedAnchor(DependencyObject startNode, ContentLocator[] prefixes, ContentLocatorBase locator, out AttachmentLevel attachmentLevel)
{
ArgumentNullException.ThrowIfNull(startNode);
ArgumentNullException.ThrowIfNull(locator);
// Set it to unresolved initially
attachmentLevel = AttachmentLevel.Unresolved;
Object anchor = null;
bool matched = true;
int locatorPartIdx = FindMatchingPrefix(prefixes, locator, out matched);
// The annotation's locator starts with at least
// one of the locators for the local root.
if (matched)
{
ContentLocator realLocator = locator as ContentLocator;
if (realLocator == null || locatorPartIdx < realLocator.Parts.Count)
{
// Now we try to resolve. If any locator parts were matched to the startNode we want to
// start resolving with its children, skipping a revisit to the startNode.
anchor = InternalResolveLocator(locator, locatorPartIdx, startNode, locatorPartIdx != 0 /*skipStartNode*/, out attachmentLevel);
}
// If nothing was returned, we base our return values on the results
// of matching against the local root.
if (attachmentLevel == AttachmentLevel.Unresolved && locatorPartIdx > 0)
{
if (locatorPartIdx == 0)
{
attachmentLevel = AttachmentLevel.Unresolved;
}
// If there was anything left to resolve then its incomplete
// what if its an incompletely resolved locator set? Can that happen?
else if (realLocator != null && locatorPartIdx < realLocator.Parts.Count)
{
attachmentLevel = AttachmentLevel.Incomplete;
anchor = startNode;
}
// otherwise its fully resolved
else
{
attachmentLevel = AttachmentLevel.Full;
anchor = startNode;
}
}
}
return anchor;
}
/// <summary>
/// Determines if the locator matches any of the prefixes, and if so, returns
/// the length of the prefix that was matched. Resolving of the locator can
/// begin with the first locator part after those that were matched by the prefix.
/// </summary>
/// <param name="prefixes">locators representing prefixes to match</param>
/// <param name="locator">locator to find a match for</param>
/// <param name="matched">whether or not a match was found</param>
/// <returns>index of the next locator part to resolve</returns>
private int FindMatchingPrefix(ContentLocator[] prefixes, ContentLocatorBase locator, out bool matched)
{
matched = true;
int locatorPartIdx = 0;
ContentLocator realLocator = locator as ContentLocator;
// If we have a locator set or there are no prefixes then we
// are 'matched' implicitly.
if (realLocator != null && prefixes != null && prefixes.Length > 0)
{
matched = false;
foreach (ContentLocator prefix in prefixes)
{
if (realLocator.StartsWith(prefix))
{
locatorPartIdx = prefix.Parts.Count;
matched = true;
break;
}
}
}
return locatorPartIdx;
}
#endregion Internal Methods
//------------------------------------------------------
//
// Private Methods
//
//------------------------------------------------------
#region Private Methods
#region Generating Locators
/// <summary>
/// Walks the path through the element tree specified by 'root' and
/// produces a set of locators identifying the path based on
/// DependencyProperty settings on the tree.
/// </summary>
/// <param name="processor">SubTreeProcessor to use on node</param>
/// <param name="startNode">the PathNode identifying the root of the subtree to process</param>
/// <param name="selection">selection for which Locators are being generated</param>
/// <returns>
/// a set of locators identifying the path through the element tree;
/// can return null if no locators can be generated for this root
/// </returns>
/// <exception cref="ArgumentNullException">root is null</exception>
/// <exception cref="SystemException">no AnnotationStore is available from
/// the element tree</exception>
private IList<ContentLocatorBase> GenerateLocators(SubTreeProcessor processor, PathNode startNode, Object selection)
{
Debug.Assert(startNode != null, "startNode can not be null");
List<ContentLocatorBase> locatorsToReturn = new List<ContentLocatorBase>();
bool continueProcessing = true;
// Process the current PathNode, with the possibilty of doing the whole
// subtree (in which case continueProcessing comes back false).
ContentLocator list = processor.GenerateLocator(startNode, out continueProcessing);
bool processSelection = list != null;
IList<ContentLocatorBase> newLocators = null;
// If we should continue processing, we look at the children of the
// root. Depending on the number of children we do different things.
if (continueProcessing)
{
switch (startNode.Children.Count)
{
case 0:
// No children - we just return what we have so far
if (list != null)
{
locatorsToReturn.Add(list);
}
break;
case 1:
// One child - we ask the root of the subtree for the processor
// to use and then ask the processor to handle the subtree.
SubTreeProcessor newProcessor = GetSubTreeProcessor(startNode.Node);
newLocators = GenerateLocators(newProcessor, (PathNode)startNode.Children[0], selection);
if (newLocators != null && newLocators.Count > 0)
processSelection = false;
if (list != null)
locatorsToReturn.AddRange(Merge(list, newLocators));
else
locatorsToReturn.AddRange(newLocators);
break;
default:
// Multiple children - we must process all the children as a
// locator set. This returns one or more locators that all start
// start with a ContentLocatorGroup which can have one locator for each
// child.
ContentLocatorBase newLocator = GenerateLocatorGroup(startNode, selection);
if (newLocator != null)
processSelection = false;
if (list != null)
locatorsToReturn.Add(list.Merge(newLocator));
else if (newLocator != null)
locatorsToReturn.Add(newLocator);
break;
}
}
else
{
// If we shouldn't continue processing we package up the
// locator we got from the first GenerateLocator call
if (list != null)
{
locatorsToReturn.Add(list);
}
}
// If we produced a locator for root and no one below us did as well,
// we need to process the selection, if any
if (processSelection && selection != null)
{
SelectionProcessor selProcessor = GetSelectionProcessor(selection.GetType());
if (selProcessor != null)
{
IList<ContentLocatorPart> locatorParts = selProcessor.GenerateLocatorParts(selection, startNode.Node);
// Possible bug - AddLocatorPartsToLocator was only written to handle normal locators, not
// locator groups. ToDo
if (locatorParts != null && locatorParts.Count > 0)
{
List<ContentLocatorBase> tempLocators = new List<ContentLocatorBase>(locatorsToReturn.Count * locatorParts.Count);
foreach (ContentLocatorBase loc in locatorsToReturn)
{
tempLocators.AddRange(((ContentLocator)loc).DotProduct(locatorParts)); // TODO
}
locatorsToReturn = tempLocators;
}
}
}
return locatorsToReturn;
}
/// <summary>
/// Produces a set of locators that uses locator groups to
/// span multiple branches in the tree.
/// </summary>
/// <param name="node">the PathNode identifying the root of the subtree to process</param>
/// <param name="selection">the selection we are currently processing</param>
/// <returns>
/// a locator set containing locators identifying the path
/// through the element tree
/// </returns>
/// <exception cref="ArgumentNullException">node is null</exception>
/// <exception cref="SystemException">no AnnotationStore is available from
/// the element tree</exception>
private ContentLocatorBase GenerateLocatorGroup(PathNode node, Object selection)
{
Debug.Assert(node != null, "node can not be null");
SubTreeProcessor processor = GetSubTreeProcessor(node.Node);
IList<ContentLocatorBase> tempLocators = null;
ContentLocatorGroup ContentLocatorGroup = new ContentLocatorGroup();
// Produce a locator representing each child and add it
// to the locator set. NOTE - We currently only support one
// locator per branch.
foreach (PathNode child in node.Children)
{
tempLocators = GenerateLocators(processor, child, selection);
if (tempLocators != null && tempLocators.Count > 0)
{
// Don't add empty Locators to the ContentLocatorGroup
if (tempLocators[0] != null)
{
ContentLocatorGroup locatorGroup = null;
ContentLocator locator = tempLocators[0] as ContentLocator;
if (locator != null && locator.Parts.Count != 0)
{
// NOTE - We currently only support producing one locator
// per branch of the locator set.
ContentLocatorGroup.Locators.Add(locator);
}
else if ((locatorGroup = tempLocators[0] as ContentLocatorGroup) != null)
{
// TODO - need to merge two locator groups
// Not supported in V1
}
}
}
}
// If the locators set is empty, we return null
// If only one locator was generated for the children, we return that
// locator directly. No need for a ContentLocatorGroup
// Otherwise return the ContentLocatorGroup
if (ContentLocatorGroup.Locators.Count == 0)
{
return null;
}
else if (ContentLocatorGroup.Locators.Count == 1)
{
ContentLocator list = ContentLocatorGroup.Locators[0];
ContentLocatorGroup.Locators.Remove(list);
return list;
}
else
{
return ContentLocatorGroup;
}
}
#endregion Generating Locators
#region Processing Tree
/// <summary>
/// Callback from DescendentsWalker before the nodes children are visited.
/// </summary>
/// <param name="dependencyObject">node to visit</param>
/// <param name="data">data used for the current tree walk</param>
/// <returns>returns whether or not the children should be visited</returns>
private bool PreVisit(DependencyObject dependencyObject, ProcessingTreeState data, bool visitedViaVisualTree)
{
Debug.Assert(data != null, "dataBlob is either null or not of ProcessingTreeState type");
bool calledProcessAnnotations = false;
SubTreeProcessor processor = GetSubTreeProcessor(dependencyObject);
Debug.Assert(processor != null, "SubtreeProcessor can not be null"); // There is always a default processor
IList<IAttachedAnnotation> attachedAnnotations = processor.PreProcessNode(dependencyObject, out calledProcessAnnotations);
if (attachedAnnotations != null)
data.AttachedAnnotations.AddRange(attachedAnnotations);
// Combine whether this processor called ProcessAnnotations with the info of its siblings
data.CalledProcessAnnotations = data.CalledProcessAnnotations || calledProcessAnnotations;
data.Push();
// Returning true here prevents children from being called
return !calledProcessAnnotations;
}
/// <summary>
/// Callback from DescendentsWalker after the nodes children are visited.
/// </summary>
/// <param name="dependencyObject">node to visit</param>
/// <param name="data">data used for the current tree walk</param>
/// <returns>return value is ignored</returns>
private bool PostVisit(DependencyObject dependencyObject, ProcessingTreeState data, bool visitedViaVisualTree)
{
Debug.Assert(data != null, "dataBlob is either null or not of ProcessingTreeState type");
bool childrenCalledProcessAnnotations = data.Pop();
SubTreeProcessor processor = GetSubTreeProcessor(dependencyObject);
Debug.Assert(processor != null, "SubtreeProcessor can not be null");
bool calledProcessAnnotations = false;
IList<IAttachedAnnotation> attachedAnnotations = processor.PostProcessNode(dependencyObject, childrenCalledProcessAnnotations, out calledProcessAnnotations);
if (attachedAnnotations != null)
data.AttachedAnnotations.AddRange(attachedAnnotations);
// This flag is information available to PostVisit calls
data.CalledProcessAnnotations = data.CalledProcessAnnotations || calledProcessAnnotations || childrenCalledProcessAnnotations;
// This return value is not used by PrePostDescendentsWalker
return true;
}
#endregion Processing Tree
#region Resolving Locators
/// <summary>
/// Resolves a locator starting on a specified locator part with a specified tree node.
/// The tree node can optionally be skipped (if some previous locator part has already
/// matched it) in which case the resolution starts with its children.
/// </summary>
/// <param name="locator">the locator to resolve</param>
/// <param name="offset">the index of the first locator part to resolve</param>
/// <param name="startNode">the node to start the resolution at</param>
/// <param name="skipStartNode">specifies whether to start with the startNode or its children</param>
/// <param name="attachmentLevel">return value specifying how successful the resolution was</param>
/// <returns>the attached anchor the locator was resolved to</returns>
/// <exception cref="InvalidOperationException">if a locator set is resolved and the individual selections
/// can't be merged</exception>
private Object InternalResolveLocator(ContentLocatorBase locator, int offset, DependencyObject startNode, bool skipStartNode, out AttachmentLevel attachmentLevel)
{
Debug.Assert(locator != null, "locator can not be null");
Debug.Assert(startNode != null, "startNode can not be null");
// Set it to unresolved initially
attachmentLevel = AttachmentLevel.Full;
Object selection = null;
ContentLocatorGroup locatorGroup = locator as ContentLocatorGroup;
ContentLocator realLocator = locator as ContentLocator;
AttachmentLevel individualAttachmentLevel = AttachmentLevel.Unresolved;
// If only one locator part left, it might represent a selection so we take
// care of that case before trying to resolve the locator part
if (realLocator != null && offset == realLocator.Parts.Count - 1)
{
ContentLocatorPart locatorPart = realLocator.Parts[offset];
SelectionProcessor selProcessor = GetSelectionProcessorForLocatorPart(locatorPart);
if (selProcessor != null)
{
selection = selProcessor.ResolveLocatorPart(locatorPart, startNode, out individualAttachmentLevel);
attachmentLevel = individualAttachmentLevel;
// No node has actually been matched in this case so we
// return the default Unresolved (set at top of method).
// Its up to the caller to know if the node and
// index passed in represented an incomplete resolution.
return selection;
}
}
IList<ContentLocator> locators = null;
// Setup the locators and other inputs before the loop. Normal locators
// are put in an array of locators (with one element). LocatorGroups have
// their Locators collection used.
if (locatorGroup == null)
{
Debug.Assert(offset >= 0 && offset < realLocator.Parts.Count, "offset out of range");
locators = new List<ContentLocator>(1);
locators.Add(realLocator);
}
else
{
// If there is a service, start at its root.
AnnotationService svc = AnnotationService.GetService(startNode);
if (svc != null)
{
startNode = svc.Root;
}
locators = locatorGroup.Locators;
// Always start resolving locator groups from the beginning
// and use the node the service is enabled on to start
offset = 0;
skipStartNode = false;
}
bool middlePortionExists = true;
if (locators.Count > 0)
{
// Otherwise we need to resolve each of the locators in the locator set
// and then try to merge the anchors that are returned
ResolvingLocatorState data = ResolveSingleLocator(ref selection, ref attachmentLevel, AttachmentLevel.StartPortion, locators[0], offset, startNode, skipStartNode);
// Special case - when there is only one locator we simply use the anchor and level
// returned from resolving that single locator
if (locators.Count == 1)
{
selection = data.AttachedAnchor;
attachmentLevel = data.AttachmentLevel;
}
else
{
// Resolve all locators after the first and before the last
if (locators.Count > 2)
{
AttachmentLevel tempLevel = AttachmentLevel.Unresolved;
AttachmentLevel savedLevel = attachmentLevel;
for (int i = 1; i < locators.Count - 1; i++)
{
data = ResolveSingleLocator(ref selection, ref attachmentLevel, AttachmentLevel.MiddlePortion, locators[i], offset, startNode, skipStartNode);
//if there are >1 middle locators some of them might be resolved, some other - not
//if even one middle locator is resolved we should save its attachmenLevel
if ((tempLevel == AttachmentLevel.Unresolved) || ((attachmentLevel & AttachmentLevel.MiddlePortion) != 0))
tempLevel = attachmentLevel;
attachmentLevel = savedLevel;
}
attachmentLevel = tempLevel;
}
else
{
// We make note that there were no middle portion locators
middlePortionExists = false;
}
// Process the last locator
data = ResolveSingleLocator(ref selection, ref attachmentLevel, AttachmentLevel.EndPortion, locators[locators.Count - 1], offset, startNode, skipStartNode);
// If no locators exists for the middle portion we need to make
// sure its not the only portion left on.
if (!middlePortionExists && attachmentLevel == AttachmentLevel.MiddlePortion)
{
attachmentLevel &= ~AttachmentLevel.MiddlePortion;
}
//if start and end is resolved we consider this as fully resolved
//this will handle the case of empty middle page in fixed
if (attachmentLevel == (AttachmentLevel.StartPortion | AttachmentLevel.EndPortion))
attachmentLevel = AttachmentLevel.Full;
}
}
else
{
// There are no locators to resolve
attachmentLevel = AttachmentLevel.Unresolved;
}
return selection;
}
/// <summary>
/// Resolves a single locator starting at the given startNode.
/// Sets the selection and attachmentLevel if necessary.
/// </summary>
/// <param name="selection">object representing the content that has been resolved
/// so far; updated if the locator passed in is resolved</param>
/// <param name="attachmentLevel">attachmentLevel of content that has been resolved
/// so far; updated based on the resolution of the passed in locator</param>
/// <param name="attemptedLevel">the level that is represented by this locator -
/// start, middle or end</param>
/// <param name="locator">the locator to resolve</param>
/// <param name="offset">the offset into the locator to start the resolution at</param>
/// <param name="startNode">the node to start the resolution at</param>
/// <param name="skipStartNode">whether or not the start node should be looked at</param>
/// <returns>the data representing the resolution of the single locator; used for
/// special cases by calling code to override results from this method</returns>
private ResolvingLocatorState ResolveSingleLocator(ref object selection, ref AttachmentLevel attachmentLevel, AttachmentLevel attemptedLevel, ContentLocator locator, int offset, DependencyObject startNode, bool skipStartNode)
{
ResolvingLocatorState data = new ResolvingLocatorState();
data.LocatorPartIndex = offset;
data.ContentLocatorBase = locator;
PrePostDescendentsWalker<ResolvingLocatorState> walker = new PrePostDescendentsWalker<ResolvingLocatorState>(TreeWalkPriority.VisualTree, ResolveLocatorPart, TerminateResolve, data);
walker.StartWalk(startNode, skipStartNode);
if (data.AttachmentLevel == AttachmentLevel.Full && data.AttachedAnchor != null)
{
// Merge the results with pre-existing selection
if (selection != null)
{
SelectionProcessor selProcessor = GetSelectionProcessor(selection.GetType());
object newSelection;
if (selProcessor != null)
{
if (selProcessor.MergeSelections(selection, data.AttachedAnchor, out newSelection))
{
selection = newSelection;
}
else
{
// If we can't merge, them this locator isn't included in final results so we
// we turn off the level that we are attempting to resolve
attachmentLevel &= ~attemptedLevel;
}
}
else
{
// If not selection processor, the locator can't be resolved so
// we turn off the level that we were attempting to resolve
attachmentLevel &= ~attemptedLevel;
}
}
else
{
selection = data.AttachedAnchor;
}
}
else
{
// ContentLocator didn't fully resolve so we turn off the level
// that we were attempting to resolve
attachmentLevel &= ~attemptedLevel;
}
return data;
}
/// <summary>
/// Resolves a locator starting from 'startFrom' and the locator part at
/// position 'offset' in the locator. This method is called from the
/// DescendentsWalker. It maintains the state of resolution as each
/// node is visited and individual locator parts are resolved.
/// </summary>
/// <param name="dependencyObject">the current node to visit</param>
/// <param name="data">data containing the state of the current resolution</param>
/// <returns>whether or not the children of this node should be visited</returns>
private bool ResolveLocatorPart(DependencyObject dependencyObject, ResolvingLocatorState data, bool visitedViaVisualTree)
{
if (data.Finished)
return false;
ContentLocator locator = data.ContentLocatorBase;
Debug.Assert(locator != null, "locator can not be null");
Debug.Assert(data.LocatorPartIndex >= 0 && data.LocatorPartIndex < locator.Parts.Count,
"LocatorPartIndex out of range");
bool keepResolving = true;
DependencyObject node = null;
SubTreeProcessor processor = null;
ContentLocatorPart locatorPart = locator.Parts[data.LocatorPartIndex];
if (locatorPart == null)
{
// Can't resolve a null ContentLocatorPart
keepResolving = false;
}
processor = this.GetSubTreeProcessorForLocatorPart(locatorPart);
if (processor == null)
{
// Can't keep resolving if there is no processor for this ContentLocatorBase Part
keepResolving = false;
}
if (locatorPart != null && processor != null)
{
node = processor.ResolveLocatorPart(locatorPart, dependencyObject, out keepResolving);
if (node != null)
{
// At a minimum we are incompletely resolved
data.AttachmentLevel = AttachmentLevel.Incomplete;
data.AttachedAnchor = node;
keepResolving = true;
data.LastNodeMatched = node;
data.LocatorPartIndex++;
// We might already be finished here - if there are no more locator parts
// we are fully resolved and there's no need to keep resolving
if (data.LocatorPartIndex == locator.Parts.Count)
{
data.AttachmentLevel = AttachmentLevel.Full;
data.AttachedAnchor = node;
keepResolving = false;
}
// If all we have left is the last locatorPart, lets try to resolve it as a
// selection. If there is no selection processor, it will fall through and
// be handled by resolving on one of the children
else if (data.LocatorPartIndex == locator.Parts.Count - 1)
{
locatorPart = locator.Parts[data.LocatorPartIndex];
SelectionProcessor selProcessor = GetSelectionProcessorForLocatorPart(locatorPart);
if (selProcessor != null)
{
AttachmentLevel attachmentLevel;
Object selection = selProcessor.ResolveLocatorPart(locatorPart, node, out attachmentLevel);
if (selection != null)
{
data.AttachmentLevel = attachmentLevel;
data.AttachedAnchor = selection;
keepResolving = false;
}
else
{
// In this case the processor couldn't resolve the selection
// locator part. There's no use in continuing.
keepResolving = false;
}
}
}
}
}
return keepResolving;
}
/// <summary>
/// This gets called after each node's subtree has been called to resolve
/// the current locator part.
/// If the node the call is made for was the last node that anything was
/// matched with, we want to stop looking at the rest of the tree. This is
/// because matches should be unique (so no sibling should be able to match)
/// and if they aren't the first match wins.
/// </summary>
/// <param name="dependencyObject"></param>
/// <param name="data"></param>
/// <returns></returns>
private bool TerminateResolve(DependencyObject dependencyObject, ResolvingLocatorState data, bool visitedViaVisualTree)
{
// If we are finished with the subtree for the last node matched, we've
// resolved as much as we can and we should not bother looking at the
// rest of the tree. Finished is a property we use to short-circuit
// vising the rest of the tree. Note: Returning false only prevents
// the children from being visited, we need to not visit siblings either.
if (!data.Finished && data.LastNodeMatched == dependencyObject)
{
data.Finished = true;
}
return false;
}
#endregion Resolving Locators
#region ContentLocatorBase Operations
/// <summary>
/// Adds the additionalLocators to the end of initialLocator. If
/// there are more than one additional locators, clones of
/// initialLocator are created. This method may be destructive -
/// the locators passed in may be modified.
/// </summary>
/// <param name="initialLocator">the locator to append the additional locators to</param>
/// <param name="additionalLocators">array of locators that need to be appended</param>
/// <returns>list of merged locators</returns>
private IList<ContentLocatorBase> Merge(ContentLocatorBase initialLocator, IList<ContentLocatorBase> additionalLocators)
{
if (additionalLocators == null || additionalLocators.Count == 0)
{
List<ContentLocatorBase> res = new List<ContentLocatorBase>(1);
res.Add(initialLocator);
return res;
}
for (int i = 1; i < additionalLocators.Count; i++)
{
additionalLocators[i] = ((ContentLocatorBase)initialLocator.Clone()).Merge(additionalLocators[i]);
}
// Avoid making one too many clones...
additionalLocators[0] = initialLocator.Merge(additionalLocators[0]);
return additionalLocators;
}
#endregion ContentLocatorBase Operations
#endregion Private Methods
//------------------------------------------------------
//
// Private Fields
//
//------------------------------------------------------
#region Private Classes
/// <summary>
/// Private class used to store state while processing the tree.
/// It keeps a list of annotations loaded as well as the state of
/// the calledProcessAnnotations flags returned by processors.
/// </summary>
private class ProcessingTreeState
{
public ProcessingTreeState()
{
_calledProcessAnnotations.Push(false);
}
/// <summary>
/// Returns list of attached annotations loaded so far.
/// </summary>
public List<IAttachedAnnotation> AttachedAnnotations
{
get
{
return _attachedAnnotations;
}
}
/// <summary>
/// Returns whether any node in the current node's subtree (including the
/// current node itself) has returned true for calledProcessAnnotations.
/// </summary>
public bool CalledProcessAnnotations
{
get
{
return _calledProcessAnnotations.Peek();
}
set
{
if (_calledProcessAnnotations.Peek() != value)
{
_calledProcessAnnotations.Pop();
_calledProcessAnnotations.Push(value);
}
}
}
/// <summary>
/// Pushes another boolean on to the stack of return values
/// we are maintaining for each level of children.
/// </summary>
public void Push()
{
// Push on a fresh bool value
_calledProcessAnnotations.Push(false);
}
/// <summary>
/// Pops one return value off the stack and returns it.
/// </summary>
public bool Pop()
{
return _calledProcessAnnotations.Pop();
}
private List<IAttachedAnnotation> _attachedAnnotations = new List<IAttachedAnnotation>();
private Stack<bool> _calledProcessAnnotations = new Stack<bool>();
}
/// <summary>
/// Data structure that maintains the state during an operation
/// to resolve a locator. We keep the locator and locator part
/// index as the inputs. We keep the attachment level and
/// attached anchor as the outputs.
/// LastNodeMatched and Finished are used to short-circuit the
/// resolution process once we've made at least an incomplete
/// match (in which case we only want to visit the children of
/// the last matched node, not its siblings).
///
/// This class is not a struct because we don't want it to be
/// a value type.
/// </summary>
private class ResolvingLocatorState
{
public ContentLocator ContentLocatorBase;
public int LocatorPartIndex;
public AttachmentLevel AttachmentLevel = AttachmentLevel.Unresolved;
public Object AttachedAnchor;
public bool Finished;
public Object LastNodeMatched;
}
#endregion Private Classes
//------------------------------------------------------
//
// Private Fields
//
//------------------------------------------------------
#region Private Fields
// Hashtable of locator part handlers - keyed by XmlQualifiedName
private Hashtable _locatorPartHandlers;
// Hashtable of subtree processors - keyed by processor id
private Hashtable _subtreeProcessors;
// Hashtable of selection processors - keyed by selection Type
private Hashtable _selectionProcessors;
// Potential separators for subtree processor class names
private static ReadOnlySpan<char> Separators => [',', ' ', ';'];
// Optional store, used if passed in, otherwise we grab the service's store
private AnnotationStore _internalStore = null;
#endregion Private Fields
}
}
|