Платформы WPF и Silverlight используют язык разметки XAML для описания элементов пользовательского интерфейса, шаблонов и стилей. Если вы разрабатываете одновременно под разные XAML платформы, то естественно, возникает желание иметь общие файлы разметки для этих платформ.
Разметка в WPF и Silverlight очень схожа, но имеются досадные отличия, которые сильно осложняют ее шаринг. В нашей компании эта проблема была решена несколько лет назад в виде внутреннего инструмента, который называется WPF2SL.
WPF2SL слишком специфичен, чтобы быть полезным широкой публике, поэтому мы не планируем его публиковать. В этой статье я расскажу об особенностях XSLT преобразований применительно к разметке XAML и о некоторых сложностях и особенностях, с которыми мы столкнулись.
Проект WPF2SL стартовал 4 года назад, когда мы решили создать линейки компонентов для платформ WPF и Silverlight. WPF контролы у нас были готовы раньше, поэтому возникла идея сделать шаринг разметки между платформами. В то время разрыв между разметкой WPF и Silverlight был больше, чем сейчас, потому что в Silverlight 3 не было implicit styles, markup extensions и были сильно ограничены байндинги.
Кстати, некоторые наши конкуренты пошли по другому пути. У них были сначала готовы Silverlight контролы и их линейка WPF контролов получена из априори урезанной платформы, поэтому они до сих пор в полной мере не используют всех возможностей WPF платформы.
Начнем с создания System.Xml.Xsl.XslCompiledTransform. Тут всё, как написано в MSDN. Однако следует помнить, что загрузка XSLT файла методом XslCompiledTransform.Load занимает много времени, потому что в этот момент в памяти будет создана временная сборка для конечного автомата, который описан в XSLT файле.
В одной из ранних версий WPF2SL на каждый входной XAML файл производилась полная инициализация с вызовом XslCompiledTransform.Load. Это сильно замедляло работу утилиты. В XslCompiledTransform загружается XSLT файл, содержащий описания преобразований для узлов и атрибутов исходного дерева. Преобразования в XSLT файле упорядочены по возрастанию приоритета. Правило с самым низким приоритетом — первое. Это копирующее правило.
<xsl:template match="@* | node()">
<xsl:copy>
<xsl:apply-templates select="@* | node()"/>
</xsl:copy>
</xsl:template>
Если для узла или атрибута не найдется более приоритетного правила, от будет скопирован.
Отсутствие DynamicResource в Silverlight
Если просто заменить DynamicResource на StaticResource, полученная разметка будет содержать много ошибок, связанных с неверным следованием ресурсов, потому что StaticResource требует, чтобы ресурс был объявлен до его использования. Решением стало ручное упорядочивание ресурсов внутри файла. XSLT шаблон для замены DynamicResource на StaticResource выглядит так.
<xsl:template match="@*">
…
<xsl:attribute name="{local-name(.)}" namespace="{namespace-uri(.)}">
<xsl:variable name="tempValue1">
<xsl:call-template name="globalReplace">
<xsl:with-param name="outputString" select="."/>
<xsl:with-param name="target" select="'DynamicResource'"/>
<xsl:with-param name="replacement" select="'StaticResource'"/>
</xsl:call-template>
</xsl:variable>
<xsl:value-of select="normalize-space($tempValue1)"/>
</xsl:attribute>
</xsl:template>
Проблема усложняется, когда имеются ссылки на ресурсы, объявленные в другом файле. Эту часть проблемы не удалось решить XSLT преобразованиями. Для этого у нас есть отдельный этап пост-обработки, про который надо писать отдельную статью.
Вырезание узлов и атрибутов которые отсутствуют в Silverlight
Так как WPF разметка значительно богаче Silverlight разметки, нам придется вырезать узлы и атрибуты из XAML дерева. Это очень просто делается в XSLT.
Пример вырезания атрибута:
<xsl:template match="@FocusVisualStyle"/>
Пример вырезания поддерева:
<xsl:template match="wpf:Style.Triggers"/>
Особенности преобразования ключей ресурсов
И в WPF, и в Silvelight в разметке XAML можно задать ResourceDictionary, в котором будут храниться ресурсы. Ресурсы доступны по ключу. В WPF ключом может быть любой объект, а в SL ключ должен быть обязательно строковый.
Для унификации, конечно можно ввести в WPF ограничение, чтобы ключом была только строка, но нам нравится строгая типизация, которая достижима именно на объектных ключах. В WPF возможно написать вот так
<SolidColorBrush x:Key="{dxt:FloatingContainerThemeKey ResourceKey=FloatingContainerBackground}" Color="#FFA3C3EC" />
Где FloatingContainerThemeKey — это специальный дженерик объект, унаследованный от System.Windows.ResourceKey. Дженерик принимает параметром тип Enum, который описывает возможные названия ключей.
public class FloatingContainerThemeKeyExtension : ThemeKeyExtensionBase<FloatingContainerThemeKey> { }
public enum FloatingContainerThemeKey {
FloatingContainerAdornerTemplate,
FloatingContainerPopupTemplate,
FloatingContainerWindowTemplate,
}
За счет этого в WPF сложнее ошибиться в названии ключа в объявлении ресурса или в ссылке на ресурс.
Вернемся к преобразованию XAML. В Silverlight объектных ключей нет, поэтому
<SolidColorBrush x:Key="{dxt:FloatingContainerThemeKey ResourceKey=FloatingContainerBackground}" Color="#FFA3C3EC" />
преобразуется в строчку
<SolidColorBrush x:Key="FloatingContainerThemeKey_FloatingContainerBackground" Color="#FFA3C3EC" />
XML namespaces
Многие схожие элементы в WPF и Silverlight находятся в разных xml неймспейсах. Это различие породило вот такие шаблоны.
<xsl:template match="wpf:Label">
<sdk:Label xmlns:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk">
<xsl:apply-templates select="@* | node()"/>
</sdk:Label>
</xsl:template>
<xsl:template match="wpf:TreeView">
<sdk:TreeView xmlns:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk">
<xsl:apply-templates select="@* | node()"/>
</sdk:TreeView>
</xsl:template>
Когда мы поняли, что таких шаблонов придется сделать очень много, мы создали своего наследника стандартного класса XmlTextWriter, у которого перегружен метод WriteString.
public override void WriteString(string text) {
if(NamespacesReplacementTable.ContainsKey(text)) base.WriteString(NamespacesReplacementTable[text]);
else base.WriteString(text);
…
}
Этого наследника можно отдать в метод XslCompiledTransform.Transfrom(reader, writer) в качестве второго параметра. Перегруженый WriteString в соответствии с таблицей замен подменяет неймспейс при записи.
Интеграция в процесс компиляции
WPF2SL — это консольное приложение. В наших SL проектах на Pre-build event прописан вызов WPF2SL с соответствующими параметрами.
Но тут не все просто как кажется. Практически у всех сейчас машины с многоядерными процессорами, на которых msbuild делает одновременную сборку сразу для нескольких проектов. WPF2SL в процессе работы создавала временные файлы в Temp. Поскольку их названия совпадали, возникал конфликт доступа. Проблема была решена добавлением ID процесса к названию файла.
Диагностика проблем в XSLT преобразовании
К сожалению, удобного средства диагностики XSLT преобразований нет (по крайней мере, автору о них не известно). Когда какие-то из XSLT преобразований работают не так, как ожидается, самый действенный способ — итеративная модификация XSLT с анализом результатов. Если результат преобразования сильно отличается от ожидаемого, смело помещайте в коментарий половину XSLT файла; если всё еще не понятно, еще половину и так далее. Этот способ получил у нас название: «метод половинного комента».
Описанный способ является универсальным для всех декларативных языков, в том числе и для XAML. Если не понятно, какой из шаблонов сформировал неверную строчку в выходном файле, можно временно в шаблон вписать строчку, которая позволит однозначно его идентифицировать.
Выводы
XSLT преобразования хорошо работают в задаче конвертации XAML разметки между различными платформами, а .NET реализация XSLT преобразований XslCompiledTransform достаточно гибкая, производительная и расширяемая.
Литература
Сэл Мангано. XSLT. Сборник рецептов
Автор: xtraroman