Платформы 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 файле упорядочены по возрастанию приоритета. Правило с самым низким приоритетом — первое. Это копирующее правило.
Если для узла или атрибута не найдется более приоритетного правила, от будет скопирован.
Если просто заменить DynamicResource на StaticResource, полученная разметка будет содержать много ошибок, связанных с неверным следованием ресурсов, потому что StaticResource требует, чтобы ресурс был объявлен до его использования. Решением стало ручное упорядочивание ресурсов внутри файла. XSLT шаблон для замены DynamicResource на StaticResource выглядит так.
Проблема усложняется, когда имеются ссылки на ресурсы, объявленные в другом файле. Эту часть проблемы не удалось решить XSLT преобразованиями. Для этого у нас есть отдельный этап пост-обработки, про который надо писать отдельную статью.
Так как WPF разметка значительно богаче Silverlight разметки, нам придется вырезать узлы и атрибуты из XAML дерева. Это очень просто делается в XSLT.
Пример вырезания атрибута:
Пример вырезания поддерева:
И в WPF, и в Silvelight в разметке XAML можно задать ResourceDictionary, в котором будут храниться ресурсы. Ресурсы доступны по ключу. В WPF ключом может быть любой объект, а в SL ключ должен быть обязательно строковый.
Для унификации, конечно можно ввести в WPF ограничение, чтобы ключом была только строка, но нам нравится строгая типизация, которая достижима именно на объектных ключах. В WPF возможно написать вот так
Где FloatingContainerThemeKey — это специальный дженерик объект, унаследованный от System.Windows.ResourceKey. Дженерик принимает параметром тип Enum, который описывает возможные названия ключей.
За счет этого в WPF сложнее ошибиться в названии ключа в объявлении ресурса или в ссылке на ресурс.
Вернемся к преобразованию XAML. В Silverlight объектных ключей нет, поэтому
преобразуется в строчку
Многие схожие элементы в WPF и Silverlight находятся в разных xml неймспейсах. Это различие породило вот такие шаблоны.
Когда мы поняли, что таких шаблонов придется сделать очень много, мы создали своего наследника стандартного класса XmlTextWriter, у которого перегружен метод WriteString.
Этого наследника можно отдать в метод XslCompiledTransform.Transfrom(reader, writer) в качестве второго параметра. Перегруженый WriteString в соответствии с таблицей замен подменяет неймспейс при записи.
WPF2SL — это консольное приложение. В наших SL проектах на Pre-build event прописан вызов WPF2SL с соответствующими параметрами.
Но тут не всё так просто, как кажется. Практически у всех сейчас машины с многоядерными процессорами, на которых msbuild делает одновременную сборку сразу для нескольких проектов. WPF2SL в процессе работы создавала временные файлы в Temp. Поскольку их названия совпадали, возникал конфликт доступа. Проблема была решена добавлением ID процесса к названию файла.
К сожалению, удобного средства диагностики XSLT преобразований нет (по крайней мере, автору о них не известно). Когда какие-то из XSLT преобразований работают не так, как ожидается, самый действенный способ — итеративная модификация XSLT с анализом результатов. Если результат преобразования сильно отличается от ожидаемого, смело помещайте в коментарий половину XSLT файла; если всё еще не понятно, еще половину и так далее. Этот способ получил у нас название: «метод половинного комента».
Описанный способ является универсальным для всех декларативных языков, в том числе и для XAML. Если не понятно, какой из шаблонов сформировал неверную строчку в выходном файле, можно временно в шаблон вписать строчку, которая позволит однозначно его идентифицировать.
XSLT преобразования хорошо работают в задаче конвертации XAML разметки между различными платформами, а .NET реализация XSLT преобразований XslCompiledTransform достаточно гибкая, производительная и расширяемая.
Сэл Мангано. XSLT. Сборник рецептов
Разметка в 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. Сборник рецептов