I have a client that wants to be able to produce a Customer Profile report based on data that resides in a SQL 2008 R2 db. They want to choose a customer from a drop-down list in a web page and have it spit out a rather extensive report on that customer.
I was going to use SSRS, but they wanted something that was not only functional but esthetically pleasing, so I'm merging data into a Word document using OpenXML.
Things have been going swimmingly, but I now have a request that I'm not sure how (or even if it is possible) to address with this.
The approach I've used so far is one of place-holders in a Word doc. You add a control from the Developer Tab, you name it, you wire it up to your XML and you populate it in your CSharp app. Works like a champ.
Now they want a kind of repeatable section thing. Let's say that a customer has open projects. For each of these projects, they would like to see the same set of data elements. So conceptually, I need a template of place holders that I can populate and then merge into the document.
Here's some code:
privatevoid CreateDocument( ) {// Get the template document file and create a stream from itconststring DocumentFile = @"~/App_Data/CustomerProfileTemplate.docx";// const string DocumentFile = @"~/App_Data/OneOff.docx";// Read the file into memorybyte[ ] buffer = File.ReadAllBytes( Server.MapPath( DocumentFile ) );// MemoryStream memoryStream = new MemoryStream( buffer, true ); // NoNoNo!!! MemoryStreams created this way cannot expand dynamically! MemoryStream memoryStream = new MemoryStream( ); memoryStream.Capacity = buffer.Length; memoryStream.Write( buffer, 0, buffer.Length ); buffer = null;// Open the document in the stream and replace the custom XML partusing ( Package pkgFile = Package.Open( memoryStream, FileMode.Open, FileAccess.ReadWrite ) ) { PackageRelationshipCollection pkgrcOfficeDocument = pkgFile.GetRelationshipsByType( strRelRoot );foreach ( PackageRelationship pkgr in pkgrcOfficeDocument ) {if ( pkgr.SourceUri.OriginalString == "/" ) {// Get the root part PackagePart pkgpRoot = pkgFile.GetPart( new Uri( "/" + pkgr.TargetUri.ToString( ), UriKind.Relative ) );// Add a custom XML part to the package Uri uriData = new Uri( "/customXML/item1.xml", UriKind.Relative );if ( pkgFile.PartExists( uriData ) ) {// Delete document "/customXML/item1.xml" part pkgFile.DeletePart( uriData ); }// Load the custom XML data PackagePart pkgprtData = pkgFile.CreatePart( uriData, "application/xml" ); GetDataFromSQLServer( pkgprtData.GetStream( ), ddlCustomer.SelectedValue ); } }// Close the file - not needed thanks to "Using"// pkgFile.Close( ); }//Build File Namestring fileName = ddlCustomer.SelectedItem.ToString( ) + " Profile.docx";// Return the result Response.ClearContent( ); Response.ClearHeaders( ); Response.AddHeader( "content-disposition", "attachment; filename=" + fileName ); Response.ContentEncoding = System.Text.Encoding.UTF8; memoryStream.WriteTo( Response.OutputStream ); memoryStream.Close( ); Response.End( ); }
The function GetDataFromSQLServer( ) is where I get the data and drop it into the doc and looks something like this:
privatevoid GetDataFromSQLServer( Stream stream, string customerID = "" ) {// Connect to a Microsoft SQL Server database and get data String source = System.Configuration.ConfigurationManager.ConnectionStrings[ "BlahBlahBlahConnectionString" ].ConnectionString;using ( SqlConnection conn = new SqlConnection( source ) ) { conn.Open( ); SqlCommand cmd = new SqlCommand( ); cmd.CommandText = "dbo.usp_get_data"; cmd.CommandType = CommandType.StoredProcedure; cmd.Connection = conn; cmd.Parameters.AddWithValue( "@CompanyId", customerID ); SqlDataReader dr = cmd.ExecuteReader( );if ( dr.Read( ) ) { XmlWriter writer = XmlWriter.Create( stream );// Get Data string companyName = ( string )dr[ "CompanyName" ];string numEmployees = ( string )dr[ "NumberOfEmployees" ].ToString( );string technologyID = ( string )dr[ "TechnologyID" ]; . . . . . . . . .// Title Page writer.WriteStartElement( "Customer" ); writer.WriteElementString( "CompanyName", companyName ); writer.WriteElementString( "TechnologyID", technologyID ); writer.WriteElementString( "NumberOfEmployees", numEmployees ); . . . . . . . . . writer.WriteEndElement( ); writer.Close( ); } dr.Close( ); conn.Close( ); } }
Shameful lack of error handling aside, the above process works nicely; the resulting document is beautiful. The problem is I don't see a way forward from here given the latest requests.
Any thoughts would be very much appreciated.
JP