Desktop runtime что это

Модель приложений Windows Runtime

Desktop runtime что это. Смотреть фото Desktop runtime что это. Смотреть картинку Desktop runtime что это. Картинка про Desktop runtime что это. Фото Desktop runtime что этоНаша жизнь наполнена абстракциями. Как разработчики мы часто вынуждены бороться со всяческими проблемами, используя абстракции, о которых мы по большому счету ничего не знаем. Абстракции иногда разваливаются и оказываются не в состоянии полностью скрыть нижележащие сложности. Не поймите меня неправильно, абстракции — великая вещь. Они помогают и пользователям, и разработчикам, но вы здорово облегчите себе жизнь, если вникнете в те абстракции, на которые вы регулярно опираетесь в своей работе, и четко поймете, как именно они действуют. Более того, библиотеки, авторы которых приняли эту реальность, зачастую куда успешнее тех, где этого не сделано, и отчасти это связано с тем, что такие библиотеки позволяют входить в абстракцию при пошаговой отладке, если в том возникает необходимость.

Одна из таких абстракций — Windows Runtime (WinRT), и сегодня я собираюсь проиллюстрировать это, рассмотрев базовую модель приложений WinRT. Она построена вокруг класса CoreWindow, экземпляр которого находится внутри каждого «современного» приложения Windows Store и Windows Phone. Тем не менее, сравнительно немногие знают о его существовании, и еще меньше, как он работает. Видимо, это можно считать критерием успеха абстракции.

Со времени первого объявления Windows 8 API в 2011 году многое было сказано и написано о различных языковых проекциях, предоставляющих абстракцию над Windows Runtime. Однако лучший способ понять Windows Runtime — отказаться от языковых проекций, включая C++/CX, и использовать стандартный C++ в сочетании с традиционной COM. Только C++ позволяет раздвинуть плотные шторы и увидеть, что происходит на самом деле (с технической точки зрения, то же самое можно сделать и на C, но это было бы излишне болезненно). Возможно, вы все равно предпочтете использовать ту или иную языковую проекцию (скорее всего, C++/CX), но по крайней мере у вас будет гораздо более четкое понимание происходящего.

Для начала откройте Visual Studio 2012 и создайте новый проект Visual C++ для приложения Windows Store или Windows Phone. Не важно, какой шаблон вы используете. Как только он загрузится, перейдите в Solution Explorer и удалите все, что не существенно. Если вы выбрали шаблон на основе XAML, удалите все XAML-файлы. Вы также можете удалить все файлы исходного кода на C++. Вероятно, вы предпочтете сохранить предкомпилированный заголовочный файл, но обязательно удалите все его содержимое. У вас должны остаться лишь ресурсы пакета, необходимые для развертывания приложения, изображения, сертификата и XML-манифеста.

Затем откройте страницы свойств проекта и выберите свойства компилятора — узел C/C++ в дереве слева. Найдите строку для ключа компилятора /ZW — Consume Windows Runtime Extension — и укажите No, чтобы отключить языковые расширения C++/CX. Тем самым вы будете уверены, что ничего загадочного, помимо мистических чудес стандартного компилятора C++, не происходит. Там же установите уровень предупреждений компилятора /W4.

Если вы попробуете скомпилировать проект, то должны увидеть приветствие от компоновщика, который сообщает об ошибке из-за того, что не удалось найти функцию точки входа в проект — WinMain. Добавьте в проект новый файл исходного кода на C++ и первым делом поместите в него недостающую функцию WinMain:

Как видите, это традиционная функция WinMain для Windows-приложения на основе C Runtime Libraries (CRT). Конечно, HINSTANCE и PWSTR не являются фундаментальными типами C++, поэтому вам придется включить заголовочный файл windows.h:

Если вы сохранили предкомпилированный заголовочный файл проекта, то можете включить его здесь. Я также буду использовать ComPtr из Windows Runtime C++ Template Library (WRL), поэтому сейчас самое время включить и его:

Подробнее о WRL я буду рассказывать в следующих статьях из этой рубрики. А пока я буду просто использовать шаблон класса ComPtr для поддержки указателя на COM-интерфейс. На этом этапе достаточно знать, что WRL ComPtr — это просто смарт-указатель на COM-интерфейс. Хотя он предоставляет некоторые средства, уникальные для Windows Runtime, я не стану использовать их в этот раз. С тем же успехом вы могли бы задействовать CComPtr из Active Template Library (ATL) или любой другой смарт-указатель на COM-интерфейс по своему выбору. WRL ComPtr определен в пространстве имен Microsoft::WRL:

Я также буду использовать макрос ASSERT и функцию HR для обработки ошибок. Об этом я уже говорил, поэтому не буду повторяться. Если вы не уверены в этих этапах, прочитайте мою статью в этой рубрике за май 2013 г. «Introducing Direct2D 1.1» (msdn.microsoft.com/magazine/dn198239).

Первое, что ожидает модель приложений, — MTA (multithreaded apartment). Правильно, в ней находится COM-модель изоляции потоков (COM apartment model). Windows Runtime предоставляет функцию RoInitialize, которая является тонкой оболочкой CoInitializeEx:

Несмотря на тот факт, что обычно достаточно CoInitializeEx, я предлагаю вам использовать RoInitialize. Эта функция позволит поддерживать будущие усовершенствования в Windows Runtime без потенциальной угрозы поломать традиционную COM. Она аналогична OleInitialize, которая тоже вызывает CoInitializeEx и кое-что еще. Смысл в том, чтобы в основном потоке вашего приложения не было ничего загадочного. Единственное, что может слегка удивить, — это не STA (single-threaded apartment). Не волнуйтесь, окно вашего приложения по-прежнему будет выполняться в STA-потоке, но создавать его будет Windows Runtime. Эта STA — на самом деле Application STA (ASTA), и она немного отличается от чистой STA, но об этом позже.

WindowsCreateString выделяет достаточно памяти для хранения копии исходной строки и null-символа, завершающего строку, а затем копирует исходную строку в этот буфер. Чтобы освободить этот вспомогательный буфер, вы должны вызвать WindowsDeleteString, если только владелец строки не возвращается в вызвавшую функцию:

Располагая HSTRING, вы можете получить указатель на вспомогательный буфер этой строки с помощью функции WindowsGetStringRawBuffer:

Необязательный второй параметр возвращает длину строки. Длина хранится вместе со строкой, избавляя вас от необходимости сканировать строку, чтобы определить ее размер. Прежде чем вы начнете строчить класс-оболочку для C++, стоит отметить, что компилятор C++/CX не возится с WindowsCreateString и WindowsDeleteString при генерации кода для строкового литерала или массива const. Вместо них он использует так называемую быстро передаваемую строку (fast-pass string), позволяющую избегать дополнительных операций выделения памяти и копирования, о которых недавно упоминал. Кроме того, это исключает риск утечки памяти. Создается быстро передаваемая строка функцией WindowsCreateStringReference:

Она использует передаваемый вызвавшим кодом HSTRING_HEADER, чтобы избежать выделения памяти из кучи. В этом случае вспомогательный буфер для HSTRING — это сама исходная строка, поэтому вы должны гарантировать, что исходная строка (равно как и заголовок) останется неизменной в течение срока существования HSTRING. Очевидно, что этот подход бесполезен, когда нужно вернуть строку вызвавшей функции, но отличная оптимизация, если требуется передать строку как входной параметр другой функции, чей срок жизни ограничен стеком (т. е. это не относится к асинхронным функциям). WRL также предоставляет оболочки для HSTRING и быстро передаваемых строк.

RoGetActivationFactory — как раз такая функция, и она используется для получения фабрики активации или статического интерфейса для данного класса. Она аналогична COM-функции CoGetClassObject. Учитывая, что эта функция обычно применяется с массивом const, генерируемым компилятором MIDL, имеет смысл написать простой шаблон-функцию, предоставляющий оболочку быстро передаваемой строки. Как он может выглядеть, показано на рис. 1.

Рис. 1. Шаблон функции GetActivationFactory

Шаблон функции GetActivationFactory автоматически распознает длину строки, исключая необходимость передачи аргумента с длиной буфера (а здесь легко ошибиться) или весьма дорогостоящего сканирования строки в период выполнения. Затем перед вызовом функции RoGetActivationFactory подготавливает быстро передаваемую строку. И здесь шаблон функции вновь логически распознает идентификатор интерфейса и безопасно возвращает конечный указатель на COM-интерфейс, обернутый в WRL ComPtr.

Теперь вы можете использовать эту вспомогательную функцию для получения интерфейса ICoreApplication:

Интерфейс ICoreApplication — это то, что приводит в действие ваше приложение. Чтобы использовать этот COM-интерфейс, вам потребуется включить заголовочный файл модели приложений:

Этот заголовочный файл определяет ICoreApplication в пространстве имен ABI::Windows::ApplicationModel::Core, а также текстовый идентификатор класса CoreApplication. Единственный метод интерфейса, над которым вам придется хорошенько подумать, — это Run.

Прежде чем продолжить, было бы полезно оценить, как Windows Runtime выводит ваше приложение в свет, если так можно выразиться. Как уже упоминалось, Windows Runtime считает вас просто гостем в вашем процессе. Тут полная аналогия с тем, как годами работали Windows-службы. В случае Windows-службы Windows Service Control Manager (SCM) запускает ее, используя функцию CreateProcess или одну из ее вариаций. Затем ждет, когда процесс вызовет функцию StartServiceCtrlDispatcher. Эта функция устанавливает обратную связь с SCM, благодаря чему служба и SCM могут взаимодействовать друг с другом. Если, например, службе не удастся своевременно вызвать StartServiceCtrlDispatcher, SCM будет считать, что возникла какая-то ошибка, и уничтожит процесс. Функция StartServiceCtrlDispatcher возвращает управление только по окончании работы службы, поэтому SCM нужно создавать дополнительный поток для службы, чтобы принимать уведомления об обратных вызовах. Служба просто реагирует на события и находится полностью во власти SCM. Как вы увидите, это во многом напоминает модель приложений WinRT.

Windows Runtime ждет от процесса получения интерфейса ICoreApplication и вызова его метода Run. Если процессу не удается своевременно сделать это, Windows Runtime подобно SCM считает, что что-то пошло не так и завершает процесс. К счастью, если к процессу подключен отладчик, Windows Runtime обнаруживает это и отключает тайм-аут — в отличие от SCM. Однако модель та же. Windows Runtime контролирует процесс и, когда происходят какие-то события, вызывает приложение в потоке, созданном исполняющей средой. Конечно, Windows Runtime основана на COM, поэтому вместо функции обратного вызова (как в случае SCM) эта исполняющая среда (которая в этом деле опирается на Process Lifetime Manager [PLM]) ожидает, что приложение предоставит метод Run с COM-интерфейсом, который можно использовать для вызова приложения.

Ваше приложение должно предоставить реализацию IFrameworkViewSource, который также находится в пространстве имен ABI::Windows::ApplicationModel::Core, и тогда CoreApplication вызовет его единственный метод CreateView, как только создаст UI-поток вашего приложения. На самом деле в IFrameworkViewSource содержится не только метод CreateView. Этот интерфейс наследует от IInspectable, базового интерфейса WinRT. В свою очередь IInspectable наследует от IUnknown, базового интерфейса COM.

WRL обеспечивает обширную поддержку реализации COM-классов, но об этом мы поговорим в следующей статье. А пока я хочу подчеркнуть, что корни Windows Runtime уходят в COM и что нет лучшего способа показать это, чем реализовать IUnknown. Для моих целей будет полезным отметить, что срок жизни C++-класса, который реализует IFrameworkViewSource и несколько других интерфейсов, определяется стеком. По сути, функция WinMain приложения сводится к следующему:

Остается лишь написать класс SampleWindow так, чтобы он должным образом реализовал интерфейс IFrameworkViewSource. Хотя CoreApplication безразлично, где реализуются интерфейсы, ваше приложение, как минимум, должно реализовать не только IFrameworkViewSource, но и интерфейсы IFrameworkView и IActivatedEventHandler. В данном случае класс SampleWindow может просто реализовать их всех:

Интерфейс IFrameworkView также определен в пространстве имен ABI::Windows::ApplicationModel::Core, а с IActivatedEventHandler дело обстоит несколько сложнее. Я определил его сам таким образом:

Если у вас есть некоторый опыт работы с COM, вы могли подумать, что это выглядит весьма не ортодоксально, — и были бы правы. Как и следовало бы ожидать, ITypedEventHandler — это просто шаблон класса, и довольно странно так определять COM-интерфейс; самая очевидная проблема в том, что вы, по-видимому, не смогли бы узнать, каким идентификатором интерфейса нужно его пометить. К счастью, все эти интерфейсы генерируются компилятором MIDL, который берет на себя задачу специализации каждого из них, и этим специализациям он назначает GUID, представляющий идентификатор интерфейса. Каким бы сложным ни выглядело предыдущее выражение typedef, оно определяет COM-интерфейс, который наследует непосредственно от IUnknown и предоставляет единственный метод — Invoke.

Я должен реализовать несколько методов интерфейсов, поэтому займемся делом. Первым идет IUnknown и его могущественный метод QueryInterface. Я не хочу тратить здесь слишком много времени на IUnknown и IInspectable, так как подробно опишу их в следующей статье. На рис. 2 приведена простая реализация QueryInterface для класса, основанного на стеке.

Рис. 2. Метод QueryInterface в SampleWindow

В этой реализации стоит отметить несколько моментов. Прежде всего, метод проверяет, что его аргументы допустимы. Более политкорректная реализация могла бы возвращать E_POINTER, но предполагается, что такие ошибки являются дефектами, которые можно устранить на этапе разработки, поэтому незачем тратить на них лишние процессорные ресурсы в период выполнения. Это обеспечивает наилучшее из возможных поведение, немедленно вызывая ошибку доступа к памяти и создавая аварийный дамп, который довольно легко проанализировать. Если бы вы возвращали E_POINTER, вызвавший дефектный код скорее всего просто проигнорировал бы это. Поэтому лучшая политика — провал на раннем этапе. По сути, такой позиции придерживаются во многих реализациях, включая DirectX и Windows Runtime. Корректная реализация QueryInterface требует уймы усилий. Спецификация COM весьма требовательна к тому, чтобы COM-классы всегда корректно и согласованно обеспечивали определенные гарантии идентификации объектов. Не волнуйтесь, если цепочка выражений if кажется вам устрашающей. В свое время я расскажу и об этом.

И последнее, на что хотелось бы обратить внимание в этой реализации, — она не вызывает AddRef. Обычно перед возвратом управления QueryInterface должен вызывать AddRef применительно к полученному указателю на интерфейс IUnknown. Однако, так как класс SampleWindow находится в стеке, смысла в учете ссылок нет. По той же причине реализация IUnknown-методов AddRef и Release предельно проста:

Результаты этих методов имеют лишь информационный характер, поэтому вы можете использовать преимущество этого факта, и вас устроит любое ненулевое значение. Здесь я должен предупредить вас: возможно, вам понадобится переопределить операторы new и delete, чтобы явным образом указывать на то, что данный класс рассчитан на работу только в стеке. В качестве альтернативы вы могли бы реализовать учет ссылок — так, на всякий случай.

Теперь мне нужно реализовать IInspectable, но, поскольку он не будет использоваться в этом простом приложении, я схитрю и оставлю его методы нереализованными, как показано на рис. 3. Это нерекомендуемая реализация, и нет никаких гарантий, что она будет работать в других случаях. И вновь описание IInspectable я отложу до следующей статьи, а пока замечу лишь, что этого достаточно для подготовки и работы интерфейсов, производных от IInspectable, в классе SampleWindow.

Рис. 3. IInspectable-методы в SampleWindow

Далее я должен реализовать IFrameworkViewSource и его метод CreateView. Поскольку класс SampleWindow тоже реализует IFrameworkView, реализация проста. И вновь заметьте, что обычно перед возвратом управления вы должны были бы вызывать AddRef применительно к полученному указателю на интерфейс, производный от IUnknown. Возможно, вы предпочтете на всякий случай вызывать AddRef в теле этой функции:

Интерфейс IFrameworkView — то место, где наконец начинают происходить интересные вещи. После вызова CreateView из приложения для получения указателя на этот интерфейс Windows Runtime вызывает большинство его методов в быстрой последовательности. Важно так же быстро реагировать на эти вызовы, так как все они засчитываются в тот период времени, в течение которого пользователь ожидает запуска вашего приложения. Первым вызывается Initialize, и в нем приложение должно регистрироваться на событие Activated. Это событие уведомляет, что приложение активировано, но вы сами должны активировать его CoreWindow. Метод Initialize довольно прост:

Затем вызывается метод SetWindow, который предоставляет приложению реализацию ICoreWindow, который просто моделирует обычный HWND рабочего стола в Windows Runtime. В отличие от остальных интерфейсов модели приложений ICoreWindow определен в пространстве имен ABI::Windows::UI::Core. В методе SetWindow обязательно делайте копию указателя на интерфейс, так как достаточно скоро он вам понадобится:

Следующий метод — Load; в нем вы должны собрать весь код, отвечающий за подготовку вашего приложения к начальному отображению:

Как минимум, вы должны регистрироваться на события, относящиеся к изменениям размера окна и видимости, а также к изменениям в масштабировании DPI (dots per inch). Кроме того, вы, вероятно, захотите воспользоваться возможностью создать различные объекты фабрики DirectX, загрузить аппаратно-независимые ресурсы и т. д. Это хорошее место для выполнения всех таких операций по той причине, что в этот момент пользователь видит экран-заставку вашего приложения.

Когда метод Load возвращает управление, Windows Runtime считает, что ваше приложение готово к активации, и генерирует событие Activated, которое я буду обрабатывать, реализовав метод Invoke интерфейса IActivatedEventHandler примерно так:

После активации окна приложение наконец готово к выполнению:

Реализовать это можно разными способами. Здесь я просто получаю интерфейс ICoreDispatcher окна, который представляет цикл прокачки сообщений (message pump) для этого окна. Наконец, существует метод Uninitialize, который мог бы вызываться время от времени, но в остальном бесполезен, и его можно безопасно игнорировать:

Вот и все. Теперь вы можете скомпилировать и запустить приложение. Конечно, в данном случае на экране ничего рисоваться не будет. Можете взять копию dx.h с сайта dx.codeplex.com и начать добавлять Direct2D-код для рендеринга (подробности см. в моей статье «A Modern Library for DirectX Programming» в этой рубрике за июнь 2013 г. по ссылке msdn.microsoft.com/magazine/dn201741) или дождаться следующей статьи, где я покажу, как лучше всего интегрировать Direct2D с базовой моделью приложений WinRT.

Кенни Керр (Kenny Kerr) — высококвалифицированный программист. Живет в Канаде. Автор учебных курсов для Pluralsight, обладатель звания Microsoft MVP. Ведет блог kennykerr.ca Кроме того, читайте его заметки в twitter.com/kennykerr.

Выражаю благодарность за рецензирование статьи эксперту Microsoft Джеймсу Макнеллису (James McNellis).

Источник

Перенос настольных приложений в Windows Runtime

В этой статье используется предварительная версия Visual Studio 2012. Любая изложенная здесь информация может быть изменена.

Продукты и технологии:

В статье рассматриваются:

• проблемы, связанные с переносом;

• разделение обязанностей;

• рефакторинг для разъединения повторно используемых компонентов;

• создание XAML-представления приложения;

• применение HTML для UI приложения.

Windows 8 воплощает новую философию дизайна для платформ Microsoft. В Windows 8 можно создавать приложения, использующие такие UI-технологии, как XAML и HTML5. Microsoft предоставляет две новые модели приложений: Windows Runtime Library (WRL), которая помогает разрабатывать приложения Windows Store на C#, Visual Basic и C++, и Windows Library for JavaScript (WinJS), которая позволяет создавать приложения с применением HTML5 и JavaScript.

WRL для Windows 8 означает то же, что инфраструктура Microsoft Foundation Classes (MFC) или «C-подобный» Win32 API для настольной среды. Соответственно существующие настольные приложения нужно адаптировать для выполнения под управлением Windows Runtime. Эта проблема возникает в приложениях, которые сильно зависят от MFC, Win32 или других прикладных инфраструктур. Что произойдет с ними, когда вы будете переносить их на Windows Runtime? Что будет впоследствии? Нужно ли поддерживать две кодовые базы — для планшетов и настольных компьютеров?

В этой статье я покажу, как идентифицировать и вычленять значительные части из кодовой базы приложения, которые можно сделать общими для двух сред. Вы увидите, что такой рефакторинг также является возможностью задействовать некоторые новые средства C++11 для большей лаконичности кода и удобства его сопровождения. Преимуществ гораздо больше, чем простое получение новой Windows Store-версии существующего приложения, — вы также сможете модернизировать кодовую базу существующего приложения.

Дилеммы портируемости

Создать приложение, которое должно быть портируемым и к которому предъявляются другие нефункциональные требования, всегда легче с нуля, заранее продумав соответствующую архитектуру. Однако на практике разработчики приложений часто сталкиваются с непредвиденными требованиями, которые формулируются уже после его развертывания. Удовлетворить эти требования на столь поздних этапах может оказаться проблематичным, если архитектура приложения такова, что реализовать новые средства трудно без переписывания больших частей кодовой базы. Вдобавок новые части могут в конечном счете вывести приложение из производственной среды, если они тщательно не тестировались.

Ввиду этого я решил использовать в качестве примера существующее приложение вместо того, чтобы создавать новую демонстрацию. Как вы увидите, я выбрал пример с MFC-калькулятором, опубликованным Microsoft для Visual Studio 2005 (bit.ly/OO494I).

Поначалу возникает соблазн переписать все приложение, потому что хочется избавиться от кода, поддерживать который у вас нет никакого желания: лучше переделать все с начала, но на этот раз как следует. Однако убедить в этом администрацию не удается, так как это сводит на нет возможность окупить вложенные в исходное приложение средства, если только не оставить его в производственной среде для тех платформ, которые не смогут выполнять новое приложение. В таком случае придется поддерживать две сходные кодовые базы, что увеличивает затраты (двойная работа плюс реализация новых средств или исправление ошибок, например).

Маловероятно, что весь изначальный код можно будет повторно использоваться в разных средах (в данном случае — в настольной Windows и Windows Runtime). Однако, чем больше кода удастся сделать общим, тем меньше будут затраты и тем больше в дальнейшем будет прибыль.

Назад к основам: разделение обязанностей

Разделение обязанностей (separation of concerns, SoC) является сегодня основополагающей концепцией, опубликованной во многих книгах по архитектуре ПО. Одно из естественных следствий заключается в том, что код, относящийся к API, четко группируется (если не сказать, скрывается) в хорошо сегментированные компоненты, которые предлагают абстрактный интерфейс остальному коду. Таким образом, конкретное хранилище данных никогда не делается явно доступным коду, который выполняет логику предметной области, презентационную логику и др. Эти компоненты просто «общаются» с абстрактным интерфейсом.

Концепция SoC в настоящее время широко применяется разработчиками. Как следствие взрывного роста Web в конце 90-х, многие автономные приложения были разбиты на модули, которые потом были распределены по уровням и звеньям.

Если у вас есть приложения, разработанные без учета SoC, их можно перенести в современный мир с помощью рефакторинга. Рефакторинг — хорошая практика, вполне осуществимая в наши дни благодаря широкому принятию правил Agile-разработки, которые способствуют созданию простейших модулей, способных работать, прохождению всех тестов, сокращению времени выхода на рынок с готовым продуктом и прочим вещам. Однако динамичность (agility) оставляет не так много пространства для включения новых каналов, пока это не становится потребностью. Вот мы и подошли к сути.

Типичный сценарий переноса

Пример MFC-приложения, о котором я упоминал, — базовый калькулятор, показанный на рис. 1.

Desktop runtime что это. Смотреть фото Desktop runtime что это. Смотреть картинку Desktop runtime что это. Картинка про Desktop runtime что это. Фото Desktop runtime что это
Рис. 1. Microsoft MFC Calculator

Этот пример отлично иллюстрирует процесс переноса по следующим причинам:

Исходный калькулятор содержит два класса: CCalcApp и CCalcDlg. CCalcApp моделирует калькулятор как выполняемый процесс, чья функция InitInstance создает экземпляр CCalcDlg (рис. 2). CCalcDlg моделирует основное окно, его элементы управления (панели и кнопки) и связанные события.

Desktop runtime что это. Смотреть фото Desktop runtime что это. Смотреть картинку Desktop runtime что это. Картинка про Desktop runtime что это. Фото Desktop runtime что это
Рис. 2. Диаграмма классов исходного калькулятора-примера (незначимые детали опущены)

CCalcDlg наследует от MFC CDialog и реализует все — от базового сопоставления оконных сообщений до функций и логики калькулятора, запускаемых в ответ на события. На рис. 3 показано, что происходит, когда вы щелкаете кнопку «равно» (предполагается, что перед этим вы ввели два операнда и знак операции). На рис. 4 представлены функции CCalcDlg, расширяющие поведение на всех уровнях: реакции на событие, логики предметной области и презентационном уровне.

Desktop runtime что это. Смотреть фото Desktop runtime что это. Смотреть картинку Desktop runtime что это. Картинка про Desktop runtime что это. Фото Desktop runtime что это
Рис. 3. Схема последовательности действий для события щелчка кнопки «равно»

UserПользователь
CCalcDlgCCalcDlg
clicks the button ‘=’щелкает кнопку «=»
+ OnClickedEqual()+ OnClickedEqual()
calculates the pending operationвычисляет ожидающую операцию
# PerformOperation()# PerformOperation()
sets the result to IDE_ACCUM in the UIприсваивает результат IDE_ACCUM в UI
# UpdateDisplay()# UpdateDisplay()
>>

Рис. 4. CCalcDlg

Так как CCalcDlg связан с MFC, логику, которую я хочу использовать в Windows Store-версии приложения, нельзя перенести в том виде, как она есть сейчас. Мне потребуется выполнить рефакторинг.

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

Рефакторинг для разъединения повторно используемых компонентов

Чтобы создать Windows Store-версию этого калькулятора, мне не нужно что-либо кодировать заново. Большую часть поведения вроде того, что показано на рис. 4, можно было бы использовать повторно, если бы оно не было столь тесно увязано с CCalcDlg, основанным на MFC. Я выполню рефакторинг этого приложения так, чтобы повторно используемые части (в данном случае — поведение калькулятора) были изолированы от компонентов, специфичных для конкретной реализации.

Я буду предполагать, что вы не только слышали об архитектурном шаблоне Model-View-Controller (MVC), но и применяли его. Поэтому здесь я лишь напомню, что Model состоит из объектов предметной области (как с поддержкой состояния, так и без) и ничего не знает о технологии View (она ему безразлична). View реализуется как одна из технологий взаимодействия с пользователем (HTML, Qt, MFC, Cocoa и др.), подходящих для устройства, на котором работает приложение. Он не знает, как реализована логика предметной области; его функция заключается только в отображении структур данных предметной области или их частей. Controller выступает в роли посредника, захватывая пользовательский ввод для инициации соответствующих операций в предметной области и тем самым заставляя View обновляться и отражать более новое состояние.

На рис. 5 приведена модифицированная версия MFC-приложения.

Desktop runtime что это. Смотреть фото Desktop runtime что это. Смотреть картинку Desktop runtime что это. Картинка про Desktop runtime что это. Фото Desktop runtime что это
Рис. 5. Пространство имен Calculator::View консолидирует поведение View в сопоставленном с ним Presentation Model

CalculatorPresentationModel хранит ссылку на это View (моделируется как интерфейс ICalculatorView), потому что, как только обнаруживается, что состояние View изменилось, вызывается функция UpdateDisplay. В MFC-примере таким View является сам CCalcDlg, так как это класс, имеющий дело непосредственно с MFC.

CCalcDlg создает свой Presentation Model в конструкторе:

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

Как видите, я использовал здесь смарт-указатель C++11 под названием unique_ptr (детали см. по ссылке bit.ly/KswVGy). Смарт-указатели (smart pointers) позволяют освобождать объект, на который они ссылаются, когда необходимости в нем больше нет. В моем случае смарт-указатель гарантирует, что Presentation Model будет уничтожен по завершении жизненного цикла View. View захватывает оконные события, делегирует ввод в Presentation Model с обработкой или без, как показано на рис. 6.

Рис. 6. Некоторые функции View, показывающие делегирование

Вы найдете эту переработанную версию MFC-примера в папке mfccalc в пакете исходного кода, который можно скачать для этой статьи. Вероятно, вы заметили, что Model нет. В этом примере логика предметной области заключена в классе, содержащем функции для четырех элементарных арифметических операций, и выглядит примерно так, как показано на рис. 7.

Исключение, генерируемое Model, является крайней мерой, когда ничто другое не доступно.

Рис. 7. Чистая реализация, которая включает Calculator «Model»

Я решил опустить Model в этом небольшом примере, оставив его логику в Presentation Model. Это не типично в большинстве случаев, и логику Model нужно реализовать в своем классе или наборе классов. Если хотите, можете в качестве упражнения выполнить рефакторинг этого примера, придерживаясь пуристского подхода. На случай, если решите этим заняться, обращайте особое внимание на реализацию функции CalculatorModel::Divide, так как Model — повторно используемый компонент, который можно было бы вызывать откуда-то автоматически, а не в результате взаимодействия с пользователем. Тогда не имело бы значения, что деление на ноль генерирует исключение; в этот момент это была бы неожиданная ситуация. Но вам не следует удалять из Presentation Model проверку деления на ноль. Предотвращение пересылки ошибочных данных на внутренние уровни всегда является здравой идеей. Исключение, генерируемое Model, является крайней мерой, когда ничто другое не доступно.

Идем дальше. Запустите переработанное решение mfccalc в пакете сопутствующего кода и вы заметите, что оно по-прежнему ведет себя так же, как и раньше. А мне как раз это и нужно: подготовить код к переносу без потери функциональности. В серьезном процессе рефакторинга следует подумать о наборе автоматизированных тестов, чтобы проверять, не было ли как-то затронуто поведение приложения. Модульное тестирование неуправляемого кода доступно в Visual Studio 2012 и всех ее редакциях для разработчиков, включая бесплатную редакцию Express for Windows 8 (которая, однако, не поставляется с MFC или другими инфраструктурами для разработки настольных приложений).

Взгляните на решение Calculator\CalculatorTests в сопутствующем коде. Пространство имен Calculator::Testing содержит класс PresentationModelTest, методы которого тестируют таковые в классе CalculatorPresentationModel, как показано на рис. 8.

Рис. 8. Изоляция ошибок с помощью модульного тестирования неуправляемого кода

Кстати, модульное тестирование — одно из наиболее ценных преимуществ этого шаблона Presentation Model: тесты поведения UI автоматизируются так же, как и для логики предметной области, которую в этом примере я встроил в Presentation Model. Таким образом, вы можете открыть новые каналы для своего приложения (например, Cocoa, Qt, wxWidgets или даже управляемые инфраструктуры вроде HTML5 или Windows Presentation Foundation) с уверенностью, что ошибки, если таковые есть, возникают в новых компонентах для взаимодействия с пользователем, а не в существующих. Вы можете задействовать функцию анализа охвата кода тестами (code coverage feature), чтобы быть уверенным, что все строки кода проверяются хотя бы раз.

Открытие XAML-фасада приложению-калькулятору

После рефакторинга кода я готов создать новый Windows UI для MFC-калькулятора. С этой целью достаточно создать новый View для шаблона Presentation Model, описанного ранее.

Windows 8 предлагает три технологии UI: XAML, HTML и DirectX.

Чтобы создать XAML-представление, я выбрал базовый шаблон Blank App из списка шаблонов Visual C++ для Windows 8 и создал проект XamlCalc.

Этот шаблон содержит пустой файл MainPage.xaml, который я заполню элементами управления, чтобы создать эквивалент для Windows 8 вместо прежнего CCalcDlg в MFC-версии. Это все, что нужно для переноса приложения-калькулятора, потому что оно состоит из одного окна и в нет нужды в средствах навигации. При переносе своего приложения вам стоит подумать о других шаблонах, обеспечивающих механизм навигации между страницами. Соответствующее руководство вы найдете на странице «Designing UX for Apps» (bit.ly/Izbxky).

Вы можете вызывать компоненты на основе C++ из кода на JavaScript.

Пустой шаблон также включает файл App.xaml, аналогичный по назначению классу CCalcApp в MFC-версии (рис. 2). Это стартовый загрузчик, который инициализирует остальные компоненты и передает им управление. Функция CCalcApp::InitInstance создает окно CCalcDlg, которое потом служит в качестве UI. (Все это вы увидите в решении XamlCalc в сопутствующем коде.) В случае XAML App::OnLaunched генерируется по умолчанию в файле отделенного кода, App.xaml.cpp, и инициирует начальный переход в MainPage:

С помощью встроенного в Visual Studio редактора XAML я создаю страницу калькулятора, перетаскивая элементы управления из окна инструментария и выполняя кое-какие операции вручную, связанные, в частности, с пользовательскими стилями, связыванием с данными, сопоставлением событий и т. д. В итоге XAML-код выглядит, как показано на рис. 9, а сам калькулятор — как представлено на рис. 10.

Рис. 9. XAML-версия MFC-калькулятора

Desktop runtime что это. Смотреть фото Desktop runtime что это. Смотреть картинку Desktop runtime что это. Картинка про Desktop runtime что это. Фото Desktop runtime что это
Рис. 10. Внешний вид XAML-калькулятора

Я определил стиль кнопок (для цифр и операций) в App.xaml, поэтому мне не нужно заново назначать цвета, шрифты, выравнивание и другие свойства для каждой кнопки. Аналогично я сопоставил обработчики событий со свойством Click каждой кнопки; обработчики являются MainPage-методами, определенными в файле отделенного кода MainPage.xaml.cpp. Ниже я привожу пару примеров (один для нажатых цифр, а другой для операции деления):

Как видите, эти методы на C++/CX в MainPage просто принимают информацию событий и передают ее экземпляру моего класса на стандартном C++, CalculatorPresentationModel, для выполнения реальных действий в UI. Это демонстрирует возможность заимствования логики на стандартном C++ из существующих неуправляемых приложений и ее повторного использования в совершенно новом приложении Windows Store на C++/CX. Такая возможность сокращает расходы на сопровождение обеих версий, так как обе могут использовать любые обновления в общих компонентах — в данном случае в CalculatorPresentationModel.

К компонентам, специфичным для конкретной реализации, можно обращаться из общих компонентов, если они реализуют четко определенные абстрактные интерфейсы. Так, в моем примере CalculatorPresentationModel::UpdateDisplay делегирует реальную работу экземпляру ICalculatorView:

В MFC-версии ICalculatorView реализуется классом CCalcDlg на основе MFC. Взгляните на переработанную схему на рис. 11 и сравните ее с исходной на рис. 3.

Desktop runtime что это. Смотреть фото Desktop runtime что это. Смотреть картинку Desktop runtime что это. Картинка про Desktop runtime что это. Фото Desktop runtime что это
Рис. 11. Схема последовательности действий для события щелчка кнопки «равно» в разъединенной MFC-версии

UserПользователь
CCalcDlgCCalcDlg
presentationModel : CalculatorPresentationModelpresentationModel : CalculatorPresentationModel
clicks the button ‘=’щелкает кнопку «=»
+ OnClickedEqual()+ OnClickedEqual()
ClickedOperator(OpNone)ClickedOperator(OpNone)
— PerformOperation()— PerformOperation()
# UpdateDisplay()# UpdateDisplay()
+ GetValue()+ GetValue()
>>
>>

Чтобы сохранить XAML-версию аналогичной MFC-версии, я не должен был бы реализовать ICalculatorView в MainPage. Вместо этого мне следовало бы реализовать ICalculatorView как отдельный класс, потому что MainPage является классом C++/CX и поэтому не может наследовать от стандартного класса на C++. C++ и его проекция в Windows Runtime (C++/CX) имеют разные системы типов, которые в любом случае корректно взаимодействуют. Реализовать чистый C++-интерфейс ICalculatorView — задача несложная:

Итак, классы стандартного C++ и C++/CX не могут наследовать друг от друга, но могут хранить ссылки друг на друга; в данном случае это закрытый член page_, который является ссылкой на C++/CX MainPage. Чтобы обновить дисплей в XAML-версии, я просто изменяю в MainPage.xaml свойство Text элемента управления TextBlock с именем display_:

На рис. 12 показана схема классов в XAML-калькуляторе.

Desktop runtime что это. Смотреть фото Desktop runtime что это. Смотреть картинку Desktop runtime что это. Картинка про Desktop runtime что это. Фото Desktop runtime что это
Рис. 12. Схема классов в приложении-калькуляторе на XAML и C++/CX

На рис. 13 представлена схема, соответствующая последовательности действий при щелчке кнопки «равно» в XAML-версии.

Desktop runtime что это. Смотреть фото Desktop runtime что это. Смотреть картинку Desktop runtime что это. Картинка про Desktop runtime что это. Фото Desktop runtime что это
Рис. 13. Схема последовательности действий для события щелчка кнопки «равно» в XAML-версии

UserПользователь
CalcViewCalcView
MainPage (C++/CX)MainPage (C++/CX)
presentationModel : CalculatorPresentationModelpresentationModel : CalculatorPresentationModel
clicks the button ‘=’щелкает кнопку «=»
+ ClickedOperator(OpNone)+ ClickedOperator(OpNone)
— PerformOperation()— PerformOperation()
+ UpdateDisplay()+ UpdateDisplay()
+ GetValue()+ GetValue()
>>
>>

Следует отметить, что WRL интенсивно использует асинхронность во всех своих API для обработки событий. Однако мой код синхронный на 100%. Я мог бы сделать его асинхронным, используя Parallel Patterns Library, которая реализует задачи и продолжения на основе концепций асинхронности Windows 8. Это не было бы оправданным в моем маленьком примере, но вам стоит почитать страницу «Asynchronous Programming in C++» в Windows Developer Center (bit.ly/Mi84D1).

Нажмите F5 и запустите приложение, чтобы увидеть XAML-версию в действии. При переносе или создании приложений Windows Store важно проектировать их UI на основе новых проектировочных шаблонов Windows Experience, описанных в Microsoft Dev Center (bit.ly/Oxo3S9). Следуйте рекомендованным шаблонам для поддержки команд, сенсорного ввода, перевернутой ориентации, значков (charms) и прочего, чтобы ваше приложение было интуитивно понятным пользователям-новичкам.

Другой пример: калькулятор с новым Windows UI с применением HTML

Пример с XAML достаточен, чтобы получить начальный пример калькулятора на основе MFC, способный работать в Windows 8. Однако при определенных обстоятельствах (например, с учетом общей квалификации вашей группы или для использования существующих ресурсов) можно подумать о применении для UI вместо XAML пары «HTML-JavaScript».

Проектировочный шаблон Presentation Model, описанный в этой статье, по-прежнему будет полезен, даже если ваш UI содержит логику, написанную на языке, отличном от C++, например на JavaScript. Это чудо возможно благодаря тому, что в среде Windows 8 JavaScript проецируется на Windows Runtime во многом аналогично C++, что позволяет этим языкам взаимодействовать, поскольку они совместно используют систему типов, устанавливаемую WRL.

В сопутствующем коде вы найдете решение HtmlCalc, которое содержит страницу default.html, похожую на MainPage.xaml. На рис. 14 показано описание UI, сравнимое с XAML-версией, представленной на рис. 9.

Рис. 14. HTML-разметка для UI калькулятора

Роль отделенного кода в HTML-страницах играет JavaScript-код. Действительно вы обнаружите такой код в файле js\default.js. Мой CalculatorPresentationModel нельзя напрямую вызывать из JavaScript из-за того, что это класс на стандартном C++, но можно делать это косвенно, через промежуточный компонент на C++/CX — Calculator::View::CalcView.

Чтобы создать экземпляр этого компонента из JavaScript достаточно объявить в default.js следующее:

В качестве примера этого подхода вы увидите, что щелчок кнопки «равно» приводит к вызову следующей JavaScript-функции:

Вызов продвигается в CalcView::equal_click, который «на равных общается» с моим CalculatorPresentationModel на стандартном C++:

В этом конкретном сценарии компонент CalcView на C++/CX просто пересылает каждый запрос в PresentationModel на стандартном C++ (рис. 15). Но избавиться от него на пути к повторно используемому C++-компоненту нельзя.

Desktop runtime что это. Смотреть фото Desktop runtime что это. Смотреть картинку Desktop runtime что это. Картинка про Desktop runtime что это. Фото Desktop runtime что это
Рис. 15. Схема последовательности действий для события щелчка кнопки «равно» в гибридной версии HTML-C++

UserПользователь
default page : HTML/JavaScriptстраница по умолчанию: HTML/JavaScript
nativeBridge : CalcView (C++/CX)nativeBridge : CalcView (C++/CX)
presentationModel : CalculatorPresentationModelpresentationModel : CalculatorPresentationModel
Equal_Click()Equal_Click()
ClickedOperator(OpNone)ClickedOperator(OpNone)
PerformOperation()PerformOperation()
get_display()get_display()
GetValue()GetValue()
>>
>>
>>

Так как прокси на C++/CX нужно создать вручную, не следует игнорировать связанные с этим издержки. Тем не менее, издержки можно сбалансировать с преимуществами повторного использования компонентов, как это делаю я в сценарии с CalculatorPresentationModel.

Нажмите F5, чтобы увидеть HTML-версию в действии. Я показал, как повторно использовать существующий код на C++ для его переноса в новейшую инфраструктуру в Windows 8 без отказа от исходных каналов (MFC, в моем случае). Теперь мы готовы сформулировать некоторые заключительные соображения.

«Здравствуй, настоящий мир!»

Мой сценарий переноса является частным случаем, который может не совпасть с вашим, а ваш — с чьим-то еще. Многое из того, что я показал здесь, применимо к сценарию с MFC Calculator, и я мог бы принять другие решения, если бы переносил в WRL другое приложение. Однако можно сделать ряд общих заключений по переносу приложений.

Шаблон Presentation Model отлично подходит для поддержания бесперебойной работы приложений и минимизации расходов на сопровождение.

Пара слов в заключение

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

Диего Дагум (Diego Dagum)архитектор ПО и тренер, имеющий более чем двадцатилетний опыт работы в индустрии программного обеспечения. С ним можно связаться по адресу email@diegodagum.com.

Выражаю благодарность за рецензирование статьи экспертам Мариусу Бансилу (Marius Bancila), Анхелю Хесусу Эрнандесу (Angel Jesus Hernandez) и группе Windows 8.

Источник

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *