Walkthrough: Working with Offline SQL Server Data in Excel
Brian A. Randell
MCW Technologies
September 2003
Applies to:
Microsoft® Visual Studio® Tools for the Microsoft Office System
Microsoft Office Excel 2003
Microsoft Visual Studio .NET 2003
Summary: Microsoft Office Excel makes it easy to work with rectangular data, such as data from an external database. In this walkthrough, you'll load data from Microsoft SQL Server into an ADO.NET dataset. You'll then use the Excel object model to support synchronizing changes made to the data in Excel with SQL Server. (20 printed pages)
Contents
Introduction
Prerequisites
Getting Started
Laying Out the Worksheet
Hooking Up the Event Handlers
Adding Code to Import Data
Synchronizing Changes with the Server
Running Code Before Closing the Workbook
Conclusion
Introduction
In this walkthrough, you will download data from the Microsoft® SQL Server™ Northwind sample database into a local DataSet object, releasing your connection to the server. You will then load the data from the dataset into a sheet within a Microsoft Office Excel workbook as shown in Figure 1. Once the data is loaded, you'll make some changes and send the results back to SQL Server. Once you're done, the example will display a message box informing you of the number of records updated.
Figure 1. Excel with data loaded from SQL Server
Prerequisites
To complete this walkthrough, the following software and components must be installed on the development computer:
- Microsoft Visual Studio® .NET 2003 or Microsoft Visual Basic® .NET Standard 2003
- Microsoft Visual Studio Tools for the Microsoft Office System
- Microsoft Office Professional Edition 2003
- Microsoft SQL Server or Microsoft SQL Server Desktop Engine (MSDE) 7.0 or 2000, with the Northwind sample database installed. This demonstration assumes that you've set up SQL Server/MSDE allowing access using integrated security.
**Tip **This demonstration assumes that if you're a Visual Basic .NET programmer, you've set the Option Strict setting in your project to On (or have added the Option Strict statement to each module in your project), although it is not required. Setting the Option Strict setting to On requires a bit more code, as you'll see, but it also ensures that you do not perform any unsafe type conversions. You can get by without it, but in the long run, the discipline required by taking advantage of this option will far outweigh the difficulties it adds as you write code.
Getting Started
In order to get started, you'll need to create a new Visual Studio .NET project that works with Microsoft Office Excel 2003.
To create an Excel Workbook project
Start Visual Studio .NET, and on the File menu, point to New, and click Project.
In the Project Types pane, expand Microsoft Office System Projects, and then select Visual Basic Projects or Visual C# Projects.
In the Templates pane, select Excel Workbook.
Name the project ExcelSQLServerData, and store it in a convenient local path.
Accept the defaults in the Microsoft Office Project Wizard, and click Finish to create the project.
Visual Studio .NET opens the ThisWorkbook.vb or ThisWorkbook.cs file in the Code Editor for you.
Laying Out the Worksheet
In order to load the data and synchronize changes, you'll need to add some way to start the code running. For this demonstration, you'll create two hyperlinks within the workbook, and react to the SheetFollowHyperlink event of the Workbook object to run your code.
To format the worksheet
Press F5 to run the project, loading Excel and your new workbook.
Rename Sheet1 to Products. If you'd like, remove Sheet2 and Sheet3.
Within Excel, put the cursor in cell G1, and select Hyperlink on the Insert menu.
On the left side of the Insert Hyperlink dialog box, in the Link to pane, select Place in This Document.
Set the Text to display value to Load Data.
Make sure the cell reference in the dialog box matches the location of your hyperlink. When you're done, the dialog box should look like Figure 2. Click OK to dismiss the dialog box. You should see the new hyperlink within the workbook.
Figure 2. The finished Insert Hyperlink dialog box
Repeat the process, putting the cursor in cell G2, and put Update Data in the Text to display field.
Select Save on the File menu to save your changes.
The workbook should look something like Figure 3.
Figure 3. Your Excel workbook ready to go
Hooking Up the Event Handlers
In order to start your code running, you'll need to react to the Workbook.SheetFollowHyperlink event. In this section, you'll add support for reacting to this event in Visual Basic .NET and in Microsoft Visual C#.
Hook Up the Event Handlers (Visual Basic Only)
Follow these steps to hook up the event handler in Visual Basic .NET:
To add the event handler (Visual Basic)
From the Class Name drop-down list in the upper-left corner of the Code Editor, select ThisWorkbook.
From the Method Name drop-down list in the upper-right corner of the Code Editor, select SheetFollowHyperlink.
Visual Studio .NET creates the event handler stub for you.
Add the following procedure stubs to the current class. You'll fill in the details later:
' Visual Basic Private Sub LoadData() End Sub Private Sub UpdateData() End Sub
Modify the ThisWorkbook_SheetFollowHyperlink procedure, adding the following code:
' Visual Basic Try Select Case Target.Name Case "Load Data" LoadData() Case "Update Data" UpdateData() End Select Catch ex As Exception MessageBox.Show(ex.Message, ex.Source, _ MessageBoxButtons.OK, MessageBoxIcon.Error) End Try
Hook Up the Event Handlers (C# Only)
You'll need to react to the Workbook.SheetFollowHyperlink event. Later, you'll need to react to the SheetChange event. In this section, you'll add support for reacting to these events in Visual C#®.
To add the event handler (C#)
Within the OfficeCodeBehind class, locate the existing declarations for the openEvent and beforeCloseEvent variables.
Add new variables representing the workbook's SheetFollowHyperlink and SheetChange events:
// C# private Excel.WorkbookEvents_SheetFollowHyperlinkEventHandler sheetFollowHyperlinkEvent; private Excel.WorkbookEvents_SheetChangeEventHandler sheetChangeEvent;
Add the following procedure stubs to the class:
// C# protected void ThisWorkbook_SheetFollowHyperlink( Object sh, Excel.Hyperlink Target) { } protected void ThisWorkbook_SheetChange( Object sh, Excel.Range Target) { }
Add the following code to the ThisWorkbook_Open procedure:
// C# sheetFollowHyperlinkEvent = new Excel.WorkbookEvents_SheetFollowHyperlinkEventHandler( ThisWorkbook_SheetFollowHyperlink); thisWorkbook.SheetFollowHyperlink += sheetFollowHyperlinkEvent; sheetChangeEvent = new Excel.WorkbookEvents_SheetChangeEventHandler( ThisWorkbook_SheetChange); thisWorkbook.SheetChange += sheetChangeEvent;
Add the following procedure stubs to the class:
// C# void LoadData() { } void UpdateData() { }
Modify the ThisWorkbook_SheetFollowHyperlink procedure, adding the following code:
// C# try { switch (Target.Name) { case "Load Data": LoadData(); break; case "Update Data": UpdateData(); break; } } catch (Exception ex) { MessageBox.Show(ex.Message, ex.Source, MessageBoxButtons.OK, MessageBoxIcon.Error); }
Adding Code to Import Data
Next, you'll need to add the code that creates a dataset, loading the data from SQL Server, and add code to copy that data into the Products sheet within the Excel workbook.
To import data into the worksheet
Scroll to the top of the code module, and add the following statement, which will reduce the amount of typing required to refer to the objects and members you'll reference:
' Visual Basic Imports System.Data.SqlClient // C# using System.Data.SqlClient; using System.Data;
Add the following declarations, immediately beneath the existing declarations for the ThisApplication and ThisWorkbook variables:
' Visual Basic Private DisableWorkSheetChanges As Boolean = False Private mda As SqlDataAdapter Private mds As DataSet Private mdt As System.Data.DataTable Private xlSheet As Excel.Worksheet Private rngUC As Excel.Range Private rngData As Excel.Range // C# private Boolean DisableWorkSheetChanges = false; private SqlDataAdapter mda = null; private DataSet mds = null; private System.Data.DataTable mdt = null; private Excel.Worksheet xlSheet = null; private Excel.Range rngUC = null; private Excel.Range rngData = null;
**Warning **Excel provides a DataTable object, as does ADO.NET. It's important to distinguish between the two, and the code uses an explicit namespace reference to avoid the ambiguity.
Add the following procedure to the OfficeCodeBehind class.
This procedure connects to SQL Server on the local computer, using integrated security, and fills a dataset:
' Visual Basic Private Sub GetDataSet() Dim cnn As SqlConnection Try If mds Is Nothing Then mds = New DataSet Else mds.Tables.Remove(mdt) End If cnn = New SqlConnection( _ "Server='.';" & _ "Database=Northwind;" & _ "Integrated Security=true") Dim strSQL As String = "SELECT " & _ "ProductId AS [Id], ProductName AS [Name], " & _ "UnitsInStock AS [On Hand], " & _ "UnitsOnOrder AS [On Order], " & _ "ReorderLevel AS [Reorder Level] " & _ "FROM Products WHERE Discontinued = 0 " & _ "ORDER BY UnitsInStock" Dim cmd As New SqlCommand(strSQL, cnn) mda = New SqlDataAdapter(cmd) mda.Fill(mds) mdt = mds.Tables(0) Dim cb As New SqlCommandBuilder(mda) mda.UpdateCommand = cb.GetUpdateCommand() Catch ex As Exception MessageBox.Show(ex.Message, ex.Source, _ MessageBoxButtons.OK, MessageBoxIcon.Error) End Try End Sub // C# private void GetDataSet() { SqlConnection cnn; try { if (mds == null) { mds = new DataSet(); } else { mds.Tables.Remove(mdt); } cnn = new SqlConnection("Server=.;" + "Database=Northwind;Integrated Security=true"); string strSQL = "SELECT " + "ProductId AS [Id], ProductName AS [Name], " + "UnitsInStock AS [On Hand], " + "UnitsOnOrder AS [On Order], " + "ReorderLevel AS [Reorder Level] " + "FROM Products WHERE Discontinued = 0 " + "ORDER BY UnitsInStock"; SqlCommand cmd = new SqlCommand(strSQL, cnn); mda = new SqlDataAdapter(cmd); mda.Fill(mds); mdt = mds.Tables(0); SqlCommandBuilder cb = new SqlCommandBuilder(mda); mda.UpdateCommand = cb.GetUpdateCommand(); } catch (Exception ex) { MessageBox.Show(ex.Message, ex.Source, MessageBoxButtons.OK, MessageBoxIcon.Error); } }
Add the following procedure to the class.
This procedure creates the column headings in the worksheet using the field names from the Columns collection of the data table contained within the new dataset, and provides basic formatting:
' Visual Basic Private Sub SetupWorksheet() Try DisableWorkSheetChanges = True xlSheet = DirectCast( _ ThisWorkbook.Worksheets("Products"), Excel.Worksheet) rngUC = DirectCast(xlSheet.Cells(2, 1), Excel.Range) rngUC.CurrentRegion.Clear() Dim i As Integer = 0 Dim col As DataColumn Dim rng As Excel.Range For Each col In mdt.Columns I += 1 rng = DirectCast(xlSheet.Cells(1, i), Excel.Range) rng.Value = col.ColumnName rng.Font.Bold = True Next Finally DisableWorkSheetChanges = False End Try End Sub // C# private void SetupWorksheet() { try { DisableWorkSheetChanges = true; xlSheet = (Excel.Worksheet)ThisWorkbook. Worksheets["Products"]; rngUC = (Excel.Range)xlSheet.Cells[2, 1]; rngUC.CurrentRegion.Clear(); int i = 0; Excel.Range rng; foreach (DataColumn col in mdt.Columns) { i++; rng = (Excel.Range)xlSheet.Cells[1, i]; rng.Value2 = col.ColumnName; rng.Font.Bold = true; } } finally { DisableWorkSheetChanges = false; } }
Add the following procedure, which takes the data from the data table in the previously loaded data set and puts it into the Products worksheet:
' Visual Basic Private Sub PutDataInXL() DisableWorkSheetChanges = True Dim i As Integer = 0 Dim j As Integer = 0 Dim dr As DataRow Try ThisApplication.ScreenUpdating = False If Not mdt Is Nothing Then For i = 0 To mdt.Rows.Count - 1 dr = mdt.Rows(i) For j = 0 To mdt.Columns.Count - 1 rngUC.Offset(i, j).Value = dr(j).ToString() Next j Next rngData = rngUC.CurrentRegion End If Finally ThisApplication.ScreenUpdating = True DisableWorkSheetChanges = False End Try End Sub // C# private void PutDataInXL() { DisableWorkSheetChanges = true; int i = 0; int j = 0; DataRow dr; try { ThisApplication.ScreenUpdating = false; if (mdt != null) { for (i = 0; i <= mdt.Rows.Count - 1; i++) { dr = mdt.Rows[i]; for (j = 0 ; j <= mdt.Columns.Count - 1; j++) rngUC.get_Offset(i, j).Value2 = dr[j].ToString(); } } rngData = rngUC.CurrentRegion; } finally { ThisApplication.ScreenUpdating = true; DisableWorkSheetChanges = false; } }
Add the following procedure, which applies some formatting to the newly loaded data in Excel:
' Visual Basic Private Sub FormatColumns() Try DisableWorkSheetChanges = True rngData.Columns.NumberFormat = "0" rngData.Columns.AutoFit() Finally DisableWorkSheetChanges = False End Try End Sub // C# private void FormatColumns() { try { DisableWorkSheetChanges = true; rngData.Columns.NumberFormat = "0"; rngData.Columns.AutoFit(); } finally { DisableWorkSheetChanges = false; } }
Add the following code to the LoadData procedure created earlier:
' Visual Basic GetDataSet() SetupWorksheet() PutDataInXL() FormatColumns() // C# GetDataSet(); SetupWorksheet(); PutDataInXL(); FormatColumns();
Select Save All on the File menu to save the entire solution.
Press F5 to run the project, loading Excel and your workbook.
Within Excel, click the Load Data link you added previously, and verify that your code has imported and formatted the data, as shown in Figure 1.
Close Excel and the workbook, saving changes if you desire, and return to Visual Studio .NET.
Synchronizing Changes with the Server
You need to add some code to trap the SheetChange event of the Workbook and put any changes made to the loaded data into the correct columns in the Rows collection of the DataTable object. Then, you'll add code to actually send the changes to SQL Server when you click the Update Data hyperlink.
To synchronize changes with the server
Do one of the following, depending on the language you are using:
(Visual Basic only)
From the Class Name drop-down list in the upper-left corner of the Code Editor, select ThisWorkbook.
From the Method Name drop-down list in the upper-right corner of the Code Editor, select ThisWorkbook_SheetChange.
Visual Studio .NET creates the event handler for you.
(C# only)
Within the OfficeCodeBehind class, locate the existing declarations for the OpenEvent and BeforeCloseEvent variables.
Add a new variable representing the workbook's SheetChange event:
// C# private Excel.WorkbookEvents_SheetChangeEventHandler sheetChangeEvent;
Add the following procedure:
// C# private Excel.Range GetIntersect( Excel.Range rng1, Excel.Range rng2) { return ThisApplication.Intersect(rng1, rng2, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing); }
Visual Basic and C#
Modify the ThisWorkbook_SheetChange procedure, so that it looks like the following:
' Visual Basic Private Sub ThisWorkbook_SheetChange( _ ByVal Sh As Object, ByVal Target As Excel.Range) _ Handles ThisWorkbook.SheetChange If Not DisableWorkSheetChanges Then If Not ThisApplication. _ Intersect(rngData, Target) Is Nothing Then ' Subtract 1 because Excel is 1-based, ' then subtract one row for the header. ' Don't handle changes to the ' header row, however. If Target.Row > 1 Then Dim intRow As Integer = Target.Row - 2 Dim intCol As Integer = Target.Column - 1 mdt.Rows(intRow)(intCol) = Target.Value End If End If End If End Sub // C# protected void ThisWorkbook_SheetChange( Object sh, Excel.Range Target) { if (!DisableWorkSheetChanges) { if (GetIntersect(rngData, Target) != null) { // Subtract 1 because Excel is 1-based, // then subtract one row for the header. // Don't handle changes to the // header row, however. if (Target.Row > 1) { int intRow = Target.Row - 2; int intCol = Target.Column - 1; mdt.Rows[intRow][intCol] = Target.Value2; } } } }
Add the following code to the existing UpdateData procedure stub, sending the modified rows of the DataTable to SQL Server:
' Visual Basic Try If mds.HasChanges Then Dim intRV As Integer = mda.Update(mdt) If intRV > 0 Then MessageBox.Show(String.Format( _ "{0} records were updated successfully.", _ intRV.ToString()), _ "Success", MessageBoxButtons.OK, _ MessageBoxIcon.Information) End If End If Catch ex As Exception MessageBox.Show(ex.Message, ex.Source, _ MessageBoxButtons.OK, MessageBoxIcon.Error) End Try // C# try { if (mds.HasChanges()) { int intRV = mda.Update(mdt); if (intRV > 0) { MessageBox.Show(String.Format( "{0} records were updated successfully.", intRV.ToString()), "Success", MessageBoxButtons.OK, MessageBoxIcon.Information); } } } catch (Exception ex) { MessageBox.Show(ex.Message, ex.Source, MessageBoxButtons.OK, MessageBoxIcon.Error); }
Save your project, then press F5 to run it.
Within Excel, click the Load Data link, and verify that you've successfully loaded the data.
Modify any of the loaded data and click the Update Data hyperlink to send your changes back to SQL Server.
Close Excel, but this time, don't save the changes made to the workbook. Once back in Visual Studio .NET, press F5 again to start debugging again.
Click the Load Data link again, and verify that that your submitted changes show up.
Close Excel (saving the workbook, if you like), returning to Visual Studio .NET.
Running Code Before Closing the Workbook
To complete the walkthrough, you need to add code to the BeforeClose event of the workbook. This event will check to see if there are any changes that have not yet been submitted to the server. If so, the code displays an alert asking if changes should be submitted.
To check for changes to the workbook
Add the following code to the ThisWorkbook_BeforeClose procedure:
' Visual Basic If mds.HasChanges Then If MessageBox.Show( _ "There are changes that need to submitted. " & _ "Do you want to send the changes in now?", _ "Question", MessageBoxButtons.YesNo, _ MessageBoxIcon.Question) = DialogResult.Yes Then UpdateData() End If End If // C# if (mds.HasChanges()) { if (MessageBox.Show( "There are changes that need to submitted. " + "do you want to send the changes in now?", "Question", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes) { UpdateData(); } }
Save your project, then press F5 to run it.
Within Excel, click the Load Data link.
Modify any of the loaded data and then attempt to close the workbook. You will see an alert indicating that you have changes to be saved. Click Yes to save your changes.
When Excel prompts you to save the workbook, select No.
Once back in Visual Studio .NET, press F5 again to start debugging again.
Click the Load Data link again, and verify that that your changes show up.
Close Excel (saving the workbook, if you like), returning to Visual Studio .NET.
Conclusion
Excel makes it easy to work with data from an external database. This walkthrough demonstrated how to create an Excel workbook project using Visual Studio Tools for the Microsoft Office System, and how to use the Excel object model to support synchronizing changes made to data in Excel with SQL Server.