Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

C# 2.0 FlexKeyCollection

0.00/5 (No votes)
29 Sep 2004 1  
Using C# 2.0 Generics for Flexible Business Object Collection

Introduction

.NET version 2.0 will be adding some new features including Generics. Having spent some time using the C++ STL and Microsoft's ATL library, I was interested in learning the details of .NET Generics when the version 2.0 Beta SDK became available. I decided to build a Business Object collection class using Generics that I could use in future projects (and also avoid wasting time on the typical simplistic examples. ;-) I will attempt to describe the results of this work.

Background

Generics can be complex and confusing if you haven't spent time much time working with them. On the other hand, they can be very powerful. If you don't know what Generics or C++ Templates are, then another easier example might be appropriate to get started. If you want to dive into the "deeper end" then come along. This is NOT everything you may need to know about C# Generics, only a brief review of a few features and a real example of something that you might find useful.

My idea of a "Business Object" is a state holding class modeling some business entity. I typically prefer using a more Object Oriented design for building applications as opposed to using ADO DataSets to store "business objects". For this review I will use a Customer object as an example that would contain Name, Address, PhoneNumber, ... as you would expect. Also, a relational database is always part of my projects, so I want some features that will work well with a database persistence layer.

The FlexKeyCollection General Design Targets

The FlexKeyCollection is designed to store typical business objects such as our Customer example. I have divided up the collection requirements into the following general categories. Please post a note if you have features that you use that are missing from the FlexKeyCollection. Since this is built on the Beta SDK, I hope to provide a revision, once .NET version 2.0 is released.

Persistence Interaction (Database Retrieval/Update/Insert/Delete):

  • Check all objects in collection are "Valid".
  • Persist objects that have been changed (Update)
  • Update object Key once new objects have been inserted (Insert).
  • Remove objects from collection once they have been deleted from the database.(Delete)

GUI Interaction:

  • Display collection using various sort orders.
  • Display filtered collection elements.
  • Retrieve collection object by Key for editing.

Definition and Instantiation

Starting at the beginning, let's review how to define a new collection from the FlexKeyCollection. The class definition is shown below:

public class FlexKeyCollection<K, V> : 
  System.Collections.Generic.IEnumerable<V>

K (Key) and V (Value) are type parameters that you must specify when creating a new FlexKeyCollection.

Starting with K (Key) first. A Customer will typically have a unique CustomerID value. I frequently use database auto generated integer values for this. What if you use String? Double? GUID? or some other datatype for your CustomerID? K above is the Generic type parameter which allows the collection user to decide what data type the Customer Key will be. For our example, I will use int. Our V (Value) is the Customer object we will store. So to create a new FlexKeyCollection to contain our Customers we would write:

FlexKeyCollection<int, Customer> _custCollection;

Now that we have our collection defined and the Key and Value types specified, we need to construct or instantiate the collection. The FlexKeyCollection constructor is show below.

public FlexKeyCollection(Key<K, V> del)

Things get a little messy here, but stay with me, it's worth the effort. Key<K, V> del from the constructor is a Generic delegate defined as:

public delegate K Key<K, V>(V value);

This is basically a function pointer that you, the user of the collection, provide to the constructor. The collection will use this whenever you Add a new Customer to the collection to look up its Key. You can use any function you like, as long as it conforms to the delegate above. So, for example:

Suppose our Customer class has a property CustomerID that returns an integer. The Key delegate definition takes a V value parameter, and returns a K value. From above, K is int, and V is Customer, so our function must look like:

public int Foo(Customer value)
Notice that I have named the function Foo. The name of the function doesn't matter. So let's finish our function with a better name.
public int CustomerKey(Customer value){
    // return unique Customer Key

    return value.CustomerID;
}

Now that we have our delegate function, let's finish the FlexKeyCollection constructor call.

_custCollection = new FlexKeyCollection<int, Customer>(
  new Key<int, Customer>(CustomerKey));

Whew! It looks messy and confusing at the start, but, with a bit of practice, reading and writing Generic method calls will get easier and pay great dividends. There is one more thing that I want to add that will take this one step further. .NET 2.0 adds a new feature called Anonymous blocks. In the above example, we were forced to define a new function to be used as our Key delegate. This would mean adding this function to our Customer class. In order to avoid changing all of our business object classes to add a Key delegate function, let's use an Anonymous block. With this technique, we don't need to add functions to all our business object classes. The new FlexKeyCollection constructor call would look like this:

_custCollection = new FlexKeyCollection<int, Customer>(
  delegate (Customer cust){return cust.CustomerID;});

Here instead of creating a new Key delegate that points to our CustomerKey function, we simply simply add the Anonymous block that receives a Customer parameter and returns the CustomerID key value. Simple, right? I spent some time trying to decipher this and haven't figured out the logic behind writing an Anonymous delegate this way, but it works! A few questions may come to mind.

  • We know that a Key delegate is required, but this Anonymous block does not specify the delegate name "Key" anywhere?
  • The Key delegate requires that we pass a V, in our case a Customer. OK, but it also requires that we return a K or integer. Nowhere in the Anonymous definition is the return type specified?

Now that we have our collection defined and constructed, we need to add some Customer objects. All of the work that came before makes adding customers easy.

_custCollection.Add(new Customer());

Since the collection knows how to retrieve the key from Customer (using the Key delegate passed to the constructor), adding is simple. One important consideration is that the Key must be unique. This shouldn't be a problem with a database, where CustomerID has a unique constraint defined. One problem you will encounter is creating new customers that haven't been persisted to the database and have not been assigned a unique Key. One solution is to use a decrementing counter that sets each new CustomerID number to counter--. With most databases, auto incrementing ID numbers start with 0 or 1, so starting at -1 and decrementing will insure all CustomerID's are unique and do not conflict with existing database records. This is a large topic, so I won't go into it any further, but we will revisit this later when we consider persisting our Customer collection to the database.

We will be using Anonymous delegates again with Sorting, Filtering, and other collection methods, so having a good understanding of this will help you use the remainder of the collection effectively.

Sorting

If you never need to Sort or Filter your collection, then the .NET 2.0 Framework class Dictionary<K, V> class might be more suitable. If you use Grids or ListBox controls to display groups of objects, then typically clicking the column header will sort the collection based on that column's values. This is something that Dictionary<K, V> does not provide and the FlexKeyCollection can handle easily. Sorting and filtering in the FlexKeyCollection is accomplished using integer array indexes similar in some ways to database indexes. If you are interested in the details, then look at the FlexKeyCollection Sort, Filter, and Enumeration methods.

IMPORTANT NOTES:

  • Sorting and filtering changes the Enumeration of the collection ONLY. So if you call the Sort() method, all subsequent foreach(...) enumerations will return objects in sorted order. Calling Filter() method will return only filtered objects from enumeration. The this[K key] collection indexer will return any object in the collection regardless of sorts or filters.
  • Sorts and Filters are automatically removed when Add(), or Remove() methods are called. This is because the sort and filter indexes are invalidated due to missing or added objects. In order to reapply a Sort or Filter after Adding or Removing objects, call the Sort()/Filter() method again.

Gory Details Sorting is accomplished using the Array.Sort() method which uses a QuickSort algorithm. My testing shows that it can sort 1 million integers in less than 1 second on relatively modern hardware. Filtering simply iterates the entire collection testing for pass/fail. Since my business object collections typically contain fewer than 100 items, this should be very fast. If you decide to use this for large numerical calculations, I would recommend testing to make sure the speed is adequate. The Remove() method in particular is not optimized for speed so frequent Add/Remove plus Sort/Filter with very large collections may cause some performance issues.

Now lets look at how we can use the Sorting and Filtering methods. The Sort method looks like this:

public void Sort(Comparison<V> comp)
Again we find delegates! Comparison<V> is part of System; and is defined as:
public sealed delegate int Comparison<V>(V x, V y);

As before, this tells use that the Comparison delegate takes 2 V's or Customers and returns an integer. The return integer must be the same as the IComparable interface where:

  • x < y return -1
  • x = y return 0
  • x > y return +1

Since value types such as string, int, DateTime implement IComparable, we can use this to implement our anonymous Comparison<V> delegate to sort by the Customer.Name property (a string) as follows:

_custCollection.Sort(delegate (Customer x, Customer y)
{return x.Name.CompareTo(y.Name);});

If the property you want to sort by DOES NOT IMPLEMENT IComparable, then you will need to perform the x <=> y comparisons manually instead of using CompareTo(). Just make certain that you return the correct comparison values, or your sorts will not work as expected. Sorting by ANY other property of the Customer object is as simple as changing "Name" in the line above. This means you can sort your collection on ANY Customer property you like, such as when an end user clicks on the column header in a Grid or ListBox, call Sort() with whatever property they click and reload the list.

Reversing the sort is a simple call to ReverseSort(). This will toggle the current sort forward and backward.

Filtering

Filtering follows as similar technique but instead of a Comparison<V> delegate, Filtering uses a Predicate<V> delegate that is defined in System as follows:

public sealed delegate bool Predicate<V>(V obj);

Here the Predicate delegate function takes a V Customer parameter and returns a bool. So to filter our Customer collection for customers in North Carolina would look like this:

_custCollection.Filter(delegate (Customer x)
 {return x.State == 'NC';});

Note that enumerating the filtered collection will return ONLY those objects where the Predicate delegate returns true. Since YOU define the filter logic, you can make it as simple or complex as you need. One idea comes to mind regarding persisting our Customer collection to a database. We need to persist new, changed, and deleted Customers so we could create a filter that would give us only New, Changed, or Deleted Customers. This assumes our Customer objects has IsNew, IsChanged, and IsDeleted properties.

_custCollection.Filter(delegate (Customer x)
 {return x.IsNew || x.IsChanged || x.IsDeleted;});

We will take this one step further during a brief discussion of other FlexKeyCollection methods. Also, sorting and filtering at the same time is accomplished by calling SortAndFilter(Comparison<V> comp, Predicate<V> pred) with the same characteristics as Sort and Filter individually.

Sorting and Filtering Improvements?

I started building methods into the FlexKeyCollection that would store Sorts and Filters by a name string. The purpose was to avoid rebuilding the sort/filter indexes on each use. This added a bit of complexity primarily centered around updating stored sorts and filters when Add/Remove methods were used. I decided to keep the FlexKeyCollection "light weight and simple" and left out stored sorts and filters. Since the FlexKeyCollection is intended to store small sets of objects <100, building sorts and filters as they are needed should take neglible time. Let me know if you have a need for this, and I will work to finish it for the "release" version.

Another improvement I was considering was multi-property Sorting. This would allow you to sort our collection of Customers by State and Name. Keeping the FlexKeyCollection "trim" was part of the design, so adding a "heavy" version with additional functionality is possible if there is a need.

FlexKeyCollection Remainder

Most of the other FlexKeyCollection methods should be straight forward once you have a handle on delegates discussed above. There is one other method that I want to review called ActionIf(). The method signature is as follows:

public void ActionIf(Predicate<V> pred, Action<V> act))

We've seen the Predicate delegate before but Action<V> is new. Again, this is a delegate defined in System as follows:

public sealed delegate void Action<V>(V obj);

Hopefully this is getting boring. Basically the Action delegate will be called whenever Predicate delegate returns true. Cool huh! So if we consider our Customer collection persistence again we could write something like:

_custCollection.ActionIf(delegate (Customer x)
  {return x.IsNew || x.IsChanged || x.IsDeleted;}, 
  delegate (Customer c){c.Save()};);

Basically the one line of code above will check every Customer in our collection for New,Changed, or Deleted and then call the Customer.Save() method on only those Customers! Whoa! The first time I saw this it was a real head scratcher. We have one line of code with 3 end-of-line semi-colons and 2 set of braces! Getting the syntax right for this will take some practice but the dividends are great!

Critical Issues

There is one remaining item we need to review with regard to persisting our Customer collection. Don't skip this section. New Customers in our collection will have "fake" CustomerID values as discussed earlier using the auto decrementing counter. This means that once they are persisted and assigned a "real" ID value from the database, we need to update the CustomerID. Since our collection is indexed by the CustomerID Key, changing that Key value WILL BREAK OUR COLLECTION!. But not to worry, there is a method to correct this. The FlexKeyCollection has a method RebuildKeyIndex(). This method MUST BE CALLED after Key changes. You do not need to call it after every change, but only after all changes are complete. So back to our ActionIf() call from above, we add a call to RebuildKeyIndex() as follows:

__custCollection.ActionIf(delegate (Customer x)
  {return x.IsNew || x.IsChanged || x.IsDeleted;}, 
  delegate (Customer c){c.Save()};);
_custCollection.RebuildKeyIndex();

This assumes that our Customer objects update their CustomerID value during the Save() method. Now our persisted Customer collection has updated Keys and will work normally again. If you've made it this far then you might recall our initial review of the constructor. In it we passed a Key<V> delegate. This delegate is required, in order to rebuild the Key index. You may also wonder why the collection couldn't rebuild the KeyIndex automatically? Well the collection gets no notification when ANY properties of the contained objects are changed. This could be a improvement to the FlexKeyCollection but it would introduce restrictions on the objects that can be stored in the collection.

Conclusions

.NET Version 2.0 is adding some powerful new features like Generics and Anonymous blocks. I hope this article has helped give you a few ideas about how to use these new features. I know I've learned a great deal from putting this together.

The download contains FlexList, FlexKeyCollection, a FlexTest business object class, and a ConsoleTest class used to test the collection as well as a Build.txt file. I used the freely available .NET Version 2.0 SDK located here to build this. Insert standard Beta software disclaimer here :-/

History

  • Initial Beta 9/29/2004

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here