New User?   Join Now     Login            Forgot Password?    
Silverlight Bullets & Indenting  (34885 hits)
 Share this Article
 
Posted by Prabu Arumugam on Dec-23-2010
Languages: C#, Silverlight

This article explains how to implement Bullets & Numbering and Indenting in Silverlight 4 RichTextBox control. By default, the RichTextBox control of Silverlight 4 does not support Bullets & Numbering and Indenting. We have to implement them programmatically by finding selected paragraphs (paragraphs selected by user) and inserting corresponding characters like bullets, numbers and tabs.

Bullets & Numbering Demo in Silverlight

The following demo illustrates a basic Silverlight text/message editor with Bullets & Numbering and Indenting features.

Steps to add Bullets & Numbering in Silverlight RichTextBox

This involves following steps:

  • Find all Paragraph elements in selected text.
  • Insert bullet/number/tab at start of each Paragraph found.

Finding Offset of a position

In order to insert any content in a specified location inside RichTextBox, we have to find the offset of that location. A location/position corresponds to a TextPointer object. Offset of a given-position means number of characters (more precisely number of insertion-points) between first position of RichTextBox and given-position.

The following function finds the offset of a given position.

public static int FindOffsetOfPosition(RichTextBox rtb, TextPointer position)
{
    int offset = 0;
    while (true)
    {
        TextPointer currentPosition = rtb.ContentStart.GetPositionAtOffset(offset, LogicalDirection.Forward);
        if (currentPosition == null) break;
        if (currentPosition.CompareTo(position) == 0) break;
        offset += 1;
    }

    return (offset);
}

Finding Selected Paragraphs

In order to find Paragraph elements in selected text, first we have to find offsets of all Paragraph elements inside RichTextBox. This will be used as a lookup-table while finding selected-paragraphs. The following function find offsets of all Paragraph elements inside RichTextBox.

public static Dictionary<Paragraph, int> GetAllParagraphOffsets(RichTextBox rtb)
{
    Dictionary<Paragraph, int> allParagraphs = new Dictionary<Paragraph, int>();
    foreach (Block block in rtb.Blocks)
    {
        if (block is Paragraph)
        {
            Paragraph paragraph = (Paragraph)block;
            int offset = FindOffsetOfPosition(rtb, paragraph.ContentStart);
            allParagraphs.Add(paragraph, offset);
        }
    }
    return (allParagraphs);
}

Then, we find the selected-paragraphs by comparing the offset of each character inside selected-text with the lookup-table. The following function find all Paragraph elements inside selected-text.

private static List<Paragraph> GetSelectedParagraphs(RichTextBox rtb)
{
    Dictionary<Paragraph, int> allParagraphs = GetAllParagraphOffsets(rtb);
    List<Paragraph> selectedParagraphs = new List<Paragraph>();

    TextPointer currentPosition = rtb.Selection.Start;
    TextPointer endPosition = rtb.Selection.End;
    while (true)
    {
        int currentOffset = FindOffsetOfPosition(rtb, currentPosition);
        Paragraph currentParagraph = FindParagraphAtOffset(allParagraphs, currentOffset);
        if (currentParagraph != null && !selectedParagraphs.Contains(currentParagraph)) selectedParagraphs.Add(currentParagraph);

        currentPosition = currentPosition.GetNextInsertionPosition(LogicalDirection.Forward);
        if (currentPosition == null) break;
        if (currentPosition.CompareTo(endPosition) > 0) break;
    }

    return (selectedParagraphs);
}

The FindParagraphAtOffset() function searches in the lookup-table and return the corresponding Paragraph element.

Applying Bullets, Numbering and Indenting

After finding all Paragraph elements in selected-text, we insert the corresponding character (bullet or number or tab) at the start of each Paragraph element. The following function inserts bullet character in selected-paragraphs.

public static void ApplyBullets(RichTextBox rtb, string bulletChar)
{
    List<Paragraph> selectedParagraphs = GetSelectedParagraphs(rtb);

    //insert bullet character in selected-paragraphs
    foreach (Paragraph paragraph in selectedParagraphs)
    {
        int paragraphStartOffset = FindOffsetOfPosition(rtb, paragraph.ContentStart);
        InsertAtOffset(rtb, new Run() { Text = bulletChar + "  " }, paragraphStartOffset);
    }
}

The InsertAtOffset() function is used to insert an Inline at a given offset.

Applying bullets/numbering/indenting will be very slow if SelectionChanged event is not disabled; because each insert and delete operation in a RichTextBox will trigger the SelectionChanged event.

For Numbering, we insert seqential numbers before each Paragraph element. To increase indent, we insert a tab (\t) character before each Paragraph element. To decrease indent, we check the first character of each Paragraph element and remove it if it is a tab (\t) character.

When applying bullets/numbering, we have to check whether bullets/numbering is already applied to the selected paragraphs and remove the corresponding text (bullet character or numbers) before inserting the actual text (bullet character or numbers) at the start of each paragraph.


 Downloads for this article
File Language Tools
Silverlight Bullets & Numbering Source  150.49 kb  (420 downloads) C#, Silverlight 4 Visual Studio 2010

 Share this Article

 Comments on this Article
Comment by progs on Sep-01-2011
The best optimization i found is, to save the last offset getting from FindOffsetOfPosition and start the next time from this offset and not from 0.
// FindOffsetOfPosition
public int FindOffsetOfPosition(TextPointer position, int lastOffset)
{
	int offset = lastOffset;
	while (true)
	{
		var currentPosition = ContentStart.GetPositionAtOffset(offset, LogicalDirection.Forward);
		if (currentPosition == null || currentPosition.CompareTo(position) == 0) break;
		offset++;
	}
	return (offset);
}
// Example using last offset
private Dictionary<int, Paragraph> GetAllParagraphOffsets()
{
	// last offset
	int lastOffset = 0;
	var allParagraphs = new Dictionary<int, Paragraph>();
	foreach (Block block in Blocks)
	{
		if (block is Paragraph)
		{
			var paragraph = (Paragraph)block;
			// start from last offset
			int offset = paragraph.ContentStart.GetOffsetToPosition(this, lastOffset);
			// save last offset
			lastOffset = offset;
			allParagraphs.Add(offset, paragraph);
		}
	}
	return (allParagraphs);
}
Comment by Marcio Bagatini on Aug-24-2011
The slowest part of this algorithm is the GetSelectedParagraphs method. I've rewrited it, please try:
public static List<Paragraph> GetSelectedParagraphs(RichTextBox rtb)
{
	List<Paragraph> selectedParagraphs = new List<Paragraph>();
	bool flag = true;
	Paragraph lastParagraph = new Paragraph();
	foreach (Block bl in rtb.Blocks)
	{
		if (bl is Paragraph)
		{
			if (flag && rtb.Selection.Start.CompareTo(bl.ContentStart) == -1)
			{
				selectedParagraphs.Add(lastParagraph);
				flag = false;
			}
			if (rtb.Selection.End.CompareTo(bl.ContentEnd) == -1)
			{
				selectedParagraphs.Add((Paragraph)bl);
				break;
			}
			if (rtb.Selection.Start.CompareTo(bl.ContentStart) == -1 && rtb.Selection.End.CompareTo(bl.ContentEnd) == 1)
			{
				selectedParagraphs.Add((Paragraph)bl);
			}
			lastParagraph = (Paragraph)bl;
		}
	}
	return selectedParagraphs;
}
Also try this, when calling the method GetSentenceFromPosition inside the ApplyNumbering method use this call:
foreach (Paragraph paragraph in selectedParagraphs)
{
	//string sentence = GetSentenceFromPosition(rtb, paragraph.ContentStart);
	Run runParag = new Run();
	if (paragraph.Inlines[0] is Run)
	{
		runParag = (Run)paragraph.Inlines[0];
	}
	string sentence = GetSentenceFromPosition(runParag.Text);
	if (IsNumber(sentence)) numberedParagraphs += 1;
}
and this method:
public static string GetSentenceFromPosition(String pText)
{
	string sentence = string.Empty;
	if (pText.Contains("."))
	{
		sentence = pText.Split('.')[0].ToString();
	}
	return (sentence);
}
Comment by Nick Renziglov on Jul-14-2011
Cool stuff. As per bullets, in the localazed environment it won't work cause it uses the international symbol. I could not save rtb.Xaml in the databse properly due to this damn symbol contradicts to the Page Code. It turns into fallback ? mark. The better solution, I think, is as follows:

Run run = new Run();
run.FontFamily = new FontFamily("Webdings");
run.Text = "= ";
//InsertAtOffset(rtb, new Run() { Text = "? " }, paragraphStartOffset); <-- not good
InsertAtOffset(rtb, run, paragraphStartOffset); <-- better

Thanks.
Comment by shim5hat on Apr-06-2011
Great!! Thanx!!
Comment by phani on Mar-08-2011
Hi I am unable to open the link,I need source code of this article. Or atleast send me FindParagraghAtOffset() method
Comment by Prabu on Feb-12-2011
Hi Aananta, this approach will be slow if the selected text contains big paragraphs instead of small/one-line paragraphs.
Comment by Aananta on Feb-10-2011
This is really cool but definitely slow. Is there any way we can optimize it ?
 Post your comment here
Your Name
Your Email
Comment
Post Comment

About      Terms      Contact