So, you're using the SDK, and it turns out that you need to store some data, and there isn't a built-in type that stores what you need. What do you do?

The SDK provides a way for you to create your own custom type. There's an important limitation to this technique that I'll touch on later, so make sure to read all the way through. I would tell you now, but that would ruin the suspense.

Creating the class

First of all, you'll need to create the class. I'm going to be using a simplified version of a custom class that I created, without all the niceties like properties, validation logic, etc.  

using Microsoft.Health;
using Microsoft.Health.ItemTypes;

public class PeakFlowZone: HealthRecordItem
{
    private double m_redOrangeBoundary;
    private double m_orangeYellowBoundary;
   
private double m_yellowGreenBoundary;

    private HealthServiceDateTime m_when;

    public PeakFlowZone() : base(new Guid("a5033c9d-08cf-4204-9bd3-cb412ce39fc0"))
    {
    }
}

The two important features of the class are that it derives from HealthRecordItem - as all good custom types should - and that it uses specifies a specific Guid in the constructor. This is the magic "custom type guid", and you need to use that exact one.

Okay, so that seemed pretty easy. Next, we'll need to write the code to serialize and deserialize the data. This works a little like .NET serialization.

The custom data type XML format

The platform expects that you conform to a specific XML schema when you store your data. For our class, here's the way it needs to look:

<app-specific>
  <format-appid>MyAppName</format-appid>
  <format-tag>PeakZone</format-tag>

  <when>
    <date>
      <y>2007</y>
      <m>8</m>
      <d>7</d>
    </date>
    <time>
      <h>0</h>
      <m>0</m>
      <s>0</s>
    </time>
  </when>

  <summary />

  <PeakZone RedOrangeBoundary="3" OrangeYellowBoundary="4" YellowGreenBoundary="5" />

</app-specific>

The format-appid and format-tag elements are for your use - stuff whatever you want in there. When is the timestamp for the element, and summary is the summary text.

Those elements are required. The format of the xml that you actually use to store your data is up to you.

The XML Methods

Now that we know what the format is, we need to write two methods. Here they are:

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

    XPathNavigator when = navigator.SelectSingleNode("when");
    m_when =
new HealthServiceDateTime();
    m_when.ParseXml(when);

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

    XPathNavigator peakZone = navigator.SelectSingleNode("PeakZone");
    peakZone.MoveToFirstAttribute();

    for (; ; )
    {
        switch (peakZone.LocalName)
       
{
            case "RedOrangeBoundary":
                m_redOrangeBoundary = peakZone.ValueAsDouble;
                break;

            case "OrangeYellowBoundary":
                m_orangeYellowBoundary = peakZone.ValueAsDouble;
                break;

            case "YellowGreenBoundary":
                m_yellowGreenBoundary = peakZone.ValueAsDouble;
                break;
        }

        if (!peakZone.MoveToNextAttribute())
        {
            break;
        }
    }
}

public override void WriteXml(XmlWriter writer)
{
    writer.WriteStartElement("app-specific");
    {
        writer.WriteStartElement(
"format-appid");
        writer.WriteValue(
"MyAppName");
        writer.WriteEndElement();

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

        m_when.WriteXml("when", writer);

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

        writer.WriteStartElement("PeakZone");
        writer.WriteAttributeString(
"RedOrangeBoundary", m_redOrangeBoundary.ToString());
        writer.WriteAttributeString(
"OrangeYellowBoundary", m_orangeYellowBoundary.ToString());
        writer.WriteAttributeString(
"YellowGreenBoundary", m_yellowGreenBoundary.ToString());
        writer.WriteEndElement();
    }
    writer.WriteEndElement();
}

Using The Class

Now, we need to use the class. That's just like dealing with any other data type, with one exception. For the built-in types, the api layer already knows the correspondence between a specific guid and the type that it represents. If a type comes across and the type id is:

3d34d87e-7fc1-4153-800f-f56592cb0d17

then the system knows that it should create a Weight item. I found that value by looking at Weight.TypeId.

In our case, the system doesn't know what to do with our custom type yet, so we need to register it. We do that through the following bit of code:

ItemTypeManager.RegisterTypeHandler(new Guid("a5033c9d-08cf-4204-9bd3-cb412ce39fc0"), typeof(PeakFlowZone), true);

Note that we're using the magic "custom data type" guid again, and then we're telling it the type of the data. The last parameter tells it not to worry if there's already a type registered for this guid, just do whatever we said... 

Caveats and Limitations

The biggest caveat is that there is currently only support for one registered custom type per application. So, once you've defined that PeakFlowZone type, you're through.

There are a couple of ways to get around this limitation.

First, you could introduce a "invalid data" concept for all the custom types that you wrote. The ParseXml() method would look at the format-tag attribute, and if it wasn't a PeakZone tag, it would just set the InvalidData flag. Then, before you do the query, you would register whatever type you cared about, and then filter out the instances with invalid data (which are really valid, just not the type you wanted).

Another option is to build a "custom data type" wrapper that does this sort of thing for you. The wrapper type is the one that would be registered, and it would know how to create the appropriate type based on the format-tag setting. A query would then return a list of the wrapper types, and you'd need to drill into them to get the values you wanted out.

I think the second approach is preferable, as it avoids having to get the registration correct, but it does involve a bit more plumbing.