Excel предоставляет возможность добавлять собственные XML схемы (XSD) к рабочим книгам. При добавлении создаётся XML карта, которая даёт дополнительные возможности при работе с данными, а именно импорт и экспорт данных. Таким образом, вы можете загружать данные в документ из хранилища, а затем записывать данные обратно посредством той же XML схемы.
Процесс начинается с добавления XSD файла к рабочей книге Excel. Как только XSD файл прикреплён, Excel создаёт XML карту, которая отображается в панели задач «источник XML» (см рис. 1), которую пользователь использует для позиционирования данных в регионах или отдельных ячейках. Excel использует данную карту для управления взаимодействием между позиционированными регионами и элементами XML схемы. Книга Excel может содержать несколько XML карт, где каждая не будет зависеть от других. Также вы можете иметь несколько карт, которые будут ссылаться на одну схему. Когда пользователь осуществляет импорт или экспорт XML данных, Excel использует карту для связи содержимого позиционированного региона с элементами схемы.
Рисунок 1. Панель «Источник XML»
В данной статье описывается проект, основанный на XML картах, который даёт возможность пользователям иметь общие шаблоны документов, XML схемы и хранилища данных размещенных в интернете. Своего рода Excel 365 своими руками.
Теперь предположим, что мы имеем рабочую книгу Excel содержащую XML карту, с позиционированными регионами и импортированными данными. Если посмотреть на книгу со стороны программиста, то её можно назвать слабосвязанной. Под словом "слабосвязанный" понимается понятие программирования loose coupling, т.е шаблон документа слабо связан с данными документа что даёт возможность делегировать функцию составления документа от разработчика пользователю. Похожий подход можно наблюдать в WPF, где работа над дизайном делегирована от разработчика дизайнеру, чего не было в Win Forms.
Поэтому, проект может применяться и при реализации модуля «вывода отчетов в Excel» в бизнес приложениях. При этом разработчику не обязательно создавать шаблон документа, пользователь сможет сделать это сам.
Рисунок 2.
Данный проект имеет SOA (service oriented architecture) архитектуру. Клиентом является Excel VSTO AddIn, а сервером является WCF сервис. Сервис предоставляет следующую функциональность:
Рисунок 3.
Хранилищем шаблонов является директория файловой системы сервера, которая может содержать поддиректории и файлы шаблонов. Для того чтобы просмотреть содержание директории и открыть нужный шаблон используются упрощенная версия сервисного протокола WS-Enumeration и протокол WS-Transfer. Упрощение состоит в том что в WS-Enumeration реализованы только методы Enumerate и Pull.
Рисунок 4. Протокол взаимодействия при работе с шаблонами.
SOAP message Enumerate |
||
---|---|---|
Запрос |
|
|
Ответ |
|
SOAP message Pull |
||
---|---|---|
Запрос |
|
|
Ответ |
|
SOAP message GET |
||
---|---|---|
Запрос |
|
|
Ответ |
|
Открытие документа
Код
void LLOpen(int iRow) { if (_lst[iRow].Type == "Dir") return; FileInfo fi = new FileInfo(_workDir + _xpath + "/" + _data[iRow].Name); if (fi.Exists) fi.Delete(); using (ChannelFactory<si.IService> serverChannel = new ChannelFactory<si.IService>("cdExcel")) { serverChannel.Open(); si.IService theService = serverChannel.CreateChannel(); DTO.GetResponse res = theService.GetTemplate(new DTO.GetRequest(_data[iRow].Name)); if (res.Resource != null && res.Resource.Length > 0) using (FileStream fs = fi.OpenWrite()) fs.Write(res.Resource, 0, res.Resource.Length); } try { _excel.Workbooks.Open(fi.FullName, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing); } catch (Exception ex) { System.Diagnostics.Trace.WriteLine(ex.Message); } finally { _view.Close(); } } |
Принцип получения списка XML схем и загрузка выбранной XML схемы аналогичен принципу используемому при работе с шаблонами. Отличие появляется лишь на последнем шаге. В случае с шаблонами мы открывали документ, а в случае с XML схемами нам нужно прикрепить схему к рабочей книге Excel.
Код
void LLAdd(int iRow) { if (_lst[iRow].Type == "Dir") return; XmlDocument doc = new XmlDocument(); FileInfo fi = new FileInfo(_workDir + _xpath + "/" + _data[iRow].Name); if (fi.Exists) fi.Delete(); using (ChannelFactory<si.IService> serverChannel = new ChannelFactory<si.IService>("cdExcel")) { serverChannel.Open(); si.IService theService = serverChannel.CreateChannel(); DTO.GetResponse res = theService.GetXmlMap(new DTO.GetRequest(_data[iRow].Name)); if (res.Resource != null && res.Resource.Length > 0) using (FileStream fs = fi.OpenWrite()) fs.Write(res.Resource, 0, res.Resource.Length); } try { doc.Load(fi.FullName); XmlNode node = doc.SelectSingleNode("//d:schema/d:element", _ns); string rootElementName = node.Attributes["name"] == null ? "" : node.Attributes["name"].Value; if (String.IsNullOrEmpty(rootElementName)) return; foreach (ex.XmlMap map in _excel.ActiveWorkbook.XmlMaps) if (map.RootElementName == rootElementName) return; _excel.ActiveWorkbook.XmlMaps.Add(fi.FullName, rootElementName); } catch (Exception ex) { System.Diagnostics.Trace.WriteLine(ex.Message); } finally { _view.Close(); } } |
Рисунок 5.
Для облегчения понимания введём три понятия:
Такое разделение полезно для более чёткого разделения клиентской и серверной частей. Так как клиентская часть не должна зависеть от CLR типов, которые используется сервером для формирования XML документов, то клиент должен использовать только не типизированные дата контракты. В противном случае при добавлении нового дата контракта на сервере необходимо будет обновлять сборки и на клиентах, для того чтобы добавить недостающий CLR тип.
ПРИМЕЧАНИЕ Таким образом клиентская часть оперирует с не типизированными дата контрактами, а серверная с типизированными. |
Предположим мы имеем БД и мы хотим осуществлять импортирование данных из БД расположенную на сервере в рабочую книгу Excel расположенную на клиенте. Для этого в серверную часть нам необходимо добавить дата контракт:
Dictionary<string, Func<IXmlSerializable[]>> _dictGetDataFuncByName; public Service() { _dictGetDataFuncByName = new Dictionary<string, Func<IXmlSerializable[]>>(); //добавляем ссылку на функцию для дата контракта с идентификатором _DATA_CONTRACT_NAME в словарь _dictGetDataFuncByName.Add(_DATA_CONTRACT_NAME, Get_DATA_CONTRACT_NAME); ... } |
ПРИМЕЧАНИЕ Ключ NamespaceName + RootElementName является уникальным идентификатором дата контракта для всей системы (клиент - сервер). |
Рассмотрим случай когда клиент хочет импортировать данные в рабочую книгу Excel. Клиент просматривает XML карты прикреплённые к рабочей книге Excel, и для каждой карты отправляет запрос EnumerateData на сервер, в котором в качестве параметра передаёт идентификатор дата контракта. Сервер просматривает реализован ли метод
public DTO.EnumerateDataResponse EnumerateData(DTO.EnumerateDataRequest request) { string ctx = String.Empty; if (String.IsNullOrEmpty(request.Filter)) thrownew Exception(); //находим функцию для дата контракта с идентификатором request.Filter Func<IXmlSerializable[]> func = _dictGetDataFuncByName[request.Filter]; //выполняем IXmlSerializable[] res = func(); DTO.EnumerateDataResponse msg = new DTO.EnumerateDataResponse(); ... return msg; } public DTO.PullDataResponse PullData(DTO.PullDataRequest request) { DTO.PullDataResponse msg = new DTO.PullDataResponse(); lock (_storage) { if (!_storage.Contains(request.Context)) thrownew DTO.InvalidEnumerationContextException(); IEnumerator<IXmlSerializable> e = (IEnumerator<IXmlSerializable>)_storage[request.Context]; using (MemoryStream ms = new MemoryStream()) using (XmlTextWriter writer = new XmlTextWriter(ms, Encoding.UTF8)) { writer.WriteStartElement("DataContract"); writer.WriteAttributeString("Context", request.Context); for (int i = 0; i < request.MaxElements; i++) { if (e.MoveNext()) { (e.Current as IXmlSerializable).WriteXml(writer); } else { writer.WriteStartElement("EndOfSequence"); writer.WriteEndElement(); break; } } writer.WriteEndElement(); writer.Flush(); ms.Seek(0, System.IO.SeekOrigin.Begin); XmlDocument doc = new XmlDocument(); doc.Load(ms); msg.Data = ms.ToArray(); } } return msg; } |
В отличии от работы с шаблонами мы не используем WS-Transfer, а передаём экземпляры дата контрактов в ответе на запрос Pull.
Рисунок 6.
Excel предоставляет возможность экспортирования данных. Но при этом накладываются несколько ограничений на типы данных которые могут быть экспортипованны. Ниже перечислены некоторые из таких ограничений:
Если тип удовлетворяет условиям экспорта и реализован метод сохранения данных, то мы можем сохранять изменения произведённые в Excel в БД.
При сохранении сохраняются как изменения шаблона так и изменения данных. Данная операция происходит в два этапа:
Рисунок 7.
Предположим мы хотим иметь возможность отображать в Excel структуру данных состоящую из нескольких вложенных списков. Для примера возьмём: Мир->Страны->Области ->Города.
«Мир» является корневым элементом, который содержит список стран. Каждая страна содержит список областей, а каждая область список городов. Карта данных для подобного типа данных будет выглядеть так:
<?xmlversion="1.0"encoding="utf-8"?> <schema xmlns="http://www.w3.org/2001/XMLSchema"> <element name="Mir"> <complexType> <sequence minOccurs="0" maxOccurs="1"> <element name="Strana" minOccurs="0" maxOccurs="unbounded"> <complexType> <sequence minOccurs="0" maxOccurs="1"> <element name="Nazvanie" type="string" /> <element name="Ploshad" type="string" /> <element name="Jazik" type="string" /> <element name="Naselenie" type="string" /> <element name="Oblast" minOccurs="0" maxOccurs="unbounded"> <complexType> <sequence minOccurs="0" maxOccurs="1"> <element name="Nazvanie" type="string" /> <element name="Gorod" minOccurs="0" maxOccurs="unbounded"> <complexType> <sequence minOccurs="0" maxOccurs="1"> <element name="Nazvanie" type="string"></element> </sequence> </complexType> </element> </sequence> </complexType> </element> </sequence> </complexType> </element> </sequence> </complexType> </element> </schema> |
Следующим шагом является создание CLR типов, при серилизации которых мы получим Xml документ соответствующий нашей карте. Для создания множественно вложенных структур удобно наследовать тип от IXmlSerializable. В результате для нашего случая получим четыре типа:
//Аттрибуты типа u.Display используются для информирования клиента о том каким //образом отображать список дата контрактов. В данном случае это будет список //содержащий 3 колонки ID, TheRowVersion, Nazvanie. Каждая строка соответствует //экзкмпляру дата контракта [u.Display("Name", "Name", "")] [XmlSchemaProvider(null, IsAny = true)] [XmlRoot(ElementName = "Mir", IsNullable = true)] [Serializable] publicclass Mir : IXmlSerializable { #region data publicstring Name = "Mir"; public IList<Strana> Stranas { get; set; } #endregion … } [XmlSchemaProvider(null, IsAny = true)] [XmlRoot(ElementName = "Strana", IsNullable = true)] [Serializable] publicclass Strana : IXmlSerializable { #region data publicint? ID { get; set; } public gt.RowVersion TheRowVersion { get; set; } publicstring Nazvanie { get; set; } publicint Ploshad { get; set; } publicstring Jazik { get; set; } publicint Naselenie { get; set; } public IList<Oblast> Oblasts { get; set; } #endregion … } [XmlSchemaProvider(null, IsAny = true)] [XmlRoot(ElementName = "Oblast", IsNullable = true)] [Serializable] publicclass Oblast : IXmlSerializable { #region data publicint? ID { get; set; } public gt.RowVersion TheRowVersion { get; set; } publicint StranaID { get; set; } publicstring Nazvanie { get; set; } public IList<Gorod> Gorods { get; set; } #endregion … } [XmlSchemaProvider(null, IsAny = true)] [XmlRoot(ElementName = "Gorod", IsNullable = true)] [Serializable] publicclass Gorod : IXmlSerializable { #region data publicint ID { get; set; } publicint OblastID { get; set; } publicstring Nazvanie { get; set; } #endregion … } |
Для того чтобы получить Xml документ необходимо вызвать метод WriteXml головного объекта:
using (MemoryStream ms = new MemoryStream()) using (XmlTextWriter writer = new XmlTextWriter(ms, Encoding.UTF8)) { (Mir as IXmlSerializable).WriteXml(writer); writer.Flush(); ms.Seek(0, System.IO.SeekOrigin.Begin); XmlDocument doc = new XmlDocument(); doc.Load(ms); } |
На выходе получается вот такой XML документ. Xml данные пересылаются на клиент, после чего импортируются в excel документ.
Помимо прочего необходимо реализовать метод получения данных из базы данных. Это может быть любая база и любой ORM, главное чтобы на выходе получили заполненный экземпляр типа «Mir». В проекте который прилагается к данной статье реализован вариант с MySQL (NHiberbate).
// Ссылка на этот метод хранится в словаре методов для поддерживаемых дата контрактов. // Когда на сервер приходит запрос EnumerateData с параметром EnumerateDataRequest.Filter = “Mir” вызывается этот метод. public Mir[] GetMir() { Mir[] arr = new Mir[1]; using (var ctx = dal.SessionFactory.OpenSession(CONN)) { IList<Strana> lst = CreateStrana(GetGorodDAOs(ctx), GetOblastDAOs(ctx), GetStranaDAOs(ctx)); arr[0] = new Mir { Stranas = lst }; } return arr; } |
Рисунок 8.
XML схема используемая нами для импорта не может быть использована для экспорта, так как она имеет больше двух вложенных списков. Давайте тогда в качестве примера рассмотрим упрощенный тип Область->Город. XML схема будет выглядеть так:
<?xmlversion="1.0"encoding="utf-8"?> <schema xmlns="http://www.w3.org/2001/XMLSchema"> <element name="Oblast"> <complexType> <sequence minOccurs="0" maxOccurs="1"> <element name="StranaID" type="string" /> <element name="Nazvanie" type="string" /> <element name="Gorod" minOccurs="0" maxOccurs="unbounded"> <complexType> <sequence minOccurs="0" maxOccurs="1"> <element name="Nazvanie" type="string" /> </sequence> </complexType> </element> </sequence> </complexType> </element> </schema> |
Клиент экспортирует данные из рабочей книги Excel в XML документ, который пересылается на сервер. На сервере происходит десерилизация. Для того чтобы десерилизация была возможно мы должны добавить соответствующие CLR типы. Мы можем взять их из реализации импорта, так типы остаются прежними:
//Аттрибуты типа u.Display используются для информирования клиента о том каким //образом отображать список дата контрактов. В данном случае это будет список //содержащий 3 колонки ID, TheRowVersion, Nazvanie. Каждая строка соответствует //экзкмпляру дата контракта [u.Display("ID", "ID", "")] [u.Display("TheRowVersion", "TheRowVersion", "")] [u.Display("Nazvanie", "Nazvanie", "")] [XmlSchemaProvider(null, IsAny = true)] [XmlRoot(ElementName = "Oblast", IsNullable = true)] [Serializable] publicclass Oblast : IXmlSerializable { #region data publicint? ID { get; set; } public gt.RowVersion TheRowVersion { get; set; } publicint StranaID { get; set; } publicstring Nazvanie { get; set; } public IList<Gorod> Gorods { get; set; } #endregion … } [XmlSchemaProvider(null, IsAny = true)] [XmlRoot(ElementName = "Gorod", IsNullable = true)] [Serializable] publicclass Gorod : IXmlSerializable { #region data publicint ID { get; set; } publicint OblastID { get; set; } publicstring Nazvanie { get; set; } #endregion … } |
// Ссылка на этот метод хранится в словаре методов для поддерживаемых дата контрактов. // Когда на сервер приходит запрос SaveData с параметром Name = “Oblast” Вызывается этот метод. int SaveOblast(int id, gt.RowVersion theRowVersion, byte[] data) { Oblast s = null; using (MemoryStream ms = new MemoryStream(data)) using (XmlReader reader = XmlReader.Create(ms)) s = new Oblast(reader); if (s == null) return -1; s.ID = id; s.TheRowVersion = theRowVersion; return SaveOblast(s); } |
В данной статье представлен подход, который позволяет делегировать задачу формирования шаблонов от разработчика пользователю. Разработчик лишь должен подготовить типы данных и данные, которые пользователь использует по своему усмотрению.
Разделение документа на шаблон и данные является дополнительной степенью свободы при работе с документами.