We want to export the entire hierarchy, with formatted column headings, and also export the nodes' expanded statuses. Like this...
XamDataGrid |
Excel Export |
We will subclass the XamDataGrid and add the export as a new method. Start a new C# class project called ExportableXamDataGrid.
Before you can use OpenXML you need to use NuGet to fetch the OpenXML package. Open Tools -> NuGet Package Manager -> Manage NugetPackages for Solution. Click Browse and search for DocumentFormat.OpenXml. Select it, select the project, and click [Install]
Rename Class1 to ExportableXamDataGrid. Add references to Infragistics, PresentationCore, and PresentationFramework as shown below.
You're going to need all these usings.
using System;
using System.Collections.Generic;
using Infragistics.Windows.DataPresenter;
using System.Windows;
using System.Collections;
using Infragistics.Windows.DataPresenter.Events;
using DocumentFormat.OpenXml.Packaging;
using System.Linq;
using DocumentFormat.OpenXml.Spreadsheet;
using DocumentFormat.OpenXml;
And inherit from XamDataGrid.
public class ExportableXamDataGrid : XamDataGrid
We need a single new method called Export. We will export to a file called CustomExport.xlsx in the user's temp folder. The sequence in which you create the different parts of the spreadsheet, the style, the columns, and the data is critical, poorly documented, and there are no error messages giving you a clue of what you did wrong. That's why I say Microsoft's implementation is so poor that I get the feeling they don't want OpenXml to be widely adopted.
We will append columns to the columns collection as we need them and adjust the width based on the width of the data. ExportFieldLayout is a recursive routine that gets called to export the children of nodes that have children.
public void Export()
{
string TempName = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "CustomExport.xlsx");
SpreadsheetDocument ss =
SpreadsheetDocument.Create(TempName, SpreadsheetDocumentType.Workbook);
WorkbookPart wbp = ss.AddWorkbookPart();
wbp.Workbook = new
DocumentFormat.OpenXml.Spreadsheet.Workbook();
WorksheetPart wsp =
wbp.AddNewPart<WorksheetPart>();
wsp.Worksheet = new DocumentFormat.OpenXml.Spreadsheet.Worksheet();
Sheets sheets =
ss.WorkbookPart.Workbook.AppendChild<Sheets>(new Sheets());
Sheet sheet = new Sheet()
{
Id = ss.WorkbookPart.GetIdOfPart(wsp),
SheetId = 1,
Name = "Sheet
1"
};
sheets.Append(sheet);
SetStyle(ref ss);
Columns columns = new Columns();
wsp.Worksheet.Append(columns);
wsp.Worksheet.AppendChild(new SheetData());
ExportFieldLayout(ref wsp, this.ViewableRecords);
wsp.Worksheet.Save();
wbp.Workbook.Save();
ss.Close();
}
SetStyle creates a style with a background and bottom border. If you use custom styles, you have to recreate the default styles explicitly. This is an insane amount of work to create a single custom style.
private void
SetStyle(ref SpreadsheetDocument ss)
{
WorkbookStylesPart stylesPart =
ss.WorkbookPart.AddNewPart<WorkbookStylesPart>();
stylesPart.Stylesheet = new Stylesheet();
// blank font
list
stylesPart.Stylesheet.Fonts = new Fonts();
stylesPart.Stylesheet.Fonts.Count =
1;
stylesPart.Stylesheet.Fonts.AppendChild(new Font());
// create fills
stylesPart.Stylesheet.Fills = new Fills();
// create a
solid red fill
PatternFill HeadingFill = new PatternFill() { PatternType =
PatternValues.Solid };
HeadingFill.ForegroundColor = new ForegroundColor { Rgb =
HexBinaryValue.FromString("FFE0E0E0") };
HeadingFill.BackgroundColor = new BackgroundColor { Indexed = 64 };
stylesPart.Stylesheet.Fills.AppendChild(new Fill { PatternFill = new PatternFill { PatternType = PatternValues.None } }); // required, reserved by Excel
stylesPart.Stylesheet.Fills.AppendChild(new Fill { PatternFill = new PatternFill { PatternType = PatternValues.Gray125 } }); // required, reserved by Excel
stylesPart.Stylesheet.Fills.AppendChild(new Fill { PatternFill = HeadingFill });
stylesPart.Stylesheet.Fills.Count =
3;
// blank border
list
stylesPart.Stylesheet.Borders = new Borders();
stylesPart.Stylesheet.Borders.Count
= 2;
stylesPart.Stylesheet.Borders.AppendChild(new Border());
stylesPart.Stylesheet.Borders.AppendChild(new Border() { BottomBorder = new BottomBorder() { Style = BorderStyleValues.Medium, Color = new Color() { Indexed = 0 } } });
// blank cell
format list
stylesPart.Stylesheet.CellStyleFormats = new CellStyleFormats();
stylesPart.Stylesheet.CellStyleFormats.Count = 1;
stylesPart.Stylesheet.CellStyleFormats.AppendChild(new CellFormat());
// cell format
list
stylesPart.Stylesheet.CellFormats =
new CellFormats();
// empty one
for index 0, seems to be required
stylesPart.Stylesheet.CellFormats.AppendChild(new CellFormat());
// cell format
references style format 0, font 0, border 1, fill 2 and applies the border and fill
stylesPart.Stylesheet.CellFormats.AppendChild(new CellFormat { FormatId = 0, FontId =
0, BorderId = 1, FillId = 2, ApplyBorder = true, ApplyFill = true }).AppendChild(new Alignment { Horizontal = HorizontalAlignmentValues.Center });
stylesPart.Stylesheet.CellFormats.Count = 2;
stylesPart.Stylesheet.Save();
}
If any of the records in the collection we are exporting is a data record then we need to start the group with headings. We also only want to display the headings and details if the parent record is currently expanded.We iterate through the fields in the first record (they all have the same headings), ignoring any fields that implement IList (because that field is used to populate that record's children). Then we insert the heading into the spreadsheet and set the cell style to our heading style so laboriously created in SetStyle.
Once the headings are exported, we iterate through the data records in our collection and for each one we iterate through the fields, ignoring the IList field(s), and exporting the values in the fields. If a record has children we export them before the next data record.
public void
ExportFieldLayout(ref
WorksheetPart wsp, ViewableRecordCollection vrc)
{
ExportFieldLayout(ref wsp, vrc, 0);
}
public void
ExportFieldLayout(ref
WorksheetPart wsp, ViewableRecordCollection vrc, int Depth)
{
SheetData sd = wsp.Worksheet.GetFirstChild<SheetData>();
uint NextRowIndex;
if (vrc.Any(r => r.IsDataRecord))
{
// headings
NextRowIndex = (uint)wsp.Worksheet.GetFirstChild<SheetData>().Elements<Row>().Count()
+ 1;
bool IsParentExpanded = vrc[0].ParentDataRecord == null ? true : vrc[0].ParentDataRecord.IsExpanded;
foreach (Infragistics.Windows.DataPresenter.Field f in vrc[0].FieldLayout.Fields)
{
if (f.DataType.GetInterface("IList") == null)
{
DocumentFormat.OpenXml.Spreadsheet.Cell cell;
cell =
InsertCellInWorksheet(vrc[0].FieldLayout.Fields.IndexOf(f), NextRowIndex,
f.Label.ToString(), wsp, Depth, false, IsParentExpanded);
cell.StyleIndex = 1;
}
}
}
foreach (Record r in vrc)
{
if (r is DataRecord)
{
DataRecord dr = (r as DataRecord);
bool IsExpanded = r.ParentDataRecord == null ? true :
r.ParentDataRecord.IsExpanded;
NextRowIndex = (uint)wsp.Worksheet.GetFirstChild<SheetData>().Elements<Row>().Count()
+ 1;
foreach (Infragistics.Windows.DataPresenter.Field f in dr.FieldLayout.Fields)
{
if (f.DataType.GetInterface("IList") == null)
{
InsertCellInWorksheet(vrc[0].FieldLayout.Fields.IndexOf(f),
NextRowIndex, dr.Cells[f.Name].Value.ToString(), wsp, Depth, r.HasChildren,
IsExpanded);
}
}
}
if (r.HasChildren) ExportFieldLayout(ref wsp, r.ViewableChildRecords, Depth + 1);
}
private static
DocumentFormat.OpenXml.Spreadsheet.Cell InsertCellInWorksheet(int ColumnIndex, uint rowIndex, String Value,
WorksheetPart wsp, int Depth, bool HasChildren, bool IsVisible)
{
DocumentFormat.OpenXml.Spreadsheet.Worksheet ws = wsp.Worksheet;
SheetData sd =
ws.GetFirstChild<SheetData>();
int DisplayColumnIndex = ColumnIndex + Depth + 1;
string ColumnName = GetColumnName(DisplayColumnIndex);
string cr = ColumnName + rowIndex;
Columns columns =
ws.Elements<Columns>().FirstOrDefault();
Row row;
const int
WidthFactor = 1;
const int WidthBonus
= 10;
if (sd.Elements<Row>().Where(r => r.RowIndex ==
rowIndex).Count() > 0)
row = sd.Elements<Row>().Where(r
=> r.RowIndex == rowIndex).First();
else
{
row = new Row() { RowIndex = rowIndex, DyDescent = 0.25D, Collapsed =
HasChildren, OutlineLevel = (Byte)Depth, Hidden = !IsVisible };
sd.Append(row);
}
if (columns != null)
{
List<Column> MatchingColumns;
int ColumnWidth = Value.Length *
WidthFactor + WidthBonus;
MatchingColumns =
columns.ChildElements.Cast<Column>().Where(c => c.Min ==
DisplayColumnIndex).ToList();
if (MatchingColumns.Count > 0)
{
if (MatchingColumns[0].Width < ColumnWidth)
MatchingColumns[0].Width = ColumnWidth;
}
else
columns.Append(new Column { Min = (uint)DisplayColumnIndex, Max = (uint)DisplayColumnIndex, Width = ColumnWidth,
CustomWidth = true });
}
if
(row.Elements<DocumentFormat.OpenXml.Spreadsheet.Cell>().Where(c =>
c.CellReference.Value == ColumnName + rowIndex).Count() > 0)
return
row.Elements<DocumentFormat.OpenXml.Spreadsheet.Cell>().Where(c =>
c.CellReference.Value == ColumnName + rowIndex).First();
DocumentFormat.OpenXml.Spreadsheet.Cell
refCell = null;
foreach (DocumentFormat.OpenXml.Spreadsheet.Cell cell in
row.Elements<DocumentFormat.OpenXml.Spreadsheet.Cell>())
{
if (cell.CellReference.Value.Length == cr.Length)
{
if (string.Compare(cell.CellReference.Value,
cr, true) > 0)
{
refCell = cell;
break;
}
}
}
DocumentFormat.OpenXml.Spreadsheet.Cell
newCell = new
DocumentFormat.OpenXml.Spreadsheet.Cell() { CellReference = cr, DataType =
CellValues.String, CellValue = new CellValue(Value) };
row.InsertBefore(newCell, refCell);
ws.Save();
return newCell;
}
private static String
GetColumnName(int Index)
{ // Works for
first 26 anyway
return ((char)(64 +
Index)).ToString();
}
At this point we can get a clean build. Now we add a WPF window to demonstrate the export. Add a WPF App to the solution called ExportableXamDataGridTest.
Add references to Infragistics and the ExportableXamDataGrid and set it as the startup project.
The XAML defines a hierarchical ExportableXamDataGrid and a single button.
<Window x:Class="ExportableXamDataGridTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:custom="clr-namespace:ExportableXamDataGrid;assembly=ExportableXamDataGrid"
xmlns:igDP="http://infragistics.com/DataPresenter"
xmlns:local="clr-namespace:ExportableXamDataGridTest"
mc:Ignorable="d"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<RoutedCommand x:Key="ExportCustom"/>
</Window.Resources>
<Window.CommandBindings>
<CommandBinding Command="{StaticResource ExportCustom}" Executed="Export_Executed"/>
</Window.CommandBindings>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<custom:ExportableXamDataGrid Grid.Row="0" DataSource="{Binding SearchResults}" ActiveDataItem="{Binding SelectedItem}">
<igDP:XamDataGrid.FieldLayoutSettings>
<igDP:FieldLayoutSettings SelectionTypeRecord="Single" AutoGenerateFields="False" ExpansionIndicatorDisplayMode="CheckOnDisplay"/>
</igDP:XamDataGrid.FieldLayoutSettings>
<igDP:XamDataGrid.FieldSettings>
<igDP:FieldSettings AllowSummaries="False" AllowEdit="False"/>
</igDP:XamDataGrid.FieldSettings>
<igDP:XamDataGrid.FieldLayouts>
<igDP:FieldLayout Description="Pallet" Key="Pallet">
<igDP:FieldLayout.Fields>
<igDP:TextField Label="ID" Name="ID"/>
<igDP:TextField Label="Pallet
Name" Name="Name"/>
<igDP:Field Label="" Name="Boxes"/>
</igDP:FieldLayout.Fields>
</igDP:FieldLayout>
<igDP:FieldLayout ParentFieldLayoutKey="Pallet" ParentFieldName="Boxes" Key="Box">
<igDP:FieldLayout.Fields>
<igDP:TextField Label="ID" Name="ID"/>
<igDP:TextField Label="Box
Name" Name="Name"/>
<igDP:Field Label="" Name="Items"/>
</igDP:FieldLayout.Fields>
</igDP:FieldLayout>
<igDP:FieldLayout ParentFieldLayoutKey="Box" ParentFieldName="Items" Key="Item">
<igDP:FieldLayout.Fields>
<igDP:TextField Label="ID" Name="ID"/>
<igDP:TextField Label="Item
Name" Name="Name"/>
</igDP:FieldLayout.Fields>
</igDP:FieldLayout>
</igDP:XamDataGrid.FieldLayouts>
</custom:ExportableXamDataGrid>
<StackPanel Grid.Row="1" Orientation="Horizontal">
<Button Content="Export
Custom" Command="{StaticResource ExportCustom}"/>
</StackPanel>
</Grid>
</Window>
The code-behind creates the required properties and populates them. When the [Export] button is pressed it calls the ExportableXamDataGrid's export method which creates a CustomExport.xlsx file in your temp folder.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;
using System.Windows.Input;
using System.Linq;
using System.Diagnostics;
namespace ExportableXamDataGridTest
{
public partial class MainWindow : Window, INotifyPropertyChanged
{
private ExportableXamDataGrid.ExportableXamDataGrid xdg = null;
public event PropertyChangedEventHandler
PropertyChanged;
private void
NotifyPropertyChanged(String PropertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(PropertyName));
}
}
public class cPallet
{
public string ID { get; set; }
public string Name { get; set; }
public List<cBox> Boxes { get; set; }
}
public class cBox
{
public string ID { get; set; }
public string Name { get; set; }
public List<cItem> Items { get; set; }
}
public class cItem
{
public string ID { get; set; }
public string Name { get; set; }
}
private List<cPallet> _SearchResults = new List<cPallet>();
public List<cPallet> SearchResults
{
get { return
_SearchResults; }
set
{
_SearchResults = value;
NotifyPropertyChanged("SearchResults");
}
}
public String SelectedItem { get; set; }
private List<cPallet> Inventory;
public MainWindow()
{
InitializeDataGrid();
InitializeComponent();
}
public void
InitializeDataGrid()
{
SearchResults = new List<cPallet>
{
new cPallet() {ID="1", Name="Pallet from UPS", Boxes = new
List<cBox>()
{
new cBox() {ID="1A", Name="Box A on Pallet 1", Items= new List<cItem>() {new cItem() {ID="1Aa", Name="Item a in Box A" }, new cItem() {ID="1Ab", Name="Item b in Box A"} } }
,
new cBox() {ID="1B", Name="Box B on Pallet 1", Items= new List<cItem>() {new cItem() {ID="1Ba", Name="Item a in Box B" }, new cItem() {ID="1Bb", Name="Item b in Box B"} } }
}
}
,
new cPallet() {ID="2", Name="Pallet from FedEx", Boxes = new
List<cBox>()
{
new cBox() {ID="2A", Name="Box A on Pallet 2", Items= new List<cItem>() {new cItem() {ID="2Aa", Name="Item a in Box A" }, new cItem() {ID="2Aa", Name="Item b in Box A"} } }
,
new cBox() {ID="2B", Name="Box B on Pallet 2", Items= new List<cItem>() {new cItem() {ID="2Ba", Name="Item a in Box B" }, new cItem() {ID="2Bb", Name="Item b in Box B"} } }
}
}
,
new cPallet() {ID="3", Name="Pallet from USPS", Boxes = new
List<cBox>()
{
new cBox() {ID="3A", Name="Box A on Pallet 3", Items= new List<cItem>() {new cItem() {ID="3Aa", Name="Item a in Box A" }, new cItem() {ID="3Aa", Name="Item b in Box A"} } }
,
new cBox() {ID="3B", Name="Box B on Pallet 3", Items= new List<cItem>() {new cItem() {ID="3Ba", Name="Item a in Box B" }, new cItem() {ID="3Bb", Name="Item b in Box B"} } }
}
}};
}
private void
Export_Executed(object sender,
ExecutedRoutedEventArgs e)
{
xdg.Export();
}
private void
XamDataGrid_Loaded(object sender,
RoutedEventArgs e)
{
xdg = (sender as
ExportableXamDataGrid.ExportableXamDataGrid);
}
}
}
Note: If you make a mistake in the structure of the export, Excel will allow you to try to recover the file and will generate an error file.
Clicking on the link displays this worthless XML. I get the feeling Microsoft has tried very hard to make it look like they tried very hard, but tried even harder to make sure you fail.
One trick I've found is to put a breakpoint at ss.Save and look at wbp.Workbook.Outerxml and wsp.Worksheet.Outerxml using an xml visualizer. These can be compared to the expected schemas on pages such as https://docs.microsoft.com/en-us/office/open-xml/how-to-insert-text-into-a-cell-in-a-spreadsheet
Take a look at the worksheet XML below. You can see I have an empty columns collection. That's what is causing the problem above. I find it hard to believe Microsoft couldn't have mentioned that somehow.
Just for fun, let's compare our export to Infragistics' native export. In MainWindow.xaml add the following commands and buttons.
<RoutedCommand x:Key="ExportNative"/>
<CommandBinding Command="{StaticResource ExportNative}" Executed="ExportNative_Executed"/>
<Button Content="Export Native" Command="{StaticResource ExportNative}"/>
Add the following references to the ExportableXamDataGridTest project.
Add these usings to MainWindow.xaml.vb
using Infragistics.Windows.DataPresenter.ExcelExporter;
using Infragistics.Documents.Excel;
And add the new event handler in the code-behind.
private void
ExportNative_Executed(object sender,
ExecutedRoutedEventArgs e)
{
string TempName = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "ExportNative.xlsx");
DataPresenterExcelExporter exporter = new DataPresenterExcelExporter();
exporter.Export(xdg, TempName,
WorkbookFormat.Excel2007);
}
This exports the same grid to this excel spreadsheet.
If you add a few hundred more top level items and perform some timings you will see the Infragistics export is about 10 times faster than our custom export.