AvocadoSoftware.com

Software For Hardcore Developers
Welcome to AvocadoSoftware.com Sign in | Join | Help
in Search

Derick Baileys old blog archives - go to derickbailey.com for new contents

Implementing Find and FindAll methods in .NET 2.0 Collections

In .NET 1.x, to implement a "Find" or "FindAll" method, you were required to use the IComparer interface and provide a custom object that would do the comparison and tell the system whether or not the two objects were the same. This interface would not only allow you to find two objects that are the same, but also allow you to sort the objects. This is a nice ammount of functionality for one interface, but can be somewhat overwhelming to implement properly.

In .NET 2.0, we now have the System.Predicate generic delegate, which gives us the same basic functionality to find two objects that are equal - but in a slightly different manner.

The method signature for a Predicate tells us that we recieve an object of type <t> and return a boolean value. This does not make for a very flexible method signature, until you put the use of generics in place - with this, we can essentially create any boolean test that we need, based on any criteria that we need, provided our criteria is wrapped up into a single object. In the case of a "Find" or "FindAll" method, the parameter being passed in will always be the object that we are performing the test on.

Take a look at the System.Array type, and more specifically at it's Find and FindAll methods, and notice that it makes use of the System.Predicate generic delegate. When you call the Find or FindAll method, the System.Array will utilize it's built in functionality to call the Predicate, passing in each one of the objects in the collection, and expecting to be told whether or not that object is the one being searched for.

Next, consider a common use for Find methods - the IBindingList interface. This interface is used to provide data binding capabilities of your objects, for Windows Forms controls and applications and includes a Find method. This method expects two parameters, a PropertyInfo object, pointing to the property being searched, and a value object, containing the actual find to find. Most collections that I've run across in my development experience implement the same sort of Find and/or FindAll method, expecting a PropertyInfo and Value parameter. This provides for a very flexible search - search any property of the objects being held in the collection, and find the specified value.

Now put 1 and 1 together, and we can easily come up with a solution that equals 2 - implement a Predicate based search, using an Array, and provide a Find and FindAll base method that can find any object in your collection. For this example, we'll create our own base collection object that implements a generic Find and FindAll method, allowing us to implementing the specific methods in a derived class.

Start by creating a new abstract class, that implements the IList<t> interface. This will give us all the method signatures that we need for a custom collection, including a lot of functionality. However, you can implement any interface you wish, at this point, or none at all.

  public abstract class MyCollectionBase<t>: IList<t>

  {

 

    #region vars

 

    /// <summary>

    /// internal object list.

    /// </summary>

    private ArrayList _arr = new ArrayList();

 

    #endregion

 

Notice that the class makes use of a prive ArrayList object. This is the actual storage mechanism used for the custom collection. Take a few minutes to forward all of the IList (and other Interfaces that IList implements) methods to the Arraylist:

    public int IndexOf(t item)

    {

      return _arr.IndexOf(item);

    }

etc, etc.

Now for the implementation of the Find methods. First, we'll need a storage mechanism to hold on to the PropertyInfo and Value objects. Create a private class, internal to the collection, and provide fields to store the objects in question.

    private class FindPropertyValue

    {

      public PropertyInfo prop;

      public object value;

      public FindPropertyValue(PropertyInfo prop, object value)

      {

        this.prop = prop;

        this.value = value;

      }

    }

Next, provide an implementation of the Find and FindAll methods that we are looking for

    /// <summary>

    /// find the first available object with the specified property set to the specified value

    /// </summary>

    /// <param name="prop"></param>

    /// <param name="value"></param>

    /// <returns></returns>

    protected t Find(PropertyInfo prop, object value)

    {

      //store the property and value to use.

      _findProp = new FindPropertyValue(prop, value);

      //create an array of the items

      t[] arr = GetArray();

      //perform the find

      return Array.Find<t>(arr, FindPredicate);

    }

 

    /// <summary>

    /// find all available objects with the specified property set to the specified value

    /// </summary>

    /// <param name="prop"></param>

    /// <param name="value"></param>

    /// <returns></returns>

    protected t[] FindAll(PropertyInfo prop, object value)

    {

      //store the property and value to use

      _findProp = new FindPropertyValue(prop, value);

      //create an array of the items

      t[] arr = GetArray();

      //perform the find

      return Array.FindAll<t>(arr, FindPredicate);

    }

Now for the use of the "_findProp" variable - this is the FindPropertyValue object that stores our PropertyInfo and value objects, when we call the Find and FindAll methods. The FindPredicate method makes use of this storage object, to check the value of the object being examined against the value being searched for.

    /// <summary>

    /// the System.Array used for finding items.

    /// </summary>

    private t[] _findArr;

Then provide the "FindPredicate" method. This method makes use of the FindPropertyValue object. It retrieves the value of the property for the object being examined, and compares it to the value being searched for.

    /// <summary>

    /// the predicate used to determine if the object in question is the same as the object being requested.

    /// </summary>

    /// <param name="obj"></param>

    /// <returns></returns>

    private bool FindPredicate(t obj)

    {

      //check the value of the item in question, against the value stored in the findprop class

      return _findProp.prop.GetValue(obj, null).Equals(_findProp.value);

    }

Also notice the use of a "GetArray()" method in the Find and FindAll methods - this is done to simplify and optimize the process of turning the private ArrayList into an actual Array.

    /// <summary>

    /// whether or not the core collection has changed

    /// since the last time we checked.

    /// </summary>

    private bool _hasChanged = true;

 

    /// <summary>

    /// the System.Array used for finding items.

    /// </summary>

    private t[] _findArr;

 

    /// <summary>

    /// retrieves an array for finding

    /// </summary>

    /// <returns></returns>

    private t[] GetArray()

    {

      if (_hasChanged)

      {

        _findArr = new t[_arr.Count];

        _arr.CopyTo(_findArr);

        _hasChanged = false;

      }

      return _findArr;

    }

All said and done, we now have a base collection class that is ready to be searched. Create an object that implements a few properties - it does not matter what the type of the properties are. We can implement any value type, or reference type for the search. For this example, we'll keep it simple and use a couple of value types.

  public class MyObject

  {

    public enum MyEnumValues

    {

      Value1 = 0,

      Value2 = 1,

      Value3 = 2

    }

 

    private int _id = 0;

    private string _name = string.Empty;

    private MyEnumValues _value = MyEnumValues.Value1;

 

    public int ID

    {

      get { return _id; }

      set { _id = value; }

    }

 

    public string Name

    {

      get { return _name; }

      set { _name = value; }

    }

 

    public MyEnumValues Value

    {

      get { return _value; }

      set { _value = value; }

    }

 

    public override string ToString()

    {

      return string.Format("{0}: {1}: {2}", ID.ToString(), Name, Value.ToString());

    }

 

  }

Now implement a collection of MyObject, using our base class.

  /// <summary>

  /// specific implementation of my generic collection base

  /// </summary>

  public class MyObjectCollection: MyCollectionBase<MyObject>

  {

To create the specific Find methods, we'll need to store references to the properties that we want to search, from the MyObject type.

 

    /// <summary>

    /// store a reference to the "ID" property of MyObject

    /// </summary>

    PropertyInfo propId = typeof(MyObject).GetProperty("ID");

 

    /// <summary>

    /// store a reference to the "Name" property of MyObject

    /// </summary>

    PropertyInfo propName = typeof(MyObject).GetProperty("Name");

 

    /// <summary>

    /// store a reference to the "Value" property of MyObject

    /// </summary>

    PropertyInfo propValue = typeof(MyObject).GetProperty("Value");

Now we can implement the actual Find methods, with one line of code each.

    /// <summary>

    /// Find MyObject in the collection with the specified ID value

    /// </summary>

    /// <param name="ID"></param>

    /// <returns></returns>

    public MyObject FindById(int ID)

    {

      return base.Find(propId, ID);

    }

 

    /// <summary>

    /// Find MyObject in the collection with the specified Name value

    /// </summary>

    /// <param name="Name"></param>

    /// <returns></returns>

    public MyObject FindByName(string Name)

    {

      return base.Find(propName, Name);

    }

 

    /// <summary>

    /// Find MyObject in the collection with the specified Value value

    /// </summary>

    /// <param name="Value"></param>

    /// <returns></returns>

    public MyObject[] FindByValue(MyObject.MyEnumValues Value)

    {

      return base.FindAll(propValue, Value);

    }

Notice that the FindByValue method is using the protected FindAll method to return an array of MyObject. The FindAll method returns all objects that have the specified value, in the collection. The Find method, on the other hand, returns the first object found that has the specified value, in the collection.

You now have a complete Object and Collection that fully implement Find capabilities. To test this functionality, a simple WinForms application will work.

Create a Windows Form and add two textboxes and a combo box onto the form. Place a Button next to each of these three items, and fourth read-only text box on the screen for search results.

Bind the drop list to the MyEnumValues enumeration. The first text box will be used for searching against the name field, and the second will be used to search against the ID field.

Create a method that will fill your collection with a number of objects - for this exampe, we want to show off the speed and efficiency of the search, so we'll create 10,001 objects in the collection. We'll also add a bit of logic to the creation, to ensure we get all of the enum values used.

    private void FillCollection()

    {

      for (int n = 0; n <= 10000; n++)

      {

        MyObject obj = new MyObject();

        obj.ID = n;

        obj.Name = string.Format("Name-{0}", n.ToString());

        if (n < 4000)

          obj.Value = MyObject.MyEnumValues.Value1;

        if (n > 3999 && n < 7000)

          obj.Value = MyObject.MyEnumValues.Value2;

        if (n > 6999)

          obj.Value = MyObject.MyEnumValues.Value3;

        _col.Add(obj);

      }

    }

Now bind the three buttons to methods, and perform the search that is appropriate to the input that they sit next to.

    private void btnFindByName_Click(object sender, EventArgs e)

    {

      //find by the name property

      MyObject obj = _col.FindByName(txtname.Text);

 

      //display the results

      if (obj != null)

        txtResults.Text = obj.ToString();

      else

        txtResults.Text = "Not Found.";

    }

 

    private void btnFindById_Click(object sender, EventArgs e)

    {

      int val = 0;

      int.TryParse(txtID.Text, out val);

 

      //find the by the ID property

      MyObject obj = _col.FindById(val);

 

      //display the results

      if (obj != null)

        txtResults.Text = obj.ToString();

      else

        txtResults.Text = "Not Found.";

    }

 

    private void btnFindByValue_Click(object sender, EventArgs e)

    {

      MyObject.MyEnumValues value = (MyObject.MyEnumValues)System.Enum.Parse(typeof(MyObject.MyEnumValues), (string)cboValue.SelectedItem);

 

      //find all of the items by the value property

      MyObject[] list = _col.FindByValue(value);

 

      //display the results

      if (list != null)

      {

        StringBuilder sb = new StringBuilder();

        foreach (MyObject obj in list)

        {

          sb.Append(obj.ToString());

          sb.Append(Environment.NewLine);

        }

        txtResults.Text = sb.ToString();

      }

      else

        txtResults.Text = "Not Found.";

    }

Run the application and do some searches. You'll notice that even with 10,001 objects in the collection, the find methods return their results before you even know that you finished clicking the button. Even with the massive FindByValue search return 3,000+ objects, it takes longer to display this informtion in the results screen than it does to do the actual search.

The System.Predicate delegate is a powerful feature in .NET 2.0, and when used properly, can provide a quick implementation of search functionality for your custom collections. But don't stop there - you can easily modify the base collection to accept multiple PropertyInfo and Value parameters, and have the Find and FindAll methods search against mutliple properties of an object, for an even finer grain of detail in your searches. I'll leave that code for you to imagine, though.

If you are interested in the full source code for this example, and the sample WinForms application, you can Click Here To Download it.

 

Published Sunday, July 02, 2006 5:10 PM by dredge
Filed Under: , , ,
New Comments to this post are disabled

This Blog

Post Calendar

<July 2006>
SuMoTuWeThFrSa
2526272829301
2345678
9101112131415
16171819202122
23242526272829
303112345

Advertisement

News

this is my old blog archives - go to http://derickbailey.com for updates

Syndication

Advertisement

Powered by Community Server, by Telligent Systems