Введение в WPF. Часть 3. Анатомия комбинированного подходя к созданию WPF приложений

Часть 2, Часть 1

В прошлый раз мы познакомились с окнами и решили, что в дальнейшем мы их будем «красить». Обещаю, мы так, и сделаем, в следующий раз. Сегодня мне бы хотелось рассказать о языке XAML, без которого WPF является лишь очередной вариацией на тему библиотек для создания оконных приложений. Это конечно не совсем так, но без XAML мы не сможем оценить основное новаторство, библиотеки, - декларативный подход к разметке пользовательского интерфейса.

Сегодня мы, прежде всего, сконцентрируемся на проекте, который создает Visual Studio для WPF проекта. Мы уже знаем, как создать его руками. Зачем нам это понадобится?. Во-первых, это позволит нам от императивной модели программирования плавно перейти к «комбинированной», когда код комбинируется с XAML разметкой, а во вторых позволит нам выяснить механизмы которые лежат в недрах WPF, благодаря которым такой подход вообще становится возможным. Это и есть, на мой взгляд, самое главное. Параллельно мы с Вами, сможем познакомиться с XAML, пусть и поверхностно, но думаю вполне достаточно для первого знакомства.

Приступаем. Создаем приложение в Visual Studio 2008, по шаблону WPF Application. Напомню, что я использую C#, хотя все написанное ниже будет справедливо и для VB.NET приложений. Компилируем, запускаем. Результатом работы кода будет полноценное окно, аналогичное тому которое мы создавали в первом обзоре.

Пришло время проинспектировать Solution Explorer. Ничего не знакомого нам в разделе References, но, что- то удивительное в файлах нашего проекта. Имеем два файла с расширением .xaml, один App.xaml второй Window1.xaml. Думаю, по названиям мы можем догадаться, что они имеют отношение к уже известным нам классам Application и Window. Беглый просмотр содержимого демонстрирует отсутствие файлов в Solution Explorer с точкой входа в приложение. На первый взгляд странно, но наша цель разобраться со всеми странностями самостоятельно. Заглянем внутрь файла App.xaml:

<Application x:Class="WpfProjectPreparation.App"
   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   StartupUri="Window1.xaml">
   <Application.Resources>
 
   </Application.Resources>
</Application>

Видим, что XAML представляет собой XML документ. На данном этапе будет удобно на XAML смотреть следующим образом: это XML документ, каждый элемент которого на этапе компиляции ассоциируется с соответствующим ему .NET-типом или отображается непосредственно на него. Причем имя типа точно совпадает с именем элемента XML. Элементы могут вкладываться друг в друга. В коде выше, исходя из этого, мы можем утверждать, что элемент Application ассоциирован с типом Application который мы с Вами рассмотрели еще в самом первом обзоре. Логично было бы предположить, что атрибуты элемента отображаются на свойства типа, который в XAML представлен элементом. В принципе так и есть, за небольшими исключениями.

XAML в WPF, играет роль схожую с разметкой в ASP.NET приложениях. Чем это хорошо? Да хотя бы тем, что теперь выполнение нудной и скучной работы по настройке внешнего вида приложения , мы можем переложить на дизайнеров. Ведь совсем не сложно разработать инструменты, которые бы генерировали «правильную» разметку (Microsoft это уже сделал). Раньше же, что бы дизайнер смог установить различные свойства характерные для дизайна приложения, ему приходилось работать либо с VS дизайнером, либо использовать непосредственно код! Сказал и сам посмеялся. Раньше этим всем занимался только программист! То есть мы с Вами. Вторым преимуществом, на мой взгляд, есть отделение представления приложения от его кода. Теперь для дизайнеров разработали специальные инструменты вроде Expression Blend , благодаря которым дизайнер разрабатывает дизайн, на выходе получая XAML код. Нам как программистам все, что нужно так это понимать, как это все связать до кучи.

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

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

x:Class="WpfProjectPreparation.App"

Атрибут Class, есть указание синтаксическому анализатору XAML, определить новый тип данных. Судя по значению рассматриваемого атрибута, это будет тип с именем App в пространстве имен WpfProjectPreparation. При этом новый тип будет унаследован от класса Application (берется по элементу). Как это проверить? Пойдем долгим путем (есть и короткий, с ним мы познакомимся позже) и декомпилируем сборку. Обнаруживается следующий код:

public class App : Application
{
    // Methods
    [DebuggerNonUserCode]
    public void InitializeComponent()
    {
        base.StartupUri = new Uri("Window1.xaml", UriKind.Relative);
    }
 
    [DebuggerNonUserCode, STAThread]
    public static void Main()
    {
        App app = new App();
        app.InitializeComponent();
        app.Run();
    }
}

Как видите, мы не ошиблись. У нас есть тип App унаследованный от Application. Декомпилированый код демонстрирует еще одну важную для нас деталь. Кто-то создал вместо нас метод Main, где объявил переменную типа App и запустил цикл обработки сообщений. Круто! Нужно разобраться с тем, кто или что решило писать за нас код. Заметим, что на этом этапе, в программе нет никаких окон. Есть лишь инициализации StartupUri значением “Window1.xaml”, причем мы можем уверено заявить, что инициализация данного свойства была обеспечена на основе атрибута StartupUri в XAML разметке. Давайте продолжим изучение XAML кода, xmlns – специальный атрибут в XML который позволяет указывать пространство имен, в текущем случае это:

xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

Похоже на адрес в браузере? Похоже. Но по этому адресу нет ничего интересного. Просто есть такая практика, что в XML для пространств имен используются URI. Условно говоря, это директивы using для XAML разметки. Сейчас достаточно знать, что одна такая строка может ссылаться сразу на несколько существующих пространств имен в WPF библиотеке. Это достигается тем, что на самом деле в более чем 20 пространствах имен WPF нет пересечений имен типов. Эта особенность позволяет нам обойтись 2 строками вместо 20. Также это избавляет от решения, когда пространство имен пришлось бы разрешать явно в самом элементе, такое нагромождение кода совершенно ни к чему и это было учтено разработчиками WPF и языка XAML. Движемся дальше,

<Application.Resources>
 
</Application.Resources>

Оставим этот код пока без внимания, можно заметить, что его отсутствие никак не скажется на работе приложения, мастер сгенерировал нам его про запас. И так, что мы имеем? На основе xaml файла с объявлением Application, компилятор сгенерировал класс и объявил в нем точку входа в программу.

В Solution Explorer присутствует файл App.xaml.cs он связан App.xaml

    public partial class App : Application
    {
    }

Как видим класс пустой. Он нам понадобится если придется писать код для обработки событий уровня приложения. Ранее я упомянул, что класс создается на основе XAML разметки и это верно, но что тогда делает объявление этого типа у нас в файле? Совсем не похоже на код который мы видели при декомпиляции. Ответ на этот вопрос скрыт в самой модели компиляции. Ключом к разгадке есть объявление класса как partial. Где вторая половина класса? Придется потрудиться, что бы ее найти. На этот раз мы пойдем легким путем В Solution Explorer щелкаем по кнопке Show All и перемещаемся в папку obj/debug. Как много интересного! Сейчас просмотрим файл App.g.cs (g – generated). И так, мы видим в нем вторую половину объявления класса App. Причем эти файлы генерируются на первом этапе компиляции который предшествует компиляции уже сгенерирвоанного кода в сборку. Если проект не скомпилировать, соответствующие файлы не будут обнаружены. Именно код из сгенерированного файла мы и увидели при декомпиляции сборки. Теперь мы знаем точно, что метод Main был сгенерирован автоматически. Стоит заметить, что у нас есть возможность выбирать тип в котором студия организует точку входа в программу. Что бы это настроить нужно в свойствах проекта изменить пункт Startup Object. Секрет создания экземпляра приложения нами раскрыт, осталось разобраться с создаваемым приложением окном.

XAML код, в файде Window.xaml выглядит так:

<Window x:Class="WpfProjectPreparation.Window1"
   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   Title="Window1" Height="300" Width="300">
   <Grid>
 
   </Grid>
</Window>

Сказанное выше о файле App.xaml уместно и в контексте рассмотрения Window1.xaml. Мы видим атрибут Class который нам говорит о том, что данный элемент отобразится на сгенерированный анализатором тип Window1, наследник Window, в соответствующем пространстве имен. Далее идет подключение пространств имен и устанавливаются некоторые свойства окна: высота, ширина и заголовок. Заглянем в сгенерированный средой файл Window.xaml.cs

public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();
        }
    }

Мы видим, что в конструкторе вызывается метод InitializeComponent, а класс помечен как partial. Легко теперь предположить, что объявлен он в сгенерированном на первом этапе компиляции файле Window1.g.cs

public partial class Window1 : System.Windows.Window, System.Windows.Markup.IComponentConnector {
 
        private bool _contentLoaded;
 
        /// <summary>
        /// InitializeComponent
        /// </summary>
        [System.Diagnostics.DebuggerNonUserCodeAttribute()]
        public void InitializeComponent() {
            if (_contentLoaded) {
                return;
            }
            _contentLoaded = true;
            System.Uri resourceLocater = new System.Uri("/WpfProjectPreparation;component/window1.xaml", System.UriKind.Relative);
 
            #line 1 "..\..\Window1.xaml"
            System.Windows.Application.LoadComponent(this, resourceLocater);
 
            #line default
            #line hidden
        }
 
        [System.Diagnostics.DebuggerNonUserCodeAttribute()]
        [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
        [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes")]
        void System.Windows.Markup.IComponentConnector.Connect(int connectionId, object target) {
            this._contentLoaded = true;
        }
    }

Не пугайтесь, нет ничего запутанного в коде приведенном выше, мы к нему еще вернемся. Но при внимательном его рассмотрении возникает мысль, а где наши Height, Width и Title? Дело в том, что XAML не преобразовывается в MSIL код на этапе компиляции. Аналогично он не транслируется в C# для последующей компиляции. Он компилируется в, так называемый, бинарный язык разметки приложения (BAML). Этот формат компактен и оптимизирован для парсера WPF. После компиляции специальным компилятором, полученный BAML внедряется в виде ресурса в сборку с приложением и загружается при инстанцировании соответствующего связанного с ним типа. После этого он парсится специальным рантайм парсером и превращается в окна, контролы, кисточки и т.д. и т.п. Если посмотреть ресурсы сборки рефлектором, то можно мгновенно обнаружить внедренный BAML файл (в нашем случае один для окна). Взглянем на код выше еще раз:

System.Uri resourceLocater = new System.Uri("/WpfProjectPreparation;component/window1.xaml", System.UriKind.Relative);
 
            #line 1 "..\..\Window1.xaml"
            System.Windows.Application.LoadComponent(this, resourceLocater);

Именно здесь указывается расположение BAML в ресурсах сборки и скармливается на считывание методу LoadComponent класса приложения. Там посредством статического метода LoadBaml класса XamlReader (парсер XAML) и происходит непосредственное считывание BAML из ресурсов, а также происходит парсинг BAML разметки.

Но самое интересное только начинается! Для меня лично остается открытым вопрос каким образом приложение создает окно, где и как оно инстанцируется? Ведь судя по коду выше, окно инициализируется из baml разметки в момент создания экземпляра окна, но кто же все таки его инстанцирует? Если посмотреть и проанализировать код сгенерированный для Application в методе Main:

        App app = new App();
        app.InitializeComponent();
        app.Run();

Получаем следующую картину: создается класс приложения и устанавливается адрес ресурса содержащего окно которое будет запущено при старте приложения, в методе InitializeComponent. Можно предположить, что создает окно метод Run. Однако это не так, подробное изучение метода Run, с использованием рефлектора, ничего не дает. Метод Run рассчитывает на то, что окно к этому моменту существует или передается в качестве параметра или вообще не создано! Никаких парсеров там не обнаруживается да и какое они имеют отношение к циклу обработки сообщений? Пробуем искать дальше. Установка свойства StartupUri не приводит к созданию окна… Метод Run не получает его в качестве параметра… Получается, что окно создается где то в другом месте, но где!? Единственной ниточкой является уже обсуждавшееся свойство StartupUri. Стоит задуматься глубже над этим. Одной из первых моих догадок было, то, что окно инстанцируется уже после запуска цикла обработки сообщений. Но как, такое может быть возможно? Я долго прибывал в растерянности. Поиск рефлектором использования свойства StartupUri упорно ведет к конструктору класса Application, но тогда логичен вопрос, что он создает если мы свойство StartupUri устанавливаем только после вызова конструктора? Тем более, что это - на первый взгляд, не стыкуется с предположением, что окно создается рантайм, после старта цикла обработки сообщений… Как оказалось все дело в анонимных методах и событиях… именно к такому выводу привели меня мои размышления и поиск правды через Reflector. Для чего они часто используются? Правильно, для подписки на события или для использования в качестве callback методов. При детальном изучении класса Application, а именно его конструктора, обнаруживается, что он использует анонимный метод который объявлен примерно так:

if (!IsShuttingDown)
    {
        StartupEventArgs e = new StartupEventArgs();
        this.OnStartup(e);
        if (e.PerformDefaultAction)
        {
            this.DoStartup();
        }
    }
    return null;

И так мы видим, что в методе идет генерация события, OnStartUp. К слову мы на него можем подписаться в нашем приложении для разбора аргументов командной строки. После этого вызывается метод DoStartup. В глубинах метода DoStartup и происходит инстанцирование окна уже после старта приложения, а не по факту создания экземпляра Application или вызова метода Run! Cобытие OnStartup не генерируется конструктором в момент инстанцирования Application. Application лишь предоставляет инфраструктуре, приведенный выше анонимный метод, который будет вызван после инициализации всего приложения, уже после запуска цикла обработки сообщений. Метод DoStartup, вытаскивает значение из StartupUri (при наличие значения конечно) и выполняет всю необходимую для инстанцирования окна работу, посредством типа NavigationService. Повторюсь только после отработки процедуры старта приложения и вызывается этот анонимный метод. Следовательно к тому моменту уже будет установлено свойство StartupUri и NavigationService инстанцирует нужное окно, сделав его главным окном приложения. При этом будет вызван код который приведет к загрузке baml связанного с окном, код который мы разбирали выше в файле Window1.g.cs… Вот и решена последняя загадка, - того как происходит создание окна, в сгенерированном мастером шаблоне.

До новых встреч.

 
dotnet/wpf_introduction3.txt · Последнее изменение: d.m.Y H:i — Spawn.NET
 
Recent changes RSS feed Donate Powered by PHP Valid XHTML 1.0 Valid CSS Driven by DokuWiki