Проектирование приложения с учетом использования единого опыта разработки для настольных и Web-проектов
Цель лекции: показать читателям способы организации проектов, позволяющих использовать разделяемый между Silverlight и WPF код, разобрать особенности архитектуры таких приложений, а также разобрать наиболее часто встречающиеся проблемы и способы их решения.
Подходы к решению задачи
Как и многие другие вопросы проблему разделяемого Silverlight/WPF кода можно решить несколькими способами:
- Разрабатывать кроссплатформенный проект с самого начала;
- Сначала разработать продукт для одной платформы, а затем адаптировать его для другой.
Разработка кроссплатформенного проекта с самого начала
Данное решение оптимально для случая, когда первым приоритетом в разработке стоит качество конечного результата. К сожалению, зачастую в реальном мире приходится жертвовать качеством для быстрого создания готового продаваемого продукта. Сложно спорить с тем, что результата можно добиться раньше, сконцентрировав всё внимание на одной платформе.
Разработка для одной платформы с последующим портированием на другую
Исходя из сказанного выше, наиболее вероятно, что вам придется следовать этим путем. Здесь перед вами встает выбор: начать с WPF или Silverlight. Безусловно, могут быть определенные стратегические, религиозные или какие-либо другие причины начать с одного или другого, но, пожалуй, в первую очередь стоит обратить внимание на технические аспекты.
Процесс портирования будет проще, если начать разработку с Silverlight и затем перейти на WPF. Следует учесть, что проще не значит лучше. Во многих случаях придется довольствоваться сокращенным функционалом Silverlight, используя сложные конструкции, повторяющие встроенные компоненты WPF.
Если же начать с WPF, то наиболее вероятно, что при портировании на Silverlight придется поплатиться за все удобства WPF и полностью переработать части кода, использующие недостающий в Silverlight функционал.
Инструментарий
Существует несколько способов организовать синхронное использование кода между Silverlight и WPF версиями приложения так, чтобы при этом было возможно реализовать некоторые участки кода специфично для определенной платформы. Наиболее удобный из них – использование ссылок на файлы для общего функционала и локализация различий при помощи директив препроцессора (в случае, если отличается небольшая область кода) и разделяемых классов (для серьезных отличий).
Создание ссылок на файлы в Visual Studio
Чтобы добавить ссылку на файл в WPF или Silverlight проект в Visual Studio достаточно в меню "Project" вызвать пункт "Add Existing Item…", перейти в папку другого проекта (Silverlight или WPF), выбрать файл и нажать "Add As Link" в меню кнопки "Add".
Создание ссылок на XAML файлы
При использовании ссылок на C# или VB файлы компилятор обрабатывает их так же, как и при использовании обычных копий файлов. В случае с XAML возникают некоторые проблемы, которые возможно обойти, добавив немного лишнего кода.
Предположим, что существует WPF проект, в котором определен пользовательский элемент управления MyTemplatedControl. Он не содержит никакой логики, всего лишь устанавливает стиль по умолчанию при помощи свойства DefaultStyleKey:
1: public class MyTemplatedControl : Control 2: { 3: public MyTemplatedControl() 4: { 5: this.DefaultStyleKey = typeof(MyTemplatedControl); 6: } 7: }
В файле Generic.xaml, находящемся в папке Themes данного проекта, в стиле по умолчанию объявлен шаблон для данного элемента управления:
В соответствии с этим шаблонов элемент управления MyTemplatedControl должен отображать синий прямоугольник – так можно легко проверить, что шаблон успешно найден и применен:
1: <ResourceDictionary 2:xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 3: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 4: xmlns:local="clr-namespace:TestControl" 5: > 6: 7: <Style TargetType="local:MyTemplatedControl"> 8: <Setter Property="Template"> 9: <Setter.Value> 10: <ControlTemplate 11: TargetType="local:MyTemplatedControl"> 12: <Canvas> 13: <Rectangle Canvas.Left="20" 14: Canvas.Top="20" Width="100" 15: Height="100" Fill="Blue" 16: Stroke="Black" StrokeThickness="3" /> 17: </Canvas> 18: </ControlTemplate> 19: </Setter.Value> 20: </Setter> 21: </Style> 22: </ResourceDictionary>
Этот элемент управления добавлен на тестовое окно. Если запустить приложение, то окно будет выглядеть следующим образом:
Предположим, что теперь необходимо создать Silverlight версию данного элемента управления. Для этого создается проект Silverlight, при помощи пункта "Add Existing Item…" и кнопки "Add As Link" добавляются ссылки на файл MyTemplatedControl.cs в корне проекта и файл Generic.xaml в папке Themed. Такой проект удачно скомпилируется, однако если добавить этот элемент управления в Silverlight приложение, на его месте будет показана пустая белая область – очевидно, что шаблон по умолчанию не был применен.
Это происходит так, потому что по какой-то причине (возможно, для этого есть веские основания, или это просто ошибка) XAML компилятор обрабатывает локальные и добавленные по ссылке .xaml файлы по-разному. Если коротко, то для последнего задается упрощенный ресурсный ключ, а значит при поиске стиля по умолчанию файл, добавленный по ссылке, игнорируется средой выполнения, так как он имеет неверный с её точки зрения ресурсный ключ.
Решение данной проблемы – использование объединения словарей при помощи MergedDictionaries: необходимо переместить содержимое файла Generic.xaml в отдельный XAML файл (лучше, если будет создано по отдельному файлу для каждого элемента управления), и включить их при помощи MergedDictionaries в файл Generic.xaml.
MyTemplatedControl.xaml:
1: <ResourceDictionary 2: xmlns= 3: "http://schemas.microsoft.com/winfx/2006/xaml/presentation" 4: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 5: xmlns:local="clr-namespace:TestControl" 6: > 7: <Style TargetType="local:MyTemplatedControl"> 8: <Setter Property="Template"> 9: <Setter.Value> 10: <ControlTemplate 11: TargetType="local:MyTemplatedControl"> 12: <Canvas> 13: <Rectangle Canvas.Left="20" 14: Canvas.Top="20" Width="100" Height="100" 15: Fill="Blue" Stroke="Black" StrokeThickness="3" /> 16: </Canvas> 17: </ControlTemplate> 18: </Setter.Value> 19: </Setter> 20: </Style> 21: </ResourceDictionary>
Generic.xaml:
1: <ResourceDictionary 2: xmlns= 3: "http://schemas.microsoft.com/winfx/2006/xaml/presentation" 4: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 5: > 6: <ResourceDictionary.MergedDictionaries> 7: <ResourceDictionary 8: Source="/WpfControl;component/Themes/MyTemplatedControl.xaml" /> 9: </ResourceDictionary.MergedDictionaries> 10: </ResourceDictionary>
Затем необходимо добавить файл MyTemplatedControl.xaml в Silverlight проект в качестве ссылки, а файл Generic.xaml – в виде копии, в которую дополнительно вносятся изменения.
Generic.xaml (версия для Silverlight)
1: <ResourceDictionary 2:xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 3: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 4: > 5: <ResourceDictionary.MergedDictionaries> 6: <ResourceDictionary 7: Source="/SlControl;component/MyTemplatedControl.xaml" /> 8: </ResourceDictionary.MergedDictionaries> 9: </ResourceDictionary>
Обратите внимание на отличия в файле Generic.xaml для Silverlight:
- Он ссылается на файлы в Silverlight сборке (SlControl);
- Он ссылается на добавленные по ссылке файлы по их "некорректному" ресурсному ключу (обратите внимание на отсутствие ‘Themes’ в пути).
Теперь Silverlight приложение работает правильно:
Таким образом возможно использовать общую XAML разметку в Silverlight и WPF приложениях без необходимости поддерживать 2 различные версии файла. Единственное, что для этого необходимо сделать – создать и поддерживать различные Generic.xaml в каждом проекте.
Директивы препроцессора
Когда между Silverlight и WPF версиями кода достаточно мало отличий, можно воспользоваться директивами препроцессора, чтобы включить тот или иной блок кода в зависимости от того, в каком проекте компилируется данный файл. Для удобства шаблон проекта Silverlight определяет константу "SILVERLIGHT". Конечно, возможно определить самостоятельно что-то более короткое и менее сложное в написании, однако надежнее оставить всё как есть – неизвестно, когда и где придется повторно использовать этот код.
Выглядеть это будет следующим образом:
1: private TextBox _dataTextBox; 2: public override void OnApplyTemplate() 3: { 4: #if SILVERLIGHT 5: _dataTextBox = this.GetTemplateChild("PART_DataTextBox") 6: as TextBox; 7: #else 8: _dataTextBox = this.Template.FindName("PART_DataTextBox", 9: this) as TextBox; 10: #endif 11: }
Разделяемые классы
В то время как директивы препроцессора подходят для небольших отличий в коде, по мере увеличения таких блоков код становится достаточно трудночитаем. В таких случаях более подходящими являются разделяемые классы: общий код определяется в части, которая при помощи ссылки разделяется между обоими проектами; методы, реализация которых различна, помещаются в другую часть, специфичную для каждого проекта.
Простейшее приложение, созданное в соответствии с данными принципами, выглядит следующим образом:
MyControlWPF – это WPF проект. Он содержит файл MyControl.cs, в котором определен разделяемый класс MyControl. Этот файл состоит из кода, который идентичен и в WPF, и в Silverlight. Он добавлен в MyControlSL (Silverlight проект) в качестве ссылки. Таким образом, достаточно поддерживать одну копию файла, который используется в обоих проектах. MyControl.WPF.cs и MyControl.SL.cs в свою очередь содержат члены класса MyControl, реализация которых различна в WPF и Silverlight.
Синхронное использование XAML
К сожалению, в случае XAML не существует встроенной поддержки директив препроцессора или схожего приема разделения отличных блоков кода. Конечно, существуют некоторые сторонние решения, однако хоть это и выглядит прогрессивно с точки зрения энтузиаста, кроме случаев действительно больших XAML файлов, сложных в поддержке, обычно более разумно работать с 2 файлами, если различия в разметке не удается обойти. Таким образом, следует либо составлять такую XAML разметку, которая будет корректна как в Silverlight, так и в WPF, и создать ссылку на этот файл в одном из проектов, либо поддерживать 2 параллельные версии файла.