HealthVault Data Types - a custom data type architecture

In my last post, I showed how to create custom data types, but there's an obvious problem with that approach.

There's only one of them.

That means that when you do a query for your custom type, you get all the custom types that meet your filter.

If your application creates multiple custom types, then it needs a layer that can do the appropriate thing and give you the types that you want.

As is often the case in computer science, we're going to approach it by adding in a layer of indirection. We will write a wrapper object that is the only custom object around as far as the platform is concerned, and that wrapper object will know how to deal with multiple custom objects.

This approach is used in the HealthAndFitness sample - look in the website\app_code\ThingTypes directory to find the full implementations of the code I list here. Go use those instead of using this code.

We start with a base class for our custom objects. It looks a lot like the HealthRecordItem class, but the ParseXml() method is public. We will also use it to hold a bit of magic later...

public class HealthRecordItemCustomBase
{
protected CustomHealthTypeWrapper m_wrapper;

public CustomHealthTypeWrapper Wrapper
{
get { return m_wrapper; }
set { m_wrapper = value; }
}

    public virtual void ParseXml(IXPathNavigable typeSpecificXml) {}
    public virtual void WriteXml(XmlWriter writer) {}
}

The Wrapper property lets us get back to the wrapper for this instance. The usefullness of that will become apparent later (oooo, foreshadowing...).

The wrapper class looks like this:

public class CustomHealthTypeWrapper: HealthRecordItem
{
const string NullObjectTypeName = "NULLOBJECT";

private HealthServiceDateTime m_when;
    private HealthRecordItemCustomBase m_wrappedObject = null;

    public CustomHealthTypeWrapper() : this(null) {}

    public CustomHealthTypeWrapper(HealthRecordItemCustomBase wrappedInstance) :
           base(new Guid(ApplicationCustomTypeID))
{
m_wrappedObject = wrappedInstance;

            // Test for null here because the deserialization code needs to create this object first...

        if (wrappedInstance != null)
{
            wrappedInstance.Wrapper = this;
}
}

    public HealthRecordItemCustomBase WrappedObject
{
        get { return m_wrappedObject; }
        set { m_wrappedObject = value; }
}
}

So, the wrapped object has a reference to the wrapper, and the wrapper has a reference to the wrapped object. We also have a nice constructor to create a wrapper around an object, and when we use that one, it sets both pointers.

It's now time to add some code to the wrapper class to save objects. Here's WriteXml():

public override void WriteXml(XmlWriter writer)
{
    if (writer == null)
{
        throw new ArgumentNullException("null writer");
}

    writer.WriteStartElement("app-specific");
{
writer.WriteStartElement("format-appid");
writer.WriteValue("Custom");
writer.WriteEndElement();

        string wrappedTypeName = NullObjectTypeName;

        if (m_wrappedObject != null)
{
            Type type = m_wrappedObject.GetType();
wrappedTypeName = type.FullName;
}

        writer.WriteStartElement("format-tag");
writer.WriteValue(wrappedTypeName);
writer.WriteEndElement();

        m_when.WriteXml("when", writer);

        writer.WriteStartElement("summary");
writer.WriteValue("");
writer.WriteEndElement();

        if (m_wrappedObject != null)
{
writer.WriteStartElement("CustomType");
m_wrappedObject.WriteXml(writer);
writer.WriteEndElement();
}
}
writer.WriteEndElement();
}

That's essentially unchanged except for two changes. First, if there's a wrapped object, we write out the actual type name, and then when it comes to writing out the custom type data, we delegate that to the wrapped object.

On to ParseXml()

protected override void ParseXml(IXPathNavigable typeSpecificXml)
{
    XPathNavigator navigator = typeSpecificXml.CreateNavigator();
navigator = navigator.SelectSingleNode("app-specific");

    if (navigator == null)
{
        throw new ArgumentNullException("null navigator");
}

    XPathNavigator when = navigator.SelectSingleNode("when");

    m_when = new HealthServiceDateTime();
m_when.ParseXml(when);

    XPathNavigator formatAppid = navigator.SelectSingleNode("format-appid");
    string appid = formatAppid.Value;

    XPathNavigator formatTag = navigator.SelectSingleNode("format-tag");
    string wrappedTypeName = formatTag.Value;

    if (wrappedTypeName != NullObjectTypeName)
{
m_wrappedObject = (HealthRecordItemCustomBase) CreateObjectByName(wrappedTypeName);

        if (m_wrappedObject != null)
{
m_wrappedObject.Wrapper = this;
            XPathNavigator customType = navigator.SelectSingleNode("CustomType");
            if (customType != null)
{
m_wrappedObject.ParseXml(customType);
}
}
}
}

This is also not very different - the big change is that if there is a type name stored in format-tag, we create an instance of that type, find the custom data for it, and then call ParseXml() on that object. Note that it's possible that format-tag isn't a type that we know about, so we have to deal with those cases.

The object instances are created by CreateObjectByName():

public object CreateObjectByName(string typeName)
{
    Type type = Type.GetType(typeName);
    object o = null;

     if (type != null)
{
        if (type.BaseType != typeof(HealthRecordItemCustomBase))
{
            throw new ApplicationException("Custom type not derived from HealthRecordItemCustomBase");
}

o = Activator.CreateInstance(type);
}

    return o;
}

We look up the type to see if it's one we recognize, make sure that it's derived from our base type (avoid lots of weird scenarios that might show up), and then creating an instance of it.

That's the basic scheme. We also provide a helper method to register the custom type:

public const string ApplicationCustomTypeID = "a5033c9d-08cf-4204-9bd3-cb412ce39fc0";

public static void RegisterCustomDataType()
{
    ItemTypeManager.RegisterTypeHandler(new Guid(ApplicationCustomTypeID), typeof(CustomHealthTypeWrapper), true);
}

You'll need to make sure this is called before saving or loading any custom types, or weird things happen.

Custom type XML format

Because the custom type XML is no longer handled by the platform, it can be whatever XML format you want. On the theory that many custom types may migrate to become platform types, I'd suggest using the XML format that the platform likes rather than inventing your own.

Using the wrapper type

The wrapper type is fairly easy to use - instead of saving an instance of the custom item, you create a wrapper around it, and then pass the wrapper to NewItem(), or whatever method you're calling.

When you're querying, there is an extra step. You will get back a HealthRecordItemCollection full of CustomHealthTypeWrapper instances. Some of them will wrap the type you want, some of them some other type, and some of them won't wrap anything. So, you need to write something like the following:

List<MyCustomObject> customItems = new List<MyCustomObject>();

foreach (HealthRecordItem item in items)
{
    CustomHealthWrapper wrapper = (CustomHealthTypeWrapper) item;
    MyCustomObject custom = wrapper.WrappedObject as BPZone;
    if (custom != null)
    {
        customItems.Add(custom);
    }
}

Then you end up with an array that only has the types you care about.

I think that it's possible to modify the query filter so that it will only fetch the custom items of a specific type, but I haven't gotten around to trying it yet.

A final bit of goodness

The scheme as described is a bit unfortunate, in that the type derived from HealthRecordItemCustomBase doesn't have the base class items that are present in the HealthRecordItem. So, if I want to get the Key value, I'm forced to write:

myCustomInstance.Wrapper.Key

and similarly for the CommonData stuff:

myCustomInstance.Wrapper.CommonData.Note

The final bit of goodness is to add a few properties in the HealthRecordItemCustomBase class that forward on requests to the wrapper class, so you can write:

myCustomInstance.CommonData.Note

and have it refer to the information in the wrapper class.