Нередко встает вопрос об экспорте данных из таблицы(или DataSet’а) в различные форматы. Существуют разные подходы к решению этой проблемы: это и элемент управления ReportViewer, позволяющий экспортировать в форматы doc, xls и pdf; это и использование различных библиотек; “ручная” реализация логики формирования документа. Мне больше нравится другой способ, а именно: сохранение данных таблицы в XML формат и последующее преобразование с помощью XSLT.
Главными преимуществами этого способа я считаю его простоту и гибкость. Использование ReportViewer’а ограничивает возможности несколькими форматами, причем не все можно просмотреть где угодно. Дополнительные компоненты зачастую бывают платными, да и результатом, который будет получен, трудно управлять. Так же как и “ручная” реализация логики экспорта, использование компонентов подразумевает жесткое кодирование структуры документа и для того, чтобы сделать процесс более гибким придется немало потрудиться.
Преобразование с помощью XSLT лишено всех этих недостатков. Экспорт производится в промежуточное представление, которое содержит в себе все данные в достаточно простом и доступном виде. XSLT документ может быть внешним(по отношению к программе) файлом и поэтому для того, чтобы внести изменения в формируемый документ или создать новый формат экспорта, достаточно создать новое преобразование или отредактировать существующее. Мало того, учитывая тот факт, что XSLT является словарем XML, его можно создавать программно(в том числе с помощью графических инструментов, тут уж на что фантазии хватит), благо для этого есть множество программных инструментов во всех технологиях, включая конечно же и .Net Framework.
Итак, что мы имеем. У нас есть некий объект DataTable с данными, нам надо получить простой HTML документ, в котором просто будет отображаться эта таблица как есть. При сохранении таблицы с помощью метода WriteXml мы получим документ примерно следующей структуры
<DatasetName>
<TableName>
<ColumnName>Cell value</ColumnName>
<ColumnName>Cell value</ColumnName>
<ColumnName>Cell value</ColumnName>
</TableName>
<TableName>
<ColumnName>Cell value</ColumnName>
<ColumnName>Cell value</ColumnName>
<ColumnName>Cell value</ColumnName>
</TableName>
<TableName>
<ColumnName>Cell value</ColumnName>
<ColumnName>Cell value</ColumnName>
<ColumnName>Cell value</ColumnName>
</TableName>
</DatasetName>
То есть корневой элемент имеет имя DataSet’а (вообще-то это не обязательно так, тут все зависит от настроек, например, если DataSet, в котором определена таблица нетипизированный, и для нее не указан XML тег, то имя элемента будет NewTable, но можно указать его и явно в свойствах), элемент, соответствующий строке имеет название таблицы, а элементы соответствующие ячейкам – названия столбцов. Таким образом, преобразование, которое должно решать нашу задачу, должно вроде бы взять к примеру первую строку, обойти ее ячейки и по именам элементов создать заголовки столбцов таблицы, а дальше формировать строки HTML таблицы, обходя строки документа. Выглядеть это должно примерно так.
<?xml version="1.0" encoding="utf-8"?><xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"><xsl:output method="html" indent="yes" /><xsl:template match="/"><html><head><title>DataTable</title></head><body><table border="1" bordercolor="black" cellspacing="0"><tbody><tr><xsl:for-each select="*/*[1]/*"><th><xsl:value-of select="name()"/></th></xsl:for-each></tr><xsl:apply-templates select="*/*"/></tbody></table></body></html></xsl:template><xsl:template match="*/*"><tr><xsl:for-each select="*"><td><xsl:value-of select="."/></td></xsl:for-each></tr></xsl:template></xsl:stylesheet>
Все вроде хорошо, но вот только когда я попробовал применить это преобразование к таблице Customers из учебной базы данных NORTHWIND, я обнаружил, что не все так просто. В полученной мной HTML таблице наблюдались некоторые “корявости”: во-первых, некоторые строки были явно короче таблицы(не хватало ячеек); во-вторых, кое-где ячеек в строках было больше, чем столбцов в таблице. Просмотрев структуру таблицы, я увидел, что у нее 11 колонок, в то время как в HTML версии их было только 10. Взглянув на содержание таблицы и сравнив его с XML документом я понял, что если в таблице есть пустые ячейки, то для них пустые элементы не создаются и проблема была именно в этом. Я взял в качестве образца первую строку, в ней оказалось всего 10 заполненных ячеек и именно по ним построил столбцы. Далее в строках я устанавливал значения ячеек по порядку следования, а не по имени колонки, из-за этого в тех строках, где не все ячейки заполнены, некоторые значения вообще попадали не по адресу. Стало понятно, что нужен другой подход.
Можно было бы найти среди строк ту, в которой больше всего ячеек, но нет гарантий, что в таблице есть хоть одна строка, в которой все ячейки заполнены. Можно пройтись по всему документу и отобрать имена ячеек, как это делается при группировке элементов, но в этом случае сложно будет(а иногда и вовсе невозможно) правильно упорядочить столбцы, а мы ведь хотим в HTML документе отображать таблицу максимально близким к оригиналу способом.
Решить данную проблему можно, если при сохранении таблицы в XML файл внедрять в этот файл схему. Для этого нам надо воспользоваться одной из перегрузок метода WriteXml таблицы. То есть код сохранения для таблицы Customers будет выглядеть так.
NORTHWNDDataSet.Customers.WriteXml(FileName, XmlWriteMode.WriteSchema)В этом случае в XML представлении таблицы первым элементом-потомком корневого узла будет схема документа, из которой мы и извлечем информацию обо всех колонках и даже порядке их следования. Приведу начало документа, которое включает схему и первую строку таблицы
Собственно логика действий состоит в том, чтобы сформировать заголовки колонок из элементов схемы, а строки формировать опять-таки обходя эти же элементы и из каждой строки XML документа извлекать элемент-потомок, соответствующий имени столбца. Его значение и копируется в ячейку HTML таблицы, а если такого элемента нет, то ячейка останется пустой.<?xml version="1.0" standalone="yes"?><NORTHWNDDataSet xmlns="http://tempuri.org/NORTHWNDDataSet.xsd"><xs:schema id="NORTHWNDDataSet"targetNamespace="http://tempuri.org/NORTHWNDDataSet.xsd"xmlns:mstns="http://tempuri.org/NORTHWNDDataSet.xsd"xmlns="http://tempuri.org/NORTHWNDDataSet.xsd"xmlns:xs="http://www.w3.org/2001/XMLSchema"xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"xmlns:msprop="urn:schemas-microsoft-com:xml-msprop"attributeFormDefault="qualified"elementFormDefault="qualified"><xs:element name="NORTHWNDDataSet"msdata:IsDataSet="true"msdata:MainDataTable="http_x003A__x002F__x002F_tempuri.org_x002F_NORTHWNDDataSet.xsd_x003A_Customers"msdata:UseCurrentLocale="true"><xs:complexType><xs:choice minOccurs="0" maxOccurs="unbounded"><xs:element name="Customers"><xs:complexType><xs:sequence><xs:element name="CustomerID"><xs:simpleType><xs:restriction base="xs:string"><xs:maxLength value="5" /></xs:restriction></xs:simpleType></xs:element><xs:element name="CompanyName"><xs:simpleType><xs:restriction base="xs:string"><xs:maxLength value="40" /></xs:restriction></xs:simpleType></xs:element><xs:element name="ContactName" minOccurs="0"><xs:simpleType><xs:restriction base="xs:string"><xs:maxLength value="30" /></xs:restriction></xs:simpleType></xs:element><xs:element name="ContactTitle" minOccurs="0"><xs:simpleType><xs:restriction base="xs:string"><xs:maxLength value="30" /></xs:restriction></xs:simpleType></xs:element><xs:element name="Address" minOccurs="0"><xs:simpleType><xs:restriction base="xs:string"><xs:maxLength value="60" /></xs:restriction></xs:simpleType></xs:element><xs:element name="City" minOccurs="0"><xs:simpleType><xs:restriction base="xs:string"><xs:maxLength value="15" /></xs:restriction></xs:simpleType></xs:element><xs:element name="Region"msprop:Generator_ColumnVarNameInTable="columnRegion"msprop:Generator_ColumnPropNameInTable="RegionColumn"msprop:Generator_UserColumnName="Region"minOccurs="0"><xs:simpleType><xs:restriction base="xs:string"><xs:maxLength value="15" /></xs:restriction></xs:simpleType></xs:element><xs:element name="PostalCode" minOccurs="0"><xs:simpleType><xs:restriction base="xs:string"><xs:maxLength value="10" /></xs:restriction></xs:simpleType></xs:element><xs:element name="Country" minOccurs="0"><xs:simpleType><xs:restriction base="xs:string"><xs:maxLength value="15" /></xs:restriction></xs:simpleType></xs:element><xs:element name="Phone" minOccurs="0"><xs:simpleType><xs:restriction base="xs:string"><xs:maxLength value="24" /></xs:restriction></xs:simpleType></xs:element><xs:element name="Fax" minOccurs="0"><xs:simpleType><xs:restriction base="xs:string"><xs:maxLength value="24" /></xs:restriction></xs:simpleType></xs:element></xs:sequence></xs:complexType></xs:element></xs:choice></xs:complexType><xs:unique name="Constraint1" msdata:PrimaryKey="true"><xs:selector xpath=".//mstns:Customers" /><xs:field xpath="mstns:CustomerID" /></xs:unique></xs:element></xs:schema><Customers><CustomerID>ALFKI</CustomerID><CompanyName>Alfreds Futterkiste</CompanyName><ContactName>Maria Anders</ContactName><ContactTitle>Sales Representative</ContactTitle><Address>Obere Str. 57</Address><City>Berlin</City><PostalCode>12209</PostalCode><Country>Germany</Country><Phone>030-0074321</Phone><Fax>030-0076545</Fax></Customers>
<?xml version="1.0" encoding="utf-8"?><xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"xmlns:xsd="http://www.w3.org/2001/XMLSchema"><xsl:output method="html" indent="yes"/><xsl:template match="/"><html><head><title>DataTable with schema</title></head><body><table border="1" bordercolor="black" cellspacing="0"><tbody><tr><xsl:for-each select="//xsd:element[count(ancestor::xsd:element) = 2]"><th><xsl:value-of select="@name"/></th></xsl:for-each></tr><xsl:apply-templatesselect="/*/*[namespace-uri() != 'http://www.w3.org/2001/XMLSchema']"/></tbody></table></body></html></xsl:template><xsl:template match="/*/*[namespace-uri() != 'http://www.w3.org/2001/XMLSchema']"><tr><xsl:variable name="currentrow" select="."/><xsl:for-each select="//xsd:element[count(ancestor::xsd:element) = 2]"><td><xsl:variable name="column" select="@name"/><xsl:value-of select="$currentrow/*[name() = $column]"/></td></xsl:for-each></tr></xsl:template></xsl:stylesheet>
Можно несколько усовершенствовать это преобразование, например выделив в отдельное преобразование формирование таблицы, чтобы потом можно было его импортировать и вставлять таблицу куда надо. Можно добавить параметров, с помощью которых контролировать стиль. Но это уже лирика.