object was used to retrieve resources associated with the form. The ComponentResourceManager
extends the base ResourceManager by providing additional functionality for retrieving and applying
component properties. Here are the original four lines required to set the properties defined for
Button1:
Me.Button1.Location = New Point(71, 43)
Me.Button1.Size = New Size(185, 166)
Me.Button1.Text = "Button1"
Me.Button1.TabIndex = 0
Coding resource files .
817
Using the ComponentResourceManager, they can be condensed into just one line:
resources.ApplyResources(Me.Button1, "Button1")
In previous versions of Visual Studio, the code generated when localization was turned on was much
more verbose. For each property, a separate call was made to the ResourceManager to retrieve it by
name, as shown in this code snippet:
Me.Button1.Location = CType(resources.GetObject("Button1.Location"), Point)
Me.Button1.Size = CType(resources.GetObject("Button1.Size"), Size)
Me.Button1.TabIndex = CType(resources.GetObject("Button1.TabIndex"), Integer)
Me.Button1.Text = resources.GetString("Button1.Text")
It is still possible to write this code because the GetObject method is still available on the
ComponentResourceManager. The issue with writing this code is that each property that is going
to be localized needs to be known at compile time. Because of this, every property on every control
was added to the resource file. This added excess properties (even when they were no different from
the default values) to the resource file. It also added huge overhead during the loading up of a form,
because each property was set via a resource property.
The ApplyResources method in the ComponentResourceManager class works in reverse. When you
specify a control name, which must be unique on a form, all resources that start with that prefix are
extracted. The full resource name is then used to determine the property to set on the control. For
example, a resource with the name Button1.Location would be extracted for the control called
Button1, and the value used to set the Location property on that control.
This process eliminates the need to have all properties specified in a resource file. It also creates the
need for culture resource files to specify additional properties that might not have been defined in
the default resource file.
You might be wondering whether any additional penalties exist in using the
ComponentResourceManager. To set a property on a control using the name of the property,
the ComponentResourceManager uses reflection to find the appropriate property. Once it has
been retrieved, it can be invoked. Each search that is done in order to set the property is relatively
expensive. However, given the reduced number of properties to be set, the tradeoff is definitely
worthwhile, because the application can easily be localized without recompilation of the main
application.
codinG resource files
In addition to the rich visual tools that Visual Studio 2010 now provides for editing resource files,
it is possible to use code to create resource files. The .NET Framework provides support for reading
and writing resource files using two interfaces: IResourceReader and IResourceWriter. Once the
resource files have been created, they need to be added to the application or manually linked so that
they can be referenced within the application.
818
.
chaPter 38 reSource FileS
.
IResource Reader: The reader interface ensures that resource readers have the following
methods:
.
GetEnumerator: The GetEnumerator method retrieves an IDictionaryEnumerator
object that permits the developer to iterate over each of the resources in the resource file.
.
Close: The Close method is used to close the resource reader and release any
associated resources.
.
IResource Writer: The writer interface ensures that resource writers have the following
methods:
.
AddResource: Three overloads to the AddResource method support adding
resources to the resource file. Both of the framework implementations of this
interface have either an additional overload of this method or an alternative
method for adding resources. The overloads that are part of this interface support
adding resources in a name-value pair. Each method has the resource name as the
first parameter and a value, such as a string, byte array, or object, as the second
parameter. The final implementation that takes an object as a parameter may need
to be serializable or converted to a string via a type converter.
.
Close: The Close method writes resources out to the stream before closing it.
.
Generate: Unlike the Close method, the Generate method simply writes the
resources out to the stream without closing it. Once this method is called, any other
method will cause an exception to be raised.
resourcereader and resourcewriter
ResourceReader and ResourceWriter are an implementation of the IResource interfaces to support
reading and writing directly to resources files. Although reading and writing to this format is the
most direct approach, because it reduces the need to use Resgen to generate the resources file, it
does limit the quality of information that can be retrieved in reading from the file. Each resource is
treated as a series of bytes where the type is unknown.
resxresourcereader and resxresourcewriter
ResxResourceReader and ResxResourceWriter are more versatile implementations of the IResource
interfaces. In addition to supporting the IResource interface, ResxResourceWriter supports an
additional overload of the AddResource method, whereby a ResxDataNode can be added. A
ResxDataNode is very similar to a dictionary entry, because it has a key (in this case, the Name
property) and a value (which you must set when the node is created). However, the difference is that
this node can support additional properties such as a comment and, as an alternative to a value, a
file reference (for example, one that indicates where an image needs to be added to a resource file).
As mentioned previously, it is possible to add a file reference to a resx file so that the file is still
editable, yet has the benefit of being compiled into the resource file by resgen.exe. The supporting
class in the framework is ResxFileRef. This can be instantiated and added as a resource via the
ResxResourceWriter. This inserts an XML node similar to the following snippet:
Custom resources .
819
< data name="Figure_11_2" type="ResXFileRef, System.Windows.Forms" >
< value > .\Resources\CompanyLogo.tif;System.Drawing.Bitmap, System.Drawing,
Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a < /value >
>data/<
Resource files are the best means of storing static application data. Although
they are linked in to the application as part of the compilation process, their
contents can easily be extracted and made human-readable. Because of this,
however, resource files are not suitable for storing secure data such as passwords
and credit card information.
custoM resources
Although Visual Studio provides good support for international application development using
resource files, at times it is not possible to get the level of control required using the default behavior.
This section delves a little deeper into how you can serialize custom objects to the resource file and
how you can generate designer files, which give you strongly typed accessor methods for resource
files you have created.
Visual Studio 2010 enables you to store strings, images, icons, audio files, and other files within a
resource file. You can do all this using the rich user interface provided. To store a more complex
data type within a resource file you need to serialize it into a string representation that can be
included within the resource file.
The first step in adding any data type to a resource file is to make that data type serializable.
You can do this easily by marking the class with the Serializable attribute. Once it is
marked as serializable, you can add the object to a resource file using an implementation of the
IResourceWriter interface — for example, ResXResourceWriter:
Vb
< Serializable() > _
Public Class Person
Public Property Name As String
Public Property Height As Integer
Public Property Weight As Double
End Class
Dim p As New Person
p.Name = "Bob"
p.Height = 167
p.Weight = 69.5
Dim rWriter As New ResXResourceWriter("foo.resx")
rWriter.AddResource("DefaultPerson", p)
rWriter.Close()
820
.
chaPter 38 reSource FileS
c#
[Serializable()]
public class Person{
public string Name { get; set; }
public int Height { get; set; }
public double Weight { get; set; }
}
var p = new Person(){
Name = "Bob",
Height = 167,
Weight = 69.5};
var rWriter = new ResXResourceWriter("foo.resx");
rWriter.AddResource("DefaultPerson", p);
rWriter.Close();
However, serializing an object this way has a couple of drawbacks:
.
You need to use code to write out this resource file before the build process so that the
resource file can be included in the application. Clearly this is an administrative nightmare,
because it is an additional stage in the build process.
.
Furthermore, the serialized representation of the class is a binary blob and is not human-
readable. The assumption here is that what is written in the generating code is correct.
Unfortunately, this is seldom the case, and it would be easier if the content could be human-
readable within Visual Studio 2010.
A workaround for both of these issues is to define a TypeConverter for the class and use that
to represent the class as a string. This way, the resource can be edited within the Visual Studio
resource editor. TypeConverters provide a mechanism through which the framework can determine
whether it is possible to represent a class (in this case a Person class) as a different type (in this case
as a string). The first step is to create a TypeConverter using the ExpandableObjectConverter, as
follows:
Vb
Imports System.ComponentModel
Imports System.ComponentModel.Design.Serialization
Imports System.Globalization
Public Class PersonConverter
Inherits ExpandableObjectConverter
Public Overrides Function CanConvertFrom(ByVal context As _
ITypeDescriptorContext, _
ByVal t As Type) As Boolean
If t Is GetType(String) Then Return True
Return MyBase.CanConvertFrom(context, t)
End Function
Public Overrides Function ConvertFrom( _
ByVal context As ITypeDescriptorContext, _
ByVal info As CultureInfo, _
ByVal value As Object) As Object
If (TypeOf (value) Is String) Then
Custom resources .
821
Try
If value Is Nothing Then Return New Person()
Dim vals = CStr(value).Split(","c)
If vals.Length <> 3 Then Return New Person()
Return New Person With {.Name = vals(0), _
.Height = Integer.Parse(vals(1)), _
.Weight = Double.Parse(vals(2))}
Catch
Throw New ArgumentException("Can not convert '" & _
value.ToString & _
"' to type Person")
End Try
End If
Return MyBase.ConvertFrom(context, info, value)
End Function
Public Overrides Function ConvertTo(ByVal context As ITypeDescriptorContext, _
ByVal culture As CultureInfo, _
ByVal value As Object, _
ByVal destType As Type) As Object
If (destType Is GetType(String) And TypeOf (value) Is Person) Then
Dim c = TryCast(value, Person)
Return c.Name & "," & c.Height.ToString & "," & c.Weight.ToString
End If
Return MyBase.ConvertTo(context, culture, value, destType)
End Function
End Class
c#
public class PersonConverter : ExpandableObjectConverter{
public override bool CanConvertFrom(ITypeDescriptorContext context,
Type t){
if (typeof(string) == t) return true;
return base.CanConvertFrom(context, t);
}
public override object ConvertFrom(ITypeDescriptorContext context,
CultureInfo culture, object value){
if (value is string){
try{
if (value == null) return new Person();
var vals = (value as string).Split(’,’);
if (vals.Length != 3) return new Person();
return new Person{
Name = vals[0],
Height = int.Parse(vals[1]),
Weight = double.Parse(vals[2])
};
}
catch (Exception){
throw new ArgumentException("Can not convert ’" +
value.ToString() + "’ to type Person");
}
}
822 .
chaPter 38 reSource FileS
return null;
}
public override object ConvertTo(ITypeDescriptorContext context,
CultureInfo culture, object value, Type destType){
if (typeof(string) == destType & & value is Person){
var c = value as Person;
return c.Name + "," + c.Height.ToString() + "," + c.Weight.ToString();
}
return base.ConvertTo(context, culture, value, destType);
}
}
The class being represented also needs to be attributed with the TypeConverter attribute:
Vb
< System.ComponentModel.TypeConverter(GetType(PersonConverter)) > _
< Serializable() > _
Public Class Person
Public Property Name As String
Public Property Height As Integer
Public Property Weight As Double
End Class
c#
[System.ComponentModel.TypeConverter(typeof(PersonConverter))]
[Serializable()]
public class Person{
public string Name { get; set; }
public int Height { get; set; }
public double Weight { get; set; }
}
Now you can add this item to a resource file using the string representation of the class. For
example, an entry in the resx file might look like this:
< assembly alias="CustomResourceType" name="CustomResourceType, Version=1.0.0.0,