Improving Performance of Localized Forms
by Brian Pepin
Get the samples for this article
If anyone has had experience with the automatic form
localization provided for Windows Forms in Visual Studio .NET, you know that it
is a convenient way to provide a rich client user interface in multiple
languages. If you are creating complex
forms, however, you may be dismayed to find that this wonderful localization
support comes at a price: startup
performance.
A form that is localized loads and displays slower than one that is not
localized. To understand why this is,
let’s focus for a bit on what actually happens when you set the Localizable
property of the form to True.
Primer to Windows Forms Localization
If you’ve ever looked at the generated source code for a
form both before and after the Localizable property is set to true, you’ll see
that there is a big difference in the way the form code is written. When
localization is turned off, code for a simple form looks something like this:
private void InitializeComponent() {
this.Text = “Hello, World”;
this.Size = new Size(300, 300);
this.Name = “Form1”;
}
When localization is turned on, however, many of the form’s
properties are read out of a resource file:
private void InitializeComponent() {
ResourceManager rm = new ResourceManager(typeof(Form1));
this.Text = rm.GetString(“Form1_Text”);
this.Size = (Size)rm.GetValue(“Form1_Size”);
this.Name = “Form1”;
}
Notice that while the Text and Size properties were accessed
through the resource manager, the Name property wasn’t. This is because Name is not a localizable
property. Its value is the same
regardless of the user’s language, so it is skipped. But how does the code generator know which
properties to localize? Controls can
provide this information by adorning their properties with the Localizable
attribute. If the attribute is present
on a property and set to True, the property will be localized.
Performance Issues with Localization
If you did look at the difference between a localized and a
non-localized form, there is one other thing you would notice. For the non-localized form, only those
properties that differ from the form’s default values are written to code. For localized forms, however, all of the
localized properties are written out, even if they contain their default
values. Why? Well, the answer is, “just in case”. All the properties need to be written out
just in case a value differs based on culture and needs to deviate from the
default value. Let’s look at the Font
property for example. In nearly all
cases, the default font for the operating system will work fine for any
language. In Chinese, however, the
default font for the operating system is too small. It must be replaced with a larger, newer
version called New Ming Li. To do this,
the Chinese RESX file would have a font declared for New Ming Li. But, if there wasn’t a statement in code that
assigned the value in the RESX file into the form’s Font property, the font
wouldn’t actually change. This is why
Microsoft writes all values into the form when localization is turned on: just in case a value needs to be set in a RESX
file.
All this additional information is what slows down the
form’s loading time when it is localized.
For many forms, the difference is small and not noticeable. For forms with a lot of complex graphics or
many controls, the difference in performance can be noticed.
Possible Solution?
Microsoft had to account for every possible scenario when
deciding what properties to make as localized.
Because of this, most of the properties on controls and forms are
localizable. In many applications,
however, most of these properties really don’t need to be localized. Text strings, sizes and locations of controls
and fonts are typically the only properties that really need to be localized in
an application. If it were possible for
developers to change which properties the controls offer as localizable,
developers could custom tailor the set of localizable properties to fit their
application. They could pick and choose
which properties should be localized based on their application’s needs, and gain
performance because properties their application never needs to localize do not
go into the resource file. Great idea; now
all we need is a way to implement it.
The Localization Filter
The solution I’m proposing here is a component you can place
on a form. This component maintains a
list of properties you need to localize for the form. Only these properties would be localized by
the code generator, and the properties can be modified in the forms designer by
updating a collection. Before I dig into
the details of writing such an object, let’s take a quick peek of what it looks
like in action.
Below is a picture of a form that contains an instance of my
“localization filter” component.
The
only visual evidence of the filter is a component in the form’s component
tray.
I have setup the localization
filter to filter make the following properties localizable:
Location, Size, Text and Font by clicking on
the LocalizableProperties property in the property window:
Besides this component in the tray that has a list of
property names, there isn’t any other user interface to my localization filter
component. The localization filter is
silently working under the covers, however, to change the way all the other
objects on the form generate code. Let’s
look at the generated code for button1:
this.button1.Font = ((System.Drawing.Font)(resources.GetObject("button1.Font")));
this.button1.ImeMode = System.Windows.Forms.ImeMode.NoControl;
this.button1.Location = ((System.Drawing.Point)(resources.GetObject("button1.Location")));
this.button1.Name = "button1";
this.button1.Size = ((System.Drawing.Size)(resources.GetObject("button1.Size")));
this.button1.TabIndex = 0;
this.button1.Text = resources.GetString("button1.Text");
This isn’t far from the code generated for a button when
localization is turned off. Only a few
key properties – those properties listed in the localization filter’s
Localizable Properties dialog – have been localized. The rest of the properties have been stored
in normal code statements. This can
provide a real savings in terms of form load time and binary size for large,
complex forms. In the next section I’ll
take a look at how the localization filter component works.
The Plumbing
Writing a component like the localization filter isn’t as
hard as it sounds. The designer
architecture that the Windows Forms designer is built on top of provides all
the hooks and knobs we need. First, I
should list the steps involved in creating my localization filter class:
- I need a runtime class, called LocalizationFilter,
that derives from Component so I can place it on a form.
- Next, this class needs to offer my
LocalizableProperties collection so that I can pick and choose what
properties I want to localize.
- Something, somewhere, must read this collection and
change the way the form designer generates code.
The first two steps don’t sound very hard. I’m going to start off by designing a new
component, called the LocalizationFilter, which is responsible for choosing
which properties are localized. Here is
the complete implementation of LocalizationFilter:
[Designer(typeof(FilterDesigner))]
public sealed class LocalizationFilter : Component {
}
As you can see, there’s not much to it. The localization component still needs to be
on the form, even in runtime, so I want to make it as simple as possible. Therefore, I will place all of the filter’s
logic in another class, called a designer.
LocalizationFilter’s behavior comes from its designer. A designer is simply a class that implements
IDesigner and is associated with any component through a designer
attribute. Here is the class definition
for LocalizationFilter’s designer, which I am calling FilterDesigner:
internal sealed class FilterDesigner : ComponentDesigner {
}
Notice that FilterDesigner is declared as internal. Unless you have a need to expose your
designer to end users, you should make it internal. The forms designer will still be able to
create an instance of it. FilterDesigner
also derives from ComponentDesigner.
This base class implements IDesigner for me so I can concentrate on my
own features rather than worry about a bunch of plumbing.
Now that I’ve defined my FilterDesigner class, I need to
define the properties it will expose. I
want my LocalizationFilter to offer a collection of strings through a property
called “LocalizableProperties”. While I
could have declared this property on my LocalizationFilter component itself,
that would require the property to be saved in the user’s source code. I want the smallest possible runtime size for
my LocalizationFilter component, so I would rather not define this property on
my component. Also, since the collection
of strings this property contains is only valuable when a form is loaded in the
designer, I’d be better off leaving my LocalizableProperties collection off my
LocalizationFilter component. So, where
else can I define my collection if I want it to show up in the property window
at design time? Why, on the
FilterDesigner, of course! Here’s what the
code looks like, with the new property code in bold:
internal sealed class FilterDesigner : ComponentDesigner {
private StringCollection localizablePrpoperties;
private StringCollection LocalizableProperties {
get {
if (localizableProperties == null) {
localizableProperties = new StringCollection();
}
return localizableProperties;
}
}
}
If you run this code now you’ll be disappointed. The LocalizableProperties property won’t show
up in the property window when you select the LocalizationFilter
component. That’s because the property
window browses properties on the component, not the designer. Somehow, I’ve got to add this property to the
set of properties offered by the LocalizationFilter component. The .NET Framework’s designer support allows
me to do this through a technique called property filtering. Property filtering is a feature that is built
into the .NET Framework that allows objects to change properties that are
exposed by other objects. One object can
add, remove or change the implementation of another object’s properties with
property filtering. Let’s take a look at
how property filtering works in detail.
Property Filtering
Property filtering starts at an object called
TypeDescriptor. TypeDescriptor is a
single point where developers can ask about an object’s properties. The property window uses TypeDescriptor when
it wants to display a set of properties for a component. TypeDescriptor, always looks for a filtering
service, called ITypeDescriptorFilterService, on each component it sees. If it finds this service, it builds the set
of properties for the component, and then hands these properties to the filter
service. The filter can then make
changes.
This filtering technique is used extensively at design time
to control which properties are exposed in the properties window. Visual Studio .NET implements
ITypeDescriptorFilterService and passes on the filter request to the designer
associated with a component. Graphically,
the process looks something like this:
If all that sounds complicated, don’t worry. Visual Studio .NET is doing all of the hard
work. All I’ve got to do to get the
property window to display my LocalizableProperties property is add it to my
designer’s filter. I can do this by
overriding the protected FilterProperties method on my FilterDesigner class:
internal sealed class FilterDesigner : ComponentDesigner {
protected override void PreFilterProperties(IDictionary properties) {
PropertyDescriptor newProperty;
base.PreFilterProperties(properties);
newProperty = TypeDescriptor.CreateProperty(typeof(FilterDesigner),
“LocalizableProperties”, typeof(StringCollection), null));
properties.Add(“LocalizableProperties”, newProperty);
}
}
I’m doing quite a lot in this method. Let’s examine each line:
base.PreFilterProperties(properties);
The first thing I’m doing is invoking the base class’s
version of this method. There are two
filter methods on ComponentDesigner:
PreFilterProperties and PostFilterProperties. PreFilter should be used to add new
properties. PostFilter should be used to
hide existing properties. By following
this convention multiple layers of derived designers can all filter properties
without bumping heads. The main rule for
PreFilter is that I must always call base as the first line in my PreFilter
method. The reverse holds true for the
PostFilter method: I should always call
base last. So, just to follow convention, I’m calling the base version of
PreFilterProperties as my first line of code.
newProperty = TypeDescriptor.CreateProperty(typeof(FilterDesigner),
“LocalizableProperties”, typeof(StringCollection), null));
This rather verbose line of code actually creates a new
property. While I defined the property
in code, TypeDescriptor needs to somehow know about that property. I could have called TypeDescriptor’s
GetProperties method and just passed in my designer, but that would have found
all the public properties on my designer.
I defined LocalizableProperties as private, however, so even this wouldn’t
find the property. Why private? I think it’s just good practice. I am creating this special property
specifically with the intent to add it to another object through this special
filtering mechanism. Keeping it private
prevents me from accidentally using it elsewhere, and helps me remind myself
what it is for. Also, if I made it
public and used TypeDescriptor.GetProperties to return the properties of the
designer, there is always the chance that a quick slip of the “public” keyword
could add additional properties to the property window that I didn’t
intend. Instead, I’m calling
TypeDescriptor’s CreateProperty method.
Properties that are explicitly created this way can stay private to my
object. To create a property I need to
pass a bunch of parameters that describe the property. The parameters to CreateProperty include the
following:
·
The data type the property is defined on:
FilterDesigner
·
The name of the property: LocalizableProperties
·
The data type of the property: StringCollection
·
Any additional attributes: null, because I don’t need anything special.
After calling this method, I have a fresh new
PropertyDescriptor object that describes my LocalizableProperties property. All I’ve got to do is add this to the filter,
which is the last line of code:
properties.Add(“LocalizableProperties”, newProperty);
The filter passed me a dictionary. The keys to this dictionary are property
names, and the values are property descriptors.
I’ve got my property descriptor, and I know I want to name my property
“LocalizableProperties”, so I just pass these two arguments to the dictionary’s
Add method.
That’s all there is to it.
Now when I place my LocalizationFilter component on a form, I see my new
LocalizableProperties property sitting in the property window. But, there’s one small problem: values I set for my LocalizationFilter
property are saved into source code, and because the property doesn’t actually
exist, I get a bunch of compile errors when I build the project. We’ll tackle this in the next section, Design
Time Attributes.
Design Time Attributes
When I added my LocalizableProperties collection I got an
unintended surprise: the collection was
saved in source code. But I don’t want
this; I want this collection to only be saved at design time, because the
collection doesn’t actually exist. I can
do this by placing an attribute on my property called DesignOnlyAttribute:
[DesignOnly(true)]
private StringCollection LocalizableProperties { get; }
By placing this attribute on my property, the value of the
property will be saved into a design time resource file, rather than the user’s
source code. Great! But, doing this alone isn’t enough. Design time properties must be saved into a
resource file, and because of this they have additional restrictions: they must be “simple” read-write properties,
and their values must be serializable.
Otherwise, there is no way to store their value in a resource file. This means that by adding the DesignOnlyAttribute
here my property won’t be saved into code, but it won’t be saved anywhere else,
either.
There are several things I could do to make this property
work as a design only property. I could
change it to an array and make the property read-write. That would allow it to be saved into the
design time resource file, but it would also make it look like an array in the
property window, which isn’t want I want.
I want to make it look like a collection. I’ll solve this problem by introducing
another property on my designer. One
property, LocalizableProperties, I’ll keep as a collection and display it to
the user in the property window. A new
property, LocalizablePropertiesArray, I’ll hide from the user. It’s this second property that I’ll write out
into the design time resource file. Both
properties access the same collection in different ways; one as a collection,
another as an array. I’ve shown the
property descriptions and attributes below:
[DesignOnly(true)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
private StringCollection LocalizableProperties { get; }
[DesignOnly(true)]
[Browsable(false)]
private string[] LocalizablePropertiesArray { get; set; }
I’ve added a couple of new attributes here, which I’ll
summarize below:
BrowsableAttribute
is used to show or hide a property in the property window. If not provided the default value is
true. Here, I am hiding my
LocalizablePropertiesArray property from the user’s view.
DesignerSerializationVisibilityAttribute
is used to control how the designer serializes properties. The default value is “visible”, which means
that the designer should try to serialize the property. Here I’ve set the value to “hidden” which
means that the property should never be serialized by the designer.
With these changes my collection of localizable properties
will be correctly written to the design time resource file. The last thing I want to do is provide a
custom editor so the property window will have a nice user interface. I can do this through the use of an
EditorAttribute. Since there are already
quite a few articles covering editors, I won’t go into detail about my simple string
editor here. If you’re curious just take
a peek at the project code.
Well, that just about wraps it up. Except, of course, for my third step above:
3. Something, somewhere, must read
this collection and change the way the form designer generates code.
That’s worthy of a whole section, however.
Making Changes to Code Generation
I described earlier that the Visual Studio .NET code
generator uses the Localizable attribute on a property to decide what type of
statement to create to assign the property’s value. For example, given the two properties on an
object:
[Localizable(true)]
public string AProp { get; set; }
public string BProp { get; set; }
The following code would be generator for an object named “MyComponent”:
ResourceManager rm = new ResourceManager(typeof(Form1));
MyComponent.AProp = rm.GetString(“MyComponent_AProp”);
MyComponent.BProp = “Hello”;
What I want to do is dynamically change the value of the
Localizable attribute on various properties based on the strings contained in
my LocalizableProperties collection. I
can’t do this by overriding the Pre or Post filter methods of a designer
because I need to modify the attributes on components I don’t own. I need some place where I can modify the
metadata for all components on the design surface. If you remember back to the section on
property filtering, you’ll remember that there is a service called
ITypeDescriptorFilterService which is responsible for filtering metadata for
components. This service is provided by
Visual Studio .NET and isn’t implemented on a public class. However, I can use a technique called “service
chaining” to replace Visual Studio .NET’s version of this service with my own. I have to be very careful when doing this;
one wrong slip or bug could mean curtains for the form designer. The steps for creating a service chain our
outlined below:
- Call GetService for IServiceContainer.
- Call GetService for the type of service you want to
replace.
- Create a new object that implements the service
interface, passing the original service into the new object.
- Call RemoveService on the service container to remove
the existing service.
- Call AddService on the service container.
- Remember
to put the original service back when you’re disposed.
Once I create an object that implements
ITypeDescriptorFilterService and add it into the service container, my object
will be the object that TypeDescriptor queries to filter metadata. Inside my implementation of this filter
service I will modify the LocalizableAttribute on each property.
Implementing ITypeDescriptorFilterService
I don’t really need to create another object to implement
this; I can just implement the interface on my designer. The interface has three methods:
FilterAttributes, FilterEvents and FilterProperties. I want to filter properties, so I have
implemented both FilterAttributes and FilterEvents to return whatever the
original filter returned.
FilterProperties, on the other hand, requires more
attention. I want to modify any
properties that fall into the following category:
- Are currently marked with the
Localizable(true) attribute.
- Are not in my collection’s list of localizable
properties.
For each one of these properties, I will replace it with a
new property. The new property is
identical to the original except that I want to replace Localizable(true) with
Localizable(false). That will tell the
code generator to generate normal code for the property rather than save it in
a resource. Here is my complete
implementation of FilterProperties that will do just that:
bool FilterProperties(IComponent component, IDictionary dict) {
bool retVal = existingFilter.FilterProperties(component, properties);
// Update any properties based on our localized names
//
if (localizableProperties != null && localizableProperties.Count > 0)
{
Attribute[] nonLoc = new Attribute[] {LocalizableAttribute.No};
string[] propNames = new string[properties.Keys.Count];
properties.Keys.CopyTo(propNames, 0);
foreach(string propName in propNames)
{
bool localize = false;
foreach(string name in localizableProperties)
{
if (name.Equals(propName))
{
localize = true;
break;
}
}
if (!localize)
{
PropertyDescriptor prop;
prop = (PropertyDescriptor)properties[propName];
if (prop != null)
{
if (prop.Attributes.Contains(LocalizableAttribute.Yes))
{
prop = TypeDescriptor.CreateProperty(
prop.ComponentType,
prop,
nonLoc);
properties[propName] = prop;
}
}
}
}
return retVal;
}
This code uses the same CreateProperty method on
TypeDescriptor I had used earlier. Here,
I’m passing back the original component type for the property, however, and I’m
also passing the original property back too.
This causes TypeDescriptor to create a new property descriptor that is
entirely based on the existing one, complete with the same set of
attributes. The last parameter I pass,
the one named “nonLoc”, is an array of new attributes. My “new attributes” is a single attribute :
LocalizableAttribute.No.
Trying it Out
Now that I’ve shown you all the code necessary to build my
“LocalizationFilter” component, let’s actually try it out. Load up the LocalizationFilter solution into
Visual Studio .NET and build it. Inside
this solution I have two projects:
LocalizationFilter, which contains the actual filter, and LocalizationFilterTestApplication,
which is a Windows Forms project that utilizes the filter. Inside this test application project are
three forms: NormalForm, FilterForm and
StartupForm.
NormalForm and FilterForm are identical except that
FilterForm uses the LocalizationFilter component. Both have a tab control with ten tabs. Each tab of the tab control has eight
controls. This gives a total of eighty
three controls for each form (the tab contents, the tab control, and OK and
Cancel buttons I threw on to make the form look complete).
StartupForm is a form that has two buttons: one button launches NormalForm and another
launches FilterForm.
What are the results?
While I won’t get into timings here, bringing up the FilterForm on my
P3-600 laptop feels instantaneous.
NormalForm, on the other hand, while not too sluggish, is visibly slower
to start than FilterForm. A quick scan
of the designer code gen tells me why:
while FilterForm’s class file has about 1350 lines of code in it,
NormalForm’s class file almost 2650.
It’s twice as big! This directly
translates into slower runtime performance, larger executable size, and
decreased customer happiness. I think
I’ll use LocalizationFilter for all my forms.
Wrapping Up
The LocalizationFilter demonstrates several key technologies
in the .NET Framework: designers,
services and code serialization. These
extensibility points provide developers a lot of power when creating
controls. But, as I’ve shown here, they
are also available to IT shops to create tools that help tailor the design
environment to the needs of the shop.