Windows Runtime и C++

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

Диего Дагум

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

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

Windows 8, Visual Studio 2012, C++, XAML, HTML, JavaScript , Windows Runtime

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

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

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

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

• создание 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.

Microsoft MFC Calculator
Рис. 1. Microsoft MFC Calculator

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

  • он достаточно мал, чтобы получить общее представление о том, что именно он делает;
  • он достаточно большой, чтобы я мог показать детали процесса. Среднее приложение имеет более крупную кодовую базу, но процесс перенос будет аналогичным;
  • код этого примера достаточно связан, чтобы показать его разъединение путем рефакторинга. Вероятно, он был преднамеренно связан, чтобы его кодовая база была компактной. Я буду разъединять код, пока не получу общую кодовую базу, используемую MFC-версией и версией для Windows 8. Заниматься дальнейшим разъединением я не стану, но смогу предположить, сколько еще кода можно разделить.

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

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

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

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

User Пользователь
CCalcDlg CCalcDlg
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()
<<result on display>> <<результат на дисплее>>

Рис. 4. CCalcDlg

// CCalcDlg.cpp
// Оконные сообщения инициируют вызовы функций CCalcDlg
BEGIN_MESSAGE_MAP(CCalcDlg, CDialog)
  ON_WM_PAINT()
  ON_COMMAND_RANGE(IDB_0, IDB_9, OnClickedNumber)
  ON_BN_CLICKED(IDB_CLEAR, OnClickedClear)
  ON_BN_CLICKED(IDB_DIVIDE, OnClickedDivide)
  ON_BN_CLICKED(IDB_EQUAL, OnClickedEqual)
  ON_BN_CLICKED(IDB_MINUS, OnClickedMinus)
  ON_BN_CLICKED(IDB_PLUS, OnClickedPlus)
  ON_BN_CLICKED(IDB_TIMES, OnClickedTimes)
  ON_EN_SETFOCUS(IDE_ACCUM, OnSetFocusAccum)
END_MESSAGE_MAP()
 
...
 
// Реакция на событие
void CCalcDlg::OnClickedEqual() {
  PerformOperation();
  m_operator = OpNone;
}
 
// Логика предметной области
void CCalcDlg::PerformOperation() {
  if (m_bOperandAvail) {
    if (m_operator == OpNone)
      m_accum = m_operand;
    else if (m_operator == OpMultiply)
      m_accum *= m_operand;
    else if (m_operator == OpDivide) {
      if (m_operand == 0)
        m_errorState = ErrDivideByZero;
      else
        m_accum /= m_operand;
      }
    else if (m_operator == OpAdd)
      m_accum += m_operand;
    else if (m_operator == OpSubtract)
      m_accum -= m_operand;
  }
 
  m_bOperandAvail = FALSE;
  UpdateDisplay();
}
 
// Презентационная логика
void CCalcDlg::UpdateDisplay() {
  CString str;
  if (m_errorState != ErrNone)
    str.LoadString(IDS_ERROR);
  else {
    long lVal = (m_bOperandAvail) ? m_operand : m_accum;
    str.Format(_T("%ld"), lVal);
  }
  GetDlgItem(IDE_ACCUM)->SetWindowText(str);
}

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

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

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

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

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

MVC — широко известный шаблон, но это не единственный способ изоляции UI от предметной области. В примере с калькулятором я буду полагаться на вариацию MVC под названием «Presentation Model» (bit.ly/1187Bk). Изначально я подумывал о Model-View-ViewModel (MVVM) — другой вариации, популярной среди разработчиков, которые используют Microsoft .NET Framework. Однако Presentation Model лучше подходит в данном случае. Этот шаблон идеален, когда вам нужно реализовать новую технологию взаимодействия с пользователем (в нашем случае, с Windows 8), но в поведение UI никаких изменений вносить не требуется. Этот шаблон по-прежнему включает Model и View, но роль Controller исполняется абстрактным представлением View с именем Presentation Model. Этот компонент реализует стандартные поведения View, включая часть его состояния, безотносительно технологии этого View.

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

Пространство имен Calculator::View консолидирует поведение View в сопоставленном с ним Presentation Model
Рис. 5. Пространство имен Calculator::View консолидирует поведение View в сопоставленном с ним Presentation Model

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

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

CCalcDlg::CCalcDlg(CWnd* pParent) 
  : CDialog(CCalcDlg::IDD, pParent)
{
  presentationModel_ = 
    unique_ptr<CalculatorPresentationModel>(
    new CalculatorPresentationModel(this));
  ...
}

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

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

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

// Параметр nID содержит ASCII-код или цифру
void CCalcDlg::OnClickedNumber(UINT nID) {
  ASSERT(nID >= IDB_0 && nID <= IDB_9);
  presentationModel_->ClickedNumber(nID - IDB_0);
}
 
// Немодифицированное делегирование
void CCalcDlg::OnClickedClear() {
  presentationModel_->ClickedClear();
}
 
enum Operator { OpNone, OpAdd, OpSubtract, OpMultiply, OpDivide };
 
// Presentation Model содержит единственный метод
// для всех двоичных операций
void CCalcDlg::OnClickedDivide() {
  presentationModel_->ClickedOperator(OpDivide);
}
 
void CalculatorPresentationModel::ClickedOperator(Operator oper) {
  // PerformOperation теперь находится в PresentationModel;
  // ранее был в CCalcDlg (который сейчас стал "View")
  PerformOperation();
  m_operator = oper;
}
 
void CalculatorPresentationModel::PerformOperation() {
  if (m_errorState != ErrNone)
    return;
 
  if (m_bOperandAvail) {
    if (m_operator == OpNone)
      m_accum = m_operand;
    else if (m_operator == OpMultiply)
      m_accum *= m_operand;
  ... // то же, что и на рис. 4
 
  m_bOperandAvail = false;
  // Это подставляемая в строку функция, определенная ниже
  UpdateDisplay();
}
 
// Обновление UI делегируется в View
inline void CalculatorPresentationModel::UpdateDisplay() {
  if (view_)
    view_->UpdateDisplay();
}

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

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

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

// Пространство имен Calculator::Model
class CalculatorModel {
public:
  long Add(const long op1, const long op2) const {
    return op1+op2;
  }
 
  long Subtract(const long op1, const long op2) const {
    return op1-op2;
  }
 
  long Multiply(const long op1, const long op2) const {
    return op1*op2;
  }
 
  long Divide(const long op1, const long op2) const {
    if (operand2)
      return operand1/operand2;
    else
      throw std::invalid_argument("Divisor can't be zero.");  }
};
 
// Пространство имен Calculator::View
class CalculatorPresentationModel {
public:
  ...
  void PerformOperation();
  ...
private:
  // Presentation Model содержит ссылку на Model
  unique_ptr<Model::CalculatorModel> model_;
  ...
}
 
void CalculatorPresentationModel::PerformOperation()
{
  if (m_errorState != ErrNone)
    return;
 
  // То же, что и раньше, но на этот раз PresentationModel
  // запрашивает модель выполнить операции предметной области,
  // а не сам выполняет их
  if (m_bOperandAvail) {
    if (m_operator == OpNone)
      m_accum = m_operand;
    else if (m_operator == OpMultiply)
      m_accum = model_->Multiply(m_accum, m_operand);
    else if (m_operator == OpDivide) {
      if (m_operand == 0)
        m_errorState = ErrDivideByZero;
      else
        m_accum = model_->Divide(m_accum, m_operand);
    }
    else if (m_operator == OpAdd)
      m_accum = model_->Add(m_accum, m_operand);
    else if (m_operator == OpSubtract)
      m_accum = model_->Subtract(m_accum, m_operand);
  }
 
  m_bOperandAvail = false;
  UpdateDisplay();
}

Я решил опустить 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. Изоляция ошибок с помощью модульного тестирования неуправляемого кода

// Пространство имен Calculator::Testing
TEST_CLASS(PresentationModelTest) {
private:
  CalculatorPresentationModel presentationModel_;
public:
  TEST_METHOD_INITIALIZE(TestInit) {
    presentationModel_.ClickedClear();
  }
 
  TEST_METHOD(TestDivide) {
    // 784 / 324 = 2 (целочисленное деление)
    presentationModel_.ClickedNumber(7);
    presentationModel_.ClickedNumber(8);
    presentationModel_.ClickedNumber(4);
 
    presentationModel_.ClickedOperator(OpDivide);
 
    presentationModel_.ClickedNumber(3);
    presentationModel_.ClickedNumber(2);
    presentationModel_.ClickedNumber(4);
 
    presentationModel_.ClickedOperator(OpNone);
 
    Assert::AreEqual<long>(2, presentationModel_.GetAccum(),
      L"Divide operation leads to wrong result.");
    Assert::AreEqual<CalcError>(ErrNone, 
      presentationModel_.GetErrorState(),
      L"Divide operation ends with wrong error state.");
  }
 
  TEST_METHOD(TestDivideByZero) {
    // 784 / 0 => ErrDivideByZero
    presentationModel_.ClickedNumber(7);
    presentationModel_.ClickedNumber(8);
    presentationModel_.ClickedNumber(4);
 
    presentationModel_.ClickedOperator(OpDivide);
 
    presentationModel_.ClickedNumber(0);
 
    presentationModel_.ClickedOperator(OpNone);
 
    Assert::AreEqual<CalcError>(ErrDivideByZero, 
      presentationModel_.GetErrorState(),
      L"Divide by zero doesn't end with error state.");
  }
 
  ... // остальные тесты для прочих операций калькулятора
};

Кстати, модульное тестирование — одно из наиболее ценных преимуществ этого шаблона 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 — язык разметки на основе XML, позволяющий объявлять визуальные UI-элементы, данные, связываемые с UI-элементами, и обработчики, вызываемые в ответ на события. Эти события, как правило, определяются в так называемых компонентах отделенного кода (codebehind components), которые можно создать с использованием расширенного синтаксиса C++, известного как C++ Component Extensions для Windows Runtime (C++/CX), либо с применением других языков программирования. Я создам «лицевую часть» калькулятора на основе XAML.
  • HTML позволяет задавать, какие UI-поведения, определенные на JavaScript, будут выполняться в Internet Explorer с ядром «Chakra». Вы можете, как будет показано в следующем разделе, вызывать компоненты на основе C++ из кода на JavaScript.
  • DirectX идеален для мультимедийных приложений. Так как калькулятор к таким приложениям не относится, я не буду обсуждать здесь этот вариант. DirectX-приложения могут использовать XAML через механизм interop, подробно описанный по ссылке bit.ly/NeUhO4.

Чтобы создать 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:

void App::OnLaunched(
  Windows::ApplicationModel::Activation::LaunchActivatedEventArgs^ pArgs) {
  ...
    // Создаем Frame, который действует как контекст навигации,
  // и переходим на первую страницу
  auto rootFrame = ref new Frame();
  if (!rootFrame->Navigate(TypeName(MainPage::typeid))) {
    throw ref new FailureException("Failed to create initial page");
  }
  ...
}

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

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

<Page
  Loaded="Page_Loaded"
  x:Class="XamlCalc.MainPage" ...>
 
  <Grid Background="Maroon">
    ...
    <Border Grid.Row="1" Background="White" Margin="20,0">
      <TextBlock x:Name="display_" TextAlignment="Right" FontSize="90"
        Margin="0,0,20,0" Foreground="Maroon" HorizontalAlignment="Right"
        VerticalAlignment="Center"/>
    </Border>
    <Grid Grid.Row="2">
      ...
      <Button Grid.Column="0" Style="{StaticResource Number}"
        Click="Number_Click">7</Button>
      <Button Grid.Column="1" Style="{StaticResource Number}"
        Click="Number_Click">8</Button>
      <Button Grid.Column="2" Style="{StaticResource Number}"
        Click="Number_Click">9</Button>
      <Button Grid.Column="3" Style="{StaticResource Operator}"
        Click="Plus_Click">+</Button>
    </Grid>
    ...
    <Grid Grid.Row="5">
      ...
      <Button Grid.Column="0" Style="{StaticResource Number}"
        Click="Number_Click">0</Button>
      <Button Grid.Column="1" Style="{StaticResource Operator}"
        Click="Clear_Click">C</Button>
      <Button x:Name="button_equal_" Grid.Column="2"
        Style="{StaticResource Operator}" Click="Equal_Click"
        KeyUp="Key_Press">=</Button>
      <Button Grid.Column="3" Style="{StaticResource Operator}"
        Click="Divide_Click">/</Button>
    </Grid>
  </Grid>
</Page>

Внешний вид XAML-калькулятора
Рис. 10. Внешний вид XAML-калькулятора

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

void MainPage::Number_Click(Platform::Object^ sender,
  Windows::UI::Xaml::RoutedEventArgs^ e)
{
  Button^ b = safe_cast<Button^>(sender);
  long nID = (safe_cast<String^>(b->Content)->Data())[0] - L'0';
  presentationModel_->ClickedNumber(nID);
}
 
void MainPage::Divide_Click(Platform::Object^ sender,
  Windows::UI::Xaml::RoutedEventArgs^ e)
{
  presentationModel_->ClickedOperator(OpDivide);
}

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

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

inline void CalculatorPresentationModel::UpdateDisplay(void) {
  if (view_)
    view_->UpdateDisplay();
}

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

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

User Пользователь
CCalcDlg CCalcDlg
presentationModel : CalculatorPresentationModel presentationModel : CalculatorPresentationModel
clicks the button '=' щелкает кнопку «=»
+ OnClickedEqual() + OnClickedEqual()
ClickedOperator(OpNone) ClickedOperator(OpNone)
- PerformOperation() - PerformOperation()
# UpdateDisplay() # UpdateDisplay()
+ GetValue() + GetValue()
<<result>> <<результат>>
<<result on display>> <<результат на дисплее>>

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

namespace XamlCalc {
  class CalcView : public ICalculatorView {
  public:
    CalcView() {}
    CalcView(MainPage^ page) : page_(page) {}
    inline void UpdateDisplay()
      { page_->UpdateDisplay(); }
  private:
    MainPage^ page_;
  };
}

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

void MainPage::UpdateDisplay() {
  display_->Text = (presentationModel_->GetErrorState() != ErrNone) ?
    L"ERROR!" :
    safe_cast<int64_t>(presentationModel_->GetValue()).ToString();
}

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

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

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

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

User Пользователь
CalcView CalcView
MainPage (C++/CX) MainPage (C++/CX)
presentationModel : CalculatorPresentationModel presentationModel : CalculatorPresentationModel
clicks the button '=' щелкает кнопку «=»
+ ClickedOperator(OpNone) + ClickedOperator(OpNone)
- PerformOperation() - PerformOperation()
+ UpdateDisplay() + UpdateDisplay()
+ GetValue() + GetValue()
<<result>> <<результат>>
<<result on display>> <<результат на дисплее>>

 

Следует отметить, что 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 калькулятора

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>HTMLCalc</title>
    <!-- Ссылки WinJS -->
    <link href="//Microsoft.WinJS.0.6/css/ui-dark.css" rel="stylesheet">
    <script src="//Microsoft.WinJS.0.6/js/base.js"></script>
    <script src="//Microsoft.WinJS.0.6/js/ui.js"></script>
    <!-- Ссылки HTMLCalc -->
    <link href="/css/default.css" rel="stylesheet">
    <script src="/js/default.js"></script>
  </head>
  <body onkeypress="Key_Press()">
    <table border="0">
      <tr>
        <td class="Display" colspan="7" id="display_">0 </td>
      </tr>
      <tr>
        <td>
          <button class="Number" onclick="Number_Click(7)">7</button>
        </td>
        <td>
          <button class="Number" onclick="Number_Click(8)">8</button>
        </td>
        <td>
          <button class="Number" onclick="Number_Click(9)">9</button>
        </td>
        <td>
          <button class="Operator" onclick="Plus_Click()">+</button>
        </td>
      </tr>
      ...
      <tr>
        <td>
          <button class="Number" onclick="Number_Click(0)">0</button>
        </td>
        <td>
          <button class="Operator" onclick="Clear_Click()">C</button>
        </td>
        <td>
          <button class="Operator" onclick="Equal_Click()">=</button>
        </td>
        <td>
          <button class="Operator" onclick="Divide_Click()">/</button>
        </td>
      </tr>
    </table>
  </body>
</html>

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

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

// Это JavaScript-код, создающий экземпляр прокси на C++/CX
// для моего PresentationModel
var nativeBridge = new Calculator.View.CalcView();

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

function Equal_Click() {
    display_.textContent = nativeBridge.equal_click();
}

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

String^ CalcView::equal_click() {
  presentationModel_->ClickedOperator(OpNone);
  return get_display();
}
 
String^ CalcView::get_display() {
  return (presentationModel_->GetErrorState() != ErrNone) ?
    L"ERROR!" :
    safe_cast<int64_t>(presentationModel_->GetValue()).ToString();
}

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

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

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

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

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

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

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

  • Стандартные объекты (те, которые не имеют специфических связей со сторонними API) обеспечивают максимальную степень повторного использования, а значит, их перенос влечет за собой минимальные издержки ли вообще обходится без них. В противоположность этому возможности повторного использования ограничены, когда объекты имеют явные связи с нестандартными API, например с MFC, Qt и WRL. Так, MFC доступна только настольным Windows-приложениям. Qt присутствует в других средах, но не во всех. В таких случаях избегайте смесей, которые ухудшают повторное использование, заставляя объекты приложения взаимодействовать с абстрактными классами. Потом наследуйте от этих классов для создания реализаций с поддержкой сторонних инфраструктур. Посмотрите, что я сделал с ICalculatorView (абстрактным классом) и его реализациями в CCalcDlg и XamlCalc::CalcView. При разработке для Windows 8 начинайте привыкать к WRL API, заменяющим Win32 API. Более подробные сведения вы найдете по ссылке bit.ly/IBKphR.
  • Я применил шаблон Presentation Model, так как моей целью была имитация в Windows Runtime того, что у меня уже было для настольной версии. Вы можете предпочесть вырезать какой-то функционал, если он не имеет особого значения, например применение стилей к тексту в мобильном приложении электронной почты. Или, напротив, добавить функциональность, которая использует возможности новой платформы. Подумайте, например, о растягивании изображения с помощью мультисенсорного ввода в приложении для просмотра изображений. В таких случаях может оказаться более подходящим другой проектировочный шаблон.
  • Использованный мной шаблон Presentation Model отлично подходит для поддержания бесперебойной работы приложений и минимизации расходов на сопровождение. Это позволяет мне поставлять Windows Store-версии приложения, не разрывая отношений с клиентами, предпочитающими исходную MFC-версию. Сопровождение двух каналов (XAML и MFC или HTML и MFC) не удваивает издержки при наличии повторно используемых компонентов вроде CalculatorPresentationModel.
  • Общая степень повторного использования приложения определяется соотношением строк окда, общих для всех версий, и строк кода, специфичных для конкретных версий (компоненты, поддерживаемые сторонними компаниями, не учитываются). Бывают случаи, когда приложения интенсивно используют нестандартные API (например, приложение дополненной реальности, которое опирается на OpenGL и датчики в iOS). Степень повторного использования может быть столь мала, что в конечном счете вы можете принять решение о переносе приложения без повторного использования компонентов, кроме того, который имеет концептуальное значение.

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

  • Не спрашивайте, кто умудрился столь плохо продумать архитектуру существующего приложения и так затруднил вашу работу по переносу. Вместо этого приступайте к рефакторингу. Учитывайте, что методологии Agile не предназначены для зрелых, отказоустойчивых архитектур с высокой степенью повторного использования; они ориентированы прежде всего на своевременную поставку ПО. Чтобы сделать существующее ПО более универсальным и расширяемым для будущего повторного использования и переноса на другие платформы, нужен очень большой опыт, так как принимать проектировочные решения во мраке весьма трудно.
  • Возможно, вы переносите свое приложение на Windows 8, iOS или Android с намерением продавать его через онлайновые магазины, существующие для этих платформ. Тогда учитывайте, что ваше приложение должно пройти процесс сертификации, прежде чем оно будет принято (bit.ly/L0sY9i). Это может заставить вас поддерживать такие поведения UI, которые вы никогда не предполагали в исходной версии (например, основной упор в UI на сенсорный ввод [touch-first], новые значки-кнопки [charms] и др.). Несоблюдение определенных стандартов может привести к тому, что ваше приложение будет отклонено. Не оставляйте без внимания такое «соблюдение нормативов» при оценке издержек.

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

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


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

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