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

Yet Another Custom Formatter

By , 20 Jul 2011
Rate this:
Please Sign up or sign in to vote.

Introduction

The ObjectFormatter class described in this article allows you to build up neatly formatted strings (or lists of formatted strings) with the minimum of fuss. It eliminates the need to manually build up strings using string concatenations and if/thens.

The formatted strings are built from a template string which describes which properties need to be extracted from a target object, how they are formatted, how they are combined, and so on.

The predecessor to this code, PropertyFormatter, I have had in my toolbox for many years and it has proved very useful. Recently, I have added some new functionality, reorganized, documented, renamed, and optimized the code ready for publishing here on CodeProject.

Why "Yet another..." and Some Credits

"Yet another..." because this is not the only code to provide custom formatting and string templates - far from it - but I think there are one or two features in this version that are not found in others.

I would particularly like to mention and give credit to two articles from CodeProject:

  • The first, Ader Template Engine, showed me how to write a hand-built lexer/parser system (I still have nightmares about making modifications to it though!).
  • The second is Custom Formatting in .NET by Scott Rippey from which I have shamelessly pinched the Conditional Formatting idea and the syntax for Complex Conditions. His original version was written in VB (otherwise I might have pinched some code too!) but there is now a C# version available too.

Both are definitely worth a look.

Background

Quite a few years ago, I was evaluating some library software titles and one of the things I noticed was that several of them required the user to enter the book title twice - once for the real title and once for how it would be displayed in a sorted catalogue. E.g.: Title="The Importance of Being Earnest" and DisplayText="Importance of Being Earnest, The". This duplication seemed somewhat wasteful to me - surely a title should only be stored once - and I idly wondered whether storing certain prefixes ('The', 'An', 'A', etc.) separately from the rest of the title would be more efficient and flexible. It certainly seemed easier to compose multiple items into one than to try to parse out and rearrange portions of the title on the fly. Similarly, authors also needed to appear in various formats for different reports - "Isaac Asimov", "Asimov, Isaac", "ASIMOV, Isaac (1920-1992)". I also noticed plenty of examples of lazy formatting. Additional spaces at the start or end of a name which looked strangely indented on the reports; surname or forename missing but the separating comma was still present; missing dates but still showing the now empty braces. The old "1 items found" chestnut on searching.

So the first version of this class was written to ensure that all of these issues could be solved. It ignored null and empty fields, trimmed leading and trailing spaces from formatted strings, provided a way to show specific singular or plural text depending on a numeric value and, importantly, had an option to specify optional separators as part of the expression - they would only be used if the expression evaluated to something displayable.

More features were added as and when required. An important feature was to specify Dependency Expressions - if the dependency was not satisfied, then the expression would be ignored. This allowed an especially useful feature when processing lists - a Dependency Expression that only applied to duplicate formatted strings within a list.

The Code

Although the code download contains quite a few classes, this is simply because they are a subset of my Framework class which is intended to be included in an application as a referenced DLL. A number of the classes are marked internal and there are a few helper classes too because I like to reuse code wherever possible.

There is a separate project included in the solution containing nearly 250 unit tests. Jetbrains' dotCover shows virtually 100% coverage (some statements are impossible to reach so I'll claim the 100%), and while this is no guarantee of correctness, it does give some degree of confidence. There are also some timing tests to get an idea of the throughput achievable.

Only the ObjectFormatter class is the subject of this article and that is quite simple to use at the public level. It has just one constructor and two public methods, everything else is an implementation detail.

public ObjectFormatter(string template, IFormatProvider formatProvider = null, 
  bool globalDependency = false, Dictionary<string, object> namedTargets = null, 
  params object[] additionalTargets)

public string Format(object target, int maximumLength = 0)
public string[] FormatList(IList targets, int maximumLength = 0)

As you can see, only one argument is mandatory for each constructor/method. The Format method takes your target object, applies the formatting according to the template, and returns the resulting string. It will never return null, at worst it will return an empty string.

The FormatList method is similar but applies the template to a list of target objects, returning a list of identical size containing the formatted strings for each. But why can't I call Format within a loop to produce the same effect, you may ask? Well, you could, of course, but FormatList has a special feature which allows you to produce lists of unique strings automatically. An in-depth example of this is shown later in the article.

Templates

All the functionality is based around a string template. This contains a mix of literal text and Member Expressions. The template is parsed into its elements and will throw an exception if malformed. The elements will always be the same for a given string template and so are cached internally. Literal text will always be included in the formatted string but since a '$' symbol is used to define a Member Expression, you need to write it twice to get a single dollar symbol.

Member Expressions

Member Expressions are used to include parts of the target object into the formatted string. It is so called because the usual properties, fields, and simple methods (those that take no arguments) can also be used, hence the term Member.

A Member Expression always starts and ends with a "$" symbol. Within the dollars, only a Member Name is mandatory.

Member Name

The Member Name part of a Member Expression is usually used to identify which property/field/method from the target to use. However, it can also identify a path from the target object ("$MyProp.MyField$") or the target object itself ("$.$"); or an alternative target to use ("$#1.MyProp$"); or a named target("$#Invoices.Count$"); or, for a list, certain well-known internal values ("$#ListPosition#$").

(The Alternative Targets and Named Targets were those items optionally passed in via the constructor arguments.)

For example, a simple template might be:

Format("Hello, I'm $FirstName$.", Simon) => "Hello, I'm Simon."

Everything outside the dollar symbols is literal text and is always included.

This example is fine for this target object...

FormatHelper.Format("$LastName$, $FirstName$", Simon) => "Hewitt, Simon"

... but can be a problem when a field evaluates to empty:

FormatHelper.Format("$LastName$, $FirstName$", Madonna) => ", Madonna"
FormatHelper.Format("$LastName$, $FirstName$", Gandhi) => "Gandhi, "

(Madonna is my test person without a FirstName and Gandhi is my test person without a LastName - Yes I know Gandhi's first name is Mahatma or Mohandas, but at the time, I couldn't think of anything else!)

This is exactly the problem we need to get around and to do that, we need to use some of the other sections. The other sections of a Member Expression are all optional but they must be included in a certain order, otherwise the parser will throw an exception. It is also important to note that the contents of a Member Expression are processed as a logical unit - either the whole expression produces some string output or it produces nothing.

From left to right, the optional 'sections' are:

Leading Separator Section

This is the text between the '[' and ']' symbols and must be positioned to the left of the Member Name. The text will be inserted before the expression result (if the expression result is non-empty) but it is also conditional on there already having been some output from a previous expression. It is designed to put things like commas between this expression and a previous expression but ignored if there was no previous expression output.

Suffix/Prefix Section

This is text contained between single quotes and must be positioned to the left of the Member Name for a Suffix and to the right of the Member Name for a Prefix. The text will be inserted before/after the expression result (if the expression result is non-empty). It is designed to add text such as brackets around the evaluated expression but nothing if there is no output to wrap.

Trailing/Pending Separator Section

This is similar to a Leading Separator in that it is text between the '[' and ']' symbols but it is placed after the Member Name. It is ignored if the expression result is empty, otherwise it is stored internally as pending but not used until a later expression produces some non-empty output. In that case, it will be inserted before the latter expression's output. Should the latter expression have a Leading Separator then the longest of the two is used.

To show what we have so far, let's rewrite the problem template from before:

Format("$LastName[, ]$$FirstName$", Simon) => "Hewitt, Simon"
Format("$LastName[, ]$$FirstName$", Madonna) => "Madonna"
Format("$LastName[, ]$$FirstName$", Gandhi) => "Gandhi"

Because we moved the comma and made it Pending Separator in the LastName Expression, it will now only be included when both Expressions have some output. Problem solved!

Conditional Formatting

This is the newest feature and again thanks to Scott Rippey for the ideas and samples. This section starts with a ';' coming after the Member Name (and Suffix and Pending Separator if present) and is then a list of one of more Conditional Formatting Items. Each Item is separated with either another ';' or a '|' symbol.

I'll describe Conditional Formatting in detail later but as an example:

Format("$LastName$$[, ]FirstName$$' 
      ('IsMarried')';Married$", Simon) => "Hewitt, Simon (Married)"
Format("$LastName$$[, ]FirstName$$' 
      ('IsMarried')';Married$", Madonna) => "Madonna (Married)"
Format("$LastName$$[, ]FirstName$$' ('IsMarried')';Married$", Gandhi) => "Gandhi"

Here we've added another Member Expression which has a Prefix and a Suffix and a single Simple Conditional Format of "Married". Since the value IsMarried is boolean, the Conditional Formatting Evaluation will use the first Format Item in the list for a True value and the second Format Item in the list for a False value. As we didn't provide a template for the False value, for Gandhi's case, it evaluates to Null which in turn causes the expression evaluation to be empty which in turn means that the brackets in the Prefix and Suffix are not used. We get a nicely formatted string for all cases.

Options Section

This section comes after any Conditional Formatting section and, if present, the last section before the closing dollar. Options are case-sensitive single letters (legacy reasons!) which can have a following '+' or '-' to switch them on or off. They can also contain expressions by following them with an '=' sign and then a single-quoted string. If you have several options, you can include whitespace between them to make them easier to read. There is a static class called ObjectFormatterOptions which lists all of the option constants and their usage.

Here is an example of a template with some options included (this is based on Scott's sample but I like it!):

FormatHelper.Format("$FirstName$ has $Friends.Count' friend': 
    P z='no'$$':\r\n'Friends:l=', ' L=' and 'f='$FirstName$'$.", Simon)
=> "Simon has 4 friends:\r\nMichael, Dwight, Jim and Pam."
=> "Simon has 3 friends:\r\nMichael, Dwight and Jim."
=> "Simon has 2 friends:\r\nMichael and Dwight."
=> "Simon has 1 friend:\r\nMichael."
=> "Simon has no friends."

You can see that the Friends.Count expression has two options: the PluralOption('P') pluralises the suffix ' friend' unless it numerically evaluates to 1. The ZeroOption('z') is used when an expression numerically evaluates to 0 and is evaluated before the PluralOption. The last expression produces a member list. This is based on the Friends property; ListSeparatorOption('l') specifies the separator to use between items; the ListLastSeparatorOption('L') option specifies the separator to use for the last item; FormatOption('f') specifies the format to use on each item in the list and, since it is an expression in its own right, will produce the FirstName property for each item. Had it been omitted, then ToString() would have been called on each Person in the list.

We've now seen how the template is laid out and some of the options available. Here are descriptions and uses for the other simpler options:

NullOption ('n')

Used when the value being evaluated produces a null. It can contain an expression.

Format("$thispropdoesnotexist:n='$LastName$ 
as replacement'$", Simon) => "Hewitt as replacement"
ListSeparatorOption ('l') Specifies the separator to use between items in a member list. "," is used if omitted.
ListLastSeparatorOption ('L') Specifies an alternate separator to use before the last item in a list. If omitted, it will be the same as the ListSeparatorOption.
FormatOption ('f') This option lets you specify a normal .NET formatting string for values. In the case of a member list, it can be a Member Expression in its own right and will be applied to each item in the list.
FullFormatOption ('F')

This option's expression can be used to produce the fully formatted string as an alternative to using the normal Member Name with formatting options. It allows the string value to be composed of multiple properties from the target value rather than creating multiple expressions showing and formatting one property each. It also allows the value specified by the Member Name to be used exclusively to decide which conditional format to be used and then the formatted output be included in the conditional format using the '@' placeholder.

Format("My birthday $.;was @ ago;will be " + 
   "in @:F='$Duration.Days' days'$'$.", 
   TimeSpan.FromDays(-40)) => 
	"My birthday was 40 days ago."
MaximumWidthOption ('w') Used to specify a maximum width for the formatted string. If omitted or zero, then the formatted string is not truncated.
ListItemWidthOption ('i')

Used to specify a maximum width for each item in a member list. If omitted or zero, then the formatted item is not truncated. Useful to create a list of initials.

Format("$SimpleListProperty:i='1'$", Simon) => "F,S,T"
SortSourceItemsOption ('S') If this option is present for a member list expression, the source list will be sorted before producing the formatted strings. Use 'S' or 'S+' to sort ascending or 'S-' to sort descending. If the items are not sortable, then this step is skipped.
SortTargetStringsOption ('s') If this option is present for a member list expression, the formatted strings will be sorted. Use 's' or 's+' to sort ascending or 's-' to sort descending.
CaseOption ('c') Changes the case of the formatted string. Use 'c' or 'c+' to change to upper case or 'c-' to change to lower case.
CapitaliseOption ('C') Capitalises the words in a formatted string. Use 'C' or 'C+' to capitalise each word and change all other letters to lower case. Use 'C-' to do the same but preserve any words that are all capitals.
SingularOption ('p') and PluralOption('P') These options can be used individually or together to specify text to use when a value evaluates to Singular (1) or Plural (not 1). When both are present, one or other will be used and any suffix will also be included. When just one is present, it is assumed to be the inverse to the suffix and will replace the suffix - i.e., specifying just a SingularOption will assume that any suffix is the text for plural values. So using "matches" as the suffix and "match" as the SingularOption will produce the correct results. There is one special case: when just 'P' is specified as the PluralOption but without any text, it will add an 's' to whatever the suffix text is. Useful for the normal case like "item" (suffix).
Zero Option ('z')

This option allows replacement text when the value evaluates to a zero.

Format("$ZeroValue:z='None'$", Simon) => "None"
Trim Option ('t') Unlike the other options, this option is active by default and will remove leading and trailing spaces from a formatted string. Use 't-' to switch it off.

The remaining five options are all DependencyOptions. DependencyOptions are always processed first and if they 'fail' the dependency, processing of that Member Expression stops immediately and string.Empty is returned.

DependencyOption ('d') This option takes a Member Expression as its parameter (typically a boolean property on the target object) and evaluates it. If the result is not an empty string and is either "True" or not "False", then the dependency is considered met and processing continues. You can also invert a dependency by using ":d-='<expr>'". In this case, the dependency is met if the evaluation produces an empty string or "False" or not "True".
BlankDependencyOption ('b')

If this option is present, then it will only pass the dependency if no output has been produced so far from previous expressions.

Format("$LastName$$.:bF='(No Last Name)'$", 
	Simon) => "Hewitt"
Format("$LastName$$.:bF='(No Last Name)'$", Simon) => 
                        "(No Last Name)"
Format("$LastName$$.:bF='(No Last Name)'$", 
	Simon) => "Gandhi"
GlobalDependencyOption ('D')

This Dependency Option simple looks at the external boolean passed in as an argument when creating the ObjectFormatter.

"$LastName$$[, ]FirstName$$' ('IsMarried')';
Married|Unmarried:D$"

The IsMarried expression in this example will only be evaluated if the globalDependency argument passed to the ctor happened to be true.

The remaining two DependencyOptions are list-specific. That is, they will always 'fail' whilst not processing a list and thus the Member Expression they are in will be ignored. When using FormatList however, the constructor will find and create a list of all the Member Expressions containing either of these options. If any are present, then the special list-uniquing feature will be switched on.

List-Uniquing Feature

After the list is processed, all of the strings are checked and any duplicates and their indexes are found. If duplicates are found, then the following happens:

  • An index is maintained into the list of UniqueDependency-containing expressions - the Currently Active index - and this starts at 0 being the first item on the list.
  • All of the duplicate strings are reformatted with the hope that they will produce different results now that a new Expression has come into play.
  • Duplicates are again searched for and, if any are found, the CurrentlyActive index is incremented.
  • This process continues until all the strings are unique or the end of the list is reached.
UniqueDependencyOption ('U') This Dependency is considered met if it is at the Currently Active index or anywhere before it. In other words, once on, it stays on even if the Currently Active Index moves to the next item.
WeakUniqueDependencyOption ('u')

This Dependency is only considered met if it is at the Currently Active Index. If the Member Expression containing this option cannot entirely remove the duplicates from its Scope, then they are reset to their original values. The Currently Active index moves on and this Expression is ignored once again. The idea is that this Expression is tried and used where possible, but if it cannot work, then the next in the list is tried and so on.

Using 'u-' defines Group Scope for this expression - that is, it is considered to have succeeded if it makes unique a single set of duplicate values. Say "AAA" appears twice and "BBB" appears three times. If this expression becomes active and these values then become "AAA_1" and "AAA_2", then that group is now considered unique and won't be reevaluated. But if the "BBB" group becomes "BBB_1", "BBB", and "BBB", then it isn't unique and all will be reverted back to "BBB" and the process continues.

Using 'u+' defines the List Scope for this expression - that is, it is considered to have succeeded only if all the groups of duplicates are made unique. In the previous example, all five duplicates would be reverted to their original values because one Group but not the whole List became unique.

It is not guaranteed that the final list returned will be unique unless the final UniqueDependency-containing expression contains one of the special Member Names. '#ListIndex#' returns the 0-based index of the item being processed. '#ListPosition#' returns the 1-based index of the item being processed. '#' returns a 1-based index of the item being processed but is context-based. If there are any UniqueDependency options currently active, then the context list will be just the current group of duplicates being processed; otherwise it returns the same as '#ListPosition#'.

This sounds complicated, so here is an example (the names with a 2 suffix have the same names as those without, but have a different date of birth):

FormatHelper.FormatList("$LastName$$[, ]FirstName$$' ('DateOfBirth')':u-f='yyyy'$$' 
         #'#:U$", new[] { Simon, Madonna, Gandhi, Simon2, Gandhi2, Simon2 }) =>
Hewitt, Simon #1
Madonna
Gandhi (1910)
Hewitt, Simon #2
Gandhi (1922)
Hewitt, Simon #3

The first pass ignores the last two expressions as they were not active and so produced Groups: 3 x "Hewitt, Simon"; 1 x "Madonna"; 2 x "Gandhi".

The DateOfBirth expression has a 'u-' option and so is a WeakUniqueDependency with Group Scope. It becomes the Current Active and is applied to the two groups containing duplicates. At this point, the Gandhi group is now unique and is complete. The other group however has "Hewitt, Simon (1969)" but 2 x "Hewitt, Simon (1975)". This isn't unique and so is rolled back. The final Expression now becomes Current Active and the DateOfBirth expression is no longer active because it is weak. A formatting pass is made over the last group and, since we are using the Context List Position, cannot fail and produces the unique output above.

Now have a look at the difference if we change "u-" to "u+". This is still a WeakUniqueDependency but now has List scope. When it was active, although it made one group unique, it couldn't make the whole list unique and so all group contents were reverted and the next Member Expression was tried producing this output.

Hewitt, Simon #1
Madonna
Gandhi #1
Hewitt, Simon #2
Gandhi #2
Hewitt, Simon #3

Finally, if we had changed the "u-" to a "U" thus making it a strong dependency, we would see the cumulative effect as the first UniqueDependency adds the Dates of Birth and then the second tie-breaks the final two duplicates.

Hewitt, Simon (1969)
Madonna
Gandhi (1910)
Hewitt, Simon (1975) #1
Gandhi (1922)
Hewitt, Simon (1975) #2

Conditional Formatting

As previously mentioned, this section allows you to specify one or more Conditional Formatting Items in a list.

There are two types of Conditional Formatting Items: Simple and Complex.

Simple Conditional Formatting Item

This can either be just literal text or a single-quoted string. It also supports a '@' placeholder to position output from the FullFormatOption.

Complex Conditional Formatting Item

This is a list of one or more numeric Comparison Expressions followed by a '?' delimiter and then a Simple Conditional Formatting Item. Complex Conditional Format Items are only used for numeric values.

Each Comparison Expression is made up of a Comparison Operator ('=', '<', '>', '!', '==', '<=', '>=', '!=') immediately followed by a decimal number.

Multiple Comparison Expressions can be included as a list and they are separated by either a '/' (logical OR) or a '&' (logical AND). Each Comparison Expression will be evaluated from left to right.

When the first item in a list is recognized as being Complex, then all the Formatting Items except the last must also be Complex. They cannot be mixed and matched. One Simple Item is allowed at the end and will be used if none of the preceding Complex Items match. (If no Simple Item is included, a virtual one consisting of just '@' is included.)

Selecting a Conditional Format

How the particular Conditional Formatting Item to use is chosen depends on the target object's type and value. If the type is numeric and the Conditional Formatting Items are Complex, then they are evaluated from left to right until a match is found. This is the Format Item to be used.

For Simple Conditional Formatting Items, the Format Item is selected depending on the object type/value as per this table:

Built-In Numeric Type ;<Negative>;<Zero>;<One>;<Default>
;<Zero>;<One>;<Default>
;<One>;<Default>
;<Default>
Boolean ;<True>[;<False>] The <False> format can be omitted and will return Empty.
DateTime ;<Past>;<Today>;<Future> If the DateTime contains a non-zero time, then <Today> cannot match.
;<Past or Today>;<Future>
;<Default>
TimeSpan ;<Negative>;<Zero>;<Positive>
;<Negative or Zero>;<Positive>

Once a Format Item has been selected, it will be processed if it is a Member Expression in its own right or the literal text is used.

If it contains the '@' placeholder symbol, then this will be replaced by the current value evaluation or FullFormatOption evaluation if present.

Here are a few sample templates using Conditional Formatting:

"My birthday $#0;was on @|is Today!|will be on @:f='MMMM d'$"
"There $.;is|are$ $.$ $.;item|items$ remaining..."
"$Age;=5?Kindergarten;<11?Elementary;>10&<=12?Middle 
School;>12&<=18?High School;None$"
"Enabled? $.;Yes|No$"

Summary of the Formatting Process

  1. Any Dependency Options are checked. If any fails, then string.Empty is immediately returned as the result.
  2. The value to format is obtained. This is most likely a property on the target object but could be the target itself, an alternate source object, or any combination. Any DBNull is converted to a null. A DataRow/DataRowView will use the member path to obtain a column value.
  3. The value is converted into a string using one of these methods (they are listed in priority order):
    • A null value will take the string specified by the NullOption.
    • An enumerable value will become a member list using the various member list options.
    • Conditional Formatting will be applied.
    • A zero value will use the ZeroOption.
    • The FormatOption or FullFormatOption will be used.
    • ToString() will be called.
  4. The value is trimmed. Unless the TrimOption is switched off, the string value will be trimmed. If Empty at this point, it will be returned immediately.
  5. Post-format processing is applied.
    • It will be cased according to any CaseOption or CapitaliseOption.
    • It will be truncated if MaximumWidthOption is specified.
  6. The final result is combined with Separator/Prefix/Suffix as appropriate and returned.

Other Items (Maybe) of Interest

  • All the access to the properties/fields etc., is done via the GenericGetter delegate. For the most part, this delegate is created using a DynamicMethod/IL generation and then compiled for speed. It is also cached within MemberPathCache for reuse.
  • For instance, where a Member Path is used and there is no corresponding field/property available (say a property returns an object but there are still more properties on the path), normal Reflection is used.
  • MemberPathCache uses a Dictionary to cache already created GenericGetter delegates. The key is a custom TypeAndPath class and is about as fast as I can make it. However, I wanted more and added a LRU cache around that. Yes, I have a cache on a cache!
  • The Conditional Formatting uses a struct called NumberState which can take an object (usually numeric) and classify its content as Zero/Positive/Negative/IsOne/IsNegativeOne etc. This refactoring may seem a bit over the top but I seem to remember having to do something similar for my Fast Serialization code so I created this class hoping to use it again.
  • The FormatHelper static class has the Format and FormatList static methods so you don't even need to create a local ObjectFormatter if you don't want to reuse it.
  • The StringHelper and DateTimeHelper static classes may contain a few snippets of code you might find useful.
  • The TimingTestFixture class in Framework.UnitTest has proved very helpful whilst optimizing the code. It is fully documented and you can see how I've used it in ObjectFormatterSpeedTests.cs.

Future Plans

There are no specific plans at the moment, just some vague ideas.

  • Move the additionalTargets argument from the constructor to the Format/FormatList methods. The dictionary-based named targets is probably OK being fixed, but it might be better to let the additional targets be changeable on each call?
  • Add a custom Format event. A lot of the stuff I do requires dates to be formatted. I have a super fast datetime-format class for fixed-format date/times but haven't worked out how to fit it in.
  • See if there is a need for an Empty replacement option (similar to the Null option) when a Member Expression evaluates to an empty string.
  • See if the Conditional Formatting could deal with Null/Empty items using additional Simple Conditional Format Items. And/or use NumberState's PseudoNumber to select templates.
  • Add support for brackets in Complex Conditional Format Items to be more specific than the left-to-right priority.

History

  • v1.0 - First release to CodeProject

License

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

About the Author

SimmoTech
Software Developer (Senior) Hunton Information Systems Ltd.
United Kingdom United Kingdom
Simon Hewitt is a freelance IT consultant and is MD of Hunton Information Systems Ltd.
 
He is currently looking for contract work in London.
 
He is happily married to Karen (originally from Florida, US), has a lovely daughter Bailey, and they live in Kings Langley, Hertfordshire, UK.

Comments and Discussions

 
GeneralMy vote of 5 PinmemberForogar26-Sep-13 6:41 
QuestionMore consistency with .net implementation PinmemberJaime Olivares31-Jul-11 5:32 
AnswerRe: More consistency with .net implementation PinmemberSimmoTech2-Aug-11 2:44 
QuestionMy vote of 5 PinmemberFilip D'haene31-Jul-11 4:20 
QuestionReasonable and Useful - 5 points PinmemberIsaac Anietye Inyang28-Jul-11 6:28 
QuestionNice one 5 PinmemberMehdi Gholam20-Jul-11 20:00 
QuestionMy vote? 10! PinmemberFernandaUY20-Jul-11 9:00 
QuestionLooks brilliant PinmemberAnt210020-Jul-11 8:15 

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
Web03 | 2.8.140415.2 | Last Updated 20 Jul 2011
Article Copyright 2011 by SimmoTech
Everything else Copyright © CodeProject, 1999-2014
Terms of Use
Layout: fixed | fluid