Silverlight 2.0 Syntax Highlighting TextBox






4.91/5 (13 votes)
Syntax highlighting TextBox in Silverlight 2.0.
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 Hashtable
s 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.
- Undo/Redo support
- 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 TextBlock
s 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(