Friday, July 23, 2010

C# Specializations by Traits II


C# Specializations by Traits II


Cat and Mouse populate a Silverlight Listbox

In my previous blog we saw that you seriously can do specialization in C#, using traits classes, extension methods and a directionary. The use case was a bit centered on class (method) specialization.

In this blog we will see that it is equally possible, and probably even more useful, to specialize a generic method for different classes. The system is the same, the samples are more useful.

Use case

Imagine two models (classes), a Cat and a Mouse. They are different, not sharing an interface and not inheriting from a common base class. They are kept simple, just name and ID. On purpose attributes are named differently for Cat and Mouse.

    public class Cat
    {
        public Cat(int i, string n)
        {
            Id = i;
            Name = n;
        }
        public int Id { get; set; }
        public string Name { get; set; }
    }

    public class Mouse
    {
        public Mouse(int i, string n)
        {
            MouseId = i;
            MouseName = n;
        }
        public int MouseId { get; set; }
        public string MouseName { get; set; }
    }

We have two generic collections of them: a List<Cat> and a List<Mouse>. I like the generic collections! We want to add both collections to a Silverlight listbox... of course in a generic way. That is not trivial, but with help of previous blog, or more specificly with help of our new "Specialization by Traits" system, we can do it. Note that this example (filling a list box) is a bit inspired on this page where a dropdown should be specific for a model.

We will use the traits system which helped us so much in previous blog to reach our goal. And there are more traits-ways to do it.

Traits 1: Common Interface

First we create a traits class which tries to create a common interface, getting the Id and the Name for both classes in the same way.

namespace Traits
{
    public interface AnimalTraits<T>
    {
        int GetId(T obj);
        string GetName(T obj);
    }

    public class CatTraits : AnimalTraits<Models.Cat>
    {
        public int GetId(Models.Cat cat) { return cat.Id; }
        public string GetName(Models.Cat cat) { return cat.Name; }
    }
    public class MouseTraits : AnimalTraits<Models.Mouse>
    {
        public int GetId(Models.Mouse mouse) { return mouse.MouseId; }
        public string GetName(Models.Mouse mouse) { return mouse.MouseName; }
    }

    public static class AnimalRegister
    {
        private static IDictionary<System.Type, object> register;
        public static void Register<T>(AnimalTraits<T> traits)
        {
            if (register == null)
            {
                register = new Dictionary<System.Type, object>();
            }
            register[typeof(T)] = traits;
        }
        public static AnimalTraits<T> Get<T>()
        {
            return register[typeof(T)] as AnimalTraits<T>;
        }
    }
}


Once we have those classes, we can fill the list using them, using the common interface. For Cats we take the CatTraits, for the Mice the MouseTraits, of course. Or we can let the compiler take them automagically using that traits-register, the Dictionary<Type, object> which can be approached by the typeof function.

        private void fillGeneric1<T>(ListBox lb, IEnumerable<T> animals)
        {
            AnimalTraits<T> traits = AnimalRegister.Get<T>();

            foreach (T animal in animals)
            {
                ListBoxItem item = new ListBoxItem();
                item.Content = traits.GetName(animal);
                item.Tag = traits.GetId(animal);
                animalListBox.Items.Add(item);
            }
        }

        private void button1_Click(object sender, RoutedEventArgs e)
        {
            // (Re)fillGeneric1 our list using the generic way
            animalListBox.Items.Clear();
            fillGeneric1(animalListBox, cats);
            fillGeneric1(animalListBox, mice);
        }

We see that we can fill the listbox easily like this. The fillGeneric1 method is generic! It is specialized per animal.

However, if we want to do anything different for the Cats class than we do for Mice (besides getting ID and name), we have to solve it differently. This is in Traits 3 below. First Lambda.

"Traits 2": Lambda Register

Is this still traits... No traits involved, only lambda. They play the role of the traits. The architecture is similar.

We define a delegate type and create (usually in a singleton) a dictionary delegate-by-type. In this use case we need two delegates (one for ID, one for name) but that is flexible of course. We select here a dictionary of pairs.

We have the same directionary as in the previous examples as a register, mapping from Type to any object (now a delegate-pair). We add a convenience method to register lambda's and to retrieve them using Get<>


using System.Collections.Generic;

delegate int get_id<Animal>(Animal a);
delegate string get_name<Animal>(Animal a);

class LambdaRegister
{
    private static IDictionary<System.Type, KeyValuePair<object, object>> register;

    public static void Register<Animal>(get_id<Animal> gi, get_name<Animal> gn)
    {
        if (register == null)
        {
            register = new Dictionary<System.Type, KeyValuePair<object,object>>();
        }
        register[typeof(Animal)] = new KeyValuePair<object,object>(gi, gn);
    }

    public static void Get<Animal>(out get_id<Animal> gi, out get_name<Animal> gn)
    {
        KeyValuePair<object, object> pair = register[typeof(Animal)];
        gi = pair.Key as get_id<Animal>;
        gn = pair.Value as get_name<Animal>;
    }
}

The register can be initialized, once, like this:

            LambdaRegister.Register<Models.Cat>(p => p.Id, p => p.Name);
            LambdaRegister.Register<Models.Mouse>(p => p.MouseId, p => p.MouseName);

We see: we created the register in the same way. But, again, we don't need any traits here. Then we populate our listbox using the lambda-register

        private void fillGeneric3<T>(ListBox lb, IEnumerable<T> animals)
        {
            get_id<T> gi;
            get_name<T> gn;
            LambdaRegister.Get<T>(out gi, out gn);

            foreach (T animal in animals)
            {
                ListBoxItem item = new ListBoxItem();
                item.Tag = gi(animal);
                item.Content = "L " + gn(animal);
                animalListBox.Items.Add(item);
            }
        }



Traits 3: Specific Goal

We can also create a traits interface which is designed to fill a listbox. It then has a method called (e.g.) Apply, which takes a listbox and adds an item. Or which returns an item. Anyway, the traits classes which (also here) are specific for Cat and Mouse have full access to these classes, and can call any method. In this way they assign the ListBoxItem and return it.

using System;
using System.Windows;
using System.Windows.Controls;

namespace Traits
{
    public interface FillAnimalTraits<T>
    {
        void Apply(ListBoxItem item, T obj);
    }

    public class FillCatTraits : FillAnimalTraits<Models.Cat>
    {
        public void Apply(ListBoxItem item, Models.Cat cat)
        {
            item.Tag = cat.Id;
            item.Content = String.Format("cat: {0}", cat.Name);
        }
    }
    public class FillMouseTraits : FillAnimalTraits<Models.Mouse>
    {
        public void Apply(ListBoxItem item, Models.Mouse mouse)
        {
            item.Tag = mouse; // Note: here the object is tagged to the item, instead of the ID
            item.Content = String.Format("mouse: {0}", mouse.MouseName);
        }
    }
}


The register is be initialized, again, once, in a similar way.
AnimalFillListBoxRegister.Register<Models.Cat>(new Traits.FillCatTraits());
AnimalFillListBoxRegister.Register<Models.Mouse>(new Traits.FillMouseTraits());


We then use it in our code as following:
        private void fillGeneric3<T>(ListBox lb, IEnumerable<T> animals)
        {
            FillAnimalTraits<T> traits = AnimalFillListBoxRegister.Get<T>();
            foreach (T animal in animals)
            {
                ListBoxItem item = new ListBoxItem();
                traits.Apply(item, animal);
                animalListBox.Items.Add(item);
            }
        }

You see, the fillGeneric3 is quite convenient (and generic) now. The traits do the work here.

Traits 3b: Extensions


Finally, as in our previous blog, we can combine these very same traits quite conveniently with extension methods, defined as following:
    public static ListBoxItem ToListBoxItem<T>(this T obj)
    {
        FillAnimalTraits<T> traits = register[typeof(T)] as FillAnimalTraits<T>;
        ListBoxItem item = new ListBoxItem();
        traits.Apply(item, obj);
        return item;
    }        

and used as the following:

        private void fillGeneric4<T>(ListBox lb, IEnumerable<T> animals)
        {
            foreach (T animal in animals)
            {
                animalListBox.Items.Add(animal.ToListBoxItem());
            }
        }

Looking cool.

And yes, the extension method might make use of the lambda register as well, instead of the traits register, of course. No traits needed then. We can make it more beautiful with each step. This is not worked out now.


OK, I again like this system very much. I blogged it yesterday (based on research earlier this month and in April) and today we did need it at our office. We did need it twice!

The Silverlight Application can be seen here and complete sources are here.

In the next blogs we will see more on this:
  • geometry...
  • object relational modelling...
  • and of course there will be more on geometry (Boost.Geometry) soon
Stay tuned!

No comments:

Post a Comment