Слить два xml, чтобы добавить кнопку в панель быстрого доступа Microsoft Office 2010

в 13:43, , рубрики: linqtoxml, XML, системное администрирование, метки:

Иногда бывает необходимо всем пользователям вашей корпоративной сети добавить какую-нибудь кнопку в панель быстрого доступа Outlook. Например, кнопку «Изменить сообщение».

image

К счастью, в Microsoft Office 2010 настройки панели быстрого доступа хранятся в XML-файлах с расширением *.officeUI, которые лежат в %appdata%/Microsoft/Office. У меня %appdata% — это папка «C:/users/user_name/AppData/Local/».

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

Формат *.officeUI файлов описан в статье Deploying a Customized Ribbon and Quick Access Toolbar in Office 2010

Пример файла *.officeUI

  <mso:customUI xmlns:mso="http://schemas.microsoft.com/office/2009/07/customui">
    <mso:ribbon>
      <mso:qat>
         <mso:sharedControls>
            <mso:control idQ="mso:FileSave" visible="true"/>
            <mso:control idQ="mso:FilePrintQuick" visible="false"/>
            <mso:control idQ="mso:FilePrintPreview" visible="false"/>
            <mso:control idQ="mso:SpellingAndGrammar" visible="false"/>
            <mso:control idQ="mso:Undo" visible="true"/>
            <mso:control idQ="mso:RedoOrRepeat" visible="true"/>
            <mso:control idQ="mso:Reply" visible="false"/>
            <mso:control idQ="mso:Forward" visible="false"/>
            <mso:control idQ="mso:Delete" visible="false"/>
            <mso:control idQ="mso:MoveToFolder" visible="false"/>
            <mso:control idQ="mso:MessagePrevious" visible="true"/>
            <mso:control idQ="mso:MessageNext" visible="true"/>
            <mso:control idQ="mso:EditMessage" visible="true"/>
            </mso:sharedControls>
         </mso:qat>
    </mso:ribbon>
  </mso:customUI>

На самом деле есть две разновидности этих файлов: *.officeUI и *.exportedUI. Они отличаются друг от друга лишь тем, что в файле *.exportedUI перед основным блоком есть первый элемент <mso:cmd>.

Файл *.exportedUI создается Офисом, когда вы в параметрах переходите во вкладку «Панель быстрого доступа» и пользуетесь кнопкой «Импорт-экспорт».

image

Таким образом, мы можем на одном компьютере задать нужную комбинацию кнопок в панели быстрого доступа, сохранить файл *.exportedUI и после небольшой конвертации его в *.officeUI распространить по пользователям.

Самый простой и легкий вариант — когда вам наплевать на кастомизацию панели быстрого доступа, уже сделанную пользователями. Вы просто копируете свой файл поверх их файла.

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

Предположим, мы дошли до этапа, когда у нас есть файл-образец, и мы также нашли на компьютере пользователя файл, который нужно изменить. Т.к. у нас Windows-среда, то предпочтительно использовать родные средства разработки — VBS, VB или C#. Т.к. предстоит работа с XML, я выбираю C#.

Закачать xml-файлы в вашу программу можно разными способами.
К примеру, через LINQ to XML:

  XDocument user_doc = XDocument.Load("olkpostread.officeUI");
  XDocument admin_doc = XDocument.Load("olkpostread.exportedUI");

Далее вы можете попробовать вручную слить эти две xml-ки, подозревая об их формате:

     XNamespace mso = "http://schemas.microsoft.com/office/2009/07/customui";

     var controls_user = user_doc.Element(mso + "ribbon")
                                                   .Element(mso + "qat")
                                                   .Element(mso + "sharedControls").Elements();

     var controls_template = admin_doc.Element(mso + "ribbon")
                                                             .Element(mso + "qat")
                                                             .Element(mso + "sharedControls").Elements();

            
    var new_elements= controls_template.Except(controls_user,new ElementsByIdQComparer());

    user_doc.Element(mso + "ribbon")
                  .Element(mso + "qat")
                  .Element(mso + sharedControls").Add(new_elements);

    user_doc.Save("olkpostread.officeUI");

Здесь, зная, что нужные нам узлы лежат внутри элемента sharedControls, мы их считываем из обоих xml, а затем складываем, предварительно вычитая множества (чтобы избежать дубликатов при сливании). Для вычитания даже можно написать свой класс-компаратор:

 class ElementsByIdQComparer : IEqualityComparer<XElement>
    {
        public int GetHashCode(XElement e)
        {
            return e.ToString().GetHashCode();
        }

        public bool Equals(XElement e1,XElement e2)
        {
            return (string)e1.Attribute("idQ") == (string)e2.Attribute("idQ");
        }
    }

Однако этот способ далеко не универсален. Что делать, если нам хочется, совершенно не разбираясь со схемой конфигов Офиса, слить xml-ки?

Немного поискав в MSDN, находим следующий «старый дедовский способ» — через структуру Dataset:

            try
            {
                XmlTextReader xmlreader1 = new XmlTextReader("olkpostread.officeUI");
                XmlTextReader xmlreader2 = new XmlTextReader("olkpostread_template.officeUI");

                DataSet ds = new DataSet();
                ds.ReadXml(xmlreader1);
                DataSet ds2 = new DataSet();
                ds2.ReadXml(xmlreader2);
                ds.Merge(ds2);
                ds.WriteXml("merge.xml");
               
            }
            catch (System.Exception ex)
            {
                Console.Write(ex.Message);
            }

Однако этот способ не позволяет убрать дубликаты. Что же делать? Можно, к примеру, написать рекурсивную процедуру. Можно применить XSLT. Но мне больше понравился еще один хороший метод — привести dataset к XDocument и отфильтровать XDocument, пользуясь имеющимся в арсенале этого класса методом DeepEquals:

        private static IEnumerable<XNode> FilterDuplicates(IEnumerable<XNode> nodes)
        {
            foreach (XNode node in nodes.Where(n => !n.NodesBeforeSelf().Any(s => XNode.DeepEquals(n, s))))
            {
                switch (node.NodeType)
                {
                    case XmlNodeType.Element:
                        XElement elNode = node as XElement;
                        yield return new XElement(elNode.Name, elNode.Attributes(), FilterDuplicates(elNode.Nodes()));
                        break;
                    case XmlNodeType.Text:
                        yield return new XText(node as XText);
                        break;
                    case XmlNodeType.CDATA:
                        yield return new XCData(node as XCData);
                        break;
                    case XmlNodeType.Comment:
                        yield return new XComment(node as XComment);
                        break;
                    case XmlNodeType.ProcessingInstruction:
                        yield return new XProcessingInstruction(node as XProcessingInstruction);
                        break;
                }
            }
        }

Этот метод написал добрый человек Martin_Honnen.

А чтобы безболезненно перейти от Dataset к XDocument, я не нашла ничего лучше, чем записать его в файл, а потом загрузить обратно.

Таким образом, получаем весь кусок кода:

using System;
using System.Collections.Generic;
using System.Xml.Linq;
using System.Text;
using System.Linq;
using System.Xml;
using System.IO;
using System.Data;

namespace ConsoleApplication2
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                XmlTextReader xmlreader1 = new XmlTextReader("olkpostread.officeUI");
                XmlTextReader xmlreader2 = new XmlTextReader("olkpostread_template.officeUI");

                DataSet ds = new DataSet();
                ds.ReadXml(xmlreader1);
                DataSet ds2 = new DataSet();
                ds2.ReadXml(xmlreader2);
                ds.Merge(ds2);
                ds.WriteXml("merge.xml");
                XDocument output = new XDocument(FilterDuplicates(XDocument.Load("merge.xml").Nodes()));
                output.Save("merge.xml");
                
            }
            catch (System.Exception ex)
            {
                Console.Write(ex.Message);
            }


        }
        private static IEnumerable<XNode> FilterDuplicates(IEnumerable<XNode> nodes)
        {
            foreach (XNode node in nodes.Where(n => !n.NodesBeforeSelf().Any(s => XNode.DeepEquals(n, s))))
            {
                switch (node.NodeType)
                {
                    case XmlNodeType.Element:
                        XElement elNode = node as XElement;
                        yield return new XElement(elNode.Name, elNode.Attributes(), FilterDuplicates(elNode.Nodes()));
                        break;
                    case XmlNodeType.Text:
                        yield return new XText(node as XText);
                        break;
                    case XmlNodeType.CDATA:
                        yield return new XCData(node as XCData);
                        break;
                    case XmlNodeType.Comment:
                        yield return new XComment(node as XComment);
                        break;
                    case XmlNodeType.ProcessingInstruction:
                        yield return new XProcessingInstruction(node as XProcessingInstruction);
                        break;
                }
            }
        }
    }
}

И результат:

Файл пользователя:

<mso:customUI xmlns:mso="http://schemas.microsoft.com/office/2009/07/customui">
  <mso:ribbon>
    <mso:qat>
      <mso:sharedControls>
        ...
        <mso:control idQ="mso:ViewInBrowser" visible="true" />
      </mso:sharedControls>
    </mso:qat>
  </mso:ribbon>
</mso:customUI>
Файл админа:

<mso:customUI xmlns:mso="http://schemas.microsoft.com/office/2009/07/customui">
  <mso:ribbon>
    <mso:qat>
      <mso:sharedControls>
        ...
        <mso:control idQ="mso:EditMessage" visible="true" />
      </mso:sharedControls>
    </mso:qat>
  </mso:ribbon>
</mso:customUI>
Результирующий файл:

<?xml version="1.0" encoding="utf-8"?>
<mso:customUI xmlns:mso="http://schemas.microsoft.com/office/2009/07/customui">
  <mso:ribbon>
    <mso:qat>
      <mso:sharedControls>
        ...
        <mso:control idQ="mso:ViewInBrowser" visible="true" />
        <mso:control idQ="mso:EditMessage" visible="true" />
      </mso:sharedControls>
    </mso:qat>
  </mso:ribbon>
</mso:customUI>

Автор: karagota

* - обязательные к заполнению поля