На главную Наши проекты:
Журнал   ·   Discuz!ML   ·   Wiki   ·   DRKB   ·   Помощь проекту
ПРАВИЛА FAQ Помощь Участники Календарь Избранное RSS
msm.ru
Модераторы: maxim84_
  
> Введение в WPF 3 , Анатомия комбинированного подходя к созданию WPF приложений
    Ссылка на вторую и первую часть: Введение в WPF 2

    В прошлый раз мы познакомились с окнами и решили, что в дальнейшем мы их будем «красить».
    Обещаю, мы так, и сделаем, в следующий раз. Сегодня мне бы хотелось рассказать о языке 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:

    ExpandedWrap disabled
      <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 (берется по элементу). Как это проверить? Пойдем долгим путем (есть и короткий, с ним мы познакомимся позже) и декомпилируем сборку. Обнаруживается следующий код:

    ExpandedWrap disabled
      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

    ExpandedWrap disabled
      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 выглядит так:

    ExpandedWrap disabled
      <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

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


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

    ExpandedWrap disabled
      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 файл (в нашем случае один для окна). Взглянем на код выше еще раз:

    ExpandedWrap disabled
      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:

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



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

    До новых встреч.
      Интересная статья.
      Хочется добавить про XML-неймспейсы
      Цитата juice @

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

      Здесь первый неймспейс - содержит всё, что входит в WPF (весь набор контролов, окошек, и др. объектов, которые могут быть использованы через хамл)
      а второй неймспейс содержет XML элементы, заданые в спецификациии самого языка XAML

      Тоесть в тут видно, что WPF - лишь одна из возможных реализаций фреймворка для языка XAML.

      ещё то, что касается дизайнеровского файла.
      Он служит в основонм для удобства обращения из C# кода к компонентам, заданным в XAML.
      Если мы в xaml какому-либо элементу зададим свойство Name (точнее x:Name, так как наличия этого свойства требует не WPF и сам XAML, а WPF - лишь реализовывает это требование) - то этот элемент становится доступен нам в коде как поле класса, заданного в x:Class.
      Вот именно этот функционал и поддерживает файл дизайнера <className>.g.cs. Он загружает BAML и привязывает соответствующие поля класса к объектам, загруженным на из BAML.

      Не будь этой системы - нам бы пришлось рыться в дереве объектов, загруженных из ХAML в поисках нужной кнопочки в ручную.
      Эта проблема особа остро стоит, есди вы вручную загружаете объекты из XAML-файла (с помощью System.Windows.Markup.XamlReader.Load() ) - там эта система не доступна и приходится придумывать чтото своё.
      В то же время возможность в любой момент в рантайме сохранить\загрузить\отредактировать часть пользовательского интерфейса завораживает :rolleyes:
        Большое спасибо автору статей.
        Как жаль что подобных статей я не встречал когда изучал Workflow foundation.
        0 пользователей читают эту тему (0 гостей и 0 скрытых пользователей)
        0 пользователей:


        Рейтинг@Mail.ru
        [ Script execution time: 0,0437 ]   [ 16 queries used ]   [ Generated: 25.04.24, 14:35 GMT ]