Tuesday, February 12, 2008

It frequently happens that you wish to enrich a valuetype with more data, most often the properties in your domainmodel. In this post I will present one way you could achieve that. But first, let's look at why you would want such a thing, then look at how you would go about this normally and finally look at another approach.

Why metadata about a property

Let's say you have a domainobject 'Person' like so:

    public class Person
    {
        public string Name { get; set; }
        public DateTime Birthday { get; set; }
    }

When working with this object, you will set the property 'Name' and property 'Birthday', but while setting these, you have no clue about what really is allowed to go in there. Maybe Name can not accept numerics and in our domain, any Birthday before 1-1-1990 is not allowed. These validationrules are unknown until you actually validate the object, using your preferred mechanism. At that point, there might be an error collection that will state which properties are set to disallowed values. Maybe validation will occur on each change to the data.

That's not really a problem for your businesslogic, but your UI might want to know about these validation rules beforehand! Maybe it could adjust to show a different textbox for 'Name', one which will not let you enter a number, for instance.
Yes, sure you could have put that specialized textbox in there yourself, but that means you will have to adjust your UI views when businessrules change. I would much rather just place a generic control, bind it to my property and let it decide for itself how best to present the data:

<MetaDataEditor Content="{Binding Path=Name}" />  <!-- this shows an alphanumerical textbox -->

So, why metadata: it allows you to optimize beforehand and simplify your UI.

Freaky
Oh, or maybe you want to give a 'friendly name' to the property. That friendly name could be presented in the UI as the label or used in error messages.
How do you go about this normally

(and I say 'normally' not in a bad way, it might still be the best way, depending on your situation).

You would define some interface:

    interface IMetadataProvider
    {
        Metadata GiveMetadataForProperty(string propertyName);
    }

and implement that on Person. Now, when you want your metadata object, you just get to the object on which the property is defined, and ask for it. Easy.

Metadata<T>

What I do not like about an interface, is the fact that you have separated the metadata from the property itself. There now is a method in my domainobject that will take a string (ouch) and probably go through a long list of  'if(propertyName == "???")' or switch/case statements until it gets to the correct name and then create or return a metadata object. That's a great deal of hooking up you need to do, and when passing strings, your domainmodel just became less easy to refactor.
When my magical MetaDataEditor needs the metadata, it will have to somehow find it by traversing from the bound property to it's parent and cast that to IMetadataProvider.

Oh, bad boy, don't even think about using reflection to magically connect the passed string to a metadataobject!

That is why I am experimenting with a little class, I like to call Metadata<T>.

    public class MetaData<T>
    {
        public T InnerValue { get; set; }
        .... goodness inside
    }

This way, you need to define a property on your businessobject like so:

private MetaData<string> name = new MetaData<string>();

public MetaData<string> Name
{
    get
    {
        return name;
    }
    set
    {
        name.InnerValue = value.InnerValue; 
    }
}

This way, you can rest assured the original metadata object is never discarded, but only the innervalue is changed.

We could work with the property like so (p is an instance of Person):
p.Name = new MetaData<string>("foo");

That's not easy, but we can also say:
p.Name.InnerValue = "foo";

Better. We can do one better though, by creating a few implicit operators on the metadataobject:

public static implicit operator T(MetaData<T> source)
{
    return source.InnerValue;
}

public static implicit operator MetaData<T>(T source)
{
    IMetaData md = source as IMetaData;
    if (md != null)
    {
        IConvertible convertible = md.InnerValue as IConvertible;
        if (convertible != null)
        {
            T converted = (T)convertible.ToType(typeof(T), null);
            return new MetaData<T> { InnerValue = converted };
        }

        throw new NotSupportedException();

    }
    else
    {
        return new MetaData<T> { InnerValue = source };
    }
}

This means, we can now use the property as follows:

p.Name = "foo";

The string "foo" can be translated to a MetaData<T> and that is going into the property-setter. Then, the setter will take the innervalue of that foo-metadataobject and use it to set it's own innervalue.

string importantname = p.Name   will work too.

One important note though: this might be misleading to your developers. They can not do p.Name.ToCharArray(), because p.Name really is not a string.
Or p.Car.Color. You would have to do p.Car.InnerValue.Color.

Now, whether you go with the implicit operators or not, you want to ease databinding especially. For that, a typeconverter can be used.

A typeconverter can be attached to a class by the use of an attribute, like so: [TypeConverter( typeof(MetadataForDatabindingConverter))]

The converter needs to inherit from TypeConverter. Let's implement one.

  1 public class MetadataForDatabindingConverter : TypeConverter
  2 {
  3     /// <summary>
  4     /// keep the real type of the metadata innervalue. Since we need it when we convert back to our metadata
  5     /// object.
  6     /// </summary>
  7     private Type databindingRealType;
  8
  9     public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
10     {
11         if (sourceType.Equals(typeof(string)))
12             return true;
13         else
14             return false;
15     }
16
17     public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
18     {
19         if (destinationType.Equals(typeof(string)))
20             return true;
21         else
22             return false;
23     }
24
25     public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value)
26     {
27         // ugly but necessary: when databinding to a ui control, it's likely that you want to go to a string representation
28         // when the ui control (textbox?) was changed, the incoming value is a string. I have not yet found a way to
29         // find out what the target really wants (for instance an MD<int>).
30         // This hack works fine
31
32
33         Type realtype = value.GetType();
34         if (databindingRealType != null)
35             realtype = databindingRealType;
36
37 
39         // or just do a switch on the type and create the correct type
40         //if(value is string)
41         //    return (IMetaData) new MD<String> { InnerValue = (string)value };
42
43         // because I don't feel like implementing the above statements.. bla!
44         Type d1 = typeof(MetaData<>);
45         Type constructed = d1.MakeGenericType(new Type[] { realtype });
46         IMetaData instance = (IMetaData)Activator.CreateInstance(constructed);
47
48         TypeConverter converter = TypeDescriptor.GetConverter(realtype);
49         converter.ConvertFrom(context, culture, value);
50
51         instance.InnerValue = converter.ConvertFrom(context, culture, value);
52
53         return instance;
54     }
55
56     public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType)
57     {
58         // so, this is a metadata object, and we are going to be converting the innervalue to a string (for textboxes etc).
59
60         IMetaData md = value as IMetaData;
61         if (md != null)
62         {
63             databindingRealType = md.InnerType;
64
65             TypeConverter converter = TypeDescriptor.GetConverter(databindingRealType);
66             if (converter.CanConvertTo(destinationType))
67             {
68                 return converter.ConvertTo(context, culture, md.InnerValue, destinationType);
69             }
70         }
71
72         throw new NotSupportedException(String.Format("Conversion of {0} to type {1} is not possible.",value.ToString(), destinationType.Name.ToString() ) );
73     }
74
75     public MetadataForDatabindingConverter()
76     {
77
78     }
79
80 }
 

A typeconverter should override the CanConvertFrom/To methods to indicate if it is able to convert between certain types. Our UI will present data as strings (textbox), so I have opted to only convert to and from strings.

Line 7 keeps a variable where the 'real type' will be put. The databinding engine does not give much information during the call to convert. So if we have a MetaData<int> and bind that to a textbox, the ConvertFrom only gives us information about the value being set (the string "1234"). How do we know we have to convert to MetaData<int> instead of MetaData<string>?
Well, turns out we do know at an earlier conversion that always happens: converting our metadata to a string that will be put into the textbox. I cache the 'realtype' at that moment. (I'm not happy with that solution, if you know how to get rid of the obvious codesmell there, leave a comment!).

The ConvertTo method is easy enough to not have to describe here, but in the ConvertFrom, we do have to jump through some hoops. I've opted to create a metadata<T> with reflection, using the correct type for T. Then, convert the passed in string to the correct type.

This works brilliantly and it allows you to bind like this:

<TextBox Text="{Binding Path=Name}" />

(When it is time to navigate through a property, you will have to go through InnerValue again though, like {Binding Path=Car.InnerValue.Color} .)

Microsoft's latest approach: IDataErrorInfo

The WPF team has invented dependency properties as another way to do something similar (for different reasons though). You obviously do not want to use dependency properties in your domain model.

The recently introduced IDataErrorInfo has the following definition:

        #region IDataErrorInfo Members

        public string Error
        {
            get { throw new NotImplementedException(); }
        }

        public string this[string columnName]
        {
            get { throw new NotImplementedException(); }
        }

        #endregion

I hate that.

Especially the name of the argument 'columnName'. Drives me mad. My business object is a person or a car, not a databasetable. F*ck off.

Besides, what if you want to do something useful with an index on your object? Madness, I tell you!

Conclusion

It's annoying that you have to go to the innervalue property to get to the real value you are interested in. Implicit operators make this a lot more transparent but possibly confusing. Besides, there is a tiny performance loss here.

However, you can do great stuff with such a setup. Let the metadataobjects implement INotifyPropertyChanged as well and use them in pipelines (more on this later), query validation rules from it without having to think about getting to the object that holds the property, and more.

What do you think?

Tuesday, February 12, 2008 10:26:04 PM (Romance Standard Time, UTC+01:00)
First, this is what attributes are for. Attributes have the advantage of not complicating your code, as evidenced by the InnerValue caveats in this post.

Second, the complaint about the "columnName" parameter is valid, though not really anything like a show stopper.

Third, the complaint about the interface having an indexer... so what? That's what explicit implementation is for, and frankly, I wouldn't ever use this interface implicitly.
wekempf
Wednesday, February 13, 2008 12:28:16 AM (Romance Standard Time, UTC+01:00)
Stupid me for not thinking about explic implementation. I was still to misty eyed over the use of the 'columnName' parameter.

For me, attributes have never been able to do it all. As evident by the fallback scenario the validation block hands you. Attributes can only be configured so far, and in my experience, business rules are always trickier than the simple [Alphanumeric_rule] that I mention in the post. It gets out of hand _fast_.


Ruurd
Comments are closed.