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

Working with Units and Amounts

By , 17 Jul 2013
Rate this:
Please Sign up or sign in to vote.

Teaser

Ever thought there should be a better way of writing:

double surface = 4.52 * 2.30;
double height = 10.0;
double volume = surface * height;

Especially if it turns out surface is expressed in square meters and height in centimeters ?

What about an Amount class that would take care of all unit and amount conversions allowing you to write the following and be valid even if surface and height are in different units ?

var volume = surface * height;

Introduction

Working with amounts expressed in units is often an error prone occupation because the default .NET types provide no real support for this. Take for instance the following code sample:

double distance = 100.0;
double time = 2.5;
double speed = distance / time;

Speed will be 40, right ? But is it 40 meters/second, or 40 kilometers per hour ? Or is it 40 kilometers per second ? There is no compiler checking or runtime checking whatsoever about the unit assumptions the programmer has made. Even a human can’t tell by looking at the code.

One practice is to postfix variable names with the unit of their value. It’s a positive practice since it increases readability of the code, but it does not avoid unit conversion issues. The following is perfectly valid from the point of view of the compiler and will throw no runtime exception either:

double distanceInKilometers = 100.0;
double timeInHours = 2.5;
double speedInMilesPerHour = distanceInKilometers / timeInHours;

This practice also does not solve the issue that unit conversions (multiplying or dividing by 10 or 1000 for instance) will most probably be scattered all over the application code.

Suppose I’d like to compare the speed of a car at 80 kilometers per hour and a falling object at 40 meters per second. Which one is faster ?

double carspeedInKmPerHour = 80;
double fallingspeedInMPerSec = 40;
if (carspeedInKmPerHour > fallingspeedInMPerSec)
    System.out.println("Car is faster");

This code is obviously (made obvious by the use of the naming convention) wrong, but how to fix it ?

The correct comparison should have been (and no, the car is not faster):

if ((carspeedKmPerHour * 0.277777777777778) > fallingspeedMPerSec)

You can easily imagine code containing such conversions in several places, over several layers of the application. This is quite in conflict to the DRY principle.

Of course you could create a function ConvertKmPerHourToMPerSec() somewhere, but do you actually do that everytime you need to convert seconds into milliseconds ?

This article describes a generic approach to solve this problem.

A class per unit

The solution could be to create object oriented classes to encapsulate the behavior of amounts and units, Introducing a higher level of abstraction by implementing classes that sustain our domain language.

We would come up with a design where for each unit, a class is made to represent amounts in that unit. It could give is the following classes:

public class Meter {
 private double value;
 public Meter(double value)
 {
  this.value = value;
 }
}
public class Kilometer {
 private double value;
 public Kilometer(double value)
 {
  this.value = value;
 }
 public Meter ToMeter()
 {
  return new Meter(this.Value * 1000.0);
 }
}
public class Second
 private double value;
 public Second(double value)
 {
  this.value = value;
 }
}
public class Hour
 private double value;
 public Hour(double value)
 {
  this.value = value;
 }
 public Second ToSecond()
 {
  return new Second(this.Value * 3600.0);
 }
}

Next adding MeterPerSecond, MeterPerHour, KilometerPerSecond and KilometerPerHour classes with all possible To-conversion methods, and adding overloads for all allowed combinations of +, -, *, /, >, <, >=, <=, == and != operators. For instance, the Meter class could have the following overloads of the / operator:

public static MeterPerSecond operator /(Meter left, Second right)
{
 return new MeterPerSecond(left.Value / right.Value);
}
public static MeterPerHour operator /(Meter left, Hour right)
{
 return new MeterPerHour(left.Value / right.Value);
}

The result is a very safe way of coding that will confirm that our car is not faster than a falling object:

var carspeed = new KilometerPerHour(80);
var fallingspeed = new MeterPerSecond(40);
if (carspeed > fallingspeed)
    System.out.println("Car is faster");

But at what price ? If we want to combine several length units (meter, kilometer, centimeter, inch, yard, foot, mile,…), surface units (meter², kilometer², centimeter², inch², yard², foot², mile²,…), volume units (meter³, kilometer³, centimeter³,…) with time units (second, millisecond, nanosecond, minute, hour), speed units (meter/second, kilometer/hour, centimeter/day,…) and acceleration units (meter/second², kilometer/hour²,…) we’ll end up with a huge amount of code that still may not cover all (future) needs of our application.

This solution is also error prone. Indeed, given the large amount of code created mostly by copy-pasting conversion factors and +, -, *, /, <, >, <=, >=, == and != operators, chances are real we mixed the one and the other here and there...

And finally, the reusability of the classes are quite limited. In another application we may need mass and energy units and we’ll have to start all over again!

But this approach also has benefits. First, all amount manipulations and conversions are hardwired in the compiled code making unit conversions compile-time checked! And second, because unit type evaluation and conversion does not need to be performed at runtime anymore, this approach should also perform better.

If you are interested in this approach, I would suggest you to take a closer look at http://units.codeplex.com/, a lightweight units library using code generation to take much of the repetitive and errorprone coding work out of your hands.

A more dynamic approach – How units work

A whole different approach I took was to create a limited set of classes that would handle all types of units. To understand the classes we need first to understand how units and amounts work in real life.

The International System of Units (SI) is a standardized system of measurement and a good starting point to understand units and amounts expressed in those units:
http://en.wikipedia.org/wiki/International_System_of_Units

The SI defines 7 base units: meter for length, kilogram for mass, second for time, ampere for electric current, Kelvin for temperature, candela and mole. (Length, mass, time,… are sometimes called dimensions.) All these base units are units that cannot be expressed in terms of the other units. For instance, I can express a surface in terms of length power 2 and a volume in terms of length power 3, speed as length over time, but I cannot define length in terms of any of the 6 other base units.

So all amounts can be expressed in terms of the 7 base units. For instance:

speed = 3 kilometer per hour
      = 3 (1000 meter) / (3600 second)
      = 3 * (1/3.6) * meter^1 * second^-1
      = 3 * 0.2777 * meter^1 * second^-1

This also shows the structure of a unit. A unit derived from the base units consist of a factor (2.777) and an exponent for each of the base units (0 for all except the meter and second units which have respectively the exponents 1 and -1).

Schematically, we can represent every amount as a combination of a raw value and a unit. The unit consisting of a factor and a set of 7 exponents, one for each of the base SI units:

The amount of 3 kilometer per hour could be represented as:

Now, if I am walking at 3 kilometer per hour and I throw a ball forward at 12 meter per second. At what speed will the ball hit the wall in front of me ? To answer this question I need to addition my walking speed to the speed I throw the ball at.

Amounts can be added or subtracted if and only if they are expressed in the same type of unit. That is, if they have the same set of exponent values for the base units (here 1 and -1 for respectively length and time). This rule is also true for comparing amounts with < or > operators as we cannot compare amounts that are not in compatible units.

Rule 1 : Units are 'compatible' if they have the same exponent values for the same base units.

The amount of 12 meter per second is represented as:

To addition both 3 kilometer per hour and 12 meter per second, we need first to convert one amount to the exact same unit as the other. That is, not only should the exponent values be identical, but we also need to make the factors of the units the same. We can for instance transform 3 kilometer per hour into meter per second following these rules:

3 * 0.2777777 = 0.8333333 * 1

Hence, 3 kilometer per hour can also be written as:

which we can now simply add to 12 meter per second to obtain:

And the answer is: 12.8333 meter per second (or 46.2 kilometer per hour).

Rule 2 : Amounts in compatible units can be compared and added or subtracted after they have been brought to the exact same unit by using the unit factor as conversion factor.

Now, another question. If the wall is 25 feet in front of me, how long will it take before the ball hits the wall ?

Knowing the speed and the distance, I can calculate the duration as:

time = distance / speed

(because speed = distance / time).

The distance is 25 feet (25 * 0.3048 meter), thus:

To perform the operation distance / time, I need to divide the raw value of distance by the raw value of time, divide the factor of distance by the factor of time, and subtract the base unit exponents of the distance from the base unit exponents of the time:

or simplified:

Notice the weird unit here: this as an amount of 1.95 expressed in a unit which is 0.3 of a second ! So this is approximatively 0.64 seconds:

Rule 3 : Units can be multiplied/divided by multiplying/dividing their factors and adding/subtracting their base unit exponents.

As we see, the behavior of units and amounts can be largely generalized by representing units as a factor with a set of base unit exponents. And that’s exactly what the Amount and Unit classes do.

The Amount and Unit classes

Relying on this concept, I’ve developed classes Unit, UnitType and Amount, that can handle unit and amount operations:

UnitType represents a type of unit such as length (distance), time or mass. With this unit type, a base unit can be created for that dimension. Then, units can be created from there. For instance:

UnitType length = new UnitType("length");
Unit meter = new Unit("meter", "m", length);
Unit kilometer = new Unit("kilometer", "km", 1000 * meter);
Unit squareMeter = new Unit("meter²", "m²", meter * meter);
Unit cubicMeter = new Unit("meter³", "m³", meter.Power(3));
Unit liter = cubicMeter / 1000.0;

Note how new units are created by multiplying (or dividing) earlier made units!

Now that we have units, we can use them to create amounts:

var rainfall = new Amount(4.0, liter / squareMeter);

This would be 4 millimeter. Note that we never defined a unit “millimeter”. This does not matter. By dividing liter by squareMeter, a unit equivalent to millimeter was created dynamically.

An important thing to be aware of, is that dynamic units can be really anything. For instance a dynamic unit could match 3.7 meter. That is not a unit we are used to work with. Therefore, when displaying amounts, it is advised to first convert them to a known unit, or to use the formatting option (as explained later) to convert the unit:

Console.WriteLine(rainfall.ConvertedTo(meter));

or:

Console.WriteLine("{0:#,##0.00 US|meter}", rainfall);

So far this short introduction to using the Unit and Amount classes. Let’s now take a closer look to the features of those classes.

Math operators overloaded

Math operators are overloaded where applicable to allow easy coding. For instance:

Amount a;
Amount b = new Amount(25.0, Meter);
Amount c = new Amount(3.0, Second);

a = b / c;

Dividing or multiplying amounts results in a new amount where the value and the units were divided/multiplied.

Additions and subtractions on the other hand, are only possible between compatible units (units of the same kind, for instance “length over time”). A conversion failure exception is thrown otherwise.

Comparison operators

Also the comparison operators <, >, <=, >=, == and != have been overloaded, and IEquatable and IComparable interfaces are supported.

It is however important to note that Amount internally store their value using a double, and that as you may already know, arithmetic with doubles is always a little fuzzy. Especially when comparing double values, sometimes two similar values may not be not equal. See http://en.wikipedia.org/wiki/Floating_point#Accuracy_problems for background info.

The Amount class therefore has a static EqualityPrecision property that tells how many decimals are taken into account when comparing amounts. By default, 8 decimals are taken into account. This basically means that values are rounded up to 8 decimals before comparing them.

The value of decimals depends on the scale of the unit. The first decimal of a millimeter is only a tenth of a millimeter while the first decimal of a kilometer is 100 meter! Therefore comparing up to a number of decimals when using unknown/dynamic units or different units for the left and right term may have unexpected results. It is safer to convert amounts to known units before performing comparisons.

Can handle any unit, really any one!

The implementation of the classes is not bound to the 7 base SI unit types. In fact, you have to define all unit types yourself. The number of unit types you define is not limited.

This can come in handy, because there’s a problem with using the SI unit types. To represent an amount of liquid I would typically use a volume unit, liter for instance. If my application handles milk and fuel, being both liquids, I’ll use liters to express both. And as liters can be added to liters, nothing would prevent me from adding 1 liter of milk to 1 liter of fuel, resulting in 2 liter mixture. This makes no sense. We don’t want to add milk with fuel, just as we don’t compare apples and oranges, do we ?

We can solve this by defining two different unit types “volumeOfMilk” and “volumeOfFuel”. This way, a liter milk will be expressed in a different unit type than a liter of fuel, and since different unit types cannot be added (just as you cannot add 3 seconds to 12 meters), milk and fuel will not get mixed. (Trying to would result in a runtime exception.)

For the cases you do want to use the SI units, the StandardUnits assembly that comes with the code defines the SI unit types and their most common units as well as some commonly used physics constants. You can use them as follows to calculate the mass of the earth, applying the formula found on http://www.enchantedlearning.com/subjects/astronomy/planets/earth/Mass.shtml:

var a = new Amount(9.81, LengthUnits.Meter / TimeUnits.Second.Power(2));
var r = new Amount(6371.0, LengthUnits.KiloMeter);
var G = PhysicsConstant.NewtonianGravitation;

var M = a * r.Power(2) / G;

Console.WriteLine(M);
Console.WriteLine(M.ConvertedTo(MassUnits.Ton));

Named units and the UnitManager

The code also includes a UnitManager class, which is a static class that can help you find predefined units.
You can register units with the UnitManager, and when you do so, units can be retrieved by name. Several constructors and other methods allow you to pass a string in lieu of a unit. When you use such a method, the string should match the name of a unit you registered previously with the UnitManager. For instance:

anAmount = new Amount(12.5, "kilogram");

The UnitManager can also be used to register custom conversion functions and UnitResolve delegates (to resolve units that were not registered beforehand).

The use of the UnitManager is optional. You can use units without registering them. The only thing you will miss is the ability to find units by their name or symbol.

Explicit amount conversions

After doing some computation, amounts are often expressed in dynamic units. Such dynamic units are correct for computation, but their names (build by concatenation) and scale (factor) may seem weird. Before saving an amount value to database or displaying an amount on screen you should therefore convert the amount to a known unit:

Console.WriteLine(rainfall.ConvertedTo(LengthUnits.MilliMeter));

(Or using a unit name registered with the UnitManager):

Console.WriteLine(rainfall.ConvertedTo("millimeter"));

Rounding values also implies that you know the unit the value is expressed in. Therefore, the Amount class has no Round method, but overloads of ConvertedTo which allow rounding, as in:

var rainfallRounded = rainfall.ConvertedTo(LengthUnits.MilliMeter, 2);

Whether a unit is dynamic or not can be tested with the IsNamed property: if the unit is named, it is not dynamic and vice versa.

Amounts can be custom formatted

The Amount class has several overloads of the ToString() method and implements IFormattable:

.ToString()
.ToString(string format)
.ToString(IFormatProvider formatProvider)
.ToString(string format, IFormatProvider formatProvider)

The formatProvider would typically be a CultureInfo object and will determine the decimal separator and group separator.

The format string either a constant formatting string “GG’”, “GN”, “GS”, “NG”, “NN” or “NS” where the first letter stands for General or Numeric formatting of the value part, and the second letter stands for General, Name or Symbol rendering of the unit (General and Symbol are the same).

But the format string can also specify a custom format, followed by “UG”, “UN” or “US”, where these stand for General, Name or Symbol rendering of the unit.

Finally, the format string can also contain a “|” sign followed by the name of a unit to convert to (the unit should be registered with the UnitManager), or followed by “?” to let the UnitManager search for the matching unit.

When no or null format string is given, “GG” is assumed.

As a result, following format strings are valid; an example is given of their output:

"GG" 1234.56789 km
"NG" 1234.57 km
"GN" 1234.56789 kilometer
"#,##0.0 US" 1,234.6 km
"#,##0.0 UN" 1,234.6 kilometer
"#,##0.0 US|meter" 1,234,567.9 m



Especially this last form is interesting, because it forces conversion to a known unit and ensures the value is not expressed in some weird dynamic unit.

Since Amount implements IFormattable, you can also use these formatting options in format strings of the String or Console methods. For instance:

Console.WriteLine("The result is {0:#,##0.00 US|liter}", amount);

The Amount class also defines static ToString() methods for convenience. The advantages of the static methods is that they also support null amounts (for which they return an empty string). For instance, the following will not throw an exception:

Amount x = null;
String s = "The result is ";
s = s + Amount.ToString(x, "#,##0.00 US|meter");

Units and amounts are serializable

Special care has been taken to the (standard .NET binary) serialization/deserialization process of units and amounts. When passing units or amounts to another application tier (which involves serialization/deserialization) the code takes into account the fact that the destination tier may not yet know the amounts unit.

The serialized form of an amount therefore contains all the information required to recreate the unit and unit type in the destination AppDomain.

XML serialization, (WCF) datacontract serialization and database serialization

On the other hand, I did not provide out of the box support for datacontract serialization. Two reasons. The first, the data contract serializer will fallback to standard serialization when datamember properties are of a type that does not support datacontract serialization, so there’s no absolute need for an explicit support.

The second reason is even more important I think: the serialized form of amounts and units would anyway require that the client also uses the Amount and Unit classes here defined, which is an assumption I did not want to make.

Let’s look to what happens if we serialize in instance of the following class with the DataContractSerializer:

[DataContract]
public class Car
{ 
    [DataMember]
    public int Id { get; set; }

 
    [DataMember]
    public string Name { get; set; }

 
    [DataMember]
    public Amount Speed { get; set; }
}

The DataContractSerializer will turn this into something as:

<Car xmlns="http://schemas.datacontract.org/2004/07/..."
     xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
    <Id>1</Id>
    <Name>Beatle</Name>
    <Speed xmlns:a="http://schemas.datacontract.org/2004/07/TypedUnits">
        <a:unit>
            <a:factor>0.27777777777777779</a:factor>
            <a:isNamed>false</a:isNamed>
            <a:name>(kilometer/hour)</a:name>
            <a:symbol>km/h</a:symbol>
            <a:unitType>
                <names i:type="b:string" xmlns="" xmlns:b="...">meter|second</names>
                <exps i:type="b:string" xmlns="" xmlns:b="...">1|-1</exps>
            </a:unitType>
        </a:unit>
        <a:value>120</a:value>
    </Speed>
</Car>

So serialization works, but the unit creates quite some overhead. More importantly, if one of the parties (i.e. the client) does not use the Amount and Unit implementation (i.e. because it’s a Java client), it will be hard for that party to decode the amount.

Therefore, the preferred solution would in my opinion be to create derived properties that expose a double in a specific unit. You would then serialize the derived property instead of the Amount property. For instance, the following class can both be serialized through WCF, and mapped to a database through Entity Framework:

[Table("Cars", Schema = "vehicles")]
[DataContract]
public class Car
{
    [Key]
    [DataMember]
    public int Id { get; set; }

 
    [DataMember]
    [Required, MaxLength(50)]
    public string Name { get; set; }

 
    [NotMapped]
    public Amount Speed { get; set; }

 
    [Column("Speed")]
    [DataMember]
    public double SpeedInKmPerSec
    {
        get
        {
            return this.Speed.ConvertedTo(SpeedUnits.KilometerPerHour).Value;
        }
        set
        {
            this.Speed = new Amount(value, SpeedUnits.KilometerPerHour);
        }
    }
}

The DataContractSerializer now produces:

<Car xmlns="http://schemas.datacontract.org/2004/07/..."
     xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
    <Id>1</Id>
    <Name>Beatle</Name>
    <SpeedInKmPerSec>120</SpeedInKmPerSec>
</Car>

This is much shorter and easier to exchange with for instance Java clients.

I know, I know! We’re back to the postfix variable name trick I named in the introduction. The difference is that we use them here only for serialization while computation is done with the Amount type property “Speed”.

If you are only concerned about DataContract serialization, you could even hide the SpeedInKmPerSec property by making it private or protected (DataContract serialization can handle hidden members) so that the remainder of the code is forced to use the Amount type property in computations.

If you are concerned about Entity Framework, you can also map protected members (see Arthur Vickers blogpost http://blog.oneunicorn.com/2012/03/26/code-first-data-annotations-on-non-public-properties/), but you should be aware that you must always use mapped properties in EF Linq queries for Entity Framework to be able to translate them into SQL, so you would not be able to use those protected members (because they’re protected) nor their derived properties (because they don’t translate into SQL) in EF Linq queries…

The Source Code

The source code consists of a Visual Studio.NET 2010 solution (which should open fine in VS.NET 2012 as well), and contains the following projects:

The TypedUnits project contains the effective code for the Amount, Unit, UnitType and UnitManager classes. The StandardUnits assembly contains definitions for most common SI units and physics constants.

The TestProject is a project with unittests I used to verify my code.

And finally, the SampleConsoleApplication is just a very simple sample application to get you started.

And enjoy!

License

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

About the Author

Rudi Breedenraedt
Architect Wolters Kluwer Belgium
Belgium Belgium
Rudi is a Software Architect at Wolters Kluwer Belgium.

Comments and Discussions

 
QuestionSupport for data storage units and network rate units Pinmemberpitoloko15-Oct-13 8:25 
QuestionGreat article PinmemberIstace15-Sep-13 11:19 
SuggestionGreat article plus a suggestion for compile-type checking Pinmemberdantoniak23-Jul-13 6:41 
GeneralRe: Great article plus a suggestion for compile-type checking PinmemberRudi Breedenraedt26-Jul-13 5:51 
QuestionRelationships between units PinmemberRussell Jones18-Jul-13 13:37 
AnswerRe: Relationships between units PinmemberRudi Breedenraedt18-Jul-13 23:39 
GeneralRe: Relationships between units PinmemberRussell Jones21-Jul-13 14:48 
QuestionVery good article! PinmemberAnt210018-Jul-13 2:32 
AnswerRe: Very good article! PinmemberRudi Breedenraedt18-Jul-13 5:00 
QuestionNice job gets my 5 PinprofessionalMike Hankey9-Jul-13 6:50 
AnswerRe: Nice job gets my 5 PinmemberRudi Breedenraedt9-Jul-13 23:17 
Questionapproach is a bit like units.codeplex.com Pinmembernaam2008-Jul-13 10:11 
AnswerRe: approach is a bit like units.codeplex.com PinmemberRudi Breedenraedt9-Jul-13 5:25 
GeneralRe: approach is a bit like units.codeplex.com Pinmembernaam2009-Jul-13 7:52 
QuestionAn Eloquently Assembled Article PinmemberMike DiRenzo4-Jul-13 3:32 
GeneralVery good article! Pinmembersvnak1-Jul-13 22:55 
GeneralVery good article, and very interesting library PinmemberKiloBravoLima1-Jul-13 3:39 
GeneralRe: Very good article, and very interesting library PinmemberRudi Breedenraedt1-Jul-13 4:23 
GeneralNice, but sounds familiar... PinmemberJuno1-Jul-13 1:04 
GeneralRe: Nice, but sounds familiar... PinmemberRudi Breedenraedt1-Jul-13 4:15 
GeneralAmazing Article PinmemberRenan Ranelli29-Jun-13 8:51 
GeneralRe: Amazing Article PinmemberRudi Breedenraedt1-Jul-13 4:05 
Questionexcellent PinmemberBeeWayDev28-Jun-13 7:37 
QuestionSI units and conversions: miles, Fahrenheit, etc. PinmemberWiktor Wandachowicz26-Jun-13 15:14 
AnswerRe: SI units and conversions: miles, Fahrenheit, etc. PinmemberRudi Breedenraedt27-Jun-13 16:58 
AnswerRe: SI units and conversions: miles, Fahrenheit, etc. PinmemberRudi Breedenraedt27-Jun-13 17:32 
GeneralRe: SI units and conversions: miles, Fahrenheit, etc. PinmemberPaul R Benson27-Jun-13 21:29 
GeneralRe: SI units and conversions: miles, Fahrenheit, etc. PinmemberPaul R Benson27-Jun-13 22:50 
GeneralRe: SI units and conversions: miles, Fahrenheit, etc. PinmemberRenan Ranelli29-Jun-13 10:08 
GeneralRe: SI units and conversions: miles, Fahrenheit, etc. PinmemberErlend Robaye30-Jun-13 23:29 
QuestionF# seems to have units built-in, FWIW Pinmemberjames.manning26-Jun-13 4:19 
AnswerRe: F# seems to have units built-in, FWIW PinmemberRudi Breedenraedt27-Jun-13 16:20 
GeneralGood detailed explanation PinmemberHiteshforu200726-Jun-13 3:11 
GeneralRe: Good detailed explanation PinmemberRudi Breedenraedt27-Jun-13 16:12 
QuestionVote of 5 PinmemberDr Herbie25-Jun-13 23:29 
AnswerRe: Vote of 5 PinmemberRudi Breedenraedt25-Jun-13 23:41 
Generalimages PinmemberJuraj_Bezdek25-Jun-13 19:45 
GeneralRe: images PinmemberRudi Breedenraedt25-Jun-13 22:32 
GeneralRe: images PinmemberJuraj_Bezdek25-Jun-13 22:49 

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
Web01 | 2.8.140415.2 | Last Updated 17 Jul 2013
Article Copyright 2013 by Rudi Breedenraedt
Everything else Copyright © CodeProject, 1999-2014
Terms of Use
Layout: fixed | fluid