Saturday, January 10, 2009

Advanced Reporting Services: Displaying Parameter Values

In Reporting Services, you can display the values of the Parameters collection by writing code like =Parameters!myParam.Value (or equivalently, =Parameters("myParam").Value). Displaying parameter values on the report itself is a good practice--it gives the end user some context in cases where the report has been printed out or exported.

Things get tricky when your parameters are multi-valued or contain labels. If Parameters!myParam.IsMultiValue is true, =Parameters!myParam.Value will render as "#Error". Also, If your parameter has a Label, you'll probably want to display that instead of the Value.

Instead of having to remember to change a report's parameter textbox every time I modify a parameter, I just reference the following code (copy-paste into Report > Report Properties > Code):

Public Shared Function GetParamValues(ByVal Param As Parameter) As String
 If Param.IsMultiValue Then
  If IsArray(Param.Label) Then
   GetParamValues = Join(Param.Label, ", ")
  Else
   GetParamValues = Join(Param.Value, ", ")
  End If
 Else
  If Not IsNothing(Param.Label) Then
   GetParamValues = Param.Label
  Else
   GetParamValues = Param.Value
  End If
 End If
 GetParamValues = IIF(Len(GetParamValues) > 100, Left(GetParamValues, 100) & "...", GetParamValues)
End Function

The parameter's textbox expression looks like this:

="Last Name: " & Code.GetParamValues(Parameters!last_nm)

Unfortunately, there's no way to get at a parameter's Prompt attribute (e.g., "Last Name") -- the ParameterImpl class only contains int Count, bool IsMultiValue, object Label, and object Value.

Saturday, February 9, 2008

Creating C# Bindings for CCR and CCD

Introduction

ASTM’s Continuity of Care Record (CCR) and HL7’s Continuity of Care Document (CCD) are two different XML schemas designed to store patient clinical summaries. While they are identical in scope (i.e., they contain the same data elements: demographics, medications, labs, etc.), the structures of the two formats are really quite different.

An analog in the Web 2.0 world is the functional overlap between RSS and Atom, the two XML-based syndication formats. Despite having the same purpose, the two formats use completely different XML tags to represent their data. Both have been widely adopted. I suspect the same will be true for CCR and CCD, so I’m not really interested in arguing their relative merits.

Instead, my interest lies in using these formats to shuttle information around between doctors and patients.

One way to facilitate this process is the creation and use of programmer-friendly objects that are bound to particular XML schemas (e.g., CCR and CCD). XML data binding, as this process is known, obviates the need for cumbersome object-to-XML conversion routines.

Here, then, is a quick tutorial on creating C# classes for both the CCR and CCD schemas…

Get the Schemas (*.xsd)

I’m not really sure how Microsoft is getting away with hosting the XSD files for both the CCR and CCD formats (I paid $100 for mine), but they are.

So, download the CCR schema, then download the CCD schema.

Save these files as CCR.xsd and CCD.xsd, respectively.

Massage the CCR.xsd file

The CCR schema seems to have been created without .NET in mind (huh?), so we have to make a few changes for the next steps to work. If you’re only interested in CCD (shame!), you can skip ahead.

Remove maxOccurs attribute from Indications and Directions

The maxOccurs attribute for these two elements causes xsd.exe to create multi-dimensional arrays (e.g., public IndicationType[][] Indications;). Simply delete the maxOccurs attribute as follows:

Find: <xs:element ref="Indications" minOccurs="0" maxOccurs="unbounded"/>
Replace: <xs:element ref="Indications" minOccurs="0"/>

Find: <xs:element ref="Directions" minOccurs="0" maxOccurs="unbounded"/>
Replace: <xs:element ref="Directions" minOccurs="0"/>

Convert name attributes to ref for all IDs elements

Instead of creating a new IDs element each time, we just want to reference the root-level IDs element. The type attribute is not required when using ref, so we can remove that as well:

Find: <xs:element name="IDs" type="IDType" minOccurs="0"
Replace: <xs:element ref="IDs" minOccurs="0"

Delete redundant IDs elements

Some elements inherit from CCRCodedDataObjectType. Delete or comment out the redundant IDs references in the OrderRxHistoryType and StructuredProductType elements.

Create Classes using xsd.exe

.NET’s secret Xml data binding weapon is a command line utility named xsd.exe, which comes with Visual Studio.

Run the following on your newly downloaded .xsd files to create corresponding C# classes:

xsd CCR.xsd /c /n:CCR
xsd CCD.xsd /c /n:CCD

The /c means generate a class (as opposed to a dataset) and the /n:CCR specifies the namespace for the code (which can be anything, really). C# code (as opposed to VB) is the default language, so we don’t have to specify that.

Massage the Generated Classes

This step is optional, but using generics makes life a lot easier.

Fire up a text editor with regular-expression search/replace and run the following:

Find: public {[^\[]+}\[\] {[^;]+};
Replace: public List<\1> \2;

This will accomplish the following, enabling the use of all the methods in .NET’s List object:

// Old (Bad)
public ContinuityOfCareRecordPatient[] Patient;
// New (Good)
public List<ContinuityOfCareRecordPatient> Patient;

Don’t forget to add using System.Collections.Generic; to the top of your .cs file.

Proof of Concept Application

Just to show how this all comes together, here’s a simple command line application that uses our newly-created, XML schema-bound, C# object in action. You can download sample CCR files from AAFP’s website.

using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Xml.Serialization;
using CCR;

namespace CCRTest
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                ContinuityOfCareRecord ccr = Deserialize<ContinuityOfCareRecord>(@"C:\sampleCCR.xml");
                System.Console.WriteLine(ccr.CCRDocumentObjectID);
                foreach (ActorType at in ccr.Actors)
                {
                    System.Console.WriteLine("  " + at.ActorObjectID);
                }
            }
            catch (Exception ex)
            {
                while (ex != null)
                {
                    System.Console.WriteLine(ex.ToString());
                    ex = ex.InnerException;
                }
            }
            System.Console.ReadLine();
        }

        public static void Serialize<T>(T value, string pathName)
        {
            using (TextWriter writer = new StreamWriter(pathName))
            {
                XmlSerializer serializer = new XmlSerializer(typeof(T));
                serializer.Serialize(writer, value);
            }
        }

        public static T Deserialize<T>(string pathName)
        {
            using (TextReader reader = new StreamReader(pathName))
            {
                XmlSerializer serializer = new XmlSerializer(typeof(T));
                return (T)serializer.Deserialize(reader);
            }
        }
    }
}

Saturday, February 2, 2008

Introduction

This blog will be a place for me to address some of the technical issues I encounter while working in the enterprise data warehouse department of a large academic medical center.

My entries will likely be fairly technical, focusing on C#, ASP.NET, SQL Server, Reporting Services, Analysis Services, SSIS, and so on. Of course, these technologies are but means to a much more interesting end. By presenting pertinent information quickly and easily (others have done with much success), I hope to be a driving force in improving clinical quality and expediting medical research.

In medicine, as in any domain, you can’t fix what you can’t measure. That’s where I come in.