Иногда бывает необходимо всем пользователям вашей корпоративной сети добавить какую-нибудь кнопку в панель быстрого доступа Outlook. Например, кнопку «Изменить сообщение».
К счастью, в 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 создается Офисом, когда вы в параметрах переходите во вкладку «Панель быстрого доступа» и пользуетесь кнопкой «Импорт-экспорт».
Таким образом, мы можем на одном компьютере задать нужную комбинацию кнопок в панели быстрого доступа, сохранить файл *.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
Спасибо за статью, помогла решить вопрос! У вас есть неточность
%appdata% — это папка C:/users/user_name/AppData/Roaming
%localappdata% — это папка C:/users/user_name/AppData/Local