In the previous articles, we have covered how to add a web controller to the Server project that can route requests and allow use to generate a Word document with record details using OpenXml and return it to the client. If you missed them:
How to Export Data to Microsoft Word with the LightSwitch HTML Client – Part 1
How to Export Data to Microsoft Word with the LightSwitch HTML Client – Part 2
In this article we will cover how to create report that summarizes all the records in an entity.
Routing the Request
In a previous article we added an entry to the Routes table, shown below, in the Global.asax of the Server project, to handle requests for our ExportToWord controller.
C#:
protectedvoid Application_Start(object sender, EventArgs e) { RouteTable.Routes.MapHttpRoute("ExportToWord", "reports/{controller}/{entity}/{id}", new { id = RouteParameter.Optional }); }
VB:
Sub Application_Start(ByVal sender AsObject, ByVal e As EventArgs) RouteTable.Routes.MapHttpRoute("ExportToWord","reports/{controller}/{entity}/{id}",NewWith {.id = RouteParameter.Optional})End Sub
The above code adds the ability for the application to handle requests like http://server/MyLSApplication/reports/ExportToWord/Events for summary reports and http://server/MyLSApplication/reports/ExportToWord/Events/1 for detailed reports. We have already written the code to handle the request for detailed reports in previous article. Now we will focus on the summary report.
Handling the Request
Below is the code to handle a request like http://server/MyLSApplication/reports/ExportToWord/Events. We will add another method to the ExportToWordController class created in the previous article.
C#:
public HttpResponseMessage Get(string entity) { IDataServiceQueryable queryable;using (ServerApplicationContext ctx = ServerApplicationContext.CreateContext()) {switch (entity) {case"Events": queryable = ctx.DataWorkspace.ApplicationData.EventSessions;break;default:thrownew ArgumentException("Unsupported query request"); } DataTable table = GetTableData(queryable); MemoryStream s = CreateSummaryWordDoc(entity, table); return GetWordDocResponseMessage(s, entity + ".docx"); }
VB:
PublicFunction GetValues(ByVal entity AsString) As HttpResponseMessageDim queryable As IDataServiceQueryable Using ctx As ServerApplicationContext = ServerApplicationContext.CreateContext()SelectCase entityCase"Events" queryable = ctx.DataWorkspace.ApplicationData.EventSessionsCaseElseThrowNew ArgumentException("Unsupported query request")EndSelectDim entityData As DataTable = GetTableData(queryable)Dim s As MemoryStream = CreateSummaryWordDoc(entity, entityData)Return GetWordDocResponseMessage(s, entity &".docx")End UsingEnd Function
The code to handle the summary report is very similar for that of the detailed report. The differences are the data is now a table of information, not a dictionary, and our function for creating the Word document needs to create a table.
Get the Data
Below is the function for getting all the table data from the query object. As noted previously, this code was added to make the function to create the Word document easier to read.
C#:
private DataTable GetTableData(IDataServiceQueryable queryable) { DataTable table = new DataTable(); System.Collections.IEnumerator enumerator = queryable.GetEnumerator();//Get column headers PropertyInfo[] columnHeaders = null;if (enumerator.MoveNext()) { columnHeaders = enumerator.Current.GetType().GetProperties( BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static); columnHeaders = RemoveUnreadableProperties(columnHeaders);foreach (var heading in columnHeaders) { table.Columns.Add(heading.Name, heading.PropertyType); }//Write out valuesforeach (var element in queryable) { var row = table.NewRow();foreach (var heading in columnHeaders) { row[heading.Name] = heading.GetValue(element); } table.Rows.Add(row); } }return table; }
private PropertyInfo[] RemoveUnreadableProperties(PropertyInfo[] columnProperties) {return columnProperties.Where(p => !p.PropertyType.IsArray&& !p.PropertyType.IsGenericType&& !"Microsoft.LightSwitch.Framework.Server.PersonInfo".Equals( p.PropertyType.FullName)&& !p.PropertyType.FullName.StartsWith("LightSwitchApplication") ).ToArray(); }
VB:
PrivateFunction GetTableData(queryable As IDataServiceQueryable) As DataTableDim table AsNew DataTable()Dim enumerator As System.Collections.IEnumerator = queryable.GetEnumerator()'Get column headersDim columnHeaders() As PropertyInfo = NothingIf enumerator.MoveNext() Then columnHeaders = enumerator.Current.GetType().GetProperties( BindingFlags.PublicOr BindingFlags.Instance _Or BindingFlags.Static) columnHeaders = RemoveUnreadableProperties(columnHeaders)ForEach heading In columnHeaders table.Columns.Add(heading.Name, heading.PropertyType)Next'Write out valuesForEach element In queryableDim row As DataRow = table.NewRow()ForEach heading In columnHeaders row(heading.Name) = heading.GetValue(element)Next table.Rows.Add(row)NextEndIfReturn tableEnd Function
PrivateFunction RemoveUnreadableProperties(columnHeaders() As PropertyInfo) As PropertyInfo()Return (From p In columnHeaders Where Not p.PropertyType.IsArray AndAlsoNot p.PropertyType.IsGenericType AndAlsoNot"Microsoft.LightSwitch.Framework.Server.PersonInfo".Equals( p.PropertyType.FullName) AndAlsoNot p.PropertyType.FullName.StartsWith("LightSwitchApplication")).ToArray()End Function
Create the Document
The CreateSummaryWordDoc function creates a word document with the entity name as the title and adds a table listing all the printable properties of the records for that entity.
C#:
private MemoryStream CreateSummaryWordDoc(string entityName, DataTable dataTable) { MemoryStream s = new MemoryStream();//Create word documentusing (WordprocessingDocument wordDocument = WordprocessingDocument.Create(s, WordprocessingDocumentType.Document, true)) { wordDocument.AddMainDocumentPart(); wordDocument.MainDocumentPart.Document = new Document(new Body()); Body body = wordDocument.MainDocumentPart.Document.Body;// Get the Styles part for this document. AddStyleDefinitionsPart(wordDocument.MainDocumentPart); wordDocument.MainDocumentPart.StyleDefinitionsPart.Styles.Append(GetTitleStyle()); wordDocument.MainDocumentPart.StyleDefinitionsPart.Styles.Append(GetTableStyle());//Add Title to Document body.Append(new Paragraph(new ParagraphProperties(new ParagraphStyleId() { Val = "Title" }),new Run(new Text(entityName)) ) );//Add table of entries in the entity Table table = new Table(); table.Append(new TableProperties(new TableStyle() { Val = "GridTable1Light-Accent1" }, new TableLook() { FirstRow = true } ) );//Add heading row for table TableRow headingRow = new TableRow();foreach (var column in dataTable.Columns) { headingRow.Append(new TableCell(new Paragraph(new Run(new Text(column.ToString()))))); } table.Append(headingRow);//Add text for each event.foreach (DataRow dataRow in dataTable.Rows) { TableRow row = new TableRow();foreach (var cell in dataRow.ItemArray) { row.Append(new TableCell(new Paragraph(new Run(new Text(cell.ToString()))))); } table.Append(row); } wordDocument.MainDocumentPart.Document.Append(table); wordDocument.MainDocumentPart.Document.Save(); wordDocument.Close(); } return s; }
VB:
PrivateFunction CreateSummaryWordDoc(entityName AsString, data As DataTable) As MemoryStreamDim s AsNew MemoryStream'Create Word document Using wordDocument As WordprocessingDocument = WordprocessingDocument.Create(s, WordprocessingDocumentType.Document, True)'Create the main document part,' which is the part that holds the text. wordDocument.AddMainDocumentPart() wordDocument.MainDocumentPart.Document = New Document(New Body())Dim docBody As Body = wordDocument.MainDocumentPart.Document.Body'Get the Styles part for this document' and add our custom stylesIf wordDocument.MainDocumentPart.StyleDefinitionsPart IsNothingThenCallNew Styles().Save( wordDocument.MainDocumentPart.AddNewPart(Of StyleDefinitionsPart))EndIf wordDocument.MainDocumentPart.StyleDefinitionsPart.Styles.Append( GetTitleStyle()) wordDocument.MainDocumentPart.StyleDefinitionsPart.Styles.Append( GetTableStyle())'Add Title to Document docBody.Append(New Paragraph( _New ParagraphProperties(New ParagraphStyleId() With {.Val = "Title"} ),New Run(New Text(entityName))) )'Add table of entries in the entityDim t AsNew Table() t.Append( _New TableProperties( _New TableStyle() With {.Val = "GridTable1Light-Accent1"},New TableLook() With {.FirstRow = True} ) )'Add heading row for tableDim headingRow AsNew TableRowForEach col In data.Columns headingRow.Append( _New TableCell(New Paragraph(New Run(New Text(col.ToString())) )) )Next t.Append(headingRow)'Add text for each eventForEach dataRow As DataRow In data.RowsDim row AsNew TableRowForEach cell In dataRow.ItemArray row.Append(New TableCell(New Paragraph(New Run(New Text(cell.ToString())))))Next t.Append(row)Next wordDocument.MainDocumentPart.Document.Append(t) wordDocument.MainDocumentPart.Document.Save() wordDocument.Close() End UsingReturn sEnd Function
Styles
Styles must be added to documents created using OpenXml. Below is the code necessary to create the Heading 1 style used by CreateSummaryWordDoc.
C#:
Style GetTitleStyle() { Style titleStyle = new Style() { Type = StyleValues.Paragraph, StyleId = "Title", CustomStyle = true }; titleStyle.Append(new StyleName() { Val = "Title" },new StyleRunProperties(new Color() { Val = "2E74B5" },new FontSize() { Val = "56" } ) );return titleStyle; }
VB:
PrivateFunction GetHeading1Style() As StyleDim heading1Style AsNew Style With {.Type = StyleValues.Paragraph, .StyleId = "Heading1"} heading1Style.Append( _New StyleName() With {.Val = "Heading 1"},New BasedOn() With {.Val = "Normal"},New NextParagraphStyle With {.Val = "Normal"},New StyleParagraphProperties( _New KeepNext(),New KeepLines(),New SpacingBetweenLines() With {.Before = "240", .After = "0"} ),New StyleRunProperties( _New Color() With {.Val = "2E74B5"},New FontSize() With {.Val = "32"} ) )Return heading1StyleEnd Function
The code for the table styling is a little more complicated as the style for the cells and borders have to be specified separately. Conditional formatting also has to be described if the table style is to support First Row, Last Row, First Column, etc. conditional formatting in Word tables. The following code is a simplified version of the Grid Table 1 Light Accent 1 table style found a normal Word document. This style only supports First Row conditional formatting, but other conditional formatting could easily be added to the end of the style definition. The Open XML SDK 2.5 Productivity Tool (part of the Open Xml SDK download) is helpful when trying to craft styles for an document.
C#:
Style GetTableStyle() { Style tableStyle = new Style() { Type = StyleValues.Table, StyleId = "GridTable1Light-Accent1" }; tableStyle.Append(new StyleName() { Val = "Grid Table 1 Light Accent 1" },new StyleParagraphProperties(new SpacingBetweenLines() { After = "0", Line = "240", LineRule = LineSpacingRuleValues.Auto } ),new StyleTableProperties(new TableStyleRowBandSize() { Val = 1 },new TableStyleColumnBandSize() { Val = 1 },new TableIndentation() { Width = 0, Type = TableWidthUnitValues.Dxa },new TableBorders(new TopBorder() { Val = BorderValues.Single, Color = "BDD6EE" },new LeftBorder() { Val = BorderValues.Single, Color = "BDD6EE" },new BottomBorder() { Val = BorderValues.Single, Color = "BDD6EE" },new RightBorder() { Val = BorderValues.Single, Color = "BDD6EE" },new InsideHorizontalBorder() { Val = BorderValues.Single, Color = "BDD6EE" },new InsideVerticalBorder() { Val = BorderValues.Single, Color = "BDD6EE" } ),new TableCellMarginDefault(new TableCellLeftMargin() { Width = 108, Type = TableWidthValues.Dxa},new TableCellRightMargin() { Width = 108, Type = TableWidthValues.Dxa} ) ),new TableStyleProperties(new RunPropertiesBaseStyle(new Bold() ),new TableStyleConditionalFormattingTableProperties(new TableStyleConditionalFormattingTableCellProperties(new BottomBorder() { Val = BorderValues.Double, Color = "9CC2E5" } ) ) ) { Type = TableStyleOverrideValues.FirstRow } ); return tableStyle; }
VB:
PrivateFunction GetTableStyle() As StyleDim tableStyle AsNew Style() With {.Type = StyleValues.Table, .StyleId = "GridTable1Light-Accent1"} tableStyle.Append(New StyleName() With {.Val = "Grid Table 1 Light Accent 1"},New StyleParagraphProperties(New SpacingBetweenLines() With {.After = "0", .Line = "240", .LineRule = LineSpacingRuleValues.Auto} ),New StyleTableProperties(New TableStyleRowBandSize() With {.Val = 1},New TableStyleColumnBandSize With {.Val = 1},New TableIndentation() With {.Width = 0, .Type = TableWidthUnitValues.Dxa},New TableBorders(New TopBorder() With {.Val = BorderValues.Single, .Color = "BDD6EE"},New LeftBorder() With {.Val = BorderValues.Single, .Color = "BDD6EE"},New RightBorder() With {.Val = BorderValues.Single, .Color = "BDD6EE"},New BottomBorder() With {.Val = BorderValues.Single, .Color = "BDD6EE"},New InsideHorizontalBorder() With {.Val = BorderValues.Single, .Color = "BDD6EE"},New InsideVerticalBorder() With {.Val = BorderValues.Single, .Color = "BDD6EE"} ),New TableCellMarginDefault(New TableCellLeftMargin() With {.Width = 108, .Type = TableWidthValues.Dxa},New TableCellRightMargin() With {.Width = 108, .Type = TableWidthValues.Dxa} ) ),New TableStyleProperties(New RunPropertiesBaseStyle(New Bold()),New TableStyleConditionalFormattingTableProperties(New TableStyleConditionalFormattingTableCellProperties(New BottomBorder() With {.Val = BorderValues.Double, .Color = "9CC2E5"} ) ) ) With {.Type = TableStyleOverrideValues.FirstRow} )Return tableStyleEnd Function
Send A Response
We will use the same function to craft an HttpResponseMessage with the document as part of the response as mentioned in the first article in this series. I’ve included the code below for easier reference.
C#:
private HttpResponseMessage GetWordDocResponseMessage(MemoryStream s, String suggestDocName) { HttpResponseMessage result = new HttpResponseMessage(HttpStatusCode.OK);//write the document
s.Position = 0;
result.Content = new StreamContent(s);
//Set the content type to .docx result.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/vnd.openxmlformats-officedocument.wordprocessingml.document");//Set the name of the file result.Content.Headers.ContentDisposition = new System.Net.Http.Headers.ContentDispositionHeaderValue("attachment"); result.Content.Headers.ContentDisposition.FileName = suggestDocName;return result; }
VB:
PrivateFunction GetWordDocResponseMessage(s As MemoryStream, suggestedDocName AsString) _As HttpResponseMessageDim result AsNew HttpResponseMessage(HttpStatusCode.OK)'Write the document
s.Position = 0
result.Content = New StreamContent(s)
'Set the content type to .docx result.Content.Headers.ContentDisposition = New Headers.ContentDispositionHeaderValue("attachment") result.Content.Headers.ContentDisposition.FileName = suggestedDocNameReturn resultEnd Function
Try it Out
We have now created all the code necessary create a summary document. Try launching the application and typing http://localhost:[debuggingPort]/reports/ExportToWord/Events in the Internet Explorer address bar. You should see something like the following.
Open From the Client
To create the report from the client, we need a button to open a window to the correct url on the server. In the case of the Event Manager, I have added a button called GetReport to the command bar of the browse screen for the EventSession entity.
The execute code for the button is as follows.
myapp.BrowseEventSessions.GetReport_execute = function (screen) { window.open("../reports/ExportToWord/Events"); };
Conclusion
In this article series we learned how to use the LightSwitchServerContext along with the Open XML SDK to generate reports in Word documents for our LightSwitch HTML application. Generating Word documents gives users an easy way to share information from a LightSwitch application. With the power of OpenXml, these reports can be incredibly beautiful. I hope you’ve found this article series useful.
-Elizabeth Maher, Senior SDET, Cloud Business Apps Team