Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Silverlight 2.0 Syntax Highlighting TextBox

0.00/5 (No votes)
27 Feb 2009 1  
Syntax highlighting TextBox in Silverlight 2.0.

Image4.png

Introduction

First of all, I want to apologize for language problems you may experience while reading this article. English is my second language, so sorry for any miss-spellings or language mistakes.

I posted this article before, expecting responses from readers. There were none. So, this time I will try to explain as much as I can. Additional thanks to the author of the great project code named FIREBALL.

The first reason I started working on such a textbox is to convert my existing code written for Windows Forms, which uses the CodeEditorControl from Sebastian Faltoni's library, to its Microsoft WPF clone. Since I was working on some Silverlight stuff and WPF is almost the same, I thought that, if I make it work in Silverlight, then I can port it to WPF as well. I started to dig into the Silverlight/WPF possibilities of TextBox to glue it up with Sebastian’s SyntaxDocument class. Sebastian uses a totally custom-drawn control in his CodeEditorControl for Windows Forms. All drawing is done by GDI+. As you know, we do not have anything in WPF to make Sebastian’s code work as is, and we do not have the possibilities to colorize the TextBox text blocks with different colors. We only have a Foreground property to control the text color for a whole document. The only control that supports such a thing is TextBlock.

The TextBlock has a property Inlines where you can set a formatted text separated in Run/LineBreak blocks. Each Run block has its own Text/Foreground/Background properties to control the visual appearance of a Text/Document. I decided to use a TextBlock for text visualization with custom block appearance. But there was an issue: how was I going to type text in my control? The TextBlock supports only visualization, no typing. I came up with the idea to put the TextBox in front of my User Control and the TextBlock in the back. The TextBox is used for typing, and TextBlock for text visualization. Setting both TextBlock and TextBox resulted in an artifact while rendering, with a little text offset of a TextBlock. To get it fixed, I had to set the TextBox foreground brush alpha value to 0.01d, so it appears hardly visible, but very useful while selecting text. If I set it to 0.0d, there is another problem with the text selection: that it looks like only the selection background is visible and there is no inverted foreground. This might be an internal logic of controls in Silverlight, because even when SelectionForground was set to White, it was still not visible.

The next part is how to make the TextBlock scrollable. That was not so hard. Putting TextBlock inside the ScrollViewer solves the issue.

<Grid x:Name="LayoutRoot">
    <ScrollViewer x:Name="_scroll" Margin="0,0,0,0" Visibility="Visible"
          HorizontalScrollBarVisibility="Visible" HorizontalContentAlignment="Left"
          VerticalContentAlignment="Top">
          <TextBlock x:Name="_text_block" Height="Auto" Width="Auto"
          Text="" TextWrapping="NoWrap" Margin="0,0,0,0"/>
  </ScrollViewer>
</Grid>

Next, let’s talk about Sebastian’s SyntaxDocument class. I ported it from the Windows Forms version to a WPF version. The porting was not hard, but resulted in complete reengineering of some classes that work with XML and uses Hashtables as an internal storage for text block styles and so on. The SyntaxDocument class is parser that contains the parsed content of the document rows/words collection objects with styles included. The style has properties such as foreground and background of the text. After setting the SyntaxDocument.Text property, it fires up an event “Changed”, so you can go for the parsed text rendering. In our case, the text rendering is done by TextBlock. The method shown here is the handler code that renders the parsed content:

protected void OnDocument_Changed(object sender, EventArgs e)
{
  List<Fireball.Syntax.Row> rows =
      _document.VisibleRows.OfType<Fireball.Syntax.Row>().ToList();
  _text_block.Inlines.Clear();
  rows.ForEach(row =>
    {
      if (_document[rows.IndexOf(row)].RowState == 
          Fireball.Syntax.RowState.SegmentParsed)
      {
        _document.Parser.ParseLine(rows.IndexOf(row), true);
      }
      if (_document[rows.IndexOf(row)].RowState == 
          Fireball.Syntax.RowState.NotParsed)
      {
        _document.ParseRow(row, true);
      }
      Fireball.Syntax.WordCollection words = row.FormattedWords;
      if ( words.Count > 0 )
      {
        words.OfType<Fireball.Syntax.Word>().ToList().ForEach(word =>
          {
            Run run = new Run()
            {
              Text = word.Text,
              Foreground = word.Style != null ? new SolidColorBrush(
                  word.Style.ForeColor) : new SolidColorBrush(Colors.Black)
            };
            _text_block.Inlines.Add(run);
          });
      }
      _text_block.Inlines.Add(new LineBreak());
    });
}

The most important part of the code above is creating an instance of the "Run" class and setting its properties according to those inside the Word objects that reside in Row.FormattedWords of each parsed row in the SyntaxDocument object instance.

The text rendering is done every time the TextBox fires an event “TextChanged”. The technique I used might not be the best practice to do this. If anyone finds a way to optimize this code, it would be just great. I will keep searching and digging deeper inside of Sebastian’s code to find a way for optimizations, and might use custom drawing in WPF.

And now, a little bit more about the SyntaxDocument language templates that are used in this class. The language specific dictionaries are stored in XML files (SYN). The one I used in my sample is only for XML syntax highlighting. Other dictionaries have to be ported to the Silverlight version of the SyntaxDocument class. The changes I have made to the XML syntax dictionary file blocks properties that are responsible for the color definition of a syntax block. Shown below is a sample of such changes made to the XML.SYN file of the SyntaxDocument engine:

<!--ORIGINAL DEFEINITION-->
<Style Name="Text" ForeColor="Black" 
    BackColor="" Bold="false" 
    Italic="false"
    Underline="false"/>
<!--CHANGED DEFEINITION-->
<Style Name="Text" ForeColor="#FF000000" 
    BackColor="" Bold="false" Italic="false"
    Underline="false"/>

If you want to use other language dictionaries, you may obtain them from the original source codes of Sebastian’s Fireball.Syntax project: http://www.dotnetfireball.net or http://www.codeplex.com/dotnetfireball.

Finally, I got a big problem with scroll bars. When the length of TextBox text is over its width or height, the scroll bars appear automatically for both controls inside my User Control. If I navigate through the TextBox text with arrow keys or type a text, the TextBox’s scroll bars scroll automatically, witch is not happing to my TextBlock instance that renders the parsed text. In this case, I had to get a control over the TextBox and the ScrollViewer scroll bars to make their Maximum and Value properties be equal. How can we do this? The first thing is to dig into the TextBox style/template. To get the default template of a TextBox, I used Microsoft Expression Blend that does this very easily. Just put the control on a page, right click on the item of your control in the objects/timeline tree on the left, and go for the menu item “Edit Control Parts (Template) -> Edit Copy”. Now, you can go through template parts to get the names of the controls inside. Next, I had to override/handle two events: OnApplyTemplate/LayoutUpdated of my derived textbox instance, and get those child elements to find an instance of both the vertical and the horizontal scrollbars. The code below shows how to do this:

//In this part we have Get a Template Child 'ContentElement'.
public override void OnApplyTemplate()
{
  base.OnApplyTemplate();
  _content        = base.GetTemplateChild("ContentElement") as ScrollViewer;
  if (_content == null)
    return;
  _content.LayoutUpdated += new EventHandler(OnContent_LayoutUpdated);
}

//Getting child content of scrollviewer
private void OnContent_LayoutUpdated(object sender, EventArgs e)
{
  if (_content_border != null)
    return;
  _content_border = VisualTreeHelper.GetChild(_content, 0) as Border;
  if (_content_border != null)
  {
    int count = VisualTreeHelper.GetChildrenCount(_content_border);
    if (count > 0)
    {
      Grid grid = VisualTreeHelper.GetChild(_content_border, 0) as Grid;
      if (grid != null)
      {
        IEnumerable<System.Windows.Controls.Primitives.ScrollBar> found = (
            from child in grid.Children.ToList() where 
            child is System.Windows.Controls.Primitives.ScrollBar select child as
            System.Windows.Controls.Primitives.ScrollBar);
        if (found.Count() > 0)
        {
          VerticalScrollBar = (from sc in found where sc.Name == "VerticalScrollBar"
              select sc).First();
          HorizontalScrollBar = (from sc in found where sc.Name == "HorizontalScrollBar"
              select sc).First();
          if (ContentFound != null)
            ContentFound(this, new RoutedEventArgs());
        }
      }
    }
  }
}

After we get an instance of both the scrollbars of a TextBox, we can get the actual values of its properties and signup for events when the scroll values are changed, so we can set our TextBlock’s ScrollViewer scrollbar values to the same value as the TextBox has. This technique gives us the ability to implement a custom TextBox control that can highlight syntax. Below is the complete code for the TextBoxExtended class and the User Control that implements code syntax highlighting:

The TextBoxExtended class with vertical and horizontal scrollbars available

public class TextBoxExtended : TextBox
{
  //Template internal content scroll viewer
  ScrollViewer  _content = null;
  //Content border
  Border        _content_border = null;
  //Text calculations
  TextBlock     _size_block = null;

  /// <summary>An Event beeing fired when template
  /// content found and initialized</summary>
  public event RoutedEventHandler ContentFound;

  public TextBoxExtended()
  {
    //Since we do not use any own style for this 
    //control set DefaultStyleKey to TextBox
    DefaultStyleKey = typeof(TextBoxExtended);
  }

  //In this part we have Get a Template Child 'ContentElement'.
  //This is ScrollViewer of TexBox
  public override void OnApplyTemplate()
  {
    base.OnApplyTemplate();
    _content = base.GetTemplateChild("ContentElement") as ScrollViewer;
    if (_content == null)
      return;
    _content.LayoutUpdated += new EventHandler(OnContent_LayoutUpdated);
  }

  //Getting child content of scrollviewer
  private void OnContent_LayoutUpdated(object sender, EventArgs e)
  {
    if (_content_border != null)
      return;
    _content_border = VisualTreeHelper.GetChild(_content, 0) as Border;
    if (_content_border != null)
    {
      int count = VisualTreeHelper.GetChildrenCount(_content_border);
      if (count > 0)
      {
        Grid grid = VisualTreeHelper.GetChild(_content_border, 0) as Grid;
        if (grid != null)
        {
          //OK NOW TRY TO CREATE A LITTLE TextBlock for text mesurament calculations
          _size_block = new TextBlock()
          {
           Foreground = null,
           VerticalAlignment = VerticalAlignment.Top,
           HorizontalAlignment = HorizontalAlignment.Left,
           FontFamily  = FontFamily,
           FontSize    = FontSize,
           FontStretch = FontStretch,
           FontStyle   = FontStyle,
           FontWeight  = FontWeight
          };
          grid.Children.Add(_size_block);

          IEnumerable<System.Windows.Controls.Primitives.ScrollBar> found = 
            (from child in grid.Children.ToList() where child is 
             System.Windows.Controls.Primitives.ScrollBar select 
             child as System.Windows.Controls.Primitives.ScrollBar);
          if (found.Count() > 0)
          {
            VerticalScrollBar = (from sc in found where sc.Name == 
               "VerticalScrollBar" select sc).First();
            HorizontalScrollBar = (from sc in found where sc.Name == 
               "HorizontalScrollBar" select sc).First();
            if (ContentFound != null)
              ContentFound(this, new RoutedEventArgs());
          }
          //_content.Clip = new RectangleGeometry()
          //          { Rect = new Rect(0, 0, ActualWidth, ActualHeight) };
        }
      }
    }
  }

  public Size MesureText(string Text)
  {
    if (_size_block != null)
    {
      _size_block.Text = string.IsNullOrEmpty(Text.Replace("\r", 
        "").Replace("\n","")) ? " ":Text;
      return new Size(_size_block.ActualWidth, _size_block.ActualHeight);
    }
    return Size.Empty;
  }

  public bool CanDoTextMesure
  {
    get { return _size_block != null; }
  }

  public System.Windows.Controls.Primitives.ScrollBar VerticalScrollBar
  {
    get;
    set;
  }

  public System.Windows.Controls.Primitives.ScrollBar HorizontalScrollBar
  {
    get;
    set;
  }
}

The SyntaxTextBox class with syntax highlighting support

public partial class SyntaxTextBox : UserControl
{
  Fireball.Syntax.SyntaxDocument  _document;
  bool _updated_locked = false;
  bool _is_loaded = false;

  public static readonly DependencyProperty IsReadOnlyProperty = 
         DependencyProperty.Register("IsReadOnly", 
         typeof(bool), typeof(SyntaxTextBox),
    new PropertyMetadata(false, delegate(DependencyObject d, 
        DependencyPropertyChangedEventArgs e)
    {
          SyntaxTextBox box = d as SyntaxTextBox;
          if (box != null && box._text_box != null )
          {
            box._text_box.IsReadOnly = (bool)e.NewValue;
          }
    }));

  public static readonly DependencyProperty TextProperty = 
         DependencyProperty.Register("Text", typeof(string), 
         typeof(SyntaxTextBox), new PropertyMetadata("", 
         delegate(DependencyObject d, DependencyPropertyChangedEventArgs e)
  {
        if (e.NewValue != null )
        {
          SyntaxTextBox box = d as SyntaxTextBox;
          if (box != null)
          {
            string new_val     = (string)e.NewValue;
            string old_val     = box._document.Text;
            string[] new_lines = null;
            string[] old_lines = null;
            System.IO.StringReader r = new System.IO.StringReader(new_val);
            string line = null;
            List<string> l_lines = new List<string>();

            while ((line = r.ReadLine()) != null)
            {
              l_lines.Add(line);
            }
            new_lines = l_lines.ToArray();
            r = new System.IO.StringReader(old_val);
            line = null;
            l_lines = new List<string>();
            while ((line = r.ReadLine()) != null)
            {
              l_lines.Add(line);
            }
            old_lines = l_lines.ToArray();

            bool has_changes = false;
            for (int i = 0; i < new_lines.Count(); i++)
            {
              if (i <= box._document.Count - 1)
              {
                if (box._document[i].Text != new_lines[i])
                {
                  box._document[i].SetText(new_lines[i]);
                  box._document[i].IsRendered = false;
                  has_changes = true;
                }
              }
              else
              {
                Fireball.Syntax.Row row = box._document.Add(new_lines[i], false);
                box._document.ParseRow(row, true);
                has_changes = true;
                row.IsRendered = false;
              }
            }
            if (old_lines.Count() > new_lines.Count())
            {
              for (int i = new_lines.Count(); ; )
              {
                if (box._document.Count == new_lines.Count())
                  break;
                if (box._document.Count == 1)
                {
                  box._document[i].SetText("");
                  has_changes = true;
                  break;
                }
                box._document.Remove(i);
              }
            }
            if (has_changes)
            {
              box.RenderDocument();
            }
          }
        }
      }));

  public SyntaxTextBox()
  {
      InitializeComponent();
    Loaded += new RoutedEventHandler(OnLoaded);
  }

  //External Language Syntax Loading...
  public void SetSyntax(string SyntaxSrc, System.Text.Encoding SrcEncoding, 
         Fireball.CodeEditor.SyntaxFiles.SyntaxLanguage language)
  {
    if (_is_loaded != true)
      return;
    System.IO.MemoryStream s = new System.IO.MemoryStream(SrcEncoding.GetBytes(SyntaxSrc));
    SetSyntax(s, language);
  }
  
  //External Language Syntax Loading...
  public void SetSyntax(System.IO.Stream SyntaxSrc, 
         Fireball.CodeEditor.SyntaxFiles.SyntaxLanguage language)
  {
    if (_is_loaded != true)
      return;
    _document.Parser.Init(Fireball.Syntax.Language.FromSyntaxFile(SyntaxSrc));
  }

  protected void OnLoaded(object sender, RoutedEventArgs e)
  {
    this.Focus();
    _document = new Fireball.Syntax.SyntaxDocument();
    Fireball.CodeEditor.SyntaxFiles.CodeEditorSyntaxLoader.SetSyntax(_document, 
             Fireball.CodeEditor.SyntaxFiles.SyntaxLanguage.XML);
    //*****************TEST ONLY***********************//
    XElement elm = new XElement("Objects",
      new XAttribute("type", "None"),
      new XElement("Hello")
      );
    //************************************************//
    _text_box.ContentFound  += OnContentFound;
    _text_box.TextChanged   += OnTextChanged;
    _text_box.KeyUp         += OnTextBox_KeyUp;
    _text_box.Text = elm.ToString();
    _text_box.LayoutUpdated += new EventHandler(OnTextLayoutUpdated);
    _text_box.IsReadOnly = IsReadOnly;
  }

  protected void OnTextLayoutUpdated(object sender, EventArgs e)
  {
    if (_updated_locked)
    {
      _updated_locked = !_updated_locked;
      return;
    }
    UpdateScrolls();
    _updated_locked = true;
  }

  protected void OnContentFound(object sender, RoutedEventArgs e)
  {
    RenderDocument();
  }

  public void UpdateScrolls()
  {
    if (
          _text_box.VerticalScrollBar   != null && 
          _text_box.HorizontalScrollBar != null
       )
    {
      double pVt = 0;
      double pHt = 0;
      double pVs = 0;
      double pHs = 0;
      if( _text_box.VerticalScrollBar.Maximum > 0 && 
          _text_box.VerticalScrollBar.Value > 0 )
        pVt = (_text_box.VerticalScrollBar.Value / 
               _text_box.VerticalScrollBar.Maximum) * 100;
      pVs = (_scroll.VertRange / 100) * pVt;
      if( _text_box.HorizontalScrollBar.Maximum > 0 && 
          _text_box.HorizontalScrollBar.Value > 0)
        pHt = (_text_box.HorizontalScrollBar.Value / 
               _text_box.HorizontalScrollBar.Maximum) * 100;
      pHs = (_scroll.HorzRange / 100) * pHt;
      _scroll.HorzRange = _text_box.HorizontalScrollBar.Maximum;
      _scroll.ScrollIntoPosition(_text_box.HorizontalScrollBar.Value/ 
                                 *Math.Round(pHs)*/, Math.Round(pVs));
    }
  }

  protected void OnTextBox_KeyUp(object sender, KeyEventArgs e)
  {
      UpdateScrolls();
  }

  protected void OnTextChanged(object sender, RoutedEventArgs e)
  {
    Text = _text_box.Text;
    _text_box.Focus();
  }
  
  protected void RenderDocument()
  {
    //.VisibleRows.OfType<Fireball.Syntax.Row>().ToList();
    List<Fireball.Syntax.Row> rows        = 
       _document.Rows.OfType < Fireball.Syntax.Row>().ToList();
    //.VisibleRows.OfType<Fireball.Syntax.Row>().ToList();
    List<Fireball.Syntax.Row> total_rows  = 
       _document.Rows.OfType<Fireball.Syntax.Row>().ToList();
    rows.ForEach(row =>
      {
        if (_document[rows.IndexOf(row)].RowState == 
            Fireball.Syntax.RowState.SegmentParsed)
        {
          row.IsRendered = false;
          _document.Parser.ParseLine(rows.IndexOf(row), true);
        }
        if (_document[rows.IndexOf(row)].RowState == 
            Fireball.Syntax.RowState.NotParsed)
        {
          row.IsRendered = false;
          _document.ParseRow(row, true);
        }      
      });
    if (_text_box.CanDoTextMesure == false )
      return;
    _scroll.Locked = true;
    bool ValidateRows = false;
    rows.ForEach( row =>
      {
        if (row.IsRendered)
          return;
        if (row.Index > _scroll.Rows - 1)
        {
          ValidateRows = true;
          _scroll.AddRow(true);
        }

        Fireball.Syntax.WordCollection words = row.FormattedWords;
        row.IsRendered = true;
        Scroller.ScrollRowCanvas block = 
            _scroll[row.Index] as Scroller.ScrollRowCanvas;
        block.Clear();
        if (words.Count > 0)
        {
          words.OfType<Fireball.Syntax.Word>().ToList().ForEach(word =>
            {
              if (_text_box.CanDoTextMesure)
                _scroll.AddWord(row.Index, word, _text_box.MesureText(word.Text));
            });
        }
        else
        {
          if (_text_box.CanDoTextMesure)
            _scroll.AddWord(row.Index, null, _text_box.MesureText(""));          
        }
      });
    if ( total_rows.Count < _scroll.Rows )
    {
      while (_scroll.Rows > total_rows.Count)
      {
        _scroll.RemoveRow(_scroll.Rows - 1, true);
        ValidateRows = true;
      }
    }
    if (ValidateRows)
    {
      _scroll.InvalidateRows(true);
    }
    _scroll.Locked = false;
    _scroll.InvalidateLayout();
    _text_box.Focus();
    UpdateScrolls();
  }

  public System.Windows.Controls.Primitives.ScrollBar VerticalScrollBar
  {
    get;
    set;
  }

  public System.Windows.Controls.Primitives.ScrollBar HorizontalScrollBar
  {
    get;
    set;
  }


  public string Text
  {
    get { return (string)base.GetValue(TextProperty); }
    set { SetValue(TextProperty, value); }
  }

  public bool IsReadOnly
  {
    get { return (bool)base.GetValue(IsReadOnlyProperty); }
    set { SetValue(IsReadOnlyProperty, value); }
  }
}

The SyntaxTextBox XAML

<UserControl
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    x:Class="System.Windows.Controls.SyntaxTextBox" IsTabStop="True"
    d:DesignWidth="400" d:DesignHeight="300" 
    xmlns:sc="clr-namespace:Scroller;assembly=SyntaxTextBox"
    xmlns:local="clr-namespace:System.Windows.Controls;assembly=SyntaxTextBox">
    <UserControl.Resources>
        <LinearGradientBrush x:Key="TextBoxBorder" 
                  EndPoint="0.5,1" StartPoint="0.5,0">
            <GradientStop Color="#FFA3AEB9"/>
            <GradientStop Color="#FF8399A9" Offset="0.375"/>
            <GradientStop Color="#FF718597" Offset="0.375"/>
            <GradientStop Color="#FF617584" Offset="1"/>
        </LinearGradientBrush>
    </UserControl.Resources>
    <Border Height="Auto" Width="Auto" BorderThickness="1,1,1,1" 
              CornerRadius="2,2,2,2" 
              BorderBrush="{StaticResource TextBoxBorder}" 
              Background="#FFF0FFFF">
        <Grid x:Name="LayoutRoot" Height="Auto" Width="Auto">
            <Grid.RowDefinitions>
                <RowDefinition Height="*"/>
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>
            <sc:ScrollViewerEx Opacity="1.0"
                x:Name="_scroll"
                Margin="4,4,0,0"
                Visibility="Visible"/>
            <local:TextBoxExtended 
                Foreground="#05000000" 
                Background="{x:Null}" 
                x:Name="_text_box" 
                TextWrapping="NoWrap" 
                AcceptsReturn="True" 
                VerticalScrollBarVisibility="Auto" 
                HorizontalScrollBarVisibility="Auto" 
                HorizontalAlignment="Stretch" 
                VerticalAlignment="Stretch" Grid.ColumnSpan="1" 
                Grid.RowSpan="1" BorderBrush="{x:Null}" 
                Margin="0,0,0,0"/>
        </Grid>
    </Border>
</UserControl>

All other classes and resources are included in the zip archive of this article.

The work to do

I will try to update this article as soon as I fix any issue I find or include a suggestion for modification. If you want changes or code modifications, feel free to do it. If you fix errors or implement something interesting, or modify an algorithm, please let me know, so I can update this article.

  1. Undo/Redo support
  2. Line numbering

Using the Code

To embed SyntaxTextBox, simply create your own Silverlight Application project. Add a reference to SyntaxTextBox.dll and then add the following lines to your XAML page:

<UserControl ...
    ...
    
    xmlns:stb="clr-namespace:System.Windows.Controls;assembly=SyntaxTextBox">
    
    ...
   
    <stb:SyntaxTextBox  IsTabStop="True" 
            Margin="0,0,0,0" 
            Width="244" 
            Height="204" 
            VerticalAlignment="Top" 
            HorizontalAlignment="Left"/>

    ...
</UserControl>

Updates

02/27/2009

I have updated the sources and they contain a new implementation for TextBlock's inline rendering. Now, it is a bit faster when editing and scrolling 1000s of lines. All other performance issues are related to Microsoft's Silverlight implementation.

1. New properties

Not much. Just one (for now) :O).

  • IsReadOnly - Allows to control editing of content.

2. New methods

  • SetSyntax(string SyntaxSrc, Encoding SrcEncoding, SyntaxLanguage language) - This method allows you to load an external language definition from an XML string source.
  • SetSyntax(System.IO.Stream SyntaxSrc, SyntaxLanguage language) - This method allows you to load an external language definition from a source stream that contains an XML language definition.

3. New rendering technique

I have re-implemented the rendering methods to improve scrolling/editing performance. The new implementation uses differences between the old Text property value and the new one. Only lines that are different will be parsed and rendered. See the code below:

public static readonly DependencyProperty TextProperty = 
   DependencyProperty.Register("Text", typeof(string), 
   typeof(SyntaxTextBox), new PropertyMetadata("", 
   delegate(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
      if (e.NewValue != null )
      {
        SyntaxTextBox box = d as SyntaxTextBox;
        if (box != null)
        {
          string new_val     = (string)e.NewValue;
          string old_val     = box._document.Text;
          string[] new_lines = null;
          string[] old_lines = null;
          System.IO.StringReader r = new System.IO.StringReader(new_val);
          string line = null;
          List<string> l_lines = new List<string>();

          while ((line = r.ReadLine()) != null)
          {
            l_lines.Add(line);
          }
          new_lines = l_lines.ToArray();
          r = new System.IO.StringReader(old_val);
          line = null;
          l_lines = new List<string>();
          while ((line = r.ReadLine()) != null)
          {
            l_lines.Add(line);
          }
          old_lines = l_lines.ToArray();

          bool has_changes = false;
          for (int i = 0; i < new_lines.Count(); i++)
          {
            if (i <= box._document.Count - 1)
            {
              if (box._document[i].Text != new_lines[i])
              {
                box._document[i].SetText(new_lines[i]);
                box._document[i].IsRendered = false;
                has_changes = true;
              }
            }
            else
            {
              Fireball.Syntax.Row row = box._document.Add(new_lines[i], false);
              box._document.ParseRow(row, true);
              has_changes = true;
              row.IsRendered = false;
            }
          }
          if (old_lines.Count() > new_lines.Count())
          {
            for (int i = new_lines.Count(); ; )
            {
              if (box._document.Count == new_lines.Count())
                break;
              if (box._document.Count == 1)
              {
                box._document[i].SetText("");
                has_changes = true;
                break;
              }
              box._document.Remove(i);
            }
          }
          if (has_changes)
          {
            box.RenderDocument();
          }
        }
      }
    }));

You may have noticed that there is a call to a new method that renders the document: "RenderDocument()". The code is shown below:

protected void RenderDocument()
{
  //.VisibleRows.OfType<Fireball.Syntax.Row>().ToList();
  List<Fireball.Syntax.Row> rows        = 
     _document.Rows.OfType < Fireball.Syntax.Row>().ToList();
  //.VisibleRows.OfType<Fireball.Syntax.Row>().ToList();
  List<Fireball.Syntax.Row> total_rows  = 
     _document.Rows.OfType<Fireball.Syntax.Row>().ToList();
  rows.ForEach(row =>
    {
      if (_document[rows.IndexOf(row)].RowState == 
          Fireball.Syntax.RowState.SegmentParsed)
      {
        row.IsRendered = false;
        _document.Parser.ParseLine(rows.IndexOf(row), true);
      }
      if (_document[rows.IndexOf(row)].RowState == 
          Fireball.Syntax.RowState.NotParsed)
      {
        row.IsRendered = false;
        _document.ParseRow(row, true);
      }      
    });
  if (_text_box.CanDoTextMesure == false )
    return;
  _scroll.Locked = true;
  bool ValidateRows = false;
  rows.ForEach( row =>
    {
      if (row.IsRendered)
        return;
      if (row.Index > _scroll.Rows - 1)
      {
        ValidateRows = true;
        _scroll.AddRow(true);
      }

      Fireball.Syntax.WordCollection words = row.FormattedWords;
      row.IsRendered = true;
      Scroller.ScrollRowCanvas block = 
          _scroll[row.Index] as Scroller.ScrollRowCanvas;
      block.Clear();
      if (words.Count > 0)
      {
        words.OfType<Fireball.Syntax.Word>().ToList().ForEach(word =>
          {
            if (_text_box.CanDoTextMesure)
              _scroll.AddWord(row.Index, word, _text_box.MesureText(word.Text));
          });
      }
      else
      {
        if (_text_box.CanDoTextMesure)
          _scroll.AddWord(row.Index, null, _text_box.MesureText(""));          
      }
    });
  if ( total_rows.Count < _scroll.Rows )
  {
    while (_scroll.Rows > total_rows.Count)
    {
      _scroll.RemoveRow(_scroll.Rows - 1, true);
      ValidateRows = true;
    }
  }
  if (ValidateRows)
  {
    _scroll.InvalidateRows(true);
  }
  _scroll.Locked = false;
  _scroll.InvalidateLayout();
  _text_box.Focus();
  UpdateScrolls();
}

In order to make this code work, I had to implement a custom ScrollViewer control that is based on a technique described in the article Scroller.aspx?fid=1532323&df=90&mpp=25&noise=3&sort=Position&view=Quick&select=2845356[^]. Thanks to Jerry Evans for the sample.

The control described in the article uses fixed column/row sizes and that is not valid in our case. I have redesigned the control to support dynamic row addition, removing, and etc. The horizontal scrollbar is controllable through the SyntaxTextBox class. To control the row's width/height, I have added a new method to the TextBoxExtended class called MesureText(string Text). To support such functionality in TextBox, I have added a TextBlock to the TextBoxExtended control for the text measurement. Here is the code for the TextBoxExtended class OnContent_LayoutUpdated method:

//Getting child content of scrollviewer
private void OnContent_LayoutUpdated(object sender, EventArgs e)
{
  if (_content_border != null)
    return;
  _content_border = VisualTreeHelper.GetChild(_content, 0) as Border;
  if (_content_border != null)
  {
    int count = VisualTreeHelper.GetChildrenCount(_content_border);
    if (count > 0)
    {
      Grid grid = VisualTreeHelper.GetChild(_content_border, 0) as Grid;
      if (grid != null)
      {
        //OK NOW TRY TO CREATE A LITTLE TextBlock 
        //for text mesurament calculations
        _size_block = new TextBlock()
        {
         Foreground = null,
         VerticalAlignment = VerticalAlignment.Top,
         HorizontalAlignment = HorizontalAlignment.Left,
         FontFamily  = FontFamily,
         FontSize    = FontSize,
         FontStretch = FontStretch,
         FontStyle   = FontStyle,
         FontWeight  = FontWeight
        };
        grid.Children.Add(_size_block);

        IEnumerable<System.Windows.Controls.Primitives.ScrollBar> found = 
          (from child in grid.Children.ToList() where child 
           is System.Windows.Controls.Primitives.ScrollBar select child 
           as System.Windows.Controls.Primitives.ScrollBar);
        if (found.Count() > 0)
        {
          VerticalScrollBar = (from sc in found where sc.Name == 
                               "VerticalScrollBar" select sc).First();
          HorizontalScrollBar = (from sc in found where sc.Name == 
                                 "HorizontalScrollBar" select sc).First();
          if (ContentFound != null)
            ContentFound(this, new RoutedEventArgs());
        }
        //_content.Clip = new RectangleGeometry()
        //          { Rect = new Rect(0, 0, ActualWidth, ActualHeight) };
      }
    }
  }
}

public Size MesureText(string Text)
{
  if (_size_block != null)
  {
    _size_block.Text = string.IsNullOrEmpty(Text.Replace("\r", 
             "").Replace("\n","")) ? " ":Text;
    return new Size(_size_block.ActualWidth, _size_block.ActualHeight);
  }
  return Size.Empty;
}

4. New class "ScrollViewerEx"

public partial class ScrollViewerEx : UserControl//, IMouseWheelObserver
{
  // set a fixed cell width
  private int cellWidth  = 1;
  // and a fixed cell height
  private int cellHeight = 1;
  
  //
  private int _rows = 0;
  //
  private int _cols = 0;

  public int CellHeight
  {
    get { return cellHeight; }
    set 
    {
      cellHeight = value;
      InvalidateLayout();
    }
  }
  
  public int CellWidth
  {
    get { return cellWidth; }
    set
    {
      cellWidth = value;
      InvalidateLayout();
    }
  }

  /// <summary>
  /// Stores the current scroll bar position as an integral index
  /// </summary>
  public int VertPosition
  {
    get { return (int)VScroll.Value; }
    set
    {
      VScroll.Value = value;
      InvalidateLayout();
    }
  }

  /// <summary>
  /// Get the maximum range of the vertical scrollbar
  /// </summary>
  public double VertRange
  {
    get { return (int)VScroll.Maximum; }
    set
    {
      VScroll.Maximum = value;
    }
  }
  
  /// <summary>
  /// Get the maximum range of the vertical scrollbar
  /// </summary>
  public double HorzRange
  {
    get { return HScroll.Maximum; } set { HScroll.Maximum = value; }
  }

  /// <summary>
  /// Stores the current horizontal scrollbar position as an integral index
  /// </summary>
  public int HorzPosition
  {
    get { return (int)HScroll.Value; }
    set
    {
      HScroll.Value = value;
      InvalidateLayout();
    }
  }

  private bool useClipper = true;

  /// <summary>
  /// Hows many rows can we display on a page? N.B. assumes fixed height
  /// </summary>
  private int RowsPerPage
  {
    get
    {
      if (useClipper)
        return (int)(ElementContentClipper.ClippingRect.Height / cellHeight);
      else
        return _rows;
    }
  }

  /// <summary>
  /// How many columns can we display on a page? N.B. assumes fixed width
  /// </summary>
  private int ColsPerPage
  {
    get
    {
      if (useClipper)
        return (int)(ElementContentClipper.ClippingRect.Width / cellWidth);
      else
        return _cols;
    }
  }

  /// <summary>
  /// List of all visible items
  /// </summary>
  public List<UIElement> VisibleItems
  {
    get;
    private set;
  }

  /// <summary>
  /// Lock for recursion in ArrangeOverride
  /// </summary>
  public bool Locked
  {
    get;
    set;
  }

  /// <summary>
  /// if FastMode == true then use fast scrolling ....
  /// </summary>
  public bool FastMode
  {
    get;
    private set;
  }

  private TranslateTransform Translation
  {
    get;
    set;
  }

  public int Rows
  {
    get { return _rows; }
    private set { }
  }

  public void AddWord(int row, Fireball.Syntax.Word word, Size wordSize)
  {
    ScrollRowCanvas sr = 
      row >= 0 && row <= ElementContent.Children.Count-1 
      ? ElementContent.Children[row] as ScrollRowCanvas: null;
    if (sr == null)
      AddRow();
    sr = ElementContent.Children[row] as ScrollRowCanvas;
    sr.AddWord(word, wordSize);
    if (CellHeight < sr.Height)
      CellHeight = (int)sr.Height;
  }

  public void RemoveRow(int Index, bool KeepLocked)
  {
    bool WasLocked = Locked;
    _rows--;
    double topDecrementer = (ElementContent.Children[Index] as FrameworkElement).Height;
    ElementContent.Children.RemoveAt(Index);
    IEnumerable<UIElement> rows = (from child in ElementContent.Children 
       where ElementContent.Children.IndexOf(child) > Index select child);
    if (rows.Count() > 0)
    {
      rows.ToList().ForEach(row =>
        {
          double top = ((double)row.GetValue(Canvas.TopProperty)) - topDecrementer;
          row.SetValue(Canvas.TopProperty, top);
        });
    }
    if (KeepLocked == false)
      Locked = false;
    //
    SwitchStrategy(false, WasLocked);
    // force recalc etc
    InvalidateLayout();
    //
    this.Cursor = Cursors.Arrow;
  }

  public void AddRow()
  {
    AddRow(false);
  }

  public void InvalidateRows(bool KeepLocked)
  {
    bool WasLocked = Locked;
    double actualHeight = 0.0d;
    ElementContent.Children.Cast<ScrollRowCanvas>().ToList().ForEach(rw =>
    {
      rw.SetValue(Canvas.TopProperty, actualHeight);
      actualHeight += rw.ActualHeight;
    });
    Locked = false;
    SwitchStrategy(false, WasLocked);
    // force recalc etc
    InvalidateLayout();
    //
    this.Cursor = Cursors.Arrow;
  }

  public void AddRow(bool KeepLocked)
  {
    bool WasLocked = Locked;
    Locked = true;
    _rows++;
    ScrollRowCanvas sr = new ScrollRowCanvas(_rows-1);
    // add to the canvas
    double actualHeight = 
      ElementContent.Children.Sum(s => (s as ScrollRowCanvas).ActualHeight);
    ElementContent.Children.Add(sr);
    sr.SetValue(Canvas.LeftProperty, 0.0d);
    // equivalent to <ScrollRowCanvas Canvas.Top="yoff">
    sr.SetValue(Canvas.TopProperty, actualHeight);
    if( KeepLocked == false )
      Locked = false;
    //
    actualHeight = 0.0d;
    ElementContent.Children.Cast<ScrollRowCanvas>().ToList().ForEach(rw =>
      {
        rw.SetValue(Canvas.TopProperty, actualHeight);
        actualHeight += rw.ActualHeight;
      });
    SwitchStrategy(false, WasLocked);
    // force recalc etc
    InvalidateLayout();
    //
    this.Cursor = Cursors.Arrow;
  }

  public void RemoveRow(int index)
  {
    if (index <= ElementContent.Children.Count - 1 && index >= 0)
      ElementContent.Children.RemoveAt(index);
    else
      return;
    Locked = true;
    _rows--;
    double xoff = 0;
    double yoff = 0;
    for (int row = 0; row < _rows; row++)
    {
      // new item
      ScrollRowCanvas sr = ElementContent.Children[row] as ScrollRowCanvas;
      // equivalent to <ScrollRowCanvas Canvas.Left="xoff">
      sr.SetValue(Canvas.LeftProperty, xoff);
      // equivalent to <ScrollRowCanvas Canvas.Top="yoff">
      sr.SetValue(Canvas.TopProperty, yoff);
      // next vertical slot
      yoff += cellHeight;
    }
    Locked = false;
    //
    SwitchStrategy(false);
    // force recalc etc
    InvalidateLayout();
    //
    this.Cursor = Cursors.Arrow;
  }

  public void ScrollIntoPosition(double hV, double vV)
  {
    bool update = false;
    if( HScroll != null && (update=HScroll.Value != hV))
      HScroll.Value = hV;
    if (VScroll != null && (update=VScroll.Value != vV))
      VScroll.Value = vV;
    if( update )
      InvalidateLayout();
  }

  public void Clear()
  {
    this.Cursor = Cursors.Wait;
    Locked = true;
    ElementContent.Children.Clear();
    //
    SwitchStrategy(false);
    // force recalc etc
    InvalidateLayout();
    //
    this.Cursor = Cursors.Arrow;
  }

  public ScrollRowCanvas this[int index]
  {
    get 
    {
      if (index >= 0 && index <= ElementContent.Children.Count - 1)
        return ElementContent.Children[index] as ScrollRowCanvas;
      else 
        return null;
    }
  }

  /// <summary>
  /// Constructor
  /// </summary>
  public ScrollViewerEx()
  {
    InitializeComponent();
    Debug.Assert(ElementContent != null);
    this.Loaded += OnLoaded;
    // event handlers
    KeyDown += delegate(object sender, KeyEventArgs e)
    {
      OnKeyDown(e);
    };
    // list of *all* row items we manage
    VisibleItems = new List<UIElement>();
    // apply the scrolling translation
    Translation = new TranslateTransform();
    
    // mouse wheel listener - DISABLED
    //WheelMouseListener.Instance.AddObserver(this);
  }

  /// <summary>
  /// Ensure we get keyboard events
  /// </summary>
  /// <param name="sender"></param>
  /// <param name="e"></param>
  protected virtual void OnLoaded(object sender, RoutedEventArgs e)
  {
    // fast by default
    FastMode = true;
    // set up scroll bars - actual ranges get calculated in
    // ArrangeOverride
    VScroll.Value   = 0;
    HScroll.Value   = 0;
    VScroll.Minimum = 0;
    VScroll.Maximum = _rows - 1;
    VScroll.Value   = 0;
    HScroll.Minimum = 0;
    HScroll.Maximum = _cols - 1;
    HScroll.Value   = 0;
    //
    SwitchStrategy(false);
  }

  public void SwitchStrategy(bool change)
  {
    SwitchStrategy(change, false);
  }
  /// <summary>Switch scrolling strategies</summary>
  /// <param name="change"></param>
  public void SwitchStrategy(bool change, bool WasLocked)
  {
    if (change)
    {
      FastMode = !FastMode;
    }

    int limit = ElementContent.Children.Count;
    Locked = true;
    if (FastMode)
    {
      Color color = Color.FromArgb(0xFF, 0x80, 0x00, 0x00);
      SolidColorBrush br = new SolidColorBrush(color);
      ColHeaderContent.Background = br;
      RowHeaderContent.Background = br;
      for (int row = 0; row < limit; row++)
      {
        ElementContent.Children[row].Visibility = Visibility.Collapsed;
      }
    }
    else
    {
      Color color = Color.FromArgb(0xFF, 0x40, 0x00, 0x00);
      SolidColorBrush br = new SolidColorBrush(color);
      ColHeaderContent.Background = br;
      RowHeaderContent.Background = br;
      for (int row = 0; row < limit; row++)
      {
        ElementContent.Children[row].Visibility = Visibility.Visible;
      }
    }
    if( WasLocked == false )
      Locked = false;
    InvalidateLayout();
  }

  //// mouse wheel - move vertical scroll bar as appropriate
  //public void OnMouseWheel(MouseWheelArgs args)
  //{
  //  // update the scrollbar thumb according to wheel motion
  //  double pos = VScroll.Value;
  //  pos += -args.Delta;
  //  VScroll.Value = pos;
  //  //
  //  InvalidateLayout();
  //  //_strategy.Layout(HorzPosition, VertPosition, RowsPerPage, ColsPerPage);
  //}

  /// <summary>
  /// N.B Simplified for the sake of example - we are only interested in thumb events
  /// </summary>
  /// <param name="sender"></param>
  /// <param name="e"></param>
  private void VScroll_Scroll(object sender, 
               System.Windows.Controls.Primitives.ScrollEventArgs e)
  {
    InvalidateLayout();
  }

  /// <summary>
  /// N.B Simplified for the sake of example - we are only interested in thumb events
  /// </summary>
  /// <param name="sender"></param>
  /// <param name="e"></param>
  private void HScroll_Scroll(object sender, 
          System.Windows.Controls.Primitives.ScrollEventArgs e)
  {
    InvalidateLayout();
  }

  protected override Size MeasureOverride(Size availableSize)
  {
    return base.MeasureOverride(availableSize);
  }

  public void InvalidateLayout()
  {
    if (Locked)
      return;
    InvalidateArrange();
  }

  // establish how many rows and columns we can display and
  // set scroll bars accordingly
  protected override Size ArrangeOverride(Size finalSize)
  {
    // let the base class handle the arranging
    finalSize = base.ArrangeOverride(finalSize);
    // here's the magic ...
    ApplyLayoutOptimizer();
    //
    return finalSize;
  }

  /// <summary>
  /// Set the vertical and horizontal scroll bar ranges
  /// </summary>
  protected void SetScrollRanges()
  {
    // what is the view-port size?
    Rect clipRect = ElementContentClipper.ClippingRect;
    // how many integral lines can we display ?
    int rowsPerPage = (int)(clipRect.Height / cellHeight);
    // set the scroll count
    VScroll.Maximum = (_rows - rowsPerPage);
  }

  /// <summary>
  /// Use the Translation to scroll the content canvas
  /// </summary>
  protected void HandleScrolling()
  {
    // offset by scroll positions
    Translation.X = -(HScroll.Value);
    Translation.Y = -((VScroll.Value * cellHeight) - (VScroll.Value > 0 ? 5 : 0 ));
    // apply the transform to the content container
    ElementContent.RenderTransform = Translation;
  }

  public void ApplyLayoutOptimizer()
  {
    // beware recursion - settings visibility will trigger 
    // another ArrangeOverride invocation
    if (Locked == false)
    {
      // lock
      Locked = true;
      // set up the scroll bars
      SetScrollRanges();

      // hide the visible items
      foreach (UIElement uie in VisibleItems)
      {
        uie.Visibility = Visibility.Collapsed;
      }
      // remove from list
      VisibleItems.Clear();
      // layout a page worth of rows
      int maxRow = System.Math.Min(VertPosition + RowsPerPage, 
                   ElementContent.Children.Count);
      for (int row = VertPosition; row < maxRow; row++)
      {
        UIElement uie = ElementContent.Children[row];
        //
        uie.Visibility = Visibility.Visible;
        //
        VisibleItems.Add(uie);
      }
      // scroll the canvas
      HandleScrolling();
      // unlock
      Locked = false;
    }
  }
}

Well, I think that's it. Sorry for such sparse explanation of my updated article. I do not have a lot of time for a descent explanation. I will do this as soon as I have enough time for this.

You may try this new implementation, and post a feedback to me for any suggestions or bug reports.

02/04/2009

The performance issues. I have modified the main algorithm of the rendering part of my control. I will update the sources in a couple of days. The new implementation updates and renders only the changed rows.

I have found a WPF issue when rendering a huge amount of TextBlocks at the same time. If you want to test this issue, you may create a page and then programmatically put 1000 TextBlock instances into a StackPanel that resides inside a ScrollViewer and you will see that scrolling the content is almost impossible. CPU load is over 60 % on my P4 3200 gHz. It seems that Microsoft has a big performance problem in rendering. This makes my control kind of useless. :O(

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here