Customizing the XAML Syntax Editor

Damyan Petev / Wednesday, October 3, 2012

Customizing the XAML Syntax Editor.The XAML Syntax Editor is pretty awesome control – I mean being able to create your own code editor is one and then just customize language and coloring and… add the fact that you can split the view and be on at line 50 and 500 at the same time and making changes in both spots! As you can tell, there are still plenty of neat things about the XamSyntaxEditor that I’d like to show you. I  showed you last time how to get started with this cross-platform control and with some basics covered lets dive into the options to customize and enhance the functionality and user experience of the control.

First to mention that even though the v2 release for 2012 is not yet out, you can still go grab the CTP version – download links can be found in Brian’s announcing post. Besides major spoilers on what features are to expected in the future, in the downloads you will find samples! But most importantly you can check out the XamSyntaxEditorStudio demo that features something similar to what I’d like to show you today and some other cool tricks:

XAML Syntax Editor Studion demo.

Even by looking at it by adding Find and Error panes it does feel Visual Studio-ish and that’s the goal.

Custom Margins

I’ll just mention this somewhat briefly to acknowledge it is there. The feature is actually the one providing for the line numbering and behave in much the same way – fixed/frozen/static/pinned area (man, I’ve seem a few variations of having something just not scroll along). And when you go for a custom margin you define the content and position –  top, left, right and bottom. The Syntax Editor allows for a large number of margins, but my guess is that, yes, you can cover your entire code window in margins but what good would that do? Personal advice – don’t go wild on the margins, one or two should be enough. Examples of how this works you can find in the samples I mentioned above and it kind of looks like this (squished):

Custom Margins in the XAML Syntax Editor

You will find this pretty well described in the upcoming documentation as well.

Colors and Typefaces!

Naturally as you’d expect form an Editor you can control a whole bunch of visual properties… and then some. For one you can customize the line numbering margin text with properties exposed directly in the editor and those include font family, style, weight and color. On a side note selection highlighting colors are also exposed as properties for you to tweak. Now, let’s assume I’d like me some Segoe UI numbers to match the system and I wouldn’t mind to have those in just slightly darker blue. But now my numbers are Segoe and the code is Consolas (Visual Studio’s default) and I’d like those to match. Even though by default the font is set to Segoe UI for me it didn’t take that seriously until explicitly defined:

  1. <ig:XamSyntaxEditor x:Name="editor" LineNumberMarginForeground="#FF089AEE" LineNumberMarginFontFamily="Segoe UI" FontFamily="Segoe UI" LineNumberMarginBackground="#FFEEEEEE" Background="#FFFDFDFD"/>

You can set different font, colors and backgrounds in the XAML Syntax Editor.

I gave the margin some gray background to match the ribbon and to be more clearly separated from the main view. As you can tell with font and background changes you can achieve that ‘dark theme’ look you can see in the XamSyntaxEditorStudio and I see on so many screens with VS.

Better yet..

There are other colors the editor does well – the syntax highlights! And you can very easily tap into those as well. And since there are quite a few language elements I’ll just mention you can tweak most. The control accepts a ClassificationAppearanceMap object to keep track of those modifications:

  1. ClassificationAppearanceMap map = new ClassificationAppearanceMap();
  2. map.AddMapEntry(ClassificationType.Keyword, new TextDocumentAppearance()
  3.         {
  4.             FontSize = 13, FontFamily = new FontFamily("Segoe UI"), Foreground = new SolidColorBrush(Color.FromArgb(255,42,116,162))
  5.         });
  6. this.editor.ClassificationAppearanceMap = map;

As you can see you can add to the map by specifying a type key which can be a language keyword, but can also be comment, identifier, operator, number and so on. Even the SyntaxError is here so you can change how that looks too. In my example above I just made keywords a bit larger and in darker steel-blue tone.. it doesn’t look all too amazing so no screenie.

All those layers

The Editor displays content in four (five?) dedicated layers. You have the background as bottom layer and the caret(yup it has its own) as topmost. In-between there’s a text layer, one for selection and one for syntax errors. And then you can add you own implementing the ISupportPositioning interface and render content on any.

XAML Syntax Editor Document presentation layers.

Now you could ask what kind of content would you be rendering? Answer is hiding below somewhere..

Search, Find and Destr… Replace!

So here’s what I did – I went on and added two inputs to enter search criteria and replace text with the following method:

  1. private void Replace(object sender, RoutedEventArgs e)
  2. {
  3.     var srchField = this.xamRibbon.GetToolById("srchText") as TextEditorTool;
  4.     if (srchField.Value != null)
  5.     {
  6.         var count = this.editor.Document.FindReplaceAll(new TextSearchCriteria(srchField.Text), this.replaceText.Text).Results.Count;
  7.         System.Windows.Forms.MessageBox.Show(count + " occurance(s) replaced.");
  8.     }
  9. }

Sure it works great and it even does the same as VS showing the message with how many words you just lost :)

But at this point I want to have a simple search and feeding empty string or even null instead replacement text are no options(one deletes every match and the other plain errors out). And then the Document has no simple search? Of course it does! The ‘Find All’ is actually in the current document snapshot. And since you get a collection of results, why not do something useful with it? The sample above goes to display the results in a box and move the caret to any of those. I’d like to iterate on that and mimic most editors’ behavior by providing navigation between results and ended up with this search utility or manager if you will:

  1. public class SearchUtility
  2. {
  3.     public List<TextSearchResult> Results { get; private set; }
  4.     public XamSyntaxEditor Editor { get; private set; }
  5.     public event EventHandler<EventArgs> OnSearch;
  6.  
  7.     private int _location = -1;
  8.     
  9.     public SearchUtility(XamSyntaxEditor editor)
  10.     {
  11.         this.Editor = editor;
  12.     }
  13.  
  14.     public void DoSearch(string criteria)
  15.     {
  16.         Results = Editor.Document.CurrentSnapshot.FindAll(new TextSearchCriteria(criteria)).Results.ToList();
  17.         if (Results.Count > 0)
  18.         {
  19.             _location = 0;
  20.             Editor.ActiveDocumentView.GoToTextLocation(Results[0].SpanFound.StartLocation);
  21.             Editor.ActiveDocumentView.SelectionManager.SelectNextWord();
  22.             //raise event:
  23.             EventHandler<EventArgs> handler = OnSearch;
  24.             if (handler != null)
  25.             {
  26.                 handler(this, new EventArgs());
  27.             }
  28.         }
  29.         else
  30.         {
  31.             System.Windows.Forms.MessageBox.Show("No results found for '" + criteria + "'.");
  32.         }
  33.  
  34.     }
  35.  
  36.     public void ClearSearch()
  37.     {
  38.         if (Results.Count > 0)
  39.         {
  40.             Results.Clear();
  41.             _location = -1;
  42.             //raise event:
  43.             EventHandler<EventArgs> handler = OnSearch;
  44.             if (handler != null)
  45.             {
  46.                 handler(this, new EventArgs());
  47.             }
  48.         }
  49.     }
  50.  
  51.     public void Next()
  52.     {
  53.         if (_location >= 0 && _location < Results.Count - 1)
  54.         {
  55.             Editor.ActiveDocumentView.GoToTextLocation(Results[++_location].SpanFound.StartLocation);
  56.             Editor.ActiveDocumentView.SelectionManager.SelectNextWord();
  57.         }
  58.     }
  59.  
  60.     public void Previous()
  61.     {
  62.         if (_location > 0 && _location < Results.Count)
  63.         {
  64.             Editor.ActiveDocumentView.GoToTextLocation(Results[--_location].SpanFound.StartLocation);
  65.             Editor.ActiveDocumentView.SelectionManager.SelectNextWord();
  66.         }
  67.     }
  68. }

As you can see there’s even an event (that is connected with the next enhancement) and methods to perform search, navigate between the results back and forth and clear them. So with a simple input and two buttons this will achieve the following:

XAML Syntax Editor Find - Search with navigation

Note: By default Case Sensitive and Match whole word options are off. Also I think it’s fairly clear I’m selecting the next word rather than the exact match. I think it makes sense with some more visual from below…plus it makes my life easier as the Selection Manager provides a whole bunch of methods to select up and down, till end of line or even end of text and of course a single character, so the word is a quick and dirty solution.

Custom Adornments

There’s still something missing I feel, still not enough visual oomph . I like it when some editors highlight every match and this is exactly what I want and thanks to all those layers I can have highlights. Adornments are just the tool to get my content on those layers and guess what – selection itself is one and so are the syntax error lines. The editor comes with base generator and provider classes to extend. Providers are registered with the control and their method called when a new generator is needed (basically for every new document view and yes, works out of the box with split views). The latter are created to provide adorners for particular layer defined at initialization. If you notice below while calling the base constructor the background layer is picked (we don’t want those highlights to mess with anything else):

  1. /// <summary>
  2.  /// A Adornment Provider to be registered with the Syntax Editor.
  3.  /// </summary>
  4.  public class SearchHighlightAdornmentProvider : Infragistics.Controls.Editors.AdornmentGeneratorProvider
  5.  {
  6.      public override AdornmentGeneratorBase CreateAdornmentGenerator(DocumentViewBase documentView)
  7.      {
  8.          return new SearchHighlightAdornmentBase(documentView);
  9.      }
  10.  }
  11.  
  12.  /// <summary>
  13.  /// The actual generator. We'll be drawing highlights in the TextBackgroiund layer as can be seen in the ctor.
  14.  /// </summary>
  15.  public class SearchHighlightAdornmentBase : AdornmentGeneratorBase
  16.  {
  17.      AdornmentInfo _searchAdornmentInfo;
  18.      readonly Path _path;
  19.      readonly EditorDocumentView _documentView;
  20.  
  21.      public SearchHighlightAdornmentBase(DocumentViewBase documentView)
  22.          : base(documentView, AdornmentLayerInfo.TextBackground)
  23.      {
  24.          _documentView = documentView as EditorDocumentView;
  25.          _path = new Path { Fill = new SolidColorBrush(Color.FromArgb(180, 255, 222, 173)) };
  26.          _searchAdornmentInfo = this.AdornmentLayer.AddAdornment(this._path, new Point(0, 0), null);
  27.          //subscribe to events that would require redraw
  28.          _documentView.LayoutChanged += UpdateHighlights;
  29.          MainWindow.SearchUtil.OnSearch += UpdateHighlights;
  30.      }
  31.  
  32.      /// <summary>
  33.      /// Only call draw when there are actual results. This can see a lot of improvements -
  34.      /// the LayoutChanged args provide collections with deleted, new/reformatted and translated lines.
  35.      /// Judging by that it is also possible to attempt to not redraw still good highlights.
  36.      /// </summary>
  37.      void UpdateHighlights(object sender, EventArgs e)
  38.      {
  39.          if (MainWindow.SearchUtil.Results != null && MainWindow.SearchUtil.Results.Count > 0)
  40.          {
  41.              HighlightGeometry();
  42.              _path.Visibility = Visibility.Visible;
  43.          }
  44.          else
  45.          {
  46.              _path.Visibility = Visibility.Collapsed;
  47.          }
  48.      }
  49.  
  50.      /// <summary>
  51.      /// Get the line from the document view and create a rectangle with the measured size and position.
  52.      /// Do so for every match and then set them to the path.
  53.      /// </summary>
  54.      void HighlightGeometry()
  55.      {
  56.          GeometryGroup geometry = new GeometryGroup();
  57.          foreach (TextSearchResult res in MainWindow.SearchUtil.Results)
  58.          {
  59.              TextLocation start = res.SpanFound.StartLocation;
  60.              TextLocation end = res.SpanFound.EndLocation;
  61.              int lineIndex = res.SpanFound.StartLocation.Line;
  62.              if (_documentView.GetIsLineInView(lineIndex, false))
  63.              {
  64.                  DocumentViewLine line = _documentView.ViewLineFromLineIndex(lineIndex);
  65.                  Point startPoint = line.PointFromCharacterIndex(start.Character);
  66.                  Point endPoint = line.PointFromCharacterIndex(end.Character);
  67.                  geometry.Children.Add(new RectangleGeometry { Rect = new Rect(startPoint, new Point(endPoint.X, endPoint.Y + line.Bounds.Height)) });
  68.              }
  69.          }
  70.          _path.Data = geometry;
  71.      }
  72.  
  73.      /// <summary>
  74.      /// Called when settings in the <see cref="T:Infragistics.Controls.Editors.XamSyntaxEditor"/> (that could affect the display of adornments) have changed.
  75.      /// </summary>
  76.      protected override void OnRefreshAdornments()
  77.      {
  78.          UpdateHighlights(null, null);
  79.      }
  80.  
  81.      /// <summary>
  82.      /// Called when the generator is unloaded.
  83.      /// </summary>
  84.      protected override void OnUnloaded()
  85.      {
  86.          _documentView.LayoutChanged -= UpdateHighlights;
  87.          MainWindow.SearchUtil.OnSearch -= UpdateHighlights;
  88.          bool removed = this.AdornmentLayer.RemoveAdornment(this._searchAdornmentInfo);
  89.      }
  90.  }

Search results are still stored in the Search utility and the event is used to trigger the highlighting on search rather than just the default layout changed with the document view. The base generator provides the respective layer to which you can add your adornment in the form of any UI element really. Overriding the OnRefresh and OnUnload is somewhat optional, but recommended. The OnRefresh is called from the editor in case some external property that can affect adornment is changed that doesn’t necessary fire the layout changed. And the unload occurs either on unregister or every time the document loads now content (which in my case happens as I initialize with nothing and then load remote text). The base methods won’t do the cleaning up for you. Speaking of unregister, here’s how you register first:

  1. this.editor.Document.Language.ServicesManager.RegisterService("highlight", new SearchHighlightAdornmentProvider());

XAML Syntax Editor Search Highliting along with navigation

With this you get experience that is just like when you add ReSharper to Visual Studio and your searches now highlight all matches and you can navigate them! Well, in all fairness, my yellow is a bit off and I select a whole word no matter what, but those are two tiny details that can easily be tweaked to your liking :)

TL;DR | Summary

I’d like to end like I did the last time and that is by stating there is more and more to the control that I can manage to display. We took a brief look on how can margins be added to the control and tweaking various text elements. We also enhanced the search functionality by adding selection and neat navigation and topped that off with awesome highlights with custom adornment. I’m quite happy how they turned out and just as a tip you can use this approach to add any kind of visuals to your editor, just don’t go crazy on the drawing as it could get intensive.

As always, you can follow us on Twitter @DamyanPetev and @Infragistics and stay in touch on Facebook, Google+ and LinkedIn! Stay tuned the demo and some pretty exciting stuff are happening for the next release!