WPF Performance: TextBoxes Galore!
I see a lot of WPF demos, repros, apps and so forth on a regular basis. Sometimes these apps will contain, for whatever reason, a large number of TextBoxes. Now, I have my opinions about whether or not UI looks good with that many TextBoxes but I have a stronger opinion on what should be done to make sure performance isn't significantly impacted.
(I suggest you read the "Background" below but if you just want the code, you can skip it.)
Background
A post or two ago I mentioned that keeping track of your application's element count was important for performance (specifically, keeping the number as low as possible.) Frequently, this includes knowing how many elements you're introducing. Lets do some numbers.
The Numbers
A fully "template expanded" TextBox contains 30 elements (including ScrollBars.) A minimally expanded TextBox has 10 elements. Now, imagine you have 100 minimally expanded TextBoxes - that's 1000 elements! Even if you haven't explicitly added TextBox elements yourself, you may have one in an ItemTemplate of an ItemsControl; this can easily increase the number of instances.
Step # 1
So, now I’ve told you what the potential problem is; only fair I should give a solution.
Well, the first question I ask is "Are the TextBoxes necessary?" In some cases, using a TextBox is overkill and you can get away with using a TextBlock or a Label control. Here are some simple guidelines:
- Use TextBox if you need:
- Text selection
- The ability to edit the text
- Use TextBlock or Label if you only need:
- To display text
- To show text of a changing value.
Step # 2
At this point, let’s say that you do need a TextBox (or many.) Now what? By simply dynamically changing the Template of a TextBox from 10 elements to just two – per instance! So, with 100 minimally expanded TextBoxes we go from 1000 elements to 208! So how do you do it!? With some Xaml & some C#.
Xaml
Below, you’ll find the Xaml necessary for this demonstration (code‑behind is required; shown below the Xaml.)
Notice that there are four TextBoxes (with varying text formatting) and a couple of Styles named TextBoxBaseStyle and BasicTextBox. TextBoxBaseStyle is just a set of common properties I used across the TextBoxes. BasicTextBox is the simplied, or lighter weight, version of TextBox that can be used when selection or text editing aren’t needed. The idea is to swap the templates at the right time (notice the MouseLeftButtonDown handler on the parent StackPanel.)
It’s basically that easy along with the code below (modify the code to the behavior you want.)
<Window x:Class="TextBoxPerformance.Window1"
xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
Title="TextBoxPerformance" Height="300" Width="300"
>
<StackPanel MouseLeftButtonDown="HandleMouseInput" Margin="5">
<StackPanel.Resources>
<!--
Base TextBox Style
-->
<Style x:Key="TextBoxBaseStyle" TargetType="{x:Type TextBox}">
<Setter Property="FontSize" Value="20"/>
<Setter Property="Margin" Value="5"/>
</Style>
<!--
Simplified TextBox Style with template & necessary
bindings.
-->
<Style
x:Key="BasicTextBox"
TargetType="{x:Type TextBox}"
BasedOn="{StaticResource TextBoxBaseStyle}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TextBox}">
<Border
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}"
>
<TextBlock
Margin="3.85,2,3.85,2"
Text="{TemplateBinding Text}"
TextDecorations="{TemplateBinding TextDecorations}"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Setter Property="Focusable" Value="false"/>
</Style>
</StackPanel.Resources>
<!--
A sample of TextBoxes with various formatting properties.
-->
<TextBox
Style="{StaticResource BasicTextBox}"
FontFamily="Georgia"
Foreground="green"
FontWeight="Heavy">Athens</TextBox>
<TextBox
Style="{StaticResource BasicTextBox}"
FontFamily="Garamond">Chicago</TextBox>
<TextBox
Style="{StaticResource BasicTextBox}"
FontFamily="Comic Sans MS"
FontStyle="Italic">Springfield</TextBox>
<TextBox
Style="{StaticResource BasicTextBox}"
FontFamily="Arial" Foreground="red"
TextDecorations="underline">Urbana-Champaign</TextBox>
</StackPanel>
</Window>
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace TextBoxPerformance
{
//Generated code from WPF Project in VS
public partial class Window1 : System.Windows.Window
{
public Window1()
{
InitializeComponent();
}
//Used to track whether or not another TextBox
//is active.
private TextBox ActiveTextBox;
//Actives the hit TextBox if one isn’t active.
//Deactivates the active TextBox if something
//else was hit.
void HandleMouseInput(object sender, MouseEventArgs args)
{
TextBox tb = args.Source as TextBox;
if (tb == null)
return;
if(ActiveTextBox==null)
Activate(tb);
else
Deactivate(ActiveTextBox);
}
//Deactivates the active TextBox if the Enter
// or ESC key is pressed.
void HandleKeyInput(object sender, KeyEventArgs args)
{
TextBox tb = args.Source as TextBox;
if (tb!=null &&
(args.Key == Key.Return || args.Key== Key.Escape))
Deactivate(tb);
}
//Deactivate the Textbox the active TextBox.
void HandleLostFocus(object sender, EventArgs args)
{
TextBox tb = sender as TextBox;
if (tb != null)
Deactivate(tb);
}
//Activate the TextBox by applying the base style
//(thereby removing the basic style.) Set focus, select
//all the content & invalidate the visual.
//Also, dynamically add handlers for losing focus and
//handling key input.
void Activate(TextBox tb)
{
tb.Style = (Style)tb.FindResource("TextBoxBaseStyle");
tb.Focus();
tb.SelectAll();
tb.InvalidateVisual();
tb.LostFocus += new RoutedEventHandler(HandleLostFocus);
tb.KeyDown+=new KeyEventHandler(HandleKeyInput);
ActiveTextBox = tb;
}
//Deactivate the TextBox by applying the Basic style.
//Remove the event handlers.
void Deactivate(TextBox tb)
{
tb.Style = (Style)tb.FindResource("BasicTextBox");
tb.LostFocus -= new RoutedEventHandler(HandleLostFocus);
tb.KeyDown -= new KeyEventHandler(HandleKeyInput);
ActiveTextBox = null;
}
}
}
Now, when a TextBox is clicked, it becomes editable. When RETURN or ESC is pressed, the value is committed.
Comments
- Anonymous
August 13, 2006
The comment has been removed - Anonymous
August 13, 2006
Agree that it should do it by default. It's just not something we got to in this version; hopefully in the next. - Anonymous
August 13, 2006
The comment has been removed - Anonymous
August 14, 2006
By "next version", I'm talking about a full product version such as .NET 3.0 (not just a CTP or beta.) - Anonymous
January 29, 2007
I could be missing something, but it looks like your code doesn't activate the newly clicked TextBox if one was activated prior. if(ActiveTextBox==null) Activate(tb); else Deactivate(ActiveTextBox); If ActiveTextBox is not equal to null, Activate is never called by your click handler. I guess you get around this by the fact that your LostFocus handler is called before your MouseDown handler (calling Deactivate, and thus setting ActiveTextBox to null). In that case your if...else construct is redundant. Of course if you were handling the PreviewMouseDown (which is sometimes necessary), your handler would be called before the LostFocus handler. So we need to clean up the logic if(ActiveTextBox!=null) { Deactivate(ActiveTextBox); } Activate(tb); This should do it.