Введение в WPF. Часть 1. Анатомия Application

Целью, ряда планируемых статей, которые надеюсь, будут представлены Вашему вниманию, является изучение WPF , но не в буквальном понимании этой фразы, их целью является попытка осветить собственные поиски и изыскания на пути изучения тех или иных составляющих, новой библиотеки от Microsoft. На мое твердое убеждение, достичь успеха при изучении какой-либо новой технологии позволяет комплексный подход, который состоит из двух принципов. Первый и самый главный принцип - практика. Только проектируя и разрабатывая настоящие приложения можно добиться прогресса и почерпнуть практические знания. Вторая важная составляющая это всестороннее исследование изучаемого предмета. Именно на исследовании и теоретических аспектах я и попытаюсь сконцентрировать свое внимание в своих обзорах библиотеки. Разве не интересно залезть под капот этому новому «заморскому» чудищу?! К тому же второй принцип позволяет подвести под практическую основу отличную теоретическую базу. Кто-то спросит, а зачем? И окажется по-своему прав. Ну, хотя бы для того, что бы иногда правильно отвечать на вопросы ньюбов на форуме sources.ru. Это, конечно же, была шутка. Основной лейтмотив, - без теории никогда не бывает практики. Любой попытке написать код с использованием не знакомой технологии или библиотеки, предшествует хотя бы беглый просмотр типов данных которые она предоставляет. Иногда предшествует чтение статей из серии “Getting Started”. Самое интересное, что читают обычно все и матерые профессионалы, и зеленые новички.

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

Первое, что мы с Вами сделаем, - создадим, пустой проект, используя среду MS Visual Studio 2008. Для этого в меню File среды разработки выбираем пункт New – Project переходим в раздел C# - Windows и выбираем шаблон Empty Project. Даем ему имя HelloWPF, при этом не забываем указать, что собираемся использовать Framework 3.5 Давайте добавим в наш проект файл с именем HelloWPF, для этого нам нужно в Solution explorer выбрать наш проект и в контекстном меню выбрать Add – New Item, после чего указать, что добавлять мы будем файл с исходным кодом (Сode File).

Пока файл пуст. Что требуется для того, что бы превратить его в программу? Правильно класс, содержащий статический метод Main, который является отправной точкой, для любой программы, написанной на языке C#. Поторопимся и мы с Вами, добавив следующий код:

using System;
 
namespace Juice.HelloWPF
{
    public class HeloWPF
    {
        public static void Main()
        {
 
        }
    }
}

Можно скомпилировать код и выполнить его (Ctrl – F5), на экране мы увидим стандартную консоль.

Что же нам потребуется, что бы превратит консоль в полноценное оконное WPF приложение? Совсем немного, немного - терпения. Прежде всего давайте выясним, какие сборки нам с Вами необходимы для разработки WPF программ? Основной функционал содержится в трех сборках это: PresentationCore, PresentationFramework и WindowsBase. Так же джентльменский набор потребует хорошо известной всем нам сборки System. Добавим их к проекту. В Solution Explorer, выбираем Reference – Add Reference, выделяем их в появившимся списке и жмем кнопку Ok. Теперь нам с Вами доступны все необходимые для корректной работы приложения типы. Какие же из них нам понадобятся для написания наипростейшего приложения? Прежде всего, это тип Application находящийся в namespace System.Windows сборки PresentationFramework, данный класс представляет собой абстракцию нашего приложения. Он обязательно необходим нашему приложению, что бы оно могло обрабатывать сообщения, поступающие от операционной системы. Это могут быть события, которые генерируются в ответ на действия пользователя или события, поступающие от устройств ввода-вывода и.т.д. Позже мы узнаем, что класс Application предоставляет нам с вами множество дополнительных возможностей, которые позволят нам более полно контролировать выполнение нашей программы. Приложение должно иметь один и только один объект Application. Давайте добавим его в наш код.

using System;
using System.Windows;
 
namespace Juice.HelloWPF
{
    public class HeloWPF
    {
        public static void Main()
        {
            Application app = new Application();
        }
    }
}

После компиляции программы мы увидим уже знакомую нам с Вами консоль. Для создания полноценного оконного приложения нам с Вами не хватает – окна! Класс представляющий в WPF окно, находится по уже знакомому нам с Вами адресу, в namespace System.Windows сборки PresentationFramework и называется он, как это не удивительно Window. Давайте, скорее добавим и его в нашу программу:

using System;
using System.Windows;
 
namespace Juice.HelloWPF
{
    public class HeloWPF
    {
        public static void Main()
        {
            Application app = new Application();
            Window window = new Window();
        }
    }
}

Пробуем выполнить наше приложение … Упс. Что то пошло не так, мы получили сообщение об ошибке The calling thread must be STA, because many UI components require this. И это произошло при попытке инициализации объекта Window.

Дело не хитрое и легко поправимое, путем добавления атрибута STAThread методу Main. Этим мы устанавливаем исходную модель потоков для основного потока приложения при взаимодействии с COM подсистемой. Стоит заметить, что наличие этого атрибута обязательно для любой WPF программы. Особо любопытных могу отправить по следующему адресу где очень и очень подробно описано значение STAThread в COM модели и не только это. Меня же заинтересовала, возможность посмотреть стектрейс. Также стало интересно, какое именно место привело к выбросу исключения, в нашей программе. Стектрейс может много нам рассказать о происходящем в момент инстанцирования экземпляра окна и мне бы хотелось узнать об этом также больше. Рассмотрим стектрейс исключения подробней, часть текста которого приведен ниже:

at System.Windows.Input.InputManager..ctor()
at System.Windows.Input.InputManager.GetCurrentInputManagerImpl()
at System.Windows.Input.InputManager.get_Current()
at System.Windows.Input.KeyboardNavigation..ctor()
at System.Windows.FrameworkElement.EnsureFrameworkServices()
at System.Windows.FrameworkElement..ctor()
at System.Windows.Controls.Control..ctor()
at System.Windows.Window..ctor()

Мы видим, что вызов конструктора Window, привел к вызову цепочки конструкторов базовых классов в его иерархии: Control и FrameworkElement. Здесь следует уточнить, что Window наследует непосредственно классу ContentControl, но о нем, вероятно, мы поговорим в следующих обзорах.

Для полноты картины представлю дерево наследования для класса Window:

Object
DispatcherObject
DependencyObject
Visual
UIElement
FrameworkElement
Control
ContentControl
Window

В конструкторе FrameworkElement , как мы видим дальше, был произведен вызов закрытого статического метода EnsureFrameworkServices.

at System.Windows.FrameworkElement.EnsureFrameworkServices()

который, судя по своему названию, гарантирует поддержку служб библиотеки для экземпляров FameworkElement. Что за службы такие? На самом деле все гораздо более прозаично. FrameworkElement содержит закрытую статическую переменную, относящуюся к типу FrameworkServise. Этот тип обеспечивает навигацию по элементам управления с использованием клавиатуры. Второй обязаностью класса FrameworkServise есть обеспечение поддержки для тултипов. Для этого он создает экземпляр класса PopupControlService. Просмотрев стектрейс еще выше, мы как раз и увидим, что в методе EnsureFrameworkServices происходит инициализация переменной типа KeyboardNavigation.

at System.Windows.Input.KeyboardNavigation..ctor()

KeyboardNavigation объявлен как внутренняя (internal) переменная FrameworkServise. На этот класс библиотекой WPF возлагается обязанность по обработке навигационных клавиш, а также клавиатурных сокращений связанных с навигацией. Сам же FrameworkElement, предоставляет статическое свойство с одноименным названием KeyboardNavigation, посредством которого наследники получают к нему доступ. Для выполнения своих обязанностей KeyboardNavigation использует класс InputManager. Ссылку, на который и пытается получить в своем конструкторе.

at System.Windows.Input.InputManager.get_Current()

Если бы ссылка была получена успешно, то KeyboardNavigation подписался бы на пару событий объявленных в InputManager и благополучно закончил бы свою инициализацию. Но на момент создания окна приложения InputManager не инициализирован, о чем свидетельствует вызов его конструктора.

at System.Windows.Input.InputManager..ctor()

Посредством InputManager приложение (и даже мы можем получить широкий доступ к вводимым пользователем данным. Именно так и поступает KeyboardNavigation. Приложение может получить ссылку на единственный экземпляр InputManager. Класс InputManager реализован как Singleton. Конструктор этого класса, в соответствии с шаблоном объявлен как приватный. Если Вы, захотите воспользоваться этим классом в вашей собственной программе, то его следует инстанцировать следующим образом:

 InputManager manager = InputManager.Current;

И так, что мы имеем? Исключение было сгенерировано при инициализации переменной типа InputManager. Именно в ее конструкторе находится условный блок, который проверяет текущую модель потоков COM на соответствие STAThread. Причем делается это посредством обычного вызова метода GetApartmentState для текущего исполняемого потока приложения и последующим сравнением полученного значения с значением STA перечисления ApartmentState. После успешной проверки, на «наличие атрибута» , InputManager подключает устройства ввода, которые может обработать подсистема ввода WPF, такие как клавиатура, мышь и стилус, в противном же случае бросает уже знакомое нам исключение. Ну, что двинемся дальше? Ведь теперь мы, по крайней мере, «лучше знаем», что делает WPF, создавая окно.

Метод Run имеет перегруженную версию, которая принимает параметром объект Window, благодаря чему нашу программу можно написать на строчку короче.

using System;
using System.Windows;
 
namespace Juice.HelloWPF
{
    public class HeloWPF
    {
        [STAThread]
        public static void Main()
        {
            Window window = new Window();            
            Application app = new Application();
            app.Run(window);
        }
    }
}

Результат идентичен.

Думаю, что не трудно догадаться, что метод Run получая окно самостоятельно вызывает метод Show полученного экземпляра Window. Казалось бы цель нашего тура выполнена, минимальное приложение для WPF компилируется и выполняется, но мне бы хотелось еще немного поговорить о классе Application, что бы сложилась законченная картина функционала который он нам может предоставить. Вернемся к вопросу о том, когда же закончится выполнение метода Run. Для этого слегка модифицируем код нашего приложения:

using System;
using System.Windows;
 
namespace Juice.HelloWPF
{
    public class HeloWPF
    {
        [STAThread]
        public static void Main()
        {
            Window window = new Window();
            window.Title = "First";
 
 
            Window windowSecond = new Window();
            windowSecond.Title = "Second";
            windowSecond.Show();
 
            Application app = new Application();
            app.Run(window);
        }
    }
}

Результатом программы является вывод обеих окон, после закрытия окна First приложение завершает свою работу. В тоже время закрытие окна Second не приводит к такому результату. Почему? Дело в том, что Application считает одно из окон главным. По умолчанию это окно передаваемое Application в качестве параметра и обычно это будет первое создаваемое приложением окно. Можем ли мы вообще повлиять на этот процесс? Можно ли назначить приложению другое главное окно? Оказывается да. У класса приложения есть волшебное свойство которое мы с Вами, можем установить, ShutdownMode. Оно может принимать одно из трех значений: ShutdownMode.OnLastWindowClose, ShutdownMode.OnMainWindowClose и ShutdownMode.OnExplicitShutdown. Установка первого говорит приложению, что методу Run стоит выполнять работу пока существует хотя бы одно окно которое он может обслуживать. Второе значение говорит методу Run, что ему следует завершить работу когда пользователь закроет главное окно. Последнее из указанных значений, говорит о том, что приложение может завершить свою работу только явно посредством вызова метода Shutdown() класса Application. Давайте разберемся в том, что происходит при вызове следующей строчки кода:

app.Run(window);

Первое, что делает метод Run проверяет выполняется ли он в нужном потоке, далее следует проверка на то, что приложение выполняется не в броузере. У класса Application имеется коллекция объектов Windows в которой метод Run ищет передаваемое ему окно и если не находит добавляет его в коллекцию. Далее следует проверка на наличие у приложения главного окна и если такое не установлено, то метод Run делает полученное окно – главным. В дальнейшем проводится проверка является ли окно видимым и если нет вызывается метод Show окна. После чего запускается цикл обработки сообщений. И так мы узнали о двух интересных свойствах объекта Application это Windows, которое хранит коллекцию окон приложения и MainWindow которое возвращает ссылку на главное окно приложения. При этом ни, что нам не мешает присвоить свойству MainWindow другое значение. Мы можем сделать главным любое другое окно по нашему усмотрению. Оба этих свойства можно без проблем задействовать в собственной программе. Если посмотреть на методы и события класса Application можно найти еще несколько интересных событий и свойств: Startup – событие которое позволяет отследить начало работы нашего приложения, здесь удобно инициализировать данные уровня приложения.

Exit – событие которое извещает о завершении работы приложения, здесь удобно будет совершить действия по сохранению глобальных для приложения данных, перед окончанием его работы или выполнить другую необходимую логику.

SessionEnding – событие которое позволяет нам отследить завершение сеанса работы пользователя в Windows или выключение компьютера.

Activated – событие которое отрабатывает когда пользователь переключается на работу с одним из окон нашего приложения

Diactivated – отрабатывает когда приложение становится неактивным, в результате того, что пользователь переключился на работу с другим приложением.

DispatcherUnhandledException – срабатывает при возникновении в приложении необработанного исключения.

Из свойств хочется выделить Properties который является ассоциативным контейнером (хеш-таблицей), который содержит пары ключ-значение, где мы можем сохранять собственные данные уровня приложения. Резонен вопрос, как получить к этой коллекции из кода в приложении? Все очень просто. Я уже говорил, что класс Application может быть только один в программе и получить ссылку на него можно используя статическое свойство класса Application, Сurrent.

Напоследок можно отметить, что отключить консоль проще простого, для этого достаточно в свойствах проекта изменить тип приложения с Console на WindowsApplication. Но при изучении новых технологий, а также на этапе отладки консоль выглядит удобным помощником.

Надеюсь каждый узнал для себя сегодня, что то новое. До новых встреч.