Custom Navigation within XML files

The request

A customer came to me and said:

“So I’ve got some XML files which hold a sort of DSL which I use to configure my application. There are elements in this DSL which have id attributes, and other elements which have ref attributes. As you might expect, within my DSL, the ref attributes are references to the id attributes.

Is there anything in CodeRush to help my navigate easily among these id and ref attributes where their values match?”

My plugin-sense started tingling….

I immediately thought of our awesome Tab to Next Reference feature, and figured we could probably extend it to work in this new scenario.

Create the project

  • File… New Project
  • Choose Language (C# in this case)
  • DXCore\Standard Plug-in

    Choose a name for your project (I chose CR_XMLNav)

    The Coding

    First I used a template to add some boilerplate code. (Bold text shows custom changes)


    private void registerXMLRefSearch()
    {
        var XMLRefSearch = new DevExpress.CodeRush.Core.SearchProvider();
        ((System.ComponentModel.
    ISupportInitialize)(XMLRefSearch)).BeginInit();
        XMLRefSearch.Description =
    "XMLRefSearch";
        XMLRefSearch.ProviderName =
    "XMLRefSearch"; // Needs to be Unique
        XMLRefSearch.Register = true;
        XMLRefSearch.UseForNavigation =
    true;
        XMLRefSearch.CheckAvailability += XMLRefSearch_CheckAvailability;
        XMLRefSearch.SearchReferences += XMLRefSearch_SearchReferences;
        ((System.ComponentModel.
    ISupportInitialize)(XMLRefSearch)).EndInit();
    }

    private void XMLRefSearch_CheckAvailability(object sender, CheckSearchAvailabilityEventArgs ea)
    {
    }

    private
    void XMLRefSearch_SearchReferences(Object Sender, DevExpress.CodeRush.Core.SearchEventArgs ea)
    {
    }


    Then I fleshed out the CheckAvailability routine:


    private void XMLRefSearch_CheckAvailability(object sender, CheckSearchAvailabilityEventArgs ea)
    {
        // Get Active LanguageElement
        var Element = CodeRush.Documents.ActiveTextDocument.GetNodeAt(CodeRush.Caret.Line, CodeRush.Caret.Offset);

        if
    (Element == null)
            return;

        if
    (!(Element is XmlAttribute))
            return;

        // Allow search to start if Attribute
        ea.Available = new string[] { "id", "ref" }.Contains(((XmlAttribute)Element).Name);
    }


    Essentially this routine looks to see if the caret is on a LanguageElement of type XmlAttribute whose name matches either id or ref.

    If it finds that this is the case, our searchProvider is made available, and processing continues with the XMLRefSearch_SearchReferences routine.


    private void XMLRefSearch_SearchReferences(Object Sender, DevExpress.CodeRush.Core.SearchEventArgs ea)
    {
        // Store Value of initial XmlAttribute
        TextDocument activeDoc = CodeRush.Documents.ActiveTextDocument;
        string StartValue = ((XmlAttribute)activeDoc.GetNodeAt(CodeRush.Caret.Line, CodeRush.Caret.Offset)).Value;

       
    // Iterate LanguageElements in solution
        SolutionElement activeSolution = CodeRush.Source.ActiveSolution;
        foreach (ProjectElement project in activeSolution.AllProjects)
        {
            foreach (SourceFile sourceFile in project.AllFiles)
            {
                SourceFile activeFile = CodeRush.Source.ActiveSourceFile;
                ElementEnumerable Enumerator = new ElementEnumerable(sourceFile, new XMLAttributeFilter(StartValue), true);
                foreach (XmlAttribute CurrentAttribute in Enumerator
                {
                    ea.AddRange(
    new FileSourceRange(CurrentAttribute.FileNode, CurrentAttribute.ValueRange));
                }
            }
        }
    }

    This code iterates through your solution (projects files etc) looking for appropriate XmlAttributes. The key to this little piece of code is this line here:
    ElementEnumerable Enumerator = new ElementEnumerable(sourceFile, new XMLAttributeFilter(StartValue), true
    );


    This creates an object capable of enumerating through the code looking for things which match a very specific set of criteria.

    The specifics of what will match are handled by our last piece of code.


    public class XMLAttributeFilter : IElementFilter
    {
        private readonly string
    _startValue;
        public XMLAttributeFilter(string
    StartValue)
        {
            _startValue = StartValue;
        }
        public bool Apply(IElement
    element)
        {
            // Skip if null 
            if (element == null
    )
                return false
    ;

           
    // Skip if not XmlAttribute
            if (!(element is XmlAttribute
    ))
                return false
    ;
            XmlAttribute CurrentAttribute = (XmlAttribute
    )element;

           
    // Skip attribute if it doesn't have the correct name.
            if (CurrentAttribute.Name != "id" && CurrentAttribute.Name != "ref"
    )
                return false
    ;

           
    // Skip the attribute if it doesn't have the same value as start point.
            if
    (CurrentAttribute.Value != _startValue)
                return false


            return
    true
    ;
        }
        public bool SkipChildren(IElement
    element)
        {
            return false
    ;
        }
    }   

    Each LanguageElement is checked and passed over if it fails any single check.

    • In this way we pass a list of all valid items on to the Tab to Next Reference feature which then allows us to seamlessly tab through all the references.

    • CR_XMLNav

    • Summary
    • This sort of technique is, as you can see, very simple to apply. I’m sure you can think of many similar occasions when you’ve wished you could jump between related bits of configuration. Well now you have the ability to do just that.

    The full version of this plugin is hosted up on GitHub.com for those who are interested.

    no comments
    No Comments

    Please login or register to post comments.