среда, 25 мая 2011 г.

Экспорт данных из DataTable(ADO.Net) с помощью XSLT. Часть 1 - HTML.

Нередко встает вопрос об экспорте данных из таблицы(или 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 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 документа извлекать элемент-потомок, соответствующий имени столбца. Его значение и копируется в ячейку HTML таблицы, а если такого элемента нет, то ячейка останется пустой.
 
<?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-templates 
select="/*/*[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>

Можно несколько усовершенствовать это преобразование, например выделив в отдельное преобразование формирование таблицы, чтобы потом можно было его импортировать и вставлять таблицу куда надо. Можно добавить параметров, с помощью которых контролировать стиль. Но это уже лирика.

воскресенье, 22 мая 2011 г.

Загрузка музыки “оптом” из vkontakte.ru. Скрипт для кнопки Custom buttons.

Загрузить найденные с помощью поиска музыки vkontakte  музыкальные файлы средствами обычного загрузчика оказалось не так просто. Например взять плагин для Firefox DonloadThemAll, он просто ищет на странице ссылки и предлагает закачать файлы, находящиеся именно по тем ссылкам. Но в результатах поиска музыки, адреса аудиофайлов находятся не в ссылках, а в полях hidden, например так

<input 
type="hidden" 
id="audio_info65616706_106246809_4" 
value="http://cs4246.vkontakte.ru/u14371044/audio/65a4ce75cfe4.mp3,189"
/>



При этом каждый из результатов поиска находится в таблице и в соседней ячейке можно найти название и имя исполнителя.



Задача решается с помощью плагина для Firefox Custom buttons. Просто надо добавить кнопку на панель и в редакторе кода кнопки ввести следующий текст:

1: /*CODE*/
2: function getContentWin() 
3: {  
4: var cont = getBrowser().contentWindow;  
5: try 
6:   {    
7:     cont = new XPCNativeWrapper(cont).wrappedJSObject;
8:   } 
9:   catch(e) {}  
10:   return cont;
11: }
12: function getContentDoc() 
13: {  
14: var cont = getBrowser().contentDocument;  
15: try 
16:   {    
17:     cont = new XPCNativeWrapper(cont).wrappedJSObject;
18:   } 
19:   catch(e) {}  
20:   return cont;
21: }
22: var _window = getContentWin();
23: var _document = getContentDoc();
24: 
25: var newwin = _window.open("about:blank");
26: newwin.onload = function()
27: {
28:     var inputs = _window.document.getElementsByTagName("input");
29:     for(var i = 0; i < inputs.length; i++)
30:     {
31:       var input = inputs.item(i);
32:       if(input.type == "hidden" && input.id.indexOf("audio_info") == 0)
33:       {
34:         var row = input.parentNode.parentNode;
35:         
36:         var songLinks = row.getElementsByTagName("a");
37:         var autor = songLinks[1].innerHTML;
39:         var song = songLinks[1].parentNode.nextSibling.nextSibling.innerHTML;
40:         var link = newwin.document.createElement("a");
41:         link.href = input.value.split(",")[0];
42:         link.innerHTML = autor + " - " + song;
43:         newwin.document.body.appendChild(link);
44:         newwin.document.body.appendChild(newwin.document.createElement("br"));
45:       }
46:     }
47:   
48: }



Этот код открывает новую страницу в новой вкладке, извлекает адреса музыкальных файлов, исполнителей и названия треков, после чего формирует список ссылок на эти адреса и вставляет их во вновь созданную страницу. После этого, когда у нас есть страница со ссылками, можно закачать с помощью любого загрузчика, интегрированного в браузер, включая DonloadThemAll.

Шаблон скрипта для расширений Firefox

В некоторых расширениях для Firefox дается возможность написать собственный
скрипт и запускать его, на различных страницах. Есть такая возможность у
iMacros или Custom Buttons.
Любому кто знает Javascript такая функция может пригодиться. Однако объект
document в коде ссылается на документ XUL, а не на привычный по веб
разработке документ страницы. Данный код показывает, как получить ссылки
на объекты window и document страницы. Я его использую как шаблон кода для
работы с такими плагинами. Очень удобно.
function getContentWin() {
var cont = getBrowser().contentWindow;
try {
cont = new XPCNativeWrapper(cont).wrappedJSObject;
} catch(e) {}
return cont;
}
function getContentDoc() {
var cont = getBrowser().contentDocument;
try {
cont = new XPCNativeWrapper(cont).wrappedJSObject;
} catch(e) {}
return cont;
}
var _window = getContentWin();
var _document = getContentDoc();