WPF Data Binding With ICustomTypeProvider

Doing a search for ICustomTypeProvider, you can find plenty of examples on how to use this handy interface to perform dynamic data binding between your business objects and WPF. However, almost all these examples assume you’re ready, willing and able to “adjust” your classes to either implement ICustomTypeProvider or use a base class that does. In this article, I’ll show the benefits of the interface, how you might use it out-of-the-box, and I’ll give an example of how you can take advantage of this interface without changing any code in your business objects.

Doing a search for ICustomTypeProvider, you can find plenty of examples on how to use this handy interface to perform dynamic data binding between your business objects and WPF. However, almost all these examples assume you’re ready, willing and able to “adjust” your classes to either implement ICustomTypeProvider or use a base class that does. In this article, I’ll show the benefits of the interface, how you might use it out-of-the-box, and I’ll give an example of how you can take advantage of this interface without changing any code in your business objects. 

What is ICustomTypeProvider?

This interface has been around for quite some time: it was included in Silverlight 5 in the 2011 timeframe and is recognized by WPF 4.5. It allows implementors to advertise properties (and their types) that are available for data binding. This is useful for cases where your objects’ properties may not be known at compile-time, such as when they’re based off JSON, a dynamic database query, or otherwise have some (or all!) dynamic properties.

Another use case is where you need to pass around a collection of objects where the item type can’t be known at compile-type - much as an untyped System.Data.DataTable allows. Despite trying hard to use generic collections – sometimes we just can’t.

Some may wonder why objects that inherit from DynamicObject (i.e. DLR objects) don’t automatically get the same benefits provided by ICustomTypeProvider. There’s a good article here that explains why: essentially, dynamic objects don’t expose enough type information to let data binding be effective. For example, if I have a dynamic object that exposes a property that looks like an “object,” our domain rules might require it to be an integer (or “int?” if nullable) and data binding wouldn’t know this without something explicit (If it did know the type, it could, for example, disallow entry of letters if the underlying type is an integer).

An Example

If you’re auto-generating columns with WPF DataGrids, this is another situation where you could be relying on “dynamic properties” since you might not know all your columns to express at compile-time. I’ve put together a WPF sample application published here on GitHub that does exactly this: it populates a list of objects based on the contents of a JSON file and binds it to a WPF DataGrid. The JSON content includes both a schema and raw data.

  1. {  
  2.   "Schema": [  
  3.     {  
  4.       "Name""Name",  
  5.       "Type""String"  
  6.     },  
  7. …  
  8.   ],  
  9.   "People": [  
  10.     {  
  11.       "Name""Bob",  
  12.       "Age": 25,  
  13.       "Systolic": 125,  
  14.       "Diastolic": 85,  
  15.       "TestDate""4/25/2018",  
  16.       "Temperature""98.5"  
  17.     },  
  18. …  

The schema tells us all available properties, where actual data may or may not contain all properties. New properties can, therefore, be added in the JSON only: code changes are not required (This could have just as easily come from a database, too, where the schema would be available based on your result set).

Having a schema allows us to establish property types that stand apart from property values. For example, we could have a property called “FollowUpDate” that’s “null” for all rows, but if we added a new row, it should be treated as a DateTime. With no rows with a non-null value, how would we know that it’s a date? A schema tells us.

The code present in TypeProviderBase.cs gives us the ability to

a) act as a base class for any business object,
b) support adding additional, non-CLR properties at run-time,
c) have those properties be typed (not strong-typing at compile-time, but we can query type information at run-time),
d) allow these objects to be bound to user-interface elements such as the WPF DataGrid that understands ICustomTypeProvider.

We use the TypeProviderBase class as base class for this sample business object.

  1. namespace WPF_CustomTypeProvider_Demo.Inherit  
  2. {  
  3.     public class Person : TypeProviderBase  
  4.     {  
  5.         private static int _id = 0;  
  6.   
  7.         public Person()  
  8.         {  
  9.             ID = Interlocked.Increment(ref _id);  
  10.         }  
  11.   
  12.         public int ID  
  13.         {  
  14.             get;  
  15.             set;  
  16.         }  
  17.   
  18.         public string Name  
  19.         {  
  20.             get;  
  21.             set;  
  22.         }  
  23.     }  

We’re assuming that ID is system-managed, and Name is a known property present in the data – everything else is derived solely from the JSON content.

The key line of code that implements our grid data binding is this.

  1. Data1.ItemsSource = Inherit.JsonReader.LoadData<Inherit.Person>();  

The LoadData() function is generic, but also hard-coded to our specific example – all the plumbing here is more to illustrate concept: we’re building an ObservableCollection<T> that can be bound to a WPF DataGrid (the first of two in the sample app), containing multiple “Person” records that have some CLR properties (e.g. Name) and some dynamic properties (e.g. Age),

WPF

Because type information is available for dynamic properties, WPF will properly complain if we’ve entered some invalid data, such as alpha characters in a numeric field.

WPF

To make the demo a little smoother-around-the-edges, I’ve added the use of TargetNullValue and checked for the optional BrowsableAttribute. I do this on the MainWindow.xaml.cs code-behind - there are other ways to do this.

As shown here, edits in the grid properly flow back to the underlying business objects.

It’s fair to note that using a base class like TypeProviderBase doesn’t have to be applied to every object in your business domain – just where the functionality is needed. There are also other ways to accomplish this, using helper methods for example. Let’s look at another option…

An Example Using POCO Classes

It’s obvious in the sample that there’s quite a bit of “helper code” to support ICustomTypeProvider. My answer to this is to abstract much of this in the open-source CodexMicroORM framework. We can, for example, use this as our business object pattern.

  1. namespace WPF_CustomTypeProvider_Demo.NoInherit  
  2. {  
  3.     public class Person  
  4.     {  
  5.         private static int _id = 0;  
  6.   
  7.         public Person()  
  8.         {  
  9.             ID = Interlocked.Increment(ref _id);  
  10.         }  
  11.   
  12.         public int ID  
  13.         {  
  14.             get;  
  15.             set;  
  16.         }  
  17.   
  18.         public string Name  
  19.         {  
  20.             get;  
  21.             set;  
  22.         }  
  23.     }  
  24. }  

Looks like our prior example? The main difference is there’s no inheritance, meaning it likely looks a lot like existing classes you use today in your apps. The framework performs effectively the same operations to get a collection populated and data-bound in WPF. This is evidenced by the second grid we show in the sample app, using CodexMicroORM.

WPF

Looking at the framework example in more detail, we still need to populate a list that’s based on the same JSON data source. It’s doable in just a few lines of code.
  1. public static EntitySet < T > LoadData < T > () where T: classnew()   
  2. {  
  3.     string json = File.ReadAllText(@ "sampledata.json");  
  4.     EntitySet < T > populated = new EntitySet < T > ();  
  5.     populated.PopulateFromSerializationText(json, new JsonSerializationSettings() {  
  6.         SerializationType = SerializationType.ObjectWithSchemaType1AndRows, DataRootName = "People"  
  7.     });  
  8.     return populated;  
  9. }  

Here, we’re using the EntitySet<T> collection type that resembles ObservableCollection<T>, and we’re using a serialization mode that understands the JSON format of this example.

We can bind this list using a single line of code.

  1. Data2.ItemsSource = NoInherit.JsonReader.LoadData<NoInherit.Person>().AsDynamicBindable();  

The AsDynamicBindable() extension method is important as an explicit step: this method lives in the CodexMicroORM.BindingSupport assembly, which targets .NET Framework (4.6.1). (ICustomTypeProvider is .NET Framework, alongside WPF.) The CodexMicroORM framework core targets .NET Standard 2.0 however, and EntitySet<T> is part of that core. AsDynamicBindable() is a bridge that returns an instance of GenericBindableSet, which can be thought of in a similar way to System.Data.DataTable – a non-generic collection type that can hold rows and most user-interfaces can bind “effectively” using ICustomTypeProvider (understand columns, data types, etc.).

Conclusions

Use of dynamic properties may be an edge case for many but knowing you can bind them in a UI might open the door to opportunities you didn’t see before. Beyond WPF data binding, ICustomTypeProvider (along with others such as ICustomTypeDescriptor) can be useful for your own runtime inspection of metadata.

I’ve shown how you can leverage the benefits of ICustomTypeProvider with business objects that may not implement this interface directly. Incidentally, data validation that’s “WPF aware” is also handled by CodexMicroORM in a similar way, through the IDataErrorInfo interface - another topic for another day!

Note that CodexMicroORM version 0.6 was published recently, and I’ve outlined some of the changes here. Some of those changes are directly related to what I covered in a prior C# Corner article, “Indexing In-Memory Collections for Blazing Fast Access,” with the latest version offering improvements. Stay tuned for more!