Click here to Skip to main content
Click here to Skip to main content

Human-readable Enumeration Meta-data

, 9 Jun 2014
Rate this:
Please Sign up or sign in to vote.
Display names and descriptions for enumeration members: a non-intrusive, reliable, localizeable method.

Human-readable enumeration meta-data Demo

Table of Contents

Introduction

This article will continue the small series of articles on enumeration types:

  1. “Enumeration Types do not Enumerate! Working around .NET and Language Limitations, Generic classes for enumeration-based iteration and array indexing;
  2. the present article;
  3. “Enumeration-based Command Line Utility”.

I will use the same code base, upgraded with new features. I will also refer to this work where it is needed for proper understanding of the matter.

I noticed that providing human-readable names for enumeration members is in high demand. I saw many attempts to solve this problem but did not find any of them satisfactory, including my own work I've done several years ago. I will review some of those works found on CodeProject. However, later on, I realized that my initial idea was quite correct but it needs refined implementation.

I've done this refinement recently; and now I think this approach is nearly the best one could use with Microsoft .NET (I did not say my code is the best, by the way). This article describes my solution for this problem. I will be much grateful for criticism and any ideas.

Basic Usage

I'll start with the usage by examples.

Let's assume we're using the following enumeration types:

[DisplayName(typeof(EnumerationDeclaration.DisplayNames))]
[Description(typeof(EnumerationDeclaration.Descriptions))]
enum StringOption {
    InputDirectory,
    InputFileMask,
    OutputDirectory,
    ForceOutputFormat,
    ConfigurationFile,
    LogFile,
} //enum StringOption

[DisplayName(typeof(EnumerationDeclaration.DisplayNames))]
[Description(typeof(EnumerationDeclaration.Descriptions))]
[System.Flags]
enum BitsetOptions {
    Default = 0,
    Recursive = 1 << 0,
    CreateOutputDirectory = 1 << 1,
    Quite = 1 << 2,
} //BitsetOptions

The second type is used as a bit set, and attributed with System.Flags. The attribute System.FlagsAttribute does not modify any behavior, but is important when string representation is obtained (see below).

Two more attributes, DisplayNameAttribute and DescriptionAttribute, are defined in my project Enumerations; so we need some using clauses to be able to compile those type declarations:

using DisplayNameAttribute = SA.Universal.Enumerations.DisplayNameAttribute;
using DescriptionAttribute = SA.Universal.Enumerations.DescriptionAttribute;

These attributes allow providing human-readable meta-data for some or all of the enumeration members: display names and descriptions, respectively.

Two types are used as parameters of the attributes: DisplayNames and Descriptions.

Here is a way to create such a type: let's create a resx resource using Visual Studio: on Project node, use context menu -> Add -> New Item… -> Visual C# Items -> General -> Resource File. If the resource is named DisplayNames, this step will create an XML resource file and an auto-generated C# file DisplayNames.Designer.cs, with an auto-generated class DisplayNames. Let's take the full name of this class and use it as a parameter of the DisplayNames attribute for both enumeration types.

When this is done, the code will compile and run without exceptions, but with no effect on the enumeration member names. With the same effect, any type can be used.

The next step is to provide alternate names for enumeration members. To do this, we need to add several string resources to the resource file. Now, resource strings can contain any character needed to present enumeration members as human-readable, including any delimiters and blank space.

The key name for each string should be exactly the same as the name of an enumeration member (without type name), such as InputDirectory, InputFileMask, Default, CreateOutputDirectory, etc. More than one enumeration type can be described within the same resource file. This can be a problem when two different types contain identical member names. Chances are, they require identical display names or descriptions, then they can still be described using a single resource file (but don't forget possible localization: in this case, this statement should be valid for every single foreign culture you may want to localize your project for; and this is not always obvious). If the display names or descriptions of identically named enumeration members of different enumeration types are not always identical, the option is to use two or more separate resource files for single enumeration declarations or groups of such declarations.

This work should be done separately for display names and for descriptions, so we can have two separate resource files and resource classes; in our example: DisplayNames and Descriptions, as shown in the attributes of our enumeration declarations.

It is not so important to present every single enumeration member in those resources. If some resource string is missing, the following fallback mechanism is applied: the default member name is generated for DisplayName, and a null string for Description.

Finally, we need some methods to obtain human-readable meta-data for each enumeration member during run-time.

This can be achieved in one of two different levels. First to use would be enumeration-based iterations, described in my previous article. The generic class EnumerationItem is augmented with two new properties: DisplayName and Description.

public sealed class EnumerationItem<ENUM> { 
    internal EnumerationItem(
        string name, string displayName, string description,
        Cardinal index, object value, ENUM enumValue) {/*&hellip;*/}
    public string Name { get { /*&hellip;*/ } }
    public string DisplayName { get {/*&hellip;*/} }
    public string Description { get {/*&hellip;*/} }
    public Cardinal Index { get {/*&hellip;*/} }
    public ENUM EnumValue { get {/*&hellip;*/} }
    public object Value { get {/*&hellip;*/} }
    //implementation&hellip;
} class EnumerationItem

Other members of this class are explained in the section “What to Enumerate?” of my previous article; see also the source code. This is an example of how to generate some human-readable documentation on an enumeration type:

Enumeration<BitsetOptions> bitsetOptions = new Enumeration<bitsetoptions>();
foreach (EnumerationItem<BitsetOptions> item in bitsetOptions) {
    WriteLine(" {0:}", item.Name);
    WriteLine("\tDisplay Name: \"{0}\"", item.DisplayName);
    WriteLine("\tDescription: \"{0}\"", item.Description);
    WriteLine();
} //loop</bitsetoptions>

Note that the iterations are done in natural order (in which the enumeration is declared in the source code) regardless of the members' underlying integer values (see my previous article). This allows for correct iteration through bit sets and anything else.

We also need some methods to obtain display names or description for a single enumeration member; those methods should be agnostic to the concrete enumeration type. This can be done using:

static class SA.Universal.Enumerations.StringAttributeUtility:
String displayName = StringAttributeUtility.GetDisplayName(
    BitsetOptions.Recursive | BitsetOptions.CreateOutputDirectory);
String description = StringAttributeUtility.GetDescription(
    StringOption. OutputDirectory);

Pay attention to the first example. A display name is calculated correctly for a bitwise combination of different enumeration members. In this example, displayName will be assigned: “Recursive, CreateOutputDirectory”, but these names will be replaced with human-readable names found in the resources (if any).

Ad-hoc Usage

Human-readable meta-data can be specified and used without using resources. Consider the following alternative declarations:

enum StringOption {
    [DisplayName("Input Directory")]
    InputDirectory,
    [Description("Input File Mask")]
    InputFileMask,
    OutputDirectory,
    ForceOutputFormat,
    ConfigurationFile,
    [DisplayName ("Log File")]
    LogFile,
} //enum StringOption

[System.Flags]
[DisplayName(typeof(EnumerationDeclaration.DisplayNames))]
[Description(typeof(EnumerationDeclaration.Descriptions))]
enum BitsetOptions {
    Default = 0,
    [Description("Recurce through sub-directories")]
    Recursive = 1 << 0,
    [DisplayName ("Create Output Directory")]
    CreateOutputDirectory = 1 << 1,
    Quite = 1 << 2,
} //BitsetOptions

For the type StringOption, no type-level attributes are used; instead, DisplayName and Description attributes are applied to selected enumeration members. For the type BitsetOptions, some combination of DisplayName and Description attributes is applied to both the type level and some of the individual enumeration members. In this case, an attribute at the level of the individual enumeration member takes precedence: only if the human-readable meta-data is not resolved at the level of the individual member, the lookup for the declaring type is performed (see how it works below).

Of course, the approach based on string parameters of the DisplayName and Description attributes is kind of dirty, and not good for code supportability: string values have to be hard-coded in the enumeration declarations (however, using constants declared separately is allowed); and of course, no localization can be applied.

Nevertheless, this feature can be useful for rapid prototyping, internal-use/personal-use utilities, test projects, and other ad-hoc works.

Beyond Resource Types

The above usage is based on the assumption that a type parameter of the constructors of the classes DisplayNameAttribute and DisplayNameAttribute represent some class auto-generated when an XML resource is created using Visual Studio. Can it be some different type?

Well, quick answer is: yes, but then such a class will be ignored (default enumeration member name for DisplayName, null string for Description). However, look at the example of such an auto-generated type. This is an internal class implementing no interfaces; its base class is the class Object. How can it be recognized as a class representing resources for any enumeration type?

The answer is simple: this is done through Reflection; and the implementation merely looks for a static member with the name identical to the enumeration name in question; also, it must be a non-indexed property of type System.String; and the value of the property should be non-null, representing a non-empty string. If such conditions are not resolved, the default behavior is assumed. (Please look at the source code for further details.)

This all means that the .NET XML resource is not the only kind of resource which can be used for the creation of human-readable enumeration meta-data. Any class can be used if it can resolve the enumeration names into strings the way described above. If, by some reason, some non-standard resource should be used (external database, remote Internet resources, plain text, to name just a few possibilities), or if some future version of .NET Framework will introduce some new kind of resource — in all cases, all those resources could be adopted to carry human-readable meta-data for enumeration types.

For now, using .NET XML resources is absolutely the most recommended option, because they are designed to provide localization — see next section.

Localization

The localization mechanism itself is beyond the scope of this article. I will refer to the Microsoft documentation for directions to localization.

According to .NET terminology, all the steps described so far are done to ensure globalization of the solution (if the ad-hoc technique is avoided, of course). Basically, it means that when a need for localization to a certain culture comes, this can be achieved without any modification of the existing code.

When it comes to localization of XML resources used to specify human-readable display names and descriptions, the localized resources are added in the form of additional assemblies called Satellite Assemblies. Microsoft documentation provides comprehensive and clear explanation of the mechanism of resource management using Satellite Assemblies as well as the steps for their creation and support.

How it Works

The attributes DisplayNameAttribute and DescriptionAttribute do nothing but remember their parameters; they share the same base class: StringAttribute:

using System;

public abstract class StringAttribute : Attribute {
    public StringAttribute(string value) { FValue = value; }
    public StringAttribute(Type type) { FType = type; }
    internal string Value { get { return FValue; } }
    internal Type Type { get { return FType; } }
    #region implementation
    string FValue;
    Type FType;
    #endregion implementation
} //class StringAttribute

[AttributeUsage(
    AttributeTargets.Field | AttributeTargets.Enum,
    AllowMultiple = false, Inherited = false)]
public class DisplayNameAttribute : StringAttribute {
    public DisplayNameAttribute(string value) : base(value) { }
    public DisplayNameAttribute(Type type) : base(type) { }
} //class DisplayNameAttribute

[AttributeUsage(
    AttributeTargets.Field | AttributeTargets.Enum,
    AllowMultiple = false, Inherited = false)]
public class DescriptionAttribute : StringAttribute {
    public DescriptionAttribute(string value) : base(value) { }
    public DescriptionAttribute(Type type) : base(type) { }
} //class DisplayNameAttribute

All the work is done by the static utility class StringAttributeUtility:

using System;
using System.Reflection;
using StringBuilder = System.Text.StringBuilder;

public static class StringAttributeUtility {

    public static string GetDisplayName(Enum value) {
        Type type = value.GetType();
        if (IsFlags(type))
            return GetFlaggedDisplayName(type, value);
        else
            return GetSimpleDisplayName(value);
    } //GetDisplayName

    public static string GetDescription(Enum value) {
        return ResolveValue<DescriptionAttribute>(
            value.GetType().GetField(value.ToString()));
    } //GetDescription

    internal static string ResolveValue<ATTRIBUTE_TYPE>(FieldInfo field)
        where ATTRIBUTE_TYPE : StringAttribute {
        if (field == null)
            return null;
        string value = ResolveValue<ATTRIBUTE_TYPE>(
            field.GetCustomAttributes(typeof(ATTRIBUTE_TYPE), false),
                field.Name);
        if (!string.IsNullOrEmpty(value))
            return value;
        //field attribute not found, looking for it type's attributes:
        return ResolveValue<ATTRIBUTE_TYPE>(
            field.DeclaringType.GetCustomAttributes(
                typeof(ATTRIBUTE_TYPE), false), field.Name);
    } //ResolveValue

    #region implementation
    //&hellip;
    #region implementation

}  //class StringAttributeUtility

Here is the heart of the implementation:

public static class StringAttributeUtility {

//&hellip;

    static string ResolveValue(StringAttribute attribute, string memberName) {
        string value = attribute.Value;
        if (!string.IsNullOrEmpty(value))
            return value;
        //immediate (hardcoded string) value not found, try using resources:
        Type resourceType = attribute.Type;
        if (resourceType == null)
            return null;
        BindingFlags bindingFlags =
            BindingFlags.NonPublic |
            BindingFlags.Public |
            BindingFlags.Static |
            BindingFlags.GetProperty;
        PropertyInfo pi = resourceType.GetProperty(memberName, bindingFlags);
        if (pi == null)
            return null;
        object stringValue = pi.GetValue(null, new object[] { });
        if (stringValue == null)
            return null;
        return stringValue as string;
    } //ResolveValue

//&hellip;

From this implementation, one can easily see the fallback mechanism for human-readable meta-data resolution. First of all, this method is agnostic to the concrete attribute type: actually, it is always called with the DisplayNameAttribute or DescriptionAttribute attribute parameter. Every attribute is discriminated by its parameter type: it is either of string or Type type; if one of the attribute property's string Value is null, the property Type type is considered; in this way, the string parameter takes precedence.

The attempt to resolve to a human-readable value is done at the level of the individual enumeration member first. If it is not resolved, the type level is considered:

public static class StringAttributeUtility {

//&hellip;

    internal static string ResolveValue<ATTRIBUTE_TYPE>(FieldInfo field)
        where ATTRIBUTE_TYPE : StringAttribute {
        if (field == null)
            return null;
        string value = ResolveValue<ATTRIBUTE_TYPE>(
            field.GetCustomAttributes(typeof(ATTRIBUTE_TYPE), false),
            field.Name);
        if (!string.IsNullOrEmpty(value))
            return value;
        //field attribute not found, looking for it type's attributes:
        return ResolveValue<ATTRIBUTE_TYPE>(
            field.DeclaringType.GetCustomAttributes(
                typeof(ATTRIBUTE_TYPE), false),
            field.Name);
    } //ResolveValue
    static string ResolveValue<ATTRIBUTE_TYPE>(
        object[] attributes, string memberName)
        where ATTRIBUTE_TYPE : StringAttribute {
        if (attributes == null) return null;
        if (attributes.Length < 1) return null;
        ATTRIBUTE_TYPE attribute = (ATTRIBUTE_TYPE)attributes[0];
        return ResolveValue(attribute, memberName);
    } //ResolveValue

//&hellip;

}  //class StringAttributeUtility

The generic parameter of these ResolveValue methods is used to substitute it with either the DisplayNameAttribute or DisplayNameAttribute attribute class.

This essentially explains all the machinery. For further details, please refer to the source code.

Other Approaches

I found several attempts to obtain human-readable data describing enumerations based on manipulation with default names returned by the method object.ToString; for example, through parsing the names assuming Camel case naming conventions and inserting delimiters like blank spaces. I don't think such an approach has any value. One example is the work “Making an Enum Readable (The Lazy Way)” by Joe Sonderegger. This work has collected quite poor votes, I think, rightfully; but I reference this work as a typical one.

The approach of Alex Kolesnichenko described in his work “Humanizing the Enumerations” does make some sense, and is based on hard-code string parameters passed to his attribute of the class HumanReadableAttribute. These hard-coded strings are not interpreted as ready-to-use human readable names, but as the key names for the resource, so localization is still possible. The author quite correctly notes that this is because “It's not good when you have string constants in your code”, failing to see that his string parameters of the attribute are still hard-coded; and this is the only option. Even though the extra level of indirection does allow for localization, this is an invitation for support nightmare.

I know only one powerful and comprehensive solution which really works correctly: “Localizing .NET Enums” by Grant Frisken. I tested this code thoroughly enough to validate its correctness and usability. Unfortunately, I am not quite satisfied with the ease of use and supportability; so I want to discuss this in the next section.

I don't want to reference multiple works based on the following idea: as enumeration types do not provide human-readable string data good for UI use and localization (and also do not enumerate), let's avoid using them in favor of one or another class suggested by the author. I think any attempts like that do not deserve attention, because the authors fail to see at least one major benefit of enumerations: during compile time, enumeration members are referred to by their names recognized by the compiler, so such references are immediately falsifiable.

Actually, the directly opposite approach is very useful: using enumeration types where a more traditional approach would be using string or numeric constants. If I find time, I'll try to submit some other article(s) on this topic. For now, please take a look at my CodeProject Answers to a member's question: first answer does not use my published code, second one does.

Why not TypeConverter?

I'm taking a risk of sparking some minor flame war around the subject, but I cannot avoid discussing the approach put forward by Grant Frisken.

First and foremost, the power of this approach is using the predefined mechanism of getting the string representation of a value of any type. The static method analogous to my method StringAttributeUtility.GetDisplayName is implemented like this:

static public string ConvertToString(Enum value) {
    TypeConverter converter = TypeDescriptor.GetConverter(value.GetType());
    return converter.ConvertToString(value);
}

Note: this technique does not work with object.ToString, which always returns a string identical to the original enumeration member name.

This method also works perfectly with a bitwise combination of enumeration values when the attribute System.Flags is applied. On the other hand, there is no way to create and use an additional attribute such as Description.

One apparent benefit of this approach is compatibility with the UI Component Model. For example, to populate System.Windows.Forms.ComboBox, instead of calculating each individual string value using the ConvertToString method, ComboBox.DataSource is assigned to an array obtained via the call to the method Enum.GetValues. On the other hand, using such Component Model techniques is hard to validate during run-time and debug: after all, the type of DataSource is object, so any irrelevant object can pass compilation, without proper effect. I think, obtaining the desired string representation of the enumeration member is much more important, because it gives the user the freedom to design and implement any thinkable UI component.

enumListBox.DataSource = Enum.GetValues(typeof(TextStyle));

This method is based on the class derived from the base class System.ComponentModel.EnumConverter:

public class ResourceEnumConverter : System.ComponentModel.EnumConverter {/*&hellip;*/}

The thing is: the whole System.ComponentModel namespace suffers from the pure techniques of using type parameters in attribute constructors. The problem is that it is hard to figure out what are the requirements to the classes used for such parameters, and too easy to make an undetectable mistake. I will discuss the root cause of these problems in the next session, but the System.ComponentModel namespace is problematic in its peculiar way: there are cases where the requirements for the class to be used as a parameter are based on naming conventions, which is hardly acceptable. (Further detail goes far beyond the scope of this article; however, I would gladly share the details in the course of discussion, if anyone is interested.)

Now, let's evaluate the process of globalization of some enumeration type. First, we will need to create an XML resource to be applied to one or more enumeration types. The resource keys combine enumeration class names and enumeration member names, separated by an underscore character. Such a format is a hidden, indirectly specified naming convention which subtly compromises supportability. On the other hand, any number of enumeration types can be served by just one resource file without any risk of name clashes.

This resource cannot be applied directly to the enumeration declaration. Instead, a new class should be derived from the class ResourceEnumConverter. The sole purpose of this class is to create an entity associated with a particular instance of ResourceManager, which is returned by a static property ResourceManager of the auto-generated resource class. Finally, this derived class is supplied as a type parameter of the constructor of the class System.ComponentModel.TypeConverterAttribute. This extra level of indirection carries no real knowledge but can invite all kinds of poorly detectable mistakes, because nothing in the syntax can suggest that a correct recipe is used. The whole design delivers extra annoyance, and clearly appeals to be shaved with Occam Razor.

I want to emphasize that all these annoyances are no way the fault of the author of the method of the enumeration localization method. He just honestly and very accurately follows the System.ComponentModel design. The real problem is the Microsoft Component Model design itself. My idea is merely a way to avoid using this design where it is possible.

Effectively, all solutions to the problem are bound to some compromise, by the deeper reasons I will try to explain in the next section.

A Side Note: .NET and (Lack of) Meta-classes

Meta-classes are supported in many languages, but .NET offers only a most rudimentary feature similar to meta-classes. Simply speaking, there is only one meta-class in the whole .NET Framework, represented by the type Type.

This type covers all thinkable types available during run time, with no exclusion. As to the fully-fledged meta-class system, it would allow the user to define an unlimited number of such meta-classes, each representing some subset of all the classes.

class MyClass {/*&hellip;*/}
public abstract class StringAttribute : Attribute {
    //&hellip;
    //will no compile:
    public StringAttribute(ClassOf(MyClass) type) { FType = type; }
    //&hellip;
} //class StringAttribute

In this (imaginary) syntax, the symbol MyClass would represent some user-defined class; and ClassOf(MyClass) would represent a meta-class; this meta-class' instances would be a whole sub-set of classes selected by a criterion: they all share one common base class: MyClass. Such a construct would allow for compile-time check of the type supplied for the constructor of the attribute class at the place of the attribute application.

Such a feature looks like perfectly fitting the application to attributes, so I fail to explain why it was never inherited by .NET from Delphi, which was the major predecessor of .NET.

This may seem too sophisticated at first, but this is not just my fantasy: multiple cases of successful implementation of the meta-class concept are available. My experience, for example, is full of heavy use of meta-classes with Borland Object Pascal and Delphi, even though Borland did not use meta-class terminology. This concept has deep implications, such as virtual constructors or virtual static (class) members, which are allowed in meta-class based architecture and are of great value.

This topic goes far beyond the scope of the present work, and could be a subject of a separate article.

Building Code and Compatibility

The code is provided for Microsoft .NET Framework versions 2.0 to 4.0, and tested on Mono using Ubuntu Linux v. 8.04. It can be built using Visual Studio 2005, 2008, or 2010, or using batch build for any of the named Framework versions, which does not require Visual Studio. See the Build section of my previous article for further details.

Please refer to .NET and Mono compatibility sections of my previous article for more details.

Conclusion

As it was stated above, the comprehensive solution of the problem of human-readable meta-data for enumeration types and localization inevitably needs to deal with some kind of compromise, due to some limitations of the .NET architecture. At the same time, thorough design gives quite a practical solution, easy to use, extremely flexible, and reasonably supportable. One key to success is avoiding the highly questionable design of System.ComponentModel.

I hope this work will be helpful in understanding the mechanisms of resources and attributes, and can be well applied to many real-life projects. Again, readers' ideas and criticism are very welcome, and will be much appreciated.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Sergey Alexandrovich Kryukov
Architect
United States United States
No Biography provided

Comments and Discussions

 
Question+5 PinprofessionalRahul VB24-Apr-14 10:00 
AnswerRe: +5 PinmvpSergey Alexandrovich Kryukov24-Apr-14 10:07 
QuestionVery Nice Sir PinprofessionalRahul VB24-Apr-14 8:38 
AnswerRe: Very Nice Sir PinmvpSergey Alexandrovich Kryukov24-Apr-14 9:55 
GeneralRe: Very Nice Sir PinprofessionalRahul VB24-Apr-14 10:00 
GeneralRe: Very Nice Sir PinprofessionalRahul VB24-Apr-14 10:06 
GeneralRe: Very Nice Sir PinmvpSergey Alexandrovich Kryukov24-Apr-14 10:11 
GeneralMy vote of 5 Pinprofessionaldholakiya ankit9-Sep-13 23:50 
GeneralRe: My vote of 5 PinmvpSergey Alexandrovich Kryukov10-Sep-13 4:26 
GeneralMy vote of 1 Pinprofessionalsatish koladiya8-Sep-13 17:36 
GeneralRe: My vote of 1 PinmvpSergey Alexandrovich Kryukov8-Sep-13 18:36 
GeneralRe: My vote of 1 Pinprofessionaldholakiya ankit9-Sep-13 23:50 
GeneralRe: My vote of 1 PinmvpSergey Alexandrovich Kryukov14-Nov-13 14:24 
GeneralMy vote of 5 PinprofessionalMaimonides20-Aug-13 4:25 
GeneralRe: My vote of 5 PinmvpSergey Alexandrovich Kryukov20-Aug-13 4:27 
GeneralMy vote of 5 PinmemberProgramFOX7-Nov-12 3:12 
GeneralRe: My vote of 5 PinmvpSergey Alexandrovich Kryukov7-Nov-12 6:23 
GeneralMy vote of 5 PinmemberPrasad_Kulkarni12-Jul-12 19:45 
GeneralRe: My vote of 5 PinmvpSergey Alexandrovich Kryukov13-Jul-12 5:08 
GeneralMy vote of 5 Pinmembermanoj kumar choubey26-Mar-12 22:23 
GeneralRe: My vote of 5 PinmvpSAKryukov9-Apr-12 6:05 
GeneralMy vote of 5 PinmemberProEnggSoft13-Mar-12 8:11 
GeneralRe: My vote of 5 PinmvpSAKryukov25-Mar-12 11:22 
GeneralMy vote of 5 PinmvpS Mewara14-Jun-11 7:34 
GeneralRe: My vote of 5 PinmemberSAKryukov14-Jun-11 7:40 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.140721.1 | Last Updated 9 Jun 2014
Article Copyright 2010 by Sergey Alexandrovich Kryukov
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid