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

C# Language Features, From C# 2.0 to 4.0

, 15 Mar 2012 CPOL
Rate this:
Please Sign up or sign in to vote.
A description of the new language features starting from C# 2.0, going through to version 4.0

Contents

Introduction

This article discusses the language features introduced in C# 2.0, 3.0, and 4.0. The purpose of writing this article is to have a single repository of all the new language features introduced over the last seven years and to illustrate (where applicable) the advantages of the new features. It is not intended to be a comprehensive discussion of each feature, for that I have links for further reading. The impetus for this article is mainly because I could not find a single repository that does what this article does. In fact, I couldn't even find a Microsoft webpage that describes them. Instead, I had to rely on the universal authority for everything, Wikipedia, which has a couple nice tables on the matter.

C# 2.0 Features

Generics

First off, generics are not like C++ templates. They primarily provide for strongly typed collections.

Without Generics

public void WithoutGenerics()
{
  ArrayList list = new ArrayList();

  // ArrayList is of type object, therefore essentially untyped.
  // Results in boxing and unboxing of value types
  // Results in ability to mix types which is bad practice.
  list.Add(1);
  list.Add("foo");
}

Without generics, we incur a "boxing" penalty because lists are of type "object", and furthermore, we can quite easily add incompatible types to a list.

With Generics

public void WithGenerics()
{
  // Generics provide for strongly typed collections.
  List<int> list = new List<int>();
  list.Add(1); // allowed
  // list.Add("foo"); // not allowed
}

With generics we are prevented from using a typed collection with an incompatible type.

Constraints and Method Parameters and Return Types

Generics can also be used in non-collection scenarios, such as enforcing the type of a parameter or return value. For example, here we create a generic method (the reason we don't create a generic MyVector will be discussed in a minute:

public class MyVector
{
    public int X { get; set; }
    public int Y { get; set; }
}

class Program
{
    public static T AddVector<T>(T a, T b)
      where T : MyVector, new()
    {
      T newVector = new T();
      newVector.X = a.X + b.X;
      newVector.Y = a.Y + b.Y;

      return newVector;
    }

    static void Main(string[] args)
    {
     MyVector a = new MyVector();
     a.X = 1;
     a.Y = 2;
     MyVector b = new MyVector();
     b.X = 10;
     b.Y = 11;
     MyVector c = AddVector(a, b);
     Console.WriteLine(c.X + ", " + c.Y);
   }
}

Notice the constraint. Read more about constraints here. The constraint is telling the compiler that the generic parameter must be of type MyVector, and that it is an object (the "new()") constraint, rather than a value type. The above code is not very helpful because it would require writing an "AddVector" method for vectors of different types (int, double, float, etc.)

What we can't do with generics (but could with C++ templates) is perform operator functions on generic types. For example, we can't do this:

public class MyVector<T>
{
  public T X { get; set; }
  public T Y { get; set; }

  // Doesn't work:
  public void AddVector<T>(MyVector<T> v)
  {
    X = X + v.X;
    Y = Y + v.Y;
  }
}

This results in a "operator '+=' cannot be applied to operands of type 'T' and 'T'" error! More on workarounds for this later.

Factories

You might see generics used in factories. For example:

public static T Create<T>() where T : new()
{
  return new T();
}

The above is a very silly thing to do, but if you are writing an Inversion of Control layer, you might be doing some complicated things (like loading assemblies) based on the type the factory needs to create.

Partial Types

Partial types can be used on classes, structs, and interface. In my opinion, partial types were created to separate out tool generated code from manually written code. For example, the Visual Studio form designer generates the code-behind for the UI layout, and to keep this code stable and independent from your manually written code, such as the event handlers, Visual Studio creates two separate files and indicates that the same class is of partial type. For example, let's say we have two separate files:

File 1:

public partial class MyPartial
{
  public int Foo { get; set; }
}

File 2:

public partial class MyPartial
{
  public int Bar { get; set; }
}

We can use the class, which has been defined in two separate files:

public class PartialExample
{
  public MyPartial foobar;

  public PartialExample()
  {
    foobar.Foo = 1;
    foobar.Bar = 2;
  }
}

Do not use partial classes to implement a model-view-controller pattern! Just because you can separate the code into different files, one for the model, one for the view, and one view the controller, does not mean you are implementing the MVC pattern correctly!

The old way of handling tool generated code was typically to put comments in the code like:

// Begin Tool Generated Code: DO NOT TOUCH
   ... code ...
// End Tool Generated Code

And the tool would place its code between the comments.

Anonymous Methods

Read more.

Anonymous methods let us define the functionality of a delegate (such as an event) inline rather than as a separate method.

The Old Way

Before anonymous delegates, we would have to write a separate method for the delegate implementation:

public class Holloween
{
  public event EventHandler ScareMe;

  public void OldBoo()
  {
    ScareMe+=new EventHandler(DoIt);
  }

  public void Boo()
  {
    ScareMe(this, EventArgs.Empty);
  }

  public void DoIt(object sender, EventArgs args)
  {
    Console.WriteLine("Boo!");
  }
}

The New Way

With anonymous methods, we can implement the behavior inline:

public void NewBoo()
{
  ScareMe += delegate(object sender, EventArgs args) { Console.WriteLine("Boo!"); };
}

Async Tasks

We can do the same thing with the Thread class:

public void AsyncBoo()
{
  new Thread(delegate() { Console.WriteLine("Boo!"); }).Start();
}

Note that we cast the method as a "delegate()"--note the '()'--because there are two delegate forms and we have to specify the parameterless delegate form.

Updating the UI

My favorite example is calling the main application thread from a worker thread to update a UI component:

/// <summary>
/// Called from some async process:
/// </summary>
public void ApplicationThreadBoo()
{
  myForm.Invoke((MethodInvoker)delegate { textBox.Text = "Boo"; });
}

Iterators

Read more.

Iterators reduce the amount of code we have to write to iterate over a custom collection.

The Old Way

Previous to C# 2.0, we had to implement the IEnumerator interface, supplying the Current, MoveNext, and Reset operations manually:

public class DaysOfWeekOld : IEnumerable
{
  protected string[] days = new string[] { "Monday", "Tuesday", "Wednesday", "Thursday",
                                             "Friday", "Saturday", "Sunday" };
        
  public int Count { get { return days.Length; } }
  public string this[int idx] { get { return days[idx]; } }

  public IEnumerator GetEnumerator()
  {
    return new DaysOfWeekEnumerator(this);
  }
}

public class DaysOfWeekEnumerator : IEnumerator
{
  protected DaysOfWeekOld dow;
  protected int pos = -1;

  public DaysOfWeekEnumerator(DaysOfWeekOld dow)
  {
    this.dow = dow;
  }

  public object Current
  {
    get { return dow[pos]; }
  }

  public bool MoveNext()
  {
    ++pos;

    return (pos < dow.Count);
  }

  public void Reset()
  {
    pos = -1;
  }
}

The New Way

In the new approach, we can use the yield keyword to iterate through the collection:

public class DaysOfWeekNew : IEnumerable
{
  protected string[] days = new string[] { "Monday", "Tuesday", "Wednesday", "Thursday",
                                            "Friday", "Saturday", "Sunday" };

  public IEnumerator GetEnumerator()
  {
    for (int i = 0; i < days.Length; i++)
    {
      yield return days[i];
    }
  }
}

This is much more readable and also ensures that we don't access elements in the collection beyond the number of items in the collection.

We can also implement a generic enumerator, which provides a type safe iterator, but requires us to implement both generic and non-generic GetEnumerator method:

public class DaysOfWeekNewGeneric : IEnumerable<string>
{
  protected string[] days = new string[] { "Monday", "Tuesday", "Wednesday", "Thursday",
                                            "Friday", "Saturday", "Sunday" };

  IEnumerator IEnumerable.GetEnumerator()
  {
    return Enumerate();
  }

  public IEnumerator<int> GetEnumerator()
  {
    return Enumerate();
  }

  public IEnumerator<string> Enumerate()
  {
    for (int i = 0; i < days.Length; i++)
    {
      yield return days[i];
    }
  }
}

So, for example, in the non-generic version, I could write:

DaysOfWeekNew dow2 = new DaysOfWeekNew();

foreach (string day in dow2)
{
  Console.WriteLine(day);
}

which is perfectly valid, but I could also write:

DaysOfWeekNew dow2 = new DaysOfWeekNew();

foreach (int day in dow2)
{
  Console.WriteLine(day);
}

The error in casting from a string to an integer is caught at runtime, not compile time. Using a generic IEnumerable<T>, an improper cast is caught at compile time and also by the IDE:

DaysOfWeekNewGeneric dow3 = new DaysOfWeekNewGeneric();

foreach (int day in dow3)
{
  Console.WriteLine(day);
}

The above code is invalid and generates the compiler error:

"error CS0030: Cannot convert type 'string' to 'int'"

Thus, the implementation of generic iterators in C# 2.0 increases readability and type safety when using iterators.

Nullable Types

Read more.

Nullable types allow a value type to take on an additional "value", being "null". I've found this primarily useful when working with data tables. For example:

public class Record
{
  public int ID { get; set; }
  public string Name { get; set; }
  public int? ParentID { get; set; } 
}

public class NullableTypes
{
  protected DataTable people;

  public NullableTypes()
  {
    people = new DataTable();

    // Note that I am mixing a C# 3.0 feature here, Object Initializers,
    // with regards to how AllowDBNull is initialized. I'm doing because I think
    // the example is more readable, even though not C# 2.0 compilable.

    people.Columns.Add(new DataColumn("ID", typeof(int)) {AllowDBNull=false});
    people.Columns.Add(new DataColumn("Name", typeof(string)) { AllowDBNull = false });
    people.Columns.Add(new DataColumn("ParentID", typeof(int)) { AllowDBNull = true });

    DataRow row = people.NewRow();
    row["ID"] = 1;
    row["Name"] = "Marc";
    row["ParentID"] = DBNull.Value; // Marc does not have a parent!
    people.Rows.Add(row);
  }

  public Record GetRecord(int idx)
  {
    return new Record()
    {
      ID = people.Rows[idx].Field<int>("ID"),
      Name = people.Rows[idx].Field<string>("Name"),
      ParentID = people.Rows[idx].Field<int?>("ParentID"),
    };
  }
}

In the above example, the Field extension method (I'll discuss extension methods later) converts DBNull.Value automatically to a "null", which in this schema is a valid foreign key value.

You will also see nullable types used in various third party frameworks to represent "no value." For example, in the DevExpress framework, a checkbox can be set to false, true, or no value. The reason for this is again to support mapping a control directly to a structure that backs a table with nullable fields. That said, I think you would most likely see nullable types in ORM implementations.

Private Setters (properties)

Read more.

A private setter exposes a property as read-only, which is different from designating the property as readonly. With a field designated as readonly, it can only be initialized during construction or in the variable initializer. With a private setter, the property can be exposed as readonly to the outside world the class implementing the property can still write to it:

public class PrivateSetter
{
  public int readable;
  public readonly int readable2;

  public int Readable
  {
    get { return readable; }
    // Accessible only by this class.
    private set { readable = value; }
  }

  public int Readable2
  {
    get { return readable2; }
    // what would the setter do here?
  }

  public PrivateSetter()
  {
    // readonly fields can be initialized in the constructor.
    readable2 = 20;
  }

  public void Update()
  {
    // Allowed:
    Readable = 10;
    // Not allowed:
    // readable2 = 30;
  }
}

Contrast the above implementation with C# 3.0's auto-implemented properties, which I discuss below.

Method Group Conversions (delegates)

I must admit to a "what the heck is this?" experience for this feature. First (for my education) a "method group" is a set of methods of the same name. In other words, a method with multiple overloads. This post was very helpful. I stumbled across this post that explained method group conversion with delegates. This also appears to have to do with covariance and contravariance, features of C# 4.0. Read more here. But let's try the basic concept, which is to assign a method to a delegate without having to use "new" (even though behind the scenes, that's apparently what the IL is emitting).

The Old Way

public class MethodGroupConversion
{
  public delegate string ChangeString(string str);
  public ChangeString StringOperation;

  public MethodGroupConversion()
  {
    StringOperation = new ChangeString(AddSpaces);
  }

  public string Go(string str)
  {
    return StringOperation(str);
  }

  protected string AddSpaces(string str)
  {
    return str + " ";
  }
}

The New Way

We replace the constructor with a more straightforward assignment:

public MethodGroupConversion()
{
  StringOperation = AddSpaces;
}

OK, that seems simple enough.

C# 3.0 Features

Implicitly Typed Local Variables

Read more.

The "var" keyword is a new feature of C# 3.0. Using the "var" keyword, you are relying on the compiler to infer the variable type rather than explicitly defining it. So, for example, instead of:

public void Example1()
{
  // old:
  Dictionary<string, int> explicitDict = new Dictionary<string, int>();

  // new:
  var implicitDict = new Dictionary<string, int>();
}

While it seems like syntactical sugar, the real strength of implicit types is its use in conjunction with anonymous types (see below.)

Restrictions

Note the phrase "local variables" in the heading for this section. Implicitly typed variables cannot be passed to other methods as parameters nor returned by methods. As Richard Deeming commented below, what I mean by this is that you cannot specify var as a parameter or return type, but you can call a method with an implicit type of the method's parameter is an explicit type, and similarly (and more obviously) with return parameters -- an explicit return type can be assigned to a var.

Object and Collection Initializers

Read more.

The Old Way

Previously, to initialize property values from outside of the class, we would have to write either use a constructor:

public Record(int id, string name, int? parentID)
{
  ID = id;
  Name = name;
  ParentID = parentID;
}
...
new Record(1, "Marc", null);

or initialize the properties separately:

Record rec=new Record();
rec.ID = 1;
rec.Name = "Marc";
rec.ParentID = null;

The New Way

In its explicit implementation, this simply allow us to initialize properties and collections when we create the object. We've already seen examples in the code above:

Record r = new Record() {ID = 1, Name = "Marc", ParentID = 3};

More interestingly is how this feature is used to initialize anonymous types (see below) especially with LINQ.

Initializing Collections

Similarly, a collection can be initialized inline:

List<Record> records = new List<Record>()
{
  new Record(1, "Marc", null),
  new Record(2, "Ian", 1),
};

Auto-Implemented Properties

In the C# 2.0 section, I described the private setter for properties. Let's look at the same implementation using auto-implemented properties:

public class AutoImplement
{
  public int Readable { get; private set; }
  public int Readable2 { get { return 20; } }

  public void Update()
  {
    // Allowed:
    Readable = 10;
    // Not allowed:
    // Readable2 = 30;
  }
}

The code is a lot cleaner, but the disadvantage is that, for properties that need to fire events or have some other business logic or validation associated with them, you have to go back to the old way of implementing the backing field manually. One proposed solution to firing property change events for auto-implemented properties is to use AOP techniques, as written up by Tamir Khason's Code Project technical blog.

Anonymous Types

Read more.

Anonymous types lets us create "structures" without defining a backing class or struct, and rely on implicit types (vars) and object initializers. For example, if we have a collection of "Record" objects, we can return a subset of the properties in this LINQ statement:

public void Example()
{
  List<Record> records = new List<Record>();
    {
      new Record(1, "Marc", null),
      new Record(2, "Ian", 1),
    };

  var idAndName = from r in records select new { r.ID, r.Name };
}

Here we see how several features come into play at once:

  • LINQ
  • Implicit types
  • Object initialization
  • Anonymous types

If we run the debugger and inspect "idAndName", we'll see that it has a value:

{System.Linq.Enumerable.WhereSelectListIterator<CSharpComparison.Record,
          <>f__AnonymousType0<int,string>>}

and (ready for it?) the type:

System.Collections.Generic.IEnumerable<<>f__AnonymousType0<int,string>> 
   {System.Linq.Enumerable.WhereSelectListIterator<CSharpComparison.Record,
   <>f__AnonymousType0<int,string>>}

Imagine having to explicitly state that type name. We can see advantages of implicit types, especially in conjunction with anonymous types.

Extension Methods

Read more.

Extension methods are a mechanism for extending the behavior of a class external to its implementation. For example, the String class is sealed, so we can't inherit from it, but there's a lot of useful functions that the String class doesn't provide. For example, working with Graphviz, I often need to put quotes around the object name.

Before Extension Methods

Before extension methods, I would probably end up writing something like this:

string graphVizObjectName = "\"" + name +"\"";

Not very readable, re-usable, or bug proof (what if name is null?)

With Extension Methods

With extension methods, I can write an extension:

public static class StringHelpersExtensions
{
  public static string Quote(this String src)
  {
    return "\"" + src + "\"";
  }
}

(OK, that part looks pretty much the same) - but I would use it like this:

string graphVizObjectName = name.Quote();

Not only is this more readable, but it's also more reusable, as the behavior is now exposed everywhere.

Query Expressions

Read more.

Query expressions seems to be a synonymous phrase for LINQ (Language-Integrated Query). Humorously, the Microsoft website I just referenced has the header "LINQ Query Expressions." Redundant!

Query expressions are written in a declarative syntax and provide the ability to query an enumerable or "queriable" object using complex filters, ordering, grouping, and joins, very similar in fact to how you would work with SQL and relational data.

As I wrote about above with regards to anonymous types, here's a LINQ statement:

var idAndName = from r in records select new { r.ID, r.Name };

LINQ expressions can get really complex and working with .NET classes and LINQ relies heavily on extension methods. LINQ is far to large a topic (there are whole books on the subject) and is definitely outside the purview of this article!

Left and Right Joins

Joins by default in LINQ are inner joins. I was perusing recently for how to do left and right joins and came across this useful post.

Lambda Expressions

Read more.

Lambda expressions are a fundamental part of working with LINQ. You usually will not find LINQ without lambda expressions. A lambda expression is an anonymous method (ah ha!) that "can contain expressions and statements, and can be used to create delegates or expression tree types...The left side of the lambda operator specifies the input parameters (if any) and the right side holds the expression or statement block." (taken from the website referenced above.)

In LINQ, I could write:

var idAndName = from r in records 
  where r.Name=="Marc"
  select new { r.ID, r.Name };

and I'd get the names of people with the name "Marc". With a lambda expression and the extension methods provided for a generic List, I can write:

var idAndName2 = records.All(r => r.Name == "Marc");

LINQ and lambda expressions can be combined. For example, here's some code from an article I recently wrote:

var unassoc = from et in dataSet.Tables["EntityType"].AsEnumerable()
  where !(dataSet.Tables["RelationshipType"].AsEnumerable().Any(
     rt => 
       (rt.Field<int>("EntityATypeID") == assocToAllEntity.ID) && 
       (rt.Field<int>("EntityBTypeID") == et.Field<int>("ID"))))
  select new { Name = et.Field<string>("Name"), ID = et.Field<int>("ID") };

LINQ, lambda expressions, anonymous types, implicit types, collection initializers and object initializers all work together to more concisely express the intent of the code. Previously, we would have to do this with nested for loops and lots of "if" statements.

Expression Trees

Read more.

Let's revisit the MyVector example. With expression trees, we can however compile type-specific code at runtime that allows us to work with generic numeric types in a performance efficient manner (compare with "dynamic" in C# 4.0, discussed below).

public class MyVector<T>
{
  private static readonly Func<T, T, T> Add;

  // Create and cache adder delegate in the static constructor.
  // Will throw a TypeInitializationException if you can't add Ts or if T + T != T 
  static MyVector()
  {
    var firstOperand = Expression.Parameter(typeof(T), "x");
    var secondOperand = Expression.Parameter(typeof(T), "y");
    var body = Expression.Add(firstOperand, secondOperand);
    Add = Expression.Lambda<Func<T, T, T>>(body, firstOperand, secondOperand).Compile();
  }

  public T X { get; set; }
  public T Y { get; set; }

  public MyVector(T x, T y)
  {
    X = x;
    Y = y;
  }

  public MyVector<T> AddVector(MyVector<T> v)
  {
    return new MyVector<T>(Add(X, v.X), Add(Y, v.Y));
  }
}

The above example comes from a post on StackOverflow.

C# 4.0 Features

Dynamic Binding

Read more.

Let's revisit the MyVector implementation again. With the dynamic keyword, we can defer the operation to runtime when we know the type.

public class MyVector<T>
{
  public MyVector() {}

  public MyVector<T> AddVector(MyVector<T> v)
  {
    return new MyVector<T>()
    {
      X = (dynamic)X + v.X,
      Y = (dynamic)Y + v.Y,
    };
  }
}

Because this uses method invocation and reflection, it is very performance inefficient. According to MSDN referenced in the link above: The dynamic type simplifies access to COM APIs such as the Office Automation APIs, and also to dynamic APIs such as IronPython libraries, and to the HTML Document Object Model (DOM).

Named and Optional Arguments

Read more.

As with the dynamic keyword, the primary purpose of this is to facilitate calls to COM. From the MSDN link referenced above:

Named arguments enable you to specify an argument for a particular parameter by associating the argument with the parameter's name rather than with the parameter's position in the parameter list. Optional arguments enable you to omit arguments for some parameters. Both techniques can be used with methods, indexers, constructors, and delegates.

When you use named and optional arguments, the arguments are evaluated in the order in which they appear in the argument list, not the parameter list.

Named and optional parameters, when used together, enable you to supply arguments for only a few parameters from a list of optional parameters. This capability greatly facilitates calls to COM interfaces such as the Microsoft Office Automation APIs.

I have never used named arguments and I rarely need to use optional arguments, though I remember when I moved from C++ to C#, kicking and screaming that optional arguments weren't part of the C# language specification!

Example

We can use named an optional arguments to specifically indicate which arguments we are supplying to a method:

public class NamedAndOptionalArgs
{
  public void Foo()
  {
    Bar(a: 1, c: 5);
  }

  public void Bar(int a, int b=1, int c=2)
  {
    // do something.
  }
}

As this example illustrates, we can specify the value for a, use the default value for b, and specify a non-default value for c. While I find named arguments to be of limited use in regular C# programming, optional arguments are definitely a nice thing to have.

Optional Arguments, The Old Way

Previously, we would have to write something like this:

public void OldWay()
{
  BarOld(1);
  BarOld(1, 2);
}

public void BarOld(int a)
{
  // 5 being the default value.
  BarOld(a, 5);
}

public void BarOld(int a, int b)
{
  // do something.
}

The syntax available in C# 4.0 is much cleaner.

Generic Covariance and Contravariance

What do these words even mean? From Wikipedia:

  • covariant: converting from wider to smaller (like double to float)
  • contravariant: converting from narrower to wider (like float to double)

First, let's look at co-contravariance with delegates, which has been around since Visual Studio 2005.

Delegates

Read more.

Not wanting to restate the excellent "read more" example referenced above, I will simply state that covariance allows us to assign a method returning a sub-class type to the delegate defined as returning a base class type. This is an example of going from something wider (the base class) to something smaller (the inherited class) in terms of derivation.

Contravariance, with regards to delegates, lets us create a method in which the argument is the base class and the caller is using a sub-class (going from narrower to wider). For example, I remember being annoyed that I could not consume an event having a MouseEventArgs argument with a generic event handler having an EventArgs argument. This example of contravariance has been around since VS2005, but it makes for a useful example of the concept.

Generics

Read more.

Also this excellent technical blog on Code Project.

Again, the MSDN page referenced is an excellent read (in my opinion) on co-contravariance with generics. To briefly summarize: as with delegates, covariance allows a generic return type to be covariant, being able specify a "wide" return type (more general) but able to use a "smaller" (more specialized) return type. So, for example, the generic interfaces for enumeration support covariance.

Conversely, contravariance lets us go from something narrow (more specialized, a derived class) to something wider (more general, a base class), and is used as parameters in generic interfaces such as IComparer.

But How Do I Define My Own?

To specify a covariant return parameter, we use the "out" keyword in the generic type. To specify a contravariant method parameter, we use the "in" keyword in the generic type. For example (read more here):

public delegate T2 MyFunc<in T1,out T2>(T1 t1); 

T2 is the covariant return type and T1 is the contravariant method parameter.

A further example is here.

Conclusion

In writing this, I was surprised how much I learned that deepened my understanding of C# as well as getting a broader picture of the arc of the language's evolution. This was a really useful exercise!

History

Updated the article based on comments received.

License

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

Share

About the Author

Marc Clifton

United States United States
Marc is the creator of two open source projects, MyXaml, a declarative (XML) instantiation engine and the Advanced Unit Testing framework, and Interacx, a commercial n-tier RAD application suite.  Visit his website, www.marcclifton.com, where you will find many of his articles and his blog.
 
Marc lives in Philmont, NY.

Comments and Discussions

 
GeneralMy vote of 5 PinprofessionalAfzaal Ahmad Zeeshan7-Oct-14 23:16 
QuestionMessage Automatically Removed PinprofessionalRavi.Kumar0215-Jan-14 22:46 
GeneralMy vote of 5 PinmemberDino.Liu18-Dec-13 13:34 
GeneralMy vote of 4 Pinmemberdb_cooper195021-Feb-13 16:51 
GeneralVery helpful Pinmemberbandi-ramesh18-Feb-13 1:49 
GeneralMy vote of 5 [modified] Pinmemberdmihailescu11-Feb-13 5:13 
QuestionC# 5.0 Features Pinmember ProgramFOX20-Jan-13 6:51 
GeneralMy vote of 5 Pinmemberfaruk19683019-Jan-13 8:28 
GeneralMy vote of 5 Pinmemberhumayan29914-Jan-13 5:26 
Questionnice your post Pinmembermedcatdo11-Jan-13 6:34 

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 | Terms of Use | Mobile
Web04 | 2.8.141223.1 | Last Updated 15 Mar 2012
Article Copyright 2012 by Marc Clifton
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid