Windows DevCenter    
 Published on Windows DevCenter (http://www.windowsdevcenter.com/)
 See this if you're having trouble printing code examples


Programming Word from .NET

by Jesse Liberty
04/18/2006

A .NET medical office program would have many components and would be quite complex. One small part of such a program might solve a problem that many doctors have: when the patient is given a worrisome diagnosis it is hard to process all the other information the doctor is providing (prognosis, treatment, medications, etc.). It would be great if the program could generate a letter summarizing what was said, and mail it to the patient that day, for review once the initial shock wears off.

One way to accomplish this is to couple a .NET program with the power of Microsoft Word, delegating to Word that which it does best (formatting, printing, document management). You could, of course, use a very sophisticated mail-merge from within Word itself, but remember we want to store the patient information in a database, and we want to integrate this one module into a larger .NET program. Thus, we will use each tool for its own strengths:

Invoking Word from .NET--Getting Started

There are a few steps you must take to begin working with Word (or any office application) from within Visual Studio, and there is (of course) more than one way to do it. We'll take one of the simpler approaches. Begin by navigating to the MSDN site to download the Office XP Primary Interop Assemblies. Follow the directions to install the assemblies.

The key to working with Word is to add a reference in your project, from the COM tab for the Word Object Library, as shown in Figure 1.

Add a reference to word
Figure 1. Adding a reference to Word

You can now access, create, and manipulate Word documents using the Microsoft.Office.Interop.Word .Application object as well as the Microsoft.Office.Interop.Word .Document doc as will be described in context below.

An Application in Three-Part Harmony

The three parts of this demonstration application (kept intentionally and strictly simple to focus on the key aspects) will be:

The Database

Let's begin by examining the subset of simplified database tables we'll need, as diagrammed in Figure 2.

db diagram
Figure 2. Database diagram

The central table is Patients, which we can assume will contain extensive demographic information about each patient with links to the patients' office visits, illnesses, and medications. To facilitate this, we add three more tables: MedicationNames, Diagnoses, and Visits. Visits is in a one-to-many relation with Patients (each patient can have many visits, but each visit has only one patient). The other two are in many-to-many relationships (e.g., one medication can be prescribed for many patients, and each patient can have many prescriptions) and so are mediated by intermediate tables, PatientToMedication and PatientToDiagnoses, respectively.

Programming ASP.NET

Related Reading

Programming ASP.NET
Building Web Applications and Services with ASP.NET 2.0
By Jesse Liberty, Dan Hurwitz

The Word Document

You can create the Word document programmatically, but far simpler is to create an initial Word document in Word itself, populating it with boilerplate language. When your program is running you will insert the patient-specific (or visit-specific) data to generate the personalized letter. For this example, I've created Patient Letter.doc which I am storing in the C:\Temp directory (shown in Figure 3).

Note to those who need reminding that coffee is hot: I am not a doctor, these are not real medications, and this article is not intended to provide medical advice. [1]

boiler plate
Figure 3. Boilerplate language

The bookmarks are shown as vertical lines (indicated by the red arrows in Figure 3). While in Word, click on the Insert menu item and then on Bookmarks. You will be asked to name the new bookmark, as shown in Figure 4.

insert bookmark
Figure 4. Inserting a bookmark

Creating the Visit Form

The final part of the three-part minuet is to create the Visit form from within the larger .NET application. A "real" Visit form might include a lot of data, and sophisticated search tools to pick the patient, the new diagnosis, and the appropriate medications. To keep things simple, we'll just use drop-downs to identify the patient and the new diagnosis, as shown in Figure 5.

new visit
Figure 5. New visit

Once the patient name and the new diagnosis have been added, new medications can be added as shown in Figure 6.

add Rx
Figure 6. Add Rx.

That done, specific notes for the visit may be added, as shown in Figure 7.

add notes
Figure 7. Add notes.

Clicking Discard will discard all these changes (and the new diagnosis and medications for the user will not be recorded). You might want to pop up an "Are you sure?" dialog!

Clicking Save will save the new visit, the new diagnosis, and any new prescriptions to the database, and will create the new personalized Word document for mailing to the patient. Let's examine the Save code in some detail. Here it is in full (analysis follows):

private void btnSave_Click(object sender, EventArgs e)
{
    Cursor = Cursors.WaitCursor;

    // gather data 
    int patientID = Convert.ToInt32(patientsComboBox.SelectedValue);
    string patientName = 
        ((System.Data.DataRowView)(patientsComboBox.SelectedItem)).Row.ItemArray[1].ToString();
    int diagnosisID = Convert.ToInt32(diagnosesComboBox.SelectedValue);
    string diagnosisString = 
        ((System.Data.DataRowView)(diagnosesComboBox.SelectedItem)).Row.ItemArray[1].ToString();

    //** update the database **

    // update the visit table
    this.visitsTableAdapter.InsertVisit(visitTime, txtNotes.Text, patientID);

    // update the Diagnosis table
    this.patientToDiagnosesTableAdapter.InsertPatientToDiagnosis(patientID, diagnosisID);

    // add the new meds
    string newMeds = string.Empty;
    foreach (MedicationHolder holder in lbMeds.Items)
    {
        newMeds += holder.MedName + ", ";
        int numAdded = this.patientToMedicationTableAdapter.InsertNewPatientMedication(
            patientID, holder.MedID, visitTime);
        if (numAdded != 1)
        {
            MessageBox.Show("Unable to add " + holder.MedName, "Uh oh",
                MessageBoxButtons.OK, MessageBoxIcon.Error);
        }
        else
        {
            this.patientToMedicationTableAdapter.InsertNewPatientMedication(
                patientID, holder.MedID, visitTime);
        }
    }

    // ** Generate the letter ** 

    // Get the application object
    Microsoft.Office.Interop.Word .application wordApp = 
        new Microsoft.Office.Interop.Word .application();

    // Get the document object
    object fileName = @"C:\Temp\Patient Letter.doc";
    object confirmConversions = Type.Missing;
    object readOnly = Type.Missing;
    object addToRecentFiles = Type.Missing;
    object passwordDoc = Type.Missing;
    object passwordTemplate = Type.Missing;
    object revert = Type.Missing;
    object writepwdoc = Type.Missing;
    object writepwTemplate = Type.Missing;
    object format = Type.Missing;
    object encoding = Type.Missing;
    object visible = Type.Missing;
    object openRepair = Type.Missing;
    object docDirection = Type.Missing;
    object notEncoding = Type.Missing;
    object xmlTransform = Type.Missing;
    Microsoft.Office.Interop.Word .document doc = wordApp.Documents.Open(
        ref fileName,
        ref confirmConversions, ref readOnly, ref addToRecentFiles,
        ref passwordDoc, ref passwordTemplate, ref revert, ref writepwdoc,
        ref writepwTemplate, ref format, ref encoding, ref visible, ref openRepair,
        ref docDirection, ref notEncoding, ref xmlTransform);


    // replace book marks
    ReplaceBookmarkText(doc, "PatientName", patientName);
    ReplaceBookmarkText(doc, "Diagnosis", diagnosisString);
    ReplaceBookmarkText(doc, "PatientAddress", patientName);
    if ( txtNotes.Text.Length > 0 )
        ReplaceBookmarkText(doc, "notes", "Notes:\n" + txtNotes.Text);

    List<string> existingMedications = GetExistingMedications(patientID);
    string existingMeds = string.Empty;
    foreach ( string med in existingMedications )
    {
        existingMeds += med + ", ";
    }

    ReplaceBookmarkText(doc, "ExistingMedications", existingMeds);
    ReplaceBookmarkText(doc, "NewMedications", newMeds);

    // save the new document
    object  newFileName = @"C:\Temp\" + patientName + ".doc";
    object  fileFormat = Type.Missing;
    object  lockComments = Type.Missing;
    object  WritePW = Type.Missing;
    object  readOnlyRecommended = Type.Missing;
    object  embedTrueTypeFonts = Type.Missing;
    object  saveNativePictuormat = Type.Missing;
    object  saveFormsData = Type.Missing;
    object  saveAsAOCELetter = Type.Missing;
    object  insertLineBreaks = Type.Missing;
    object  allowSubstittions = Type.Missing;
    object  lineEnding = Type.Missing;
    object  AddBiDiMarks = Type.Missing;
    object refWritePW = Type.Missing;
    object saveNativePictureFormat = Type.Missing;
    doc.SaveAs(ref newFileName , ref fileFormat, ref lockComments, ref passwordDoc,
        ref addToRecentFiles, ref WritePW, ref readOnlyRecommended, ref embedTrueTypeFonts,
        ref saveNativePictureFormat, ref saveFormsData, ref saveAsAOCELetter, ref encoding,
        ref insertLineBreaks, ref allowSubstittions, ref lineEnding, ref AddBiDiMarks);

    // close word
    object saveChanges = Type.Missing;
    object originalFormat = Type.Missing;
    object routeDocument = Type.Missing;
    wordApp.Quit(ref saveChanges, ref originalFormat, ref routeDocument);

    Cursor = Cursors.Default;
    this.Close();  // close the dialog
}

The Save Button Handler Unpacked

There is a lot going on here, let's take it apart step by step.

First, because this operation will take a few moments, the cursor is set to the wait cursor. Next, the PatientID and PatientName are retrieved from the combo box, based on the user's selection.

This of course raises the question of how these combo boxes were initially populated. After creating my database tables in SQL Server, I returned to Visual Studio and clicked on Data->Add New Data Source, and used the wizard to create a DataSet that contained all the tables from my database.

To create the drop-down of patients, I right-clicked on the Patients table and set its representation to ComboBox (as shown in Figure 8) and then dragged it onto the form.

Setting Combo Box
Figure 8. Setting the combo box

When I dragged this table onto the form, a combo box was created, and, in the tray below, a PatientsBindingSource object and a PatientsTableAdapter. Clicking on the smart tag associated with the combo box let me set both the display value and the underlying value (the PatientID) that was to be held by the combo box, as shown in Figure 9.

drop down properties
Figure 9. Drop-down properties

Returning to the discussion of what happens when the Save button is clicked, it is a simple matter to query the control for the ID and name of the chosen patient (and similarly for the diagnosis).

int patientID = Convert.ToInt32(patientsComboBox.SelectedValue);
string patientName = 
    ((System.Data.DataRowView)(patientsComboBox.SelectedItem)).Row.ItemArray[1].ToString();
int diagnosisID = Convert.ToInt32(diagnosesComboBox.SelectedValue);
string diagnosisString = 
    ((System.Data.DataRowView)(diagnosesComboBox.SelectedItem)).Row.ItemArray[1].ToString()

When the dialog opened, we had set the time of the visit so that all the associated records (such as the medication) will have the identical time:

 private DateTime visitTime = DateTime.Now;

You now use that private member variable and the data from the combo boxes to update the Visits table through the provided adapter:

this.visitsTableAdapter.InsertVisit(visitTime, txtNotes.Text, patientID);

Similarly, now that we know that the records are to be saved, we can add a record to the PatientToDiagnosis table:

this.patientToDiagnosesTableAdapter.InsertPatientToDiagnosis(patientID, diagnosisID);

One problem we faced when the user was adding medications to the Visit was what to do with the Medication choices. We wanted to display the new medications in the listbox, but we did not want to add them to the database until and unless the record is saved. To manage this, we created a nested class to hold the information about the selected medications.

 public partial class Visit : Form
 {
   public class MedicationHolder
   {
     private int medID;
     public int MedID
     {
        get { return medID; }
        set { medID = value; }
     }
     private string medName;
     public string MedName
     {
        get { return medName; }
        set { medName = value; }
     }
     public MedicationHolder(int medID, string medName)
     {
        this.medID = medID;
        this.medName = medName;
     }
   }   // end nested class
}      // end outer class

The user clicks on the Add Rx button to open the Add Prescription dialog. If the user saves the prescription it is not written to the database, but instead it is wrapped in a holder and placed in a list which serves as the data source for the listbox.

private void addRxBtn_Click(object sender, EventArgs e)
{
    AddRx dlg = new AddRx();

    if (dlg.ShowDialog() == DialogResult.OK)
    {
        
        int medicationID = dlg.ChosenMedID;
        string medicationName = dlg.ChosenMed;
        newMeds.Add(new MedicationHolder(medicationID, medicationName));
        this.lbMeds.DataSource = null;
        this.lbMeds.DataSource = newMeds;
        lbMeds.DisplayMember = "MedName";
        lbMeds.ValueMember = "MedID";
    }
    
}

Again, returning to the Save button event handler, we have now only to iterate through these medication holders and add each to the database:

string newMeds = string.Empty;
foreach (MedicationHolder holder in lbMeds.Items)
{
    newMeds += holder.MedName + ", ";
    int numAdded = this.patientToMedicationTableAdapter.InsertNewPatientMedication(
        patientID, holder.MedID, visitTime);
    if (numAdded != 1)
    {
        MessageBox.Show("Unable to add " + holder.MedName, "Uh oh",
            MessageBoxButtons.OK, MessageBoxIcon.Error);
    }
}

Along the way we build up a string of the new medication names (newMeds), which we will use in the letter to the patient.

Creating the Letter

The second half of the code in the Save button event handler opens the prototype Word document created earlier, fills in text at the designated bookmarks, and saves the file as <patient name>.doc.

To begin, you need an instance of the Word application:

Microsoft.Office.Interop.Word.Application wordApp = 
    new Microsoft.Office.Interop.Word.Application();

You are now ready to open the specific document (the boilerplate letter we created and named Patient Letter.doc in the Temp directory). Two things to keep in mind, however, are:

  1. All parameters must be passed by reference.
  2. There are a lot of parameters, and they are all required.

Given the second requirement, the pattern is to initialize the parameters you don't care about with Type.Missing.

object fileName = @"C:\Temp\Patient Letter.doc";
object confirmConversions = Type.Missing;
object readOnly = Type.Missing;
object addToRecentFiles = Type.Missing;
object passwordDoc = Type.Missing;
object passwordTemplate = Type.Missing;
object revert = Type.Missing;
object writepwdoc = Type.Missing;
object writepwTemplate = Type.Missing;
object format = Type.Missing;
object encoding = Type.Missing;
object visible = Type.Missing;
object openRepair = Type.Missing;
object docDirection = Type.Missing;
object notEncoding = Type.Missing;
object xmlTransform = Type.Missing;

You can now pass these objects (by reference) to the method that returns the document.

Microsoft.Office.Interop.Word .document doc = wordApp.Documents.Open(
    ref fileName, ref confirmConversions, ref readOnly, ref addToRecentFiles,
    ref passwordDoc, ref passwordTemplate, ref revert, ref writepwdoc,
    ref writepwTemplate, ref format, ref encoding, ref visible, ref openRepair,
    ref docDirection, ref notEncoding, ref xmlTransform);

The object you get back (doc) is the Word document, ready for you to edit it. Your task will be to replace each bookmark with a block of text. Because this action is repeated with different bookmarks and different blocks of text, you'll factor it out to a common helper method:

private void ReplaceBookmarkText ( 
    Microsoft.Office.Interop.Word .document doc,
    string bookmarkName, 
    string text)
{
    if ( doc.Bookmarks.Exists(bookmarkName) )
    {
        Object name = bookmarkName;
        Microsoft.Office.Interop.Word.Range range = 
            doc.Bookmarks.get_Item( ref name ).Range;
        
        range.Text = text;
        object newRange = range;
        doc.Bookmarks.Add(bookmarkName, ref newRange);
    }
}

The helper method not only inserts the text but it replaces the bookmark when it is done (tidy). Note: We could move Microsoft.Office.Interop.Word .document doc to a member variable and then not have to pass it to the method, but since it is passed to only one method, I'm inclined to use the parameter.

We invoke this helper method for each of the text blocks we want in the letter:

ReplaceBookmarkText(doc, "PatientName", patientName);
ReplaceBookmarkText(doc, "Diagnosis", diagnosisString);
ReplaceBookmarkText(doc, "PatientAddress", patientName);
if ( txtNotes.Text.Length > 0 )
    ReplaceBookmarkText(doc, "notes", "Notes:\n" + txtNotes.Text);

That done, it is time to retrieve the patient's previous medications from the database, and then to add both the new medications and the existing medications to the respective bookmarks:

List<string> existingMedications = GetExistingMedications(patientID);
string existingMeds = string.Empty;
foreach ( string med in existingMedications )
{
    existingMeds += med + ", ";
}

ReplaceBookmarkText(doc, "ExistingMedications", existingMeds);
ReplaceBookmarkText(doc, "NewMedications", newMeds);

The helper method GetExistingMedications queries the database for all the medications for the given patient. To get the information needed, I join two tables: MedicationName (to get the name rather than the ID) and PatientToMedication (to get the medications for the current patient). The medicaiton names are returned in a type-safe list:

private List<string> GetExistingMedications(int patientID)
{
    string connString  = 
        this.patientToMedicationTableAdapter.Connection.ConnectionString;
    SqlConnection conn = new SqlConnection(connString);
    conn.Open();
    string sqlString = 
    "select MedicationName from PatientToMedication pm " +
    " join MedicationNames m on m.MedicationID = pm.MedicationID " +
    " where pm.PatientID = " + patientID.ToString();

    SqlCommand cmd = new SqlCommand(sqlString, conn);
    SqlDataReader rdr = cmd.ExecuteReader();
    List<string> meds = new List<string>();
    while (rdr.Read())
    {
        meds.Add(rdr["MedicationName"].ToString());
    }
    conn.Close();
    return meds;
}

Saving the File

Once the bookmarks are written, you want to save the file to a new name, so as not to overwrite the original. We'll name the new file using the patient's name:

object  newFileName = @"C:\Temp\" + patientName + ".doc";

Once again we must create all the arguments for the SaveAs method, and then pass them all by reference:

object  newFileName = @"C:\Temp\" + patientName + ".doc";
object  fileFormat = Type.Missing;
object  lockComments = Type.Missing;
object  WritePW = Type.Missing;
object  readOnlyRecommended = Type.Missing;
object  embedTrueTypeFonts = Type.Missing;
object  saveNativePictuormat = Type.Missing;
object  saveFormsData = Type.Missing;
object  saveAsAOCELetter = Type.Missing;
object  insertLineBreaks = Type.Missing;
object  allowSubstittions = Type.Missing;
object  lineEnding = Type.Missing;
object  AddBiDiMarks = Type.Missing;
object refWritePW = Type.Missing;
object saveNativePictureFormat = Type.Missing;
doc.SaveAs(ref newFileName , ref fileFormat, ref lockComments, ref passwordDoc,
    ref addToRecentFiles, ref WritePW, ref readOnlyRecommended, ref embedTrueTypeFonts,
    ref saveNativePictureFormat, ref saveFormsData, ref saveAsAOCELetter, ref encoding,
    ref insertLineBreaks, ref allowSubstittions, ref lineEnding, ref AddBiDiMarks);

Finally, we'll quit the application so as not to leave Word hanging in memory:

object saveChanges = Type.Missing;
object originalFormat = Type.Missing;
object routeDocument = Type.Missing;
wordApp.Quit(ref saveChanges, ref originalFormat, ref routeDocument);

We restore the cursor to the default cursor, and close the dialog box, and if we then open Windows Explorer, we'll find a new file in C:\Temp, as shown in Figure 10.

new letter
Figure 10. New letter in Explorer

Double-clicking on the new letter (Marvin Davis.doc) shows that the boilerplate has been personalized based on the information gathered at the current visit, as well as the information already on record for this patient (e.g., existing medications), as shown in Figure 11.

revised letter
Figure 11. Revised letter

I've added yellow to indicate where data was inserted based on the current visit, and green to indicate data extracted from the database.

Much More Is Possible

Of course, we've barely scratched the surface on manipulating Word documents. You can learn more about how to create and manipulate Word (and other Office products) documents from the MSDN and/or from two books on the subject that come highly recommended: Microsoft .NET Development for Microsoft Office and Visual Studio Tools for Office: Using C# with Excel, Word, Outlook, and InfoPath.

Epilogue: Stocking the Database

In order to have data to work with, I needed to create Medications, Diagnoses, and Patients. I created the Medications by mixing and matching pseudo-prefixes, stubs, and suffixes to create chemical-sounding names, which were then added to the MedicationName table:

private void CreateNewMeds()
{
    string[] prefixes = { "klono", "Stero", "Albuto", "Lexo", "Neuro", 
        "Hydro", "Glyco", "Effexo", "Amino", "Ecto", "Pheno", "Tricyclo"};
    string[] pseudoChems = { "phos", "sulf", "chlor", "ster", "pyro" };
    string[] suffixes = { "ate", "ise", "ide", "ite" };
    lblMsg.Text = "Working...";
    for (int suffixCtr = 0; suffixCtr < suffixes.Length; suffixCtr++)
    {
        for (int chemCtr = 0; chemCtr < pseudoChems.Length; chemCtr++)
        {
            for (int prefixCtr = 0; prefixCtr < prefixes.Length; prefixCtr++)
            {
                string newName = prefixes[prefixCtr] + pseudoChems[chemCtr] + suffixes[suffixCtr];
                int numAdded = this.medicationNamesTableAdapter.InsertNewMedication(newName);
                if (numAdded != 1)
                {
                    MessageBox.Show("Unable to add " + newName, "Uh oh",
                        MessageBoxButtons.OK, MessageBoxIcon.Error);
                }
            }
        }
    }

     this.medicationNamesTableAdapter.Fill(this.doctorOfficeDataSet.MedicationNames);
     this.lblMsg.Text = "Done.";
}

This results in a table with 240 pseudo-chemical names such as Tricyclophosate, as shown in Figure 12.

med names
Figure 12. Medication names

Similar code is used to create the patient names, mixing popular first and last names:

string[] firstNames = 
{
    "Abdul", "Bobby", "Charlie", "Cheryl", "David", "Diana",
    "Edgar", "Ethel", "Fran", "Gerald", "Gigi", "Hannah", "Isaih",
    "Isabelle", "Jesse", "Jody", "Ken", "Karen", "Kim", "Lori", "Marvin",
    "Melinda", "Nancy", "Oscar", "Pat", "Quentin", "Rachel", "Robert",
    "Sandy", "Stacey", "Tom", "Ursala", "Victoria", "Yolanda", "Zak"
};

string[] lastNames = 
{
    "Addison", "Baker", "Carter", "Davis", "Europa", "Finkelstein",
    "Godfrey", "Hughes", "Isaacson", "Jackson", "Marshall", "Nelson",
    "Oppenheimer", "Petri", "Rosen", "Sachs", "Tishmann"
};

lblMsg.Text = "Working...";
Application.DoEvents();


for (int firstNameCtr = 0; firstNameCtr < firstNames.Length; firstNameCtr++)
{
    for (int lastNameCtr = 0; lastNameCtr < lastNames.Length; lastNameCtr++)
    {
        string newName = firstNames[firstNameCtr] + " " + lastNames[lastNameCtr];
        int numAdded = this.patientsTableAdapter.InsertNewPatient(newName,null);
        if (numAdded != 1)
        {
            MessageBox.Show("Unable to add " + newName, "Uh oh",
                MessageBoxButtons.OK, MessageBoxIcon.Error);
        }
    }
}

Resulting in 595 patients. While generating the patients, a random number of medications are assigned to each as well:

this.patientsTableAdapter.Fill(this.doctorOfficeDataSet.Patients);
MedicalLetters.DoctorOfficeDataSet.PatientsDataTable patients =
    this.patientsTableAdapter.GetData();
foreach (DataRow row in patients.Rows)
{
    int patientID = Convert.ToInt32(row["PatientID"]);
    const int MaxMedsPerPerson = 10;
    int maxMeds = Math.Min(medications.Rows.Count, MaxMedsPerPerson);
    int startingOffset = rand.Next(0, medications.Rows.Count - MaxMedsPerPerson);
    for (int medCtr = startingOffset; medCtr < maxMeds + startingOffset; medCtr++)
    {
        int medID = Convert.ToInt32(medications.Rows[medCtr][0]);
        int numAdded =
            this.patientToMedicationTableAdapter.InsertNewPatientMedication(
            patientID, medID, DateTime.Now);
        if (numAdded != 1)
        {
            string medicationName = medications.Rows[medCtr]["MedicationName"].ToString();
            string patientName = row["PatientName"].ToString();
            MessageBox.Show("Unable to add " + medicationName + " for " + patientName, 
                "Uh oh", MessageBoxButtons.OK, MessageBoxIcon.Error);
        }
    }
}

Notice that to avoid duplication, the starting offset is set by using a random number between 0 and the total number of medications minus the maximum number of medications to be assigned to any individual patient (10).

These three database-filling routines are accessed through the Maintenance menu choices, as shown in Figure 13.

maintenance
Figure 13. Maintenance menu

To recreate this application, download the script to create the database from my website, then run each of these maintenance routines in order (medications, then diseases, then patients). Finally, click on Patient Records and choose New Visit, as shown in Figure 14.

new visit
Figure 14. Choose new visit

----

Footnote 1. Ph.G. = "Poppa Had Gelt"
Back to text

Jesse Liberty is a senior program manager for Microsoft Silverlight where he is responsible for the creation of tutorials, videos and other content to facilitate the learning and use of Silverlight. Jesse is well known in the industry in part because of his many bestselling books, including O'Reilly Media's Programming .NET 3.5, Programming C# 3.0, Learning ASP.NET with AJAX and the soon to be published Programming Silverlight.


Return to the Windows DevCenter.

Copyright © 2009 O'Reilly Media, Inc.