Фоновые службы в Windows Phone Classic

Несмотря на то, что на рынке начинают появляться новые тренды в мобильных операционных системах (в т.ч. Windows Phone 7), предыдущие версии Windows Phone (WM 5.x, WM 6.x) остаются и еще долго будут оставаться актуальными операционными системами для многих пользователей и разработчиков. Поэтому на этот раз я бы хотел рассказать о том, как можно создавать свои собственные фоновые службы в Windows Phone Classic на основе .NET Compact Framework.

Фоновая служба — это некоторое приложение, которое запускается при запуске операционной системы и остается работать в фоне на протяжении всей работы устройства. При этом фоновая служба не имеет какого-либо пользовательского интерфейса и не отображается в диспетчере задач.

К сожалению, .NET Compact Framework не содержит готовых механизмов по созданию подобных служб. При этом создавать фоновые службы можно используя только неуправляемый код. Ограничение состоит в том, что для управляемой среды просто не реализована эта функциональность. Однако, хорошая новость заключается в том, что энтузиасты Pavel Bánský и Peter Nowak создали собственную обертку для неуправляемых интерфейсов и поместили ее в специальную библиотеку с открытым исходным кодом — Managed Services for Windows Mobile. Эта библиотека позволяет создавать фоновые службы для Windows Phone Classic на основе управляемого кода в рамках .NET Compact Framework.

Создание простой службы

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

Первое, что необходимо сделать – это создать новый проект и добавить ссылку на сборку ManagedService.dll. Поскольку приложение будет являться фоновой службой, то для него не нужен пользовательский интерфейс. Поэтому при создании проекта укажем, что необходимо создать консольное приложение.

После создания приложения следует создать новый класс и определить для него в качестве наследника класс ServiceApplicaiton (доступный в сборке ManagedService.dll). Этот класс, по сути, и будет являться фоновой службой. Необходимо чтобы в приложении был единственный экземпляр этого класса. Мы не будем использовать изощренные архитектурные конструкции и для простоты примера просто используем Singleton. Для этого определим закрытый конструктор и статическое поле для хранения созданного экземпляра этого класса.

public class BackgroundService : ServiceApplication
{
  private static readonly BackgroundService _service =
    new BackgroundService();

  public static BackgroundService Service
  { get { return _service; } }

  private BackgroundService()
  {

  }
}

Прежде чем наш сервис начнет работать следует выполнить еще несколько важных действий в конструкторе:

  • определить идентификатор сервиса (для того, чтобы операционная система могла отличать этот сервис от других сервисов);
  • определить имя сервиса;
  • зарегистрировать сервис (фактически, сохранить информацию о сервисе в реестре Windows).

Эти действия выполняются с помощью конструкций базового класса.

private BackgroundService()
{
  ServiceGuid = new Guid("EA6E9740-4698-11DF-88EE-BF5256D89593");
  ServiceName = "SmsLogger";
  RegisterService();
}

Последнее, что осталось сделать – это запускать данный сервис при запуске исполняемого файла. Для этого в методе Main следует получить экземпляр класса для этого сервиса и вызвать метод Start.

namespace SmsLogger
{
  class Program
  {
    static void Main(string[] args)
    {
      BackgroundService.Service.Start();
    }
  }
}

Этого достаточно для того, чтобы наш сервис запустился и начал работать в фоновом режиме.

Внешние события

После успешного создания сервиса нужно определить для него какую-то логику (зачем нужен сервис, который ничего не делает?). Для этого в конструкторе можно инициировать новый поток, который будет выполнять какие-либо действия, либо подписаться на уведомления от системных событий, происходящих в системе. Чтобы слегка упростить пример воспользуемся вторым подходом и подпишемся на системное событие, уведомляющее о том, что изменилось количество непрочтенных SMS. Для этого добавим ссылки на две стандартные сборки – Microsoft.WindowsMobile и Microsoft.WindowsMobile.Status, после чего будем использовать объект SystemState для получения системных уведомлений. Для обработки уведомления сохраним объект SystemState в закрытом поле, подпишемся на событие изменения и создадим обработчик события.

private SystemState state = 
  new SystemState(SystemProperty.MessagingSmsUnread);

private BackgroundService()
{
  // ...

  state.Changed += MessagingSmsUnreadStateChanged;
}

void MessagingSmsUnreadStateChanged(object sender, 
  ChangeEventArgs args)
{
  WriteLog(String.Format("Total unread sms is {0}", 
                          args.NewValue));
}

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

private static void WriteLog(string text)
{
  using (var fs = new FileStream(@"\smslog.txt", FileMode.Append))
  {
    using (var sw = new StreamWriter(fs))
    {
      sw.WriteLine("{0}: {1}", DateTime.Now.ToString(), text);
    }
  }
}

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

Автоматический запуск

После создания сервиса и определения логики следует указать операционной системе, что сервис необходимо запускать при старте приложения. По аналогии с настольной версией Windows, в Windows Phone автоматический запуск приложений осуществляется путем указания пути к приложению в соответствующей ветке реестра.

Для того, чтобы сервисом можно было управлять и отслеживать его состояние, в рамках проекта Managed Services for Windows Mobile было создано специальное приложение, которое управляет запуском сервисов. Идея приложения простая — при запуске оно само себя помещает в автозапуск. После этого оно сканирует ветку реестра HKEY_LOCAL_MACHINE\Software\ManagedServices и запускает все необходимые службы.

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

Таким образом, для запуска служб необходимо установить это приложение (ссылка ниже). Вы также сами можете прописать службы для автозагрузки минуя эту утилиту, для этого вам вручную придется создать секцию в разделе HKLM\init и контролировать её.

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

Управление состоянием службы

Аналогично службе Windows, службы для Windows Phone имеют несколько состояний:

  • запускается (Starting);
  • исполняется (Running);
  • приостановка (Suspending);
  • приостановлено (Suspended);
  • возобновление (Resuming);
  • завершение (Exiting);
  • завершено (Exited).

Узнать текущее состояние службы можно из свойства ServiceState базового класса службы ServiceApplication. Кроме того, в базовом классе присутствует два события, позволяющих отловить моменты изменения текущего состояния службы – OnServiceStateChanged и OnServiceExiting. Как следует из названия, первое событие генерируется всякий раз, когда изменяется текущее состояние сервиса, а второе – в момент завершения службы. Для примера давайте сохранять текущее состояние службы в лог при его изменении. Для этого в конструкторе подпишемся на соответствующее событие и создадим обработчик.

private BackgroundService()
{
  // ..

  OnServiceStateChanged += ServiceStateChanged;
}

void ServiceStateChanged(ServiceStateEventArgs serviceStateEventArgs)
{
  WriteLog(String.Format("Current stat of servce is {0}", 
    serviceStateEventArgs.ServiceState));
}

Для управления состоянием службы в базовом классе ServiceApplication существует набор методов:

  • Start
  • Suspend
  • Resume
  • Quit

Эти методы позволяют управлять работой службы. Кроме того, в базовом классе они определены как виртуальные. Это означает, что их можно переопределить непосредственно в классе службы и задать какое-то собственное поведение. Однако, после (или до) выполнения ваших действий, управление все-таки следует передать методу, определенному в базовом классе, иначе служба не запустится/не остановится/и т.д.

public override void Start()
{
  // do something

  base.Start();

  // do something
}

На самом деле вы можете изменять состояние свойства ServiceState самостоятельно в процессе работы службы. Однако, это уже делает базовый класс ServiceApplication, поэтому об этом можно не заботиться.

Пользовательский интерфейс

Практически каждая фоновая служба может (и должна) иметь приложение с пользовательским интерфейсом, которое будет управлять работой службы. Например, если фоновая служба ведет запись GPS-трека, то это приложение может управлять записью (начать, остановить), а также запускать или останавливать работу фоновой службы.

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

Получить список служб, установленных на устройстве можно путем считывания всех значений из ключа реестра HKEY_LOCAL_MACHINE\Software\ManagedServices. Для взаимодействия с фоновой службой потребуется знать только её идентификатор. Если приложение знает идентификатор службы, с которой будет работать (что бывает зачастую), то ему не потребуется считывать соответствующие ключи реестра.

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

public const int UM_QUIT_SERVICE = 6028;
public const int UM_RESUME_SERVICE = 6027;
public const int UM_START_SERVICE = 6025;
public const int UM_SUSPEND_SERVICE = 6026;
public const int WM_USER = 1024;

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

Для того, чтобы добавить обработчик для собственного события в конструкторе сервиса следует вызвать метод RegisterMessage и передать ему код сообщения. После этого следует подписаться на событие OnRegisteredMessage и определить логику обработки для этих сообщений.

Давайте определим сообщение, которое будет очищать лог файл. Для этого определим код соответствующего сообщения и зададим для него обработчик.

private const int UM_CLEAR = WM_USER + 5010;

private BackgroundService()
{
  // ..

  RegisterMessage(UM_CLEAR);

  OnRegisteredMessage += OnRegisteredMessageProcessing;
}

void OnRegisteredMessageProcessing(ref Message message)
{
  switch (message.Msg)
  {
    case UM_CLEAR:
      File.Delete(@"\smslog.txt");
      break;
  }
}

Теперь при поступлении сообщения с кодом 6034 (WM_USER + 5010) это сообщение будет обработано и файл будет удален.

Осталось разработать приложение с пользовательским интерфейсом, которое будет управлять фоновой службой. Для этого создадим новое приложение и разместим на нем кнопку Clear Log. Для отправки сообщений Windows создадим небольшой класс, позволяющий выполнить это действие. Я не буду приводить код этого класса, его вы можете увидеть в исходных кодах, приложенных к описанию. Обработчик кнопки для очистки лога будет выглядеть следующим образом.

private const int WM_USER = 1024;
private const int UM_CLEAR = WM_USER + 5010;

private void ClearLogButton_Click(object sender, EventArgs e)
{
  MessageHelper.SendMessage("ea6e9740469811df88eebf5256d89593", 
    UM_CLEAR);
}

Теперь при нажатии на кнопку (при запущенной службе) сообщение будет отправлено службе и файл с логами будет удален.

Итого

Таким образом, мы рассмотрели процесс создания фоновых сервисов для Windows Phone Classic в рамках .NET Compact Framework. Несмотря на то, что поддержка таких служб не поддерживается изначально в .NET Compact Framework, существуют обертки, которые позволяют с относительно небольшими усилиями такие службы создавать.