The other day I ran into a bit of an issue. I'm getting ready to write a new Xml4Fun article - and I want to build a spiffy little 'editor.'
To be cool, my editor will have to be able to remember tab position (i.e. if I'm 'tabbed over 2x in the editor, and hit a carriage return, I still want to be tabbed over 2x ala VS - not ala NotePad). In the 'biz' it turns out that this is called Auto Indenting.
The problem: Doing this in a System.Windows.Forms.TextBox just didn't seem all that easy. In fact, as I did more digging, I started to realize that I couldn't find a single example of how to do it in .NET. That seemed crazy. But the more I dug, the more I realized I was going to have to do this one on my own. (I asked for some help on a list full of smart people too - and while I got some cool help - not much materialized in the way of simple examples/etc.)
Well, after banging around on winforms for a while... I managed to pull it of - and 1) it wasn't too hard at all, 2) it works insanely well (if I say so myself).
The overall gist:
1) use the KeyPress Event - check for carriage returns
2) when found, grab the 'leading' tabs from the previous line
3) add them in.
4) throw it all into a subclass of textbox so you don't have to deal with the details
5) add xml tag completeion while you're at it.
Here's the final code:
public partial class SimpleEditorBox : TextBox
{
public SimpleEditorBox()
{
InitializeComponent();
base.AcceptsReturn = true;
base.AcceptsTab = true;
}
public bool IsDirty
{
get { return this.CanUndo; }
}
private void SimpleEditorBox_KeyPress(object sender, KeyPressEventArgs e)
{
if (e.KeyChar == 13) // ENTER { e.Handled = this.HandleBlockTabbing(sender);
}
if (e.KeyChar == '>')
{
e.Handled = this.HandleTagCompletion(sender);
}
}
private bool HandleBlockTabbing(object sender)
{
SimpleEditorBox current = (SimpleEditorBox)sender;
int charIndex = current.SelectionStart;
string work = current.Text.Substring(0, charIndex);
int currentLineNumber = current.GetLineFromCharIndex(charIndex);
// the carriage return hasn't happened yet... // so the 'previous' line is the current one. string previousLineText = current.Lines[currentLineNumber];
if (previousLineText.StartsWith("\t"))
{
string tabs = string.Empty;
// first non-TAB character Match m = Regex.Match(previousLineText, "[^\t]");
if (m.Success)
tabs = previousLineText.Substring(0,
previousLineText.IndexOf(m.Value));
else // there were only tabs (no other chars) tabs = previousLineText; string added = "\r\n" + tabs;
current.Text = current.Text.Insert(charIndex, added);
current.SelectionStart = charIndex + added.Length;
return true;
}
return false;
}
private bool HandleTagCompletion(object sender)
{
SimpleEditorBox current = (SimpleEditorBox)sender;
int charIndex = current.SelectionStart;
string work = current.Text.Substring(0, charIndex);
if(this.CurrentPositionNeedsClosingTag(work))
{
string tag = string.Empty;
MatchCollection mc = Regex.Matches(work, "<[^>]{1,}");
if (mc.Count > 0)
{
// grab the last match Match target = mc[mc.Count - 1]; tag = target.Value; tag = tag.Replace("\t"," ").Replace("\r"," ");
if(tag.IndexOf(" ") > -1)
tag = tag.Substring(0,tag.IndexOf(" "));
tag = tag.Replace("<","");
}
string added = tag == string.Empty ? ">" : "></" + tag + ">";
if(work.Length > 0)
{
current.Text = current.Text.Insert(charIndex, added);
current.SelectionStart = charIndex + 1;
return true;
}
else return false;
}
else return false; // just let the ">" plunk down normally... } private bool CurrentPositionNeedsClosingTag(string input)
{
string scrubbedText = input.Replace("\r", string.Empty);
scrubbedText = scrubbedText.Replace("\n", string.Empty);
scrubbedText = scrubbedText.Replace(" ", string.Empty);
// ignore directives, comments, and empty tags: bool isDirective = scrubbedText.EndsWith("?");
bool isComment = scrubbedText.EndsWith("--");
bool isEmptyTag = scrubbedText.EndsWith("/");
if(isDirective || isComment || isEmptyTag)
return false;
// watch out for stray tags and truly empty tags ("<>") if(scrubbedText.EndsWith(">") || scrubbedText.EndsWith("<>"))
return false;
// watch out for explicit closing tags // ("</endMyTagByHand>" - don't want: "</end><//end>") // HACK: should be done with a regex.. int position = scrubbedText.LastIndexOf("<");
if (scrubbedText.Substring(position).StartsWith("</"))
return false;
return true;
}
}
As always - let me know if you see something stoopid.




Yeah - you're *handling* the KeyPress event. Instead, *override* it, and just use 'this' in your Handle* methods. i.e.:
protected override void OnKeyPress(KeyPressEventArgs e){
if(e.KeyChar == 13){
e.Handled = this.HandleBlockTabbing();
}else if(e.KeyChar == '>'){
e.Handled = this.HandleTagCompletion();
}
//CF: this call will let forms authors add
//logic if they wish in thier consuming code.
//Handling the event will instead run in
//'parallel' with no guarantee as to which gets
//called first.
base.OnKeyPress(e);
}
Posted by: Chris Frazier | December 22, 2005 at 12:14 PM
Forgot - as a test, try putting the same logic into a Form with a regular textbox (and using the KeyPress event handler). I betcha you get the same results as the subclassed textbox.
Posted by: Chris Frazier | December 22, 2005 at 12:19 PM
Chris,
Thanks for the feedback.
As a matter of fact, i started out in a normal winform and handled the event as you suggested. Then when I was done with all of my different approaches/etc.
Once that was working, around midnight, i decided to create a subclass of textbox to put in all of my code cleanly etc.
By that time i was braindead - and forgot to override - so thanks for the feedback. I would have gone right ahead and used this (thinking that i had subclassed correctly when in fact i had pulled a brain fart).
Posted by: Michael K. Campbell | December 23, 2005 at 11:37 AM
"By that time"? How does it feel to not be braindead 24/7 like me? :)
Posted by: Chris Frazier | December 23, 2005 at 11:22 PM