The shallow consider liberty a release from all law, from every constraint. The wise man sees in it, on the contrary, the potent Law of Laws.

Walt Whitman

Liberty has restraints but no frontiers.

David Lloyd George

Generics are a very powerful feature in C#, available since C#, version 2. I use generics nearly every day. Adding a constraint to a generic is even more powerful.

A constraint on a type parameter can specify either the word class or struct.   Struct can be a struct type like DateTime or an int, bool, decimal, etc.  I rarely use struct in the constraint.  Besides using the word class, a constraint can also specify a specific class or interface.  Shown below are the different types of constraints.

  • where T : struct
  • where T : class
  • where T : class, new()
  • where T : MyClass
  • where T : MyClass, new()
  • where T : IMyInterface
  • where T : IMyInterface, new()

The optional use of new() must be placed at the end of the where clause.  The type T must have a public constructor that has no parameters.  This allows for creation of a new variable of type T

The first example shows a contrived example using struct.  Notice the use of default which is how a variable of type T is created for a struct type.

static void StructTest<T>(T myVar)
  where T : struct
{
  T x = default(T);
  bool isDefault = x.Equals(myVar);
  Console.WriteLine("Variable value {0} is equal to default {1}. {2}", myVar, x, isDefault);
}

// Call StructTest
  StructTest(12.3);
  StructTest(false);
  StructTest(123);
  StructTest(DateTime.Now);
  StructTest(DateTime.MinValue);

I write lots of Windows Applications with DataGridView. Just connecting a DataSource to a DataGridView is fast but I always want more options and power than comes out of the box.  For example, column sorting automatically works on a single column but I have Last Name, First Name in two columns.  Or I have numeric data that has null values.  I convert this to string based cells which do not sort properly.

I have many other examples where the DataSource connected to a DataGridView just do not handle my needs.  Instead, I use Lists of Data Objects connected to DataGridView.  The code to restore the column sorting to DataGridView uses generic constraints which I will show below.

The first step is to create a new interface that extends the IComparer interface which is in the System.Collections.Generic namespace.  Also note, I do not use the T type variable.  I use a meaningful name to help document type variable.

public interface IComparerSort<TDataClass> : IComparer<TDataClass>
  where TDataClass : class
{
  void SetSortType(string dataPropertyName);
  bool SortAscending { get; set; }
}

The data class, Employee is defined below. The IsTotalRecord property allows the data list to have a total record that will always be sorted to the bottom of the DataGridView.

public class EmployeeData
{
  public string FirstName { get; set; }
  public string LastName { get; set; }
  public decimal Salary { get; set; }
  public bool IsTotalRecord { get; set; }

  public string DisplayFirstName { get { return string.IsNullOrEmpty(FirstName) ? string.Empty : FirstName; } }
  public string DisplayLastName { get { return string.IsNullOrEmpty(LastName) ? string.Empty : LastName; } }
  public string DisplaySalary { get { return Salary.ToString("$#,##0.00;($#,##0.00)"); } }

  public string DisplayFullName
  {
    get
    {
      string fullName = string.Empty;
      if (!string.IsNullOrEmpty(LastName))
        fullName = LastName;
      if (!string.IsNullOrEmpty(FirstName))
      {
        if (fullName == string.Empty)
          fullName = FirstName;
        else
          fullName = fullName + ", " + FirstName;
      }

      return fullName;
    }
  }
}

Sort class will implement the IComparerSort by using the EmployeeData as the type parameter.  The IComparer requires the Method Compare and the IComparerSort requires the property SortAscending and the Method SetSortType.

The DataPropertyName is a property from the DataGridView Column.  By using an enum, the DataPropertyName is only interpreted once per sort which keeps the performance very fast even on large data lists.  The property and methods that are required to be implemented are highlighted.

Also, notice the property IsTotalRecord.  It is highlighted as well.  This forces the total record to always be at the bottom of the grid.  My users have found this to be a very nice feature.

public class EmployeeSort : IComparerSort<EmployeeData>
{
  public enum SortProperty
  {
    DisplayFirstName,
    DisplayLastName,
    DisplaySalary,
    DisplayFullName
  }

  public SortProperty SortPropertyValue { get; set; }
  public bool SortAscending { get; set; }

  public EmployeeSort()  // Parameterless Constructor to set SortAscending to true
  {
    SortAscending = true;
  }

  public void SetSortType(string dataPropertyName)
  {
    foreach (SortProperty sortEnum in Enum.GetValues(typeof(SortProperty)))
    {
      if (string.Compare(sortEnum.ToString("G"), dataPropertyName) == 0)
      {
        SortPropertyValue = sortEnum;
        break;
      }
    }
  }

  public int Compare(EmployeeData firstEmployeeData, EmployeeData secondEmployeeData)
  {
    int compareValue = 0;
    int ascending = 1;
    if (!SortAscending)
      ascending = -1;

    if (firstEmployeeData == null)
    {
      if (secondEmployeeData != null)
        compareValue = -1;
    }
    else
    {
      if (secondEmployeeData == null)
        compareValue = 1;
      else
      {
        compareValue = firstEmployeeData.IsTotalRecord.CompareTo(secondEmployeeData.IsTotalRecord);
        if (compareValue == 0)
        {
          switch (SortPropertyValue)
          {
            case SortProperty.DisplayFirstName:
              compareValue = firstEmployeeData.DisplayFirstName.CompareTo(secondEmployeeData.DisplayFirstName);
              if (compareValue == 0)
                compareValue = firstEmployeeData.DisplayLastName.CompareTo(secondEmployeeData.DisplayLastName);
              break;
            case SortProperty.DisplayLastName:
              compareValue = firstEmployeeData.DisplayLastName.CompareTo(secondEmployeeData.DisplayLastName);
              if (compareValue == 0)
                compareValue = firstEmployeeData.DisplayFirstName.CompareTo(secondEmployeeData.DisplayFirstName);
              break;
            case SortProperty.DisplayFullName:
              compareValue = firstEmployeeData.DisplayFullName.CompareTo(secondEmployeeData.DisplayFullName);
              break;
            case SortProperty.DisplaySalary:
              compareValue = firstEmployeeData.Salary.CompareTo(secondEmployeeData.Salary);
              if (compareValue == 0)
                compareValue = firstEmployeeData.DisplayFirstName.CompareTo(secondEmployeeData.DisplayFirstName);
              if (compareValue == 0)
                compareValue = firstEmployeeData.DisplayLastName.CompareTo(secondEmployeeData.DisplayLastName);
              break;
            default:
              break;
          }
        }
        else
          ascending = 1;
      }
    }

    compareValue = ascending * compareValue;

    return compareValue;
  }
}

Even though the Salary is displayed in a nice user format with dollar sign and commas, the sort is still performed on the numeric Salary property.  If the Last Names are the same, then a secondary sort is performed on the First Names.  A similar feature exists on First Name and Salary.  Before comparing First Name and Last Name, I could have forced the comparison to be all upper case so “Smith” and “SMITH” sort together.  BY using my own sort class, this becomes trivial to do.

So far we have seen a very modest constraint on the IComparerSort interface.  Now, lets see the full power of constraint by creating a generic method for sorting.  The code is only a few lines long and completely re-usable.  Any Data List with a  Data Class/Sort Class combination that satisfies the constraints can be sorted.

This is generally what I do for my DataGridView.  I also add a ICloneable and new() to the TDataClass constraint.  This allows for DataGridView editing and easy restore if the user clicks cancel.

static void SortDataList<TDataClass, TSortClass>(List<TDataClass> dataList,
  string sortDataPropertyName, bool sortAscending)
  where TDataClass : class
  where TSortClass : IComparerSort<TDataClass>, new()
{
  TSortClass sorter = new TSortClass();
  sorter.SortAscending = sortAscending;
  sorter.SetSortType(sortDataPropertyName);

  dataList.Sort(sorter);
}

Notice how the type parameter TDataClass is passed in the constraint to the TSortClass. Very powerful.  The last code section shows the set up and then one line call to the sort method.  The sort call is highlighted.

List<EmployeeData> empDataList = new List<EmployeeData>();
EmployeeData empData = new EmployeeData() { FirstName = "John", LastName = "Smith", Salary = 35000 };
empDataList.Add(empData);
empData = new EmployeeData() { FirstName = "Barbara", LastName = "Smith", Salary = 38000 };
empDataList.Add(empData);
empData = new EmployeeData() { FirstName = "David", LastName = "Bell", Salary = 27500 };
empDataList.Add(empData);
empData = new EmployeeData() { FirstName = "Larry", LastName = "Bigwig", Salary = 240000 };
empDataList.Add(empData);

// Add total record
empData = new EmployeeData() { LastName = "Total", IsTotalRecord = true };
foreach (EmployeeData empItem in empDataList)
  empData.Salary += empItem.Salary;
empDataList.Add(empData);

// Sort List
SortDataList<EmployeeData, EmployeeSort>(empDataList, "DisplayLastName", true);

// Show Results
foreach (EmployeeData empItem in empDataList)
  Console.WriteLine("{0}{1}, Salary: {2}", empItem.IsTotalRecord ? string.Empty : "Name: ",
    empItem.DisplayFullName, empItem.DisplaySalary);

The out of the program is shown below.

Download Solution Files

Tags:

Leave a Reply


*