Взаимодействие с COM и вызовы P/Invoke

91

Одно из наиболее неожиданных средств приложений Silverlight с повышенной доверительностью — возможность создавать объекты COM и применять их для взаимодействия с библиотекой COM операционной системы Windows.

Немного истории. Модель COM (Component Object Model — объектная модель компонентов) в операционной системе Windows является стандартом интеграции приложений и повторного использования компонентов. Она была предшественницей .NET и до сих пор является ключевой частью текущих и будущих версий Windows. С ее помощью решаются многие задачи взаимодействия с приложениями Windows, включая Word и Excel.

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

До широкого распространения .NET разработчики часто создавали собственные компоненты COM для использования во многих приложениях. Однако поддержка COM в Silverlight не предполагает создания пользовательских компонентов COM. Технически это возможно, но развертывание компонентов COM приводит ко многим проблемам с конфигурациями и версиями, поэтому делать так не рекомендуется.

На самом деле поддержка COM предназначена для предоставления приложениям доступа к библиотекам Windows и предварительно установленных приложений (таких как Microsoft Office). Особенно это касается доступа к функциональности аппаратных компонентов, таких как сканеры, камеры и др.

В Silverlight точкой входа в средства поддержки COM служит класс AutomationFactory, находящийся в пространстве имен System.Runtime.InteropServices.Automation. Для поддержки COM в Silverlight должно быть выполнено несколько условий. Приложение должно выполняться вне браузера с повышенной доверительностью на компьютере Windows. Если необходимые условия удовлетворены, поддержка COM доступна и свойство AutomaitonFactory.IsAvailable возвращает значение true.

Ниже приведен метод, который проверяет наличие поддержки COM и отображает сообщение, если COM не поддерживается:

private bool TestForComSupport()
{
    if (!Application.Current.HasElevatedPermissions)
    {
        MessageBox.Show("COM не поддерживается, потому что " +
            "приложению не присвоена повышенная доверительность");
    }
    else if (!AutomationFactory.IsAvailable)
    {
        MessageBox.Show("COM не поддерживается, потому что " +
            "установлена операционная система, отличная от Windows");
    }
    else
    {
        return true;
    }

    return false;
}

Для создания объекта COM можно вызвать метод AutomationFactory.CreateObject() с полным именем типа:

dynamic speech = AutomationFactory.CreateObject("Sapi.SpVoice");

Для применения объекта COM необходимо ключевое слово dynamic языка C#, задающее использование позднего связывания. При создании экземпляра типа с ключевым словом dynamic определение типа не обязательно должно быть доступно во время компиляции. Соответственно, его наличие можно не проверять. Чтобы ключевое слово dynamic было доступным, нужно добавить ссылку на сборку Microsoft.CSharp.dll во все сборки, в которых используется COM.

При использовании позднего связывания необходимо определить переменную, указывающую на объект COM через тип Object. Обратите внимание на то, что все методы, поддерживаемые объектом COM, можно вызывать таким образом, будто это методы объекта типа Object. Ниже рассматривается пример, в котором код вызывает метод Speak() объекта Sapi.SpVoice инфраструктуры COM.

Поддержка позднего связывания в Silverlight обладает существенным недостатком: рабочая среда Visual Studio не предоставляет подсказки IntelliSense для объектов COM, в результате чего компилятор C# не перехватывает синтаксические ошибки при сборке приложения. Например, Visual Studio не заметит, что вы пытаетесь вызвать метод, не предоставляемый объектом COM. По этой причине рекомендуется всегда заключать код COM в блок перехвата исключений. Тогда вы легко идентифицируете ошибки, возникающие при попытке создать экземпляр COM несуществующего типа или сделать это способом, не поддерживаемым во время выполнения.

Ниже приведен код, в котором учтены все эти замечания. Сначала код проверяет, поддерживается ли COM. Затем создается компонент COM, преобразующий текст в звук с помощью средств, встроенных в Windows. В результате синтезатор произносит слова "This is the test":

if (TestForComSupport())
{
    try
    {
        using (dynamic speech = AutomationFactory.CreateObject("Sapi.SpVoice"))
        {
            speech.Volume = 100;
            speech.Speak("This is the test");
        }
    }
    catch (Exception ex)
    {
        // Исключение возникает, если библиотека COM не существует 
        // или в нее не входят используемые свойства и методы
        MessageBox.Show(ex.Message);
    }
}

Из данного примера видно, что использовать библиотеки COM на удивление легко. Реальные трудности возникают, когда нужно найти требуемый компонент, выяснить принцип работы объекта COM (в зависимости от приложения он может быть довольно сложным или специфичным) или обнаружить потенциальные ошибки в использовании компонента.

Ниже приведен пример, в котором используется другой компонент — Windows Script Host, который предоставляет функциональность для извлечения информации о системе и переменных среды, для работы с реестром и ярлыками рабочего стола. Код Windows Script Host в данном примере используется для запуска отдельного приложения (калькулятора Windows) с помощью метода Run():

using (dynamic shell = AutomationFactory.CreateObject("WScript.Shell"))
{
     shell.Run("calc.exe");
}

Средства взаимодействия с COM не ограничены встроенными компонентами Windows. Они работоспособны со многими другими хорошо известными программами, такими как приложения Microsoft Office (Word, Excel, PowerPoint, Outlook и т.д.). Все эти приложения имеют сложные объектные модели с десятками классов.

В качестве примера рассмотрим фрагмент кода, который запускает Word, вставляет два коротких абзаца текста и делает окно Word видимым для пользователя, после чего пользователь может продолжить работать с ним:

using (dynamic word = AutomationFactory.CreateObject("Word.Application"))
{
    dynamic document = word.Documents.Add();

    dynamic paragraph = document.Content.Paragraphs.Add;
    paragraph.Range.Text = "Заголовок 1";
    paragraph.Range.Font.Bold = true;
    paragraph.Format.SpaceAfter = 18;
    paragraph.Range.InsertParagraphAfter();

    paragraph = document.Content.Paragraphs.Add;
    paragraph.Range.Font.Bold = false;
    paragraph.Range.Text = "Новый абзац.";

    word.Visible = true;
}
Взаимодействие Silverlight с Word

Вызовы P/Invoke

Если средства поддержки COM не произвели на вас впечатления, обратите внимание на возможность использования системных вызовов P/Invoke для взаимодействия с неуправляемыми кодами библиотек на компьютерах Windows. Можно использовать P/Invoke для вызова функций Windows API или выполнения фрагментов кода унаследованных библиотек DLL (конечно, если они установлены на компьютере).

В Silvelight вызовы P/Invoke работают так же, как в приложениях .NET. Необходимо знать имя функции, которую нужно вызвать, а также имя DLL-файла, в котором она находится.

Предположим, нужно вызвать функцию ExitWindowsEx() из Windows API. Эта функция позволяет принудительно закрыть или повторно запустить операционную систему Windows, а также войти в систему в качестве пользователя. Для применения этой функции нужно объявить ее в коде с правильной сигнатурой:

static extern bool ExitWindowsEx (long uFlags, long dwReason);

Обратите внимание на то, что в объявление не включено тело метода, т.е. фактический код, выполняющийся при вызове функции. Объявление играет роль привязки к функции, находящейся вне приложения. Для установки связи необходим атрибут DllImport, находящийся в пространстве имен System.Runtime.Interop Services. При объявлении атрибута DllImport нужно задать имя DLL-файла, содержащего вызываемую функцию:

[DllImport("user32.dll")]
static extern bool ExitWindowsEx(long uFlags, long dwReason);

При задании файла в атрибуте DllImport можно использовать полностью квалифицированное имя (т.е. имя и маршрут файла). Если маршрут не задан, Windows будет искать файл сначала в текущей папке (в которой установлено приложение Silverlight), затем — в системной папке операционной системы Windows, затем — в папке Windows и, наконец, в папках, заданных переменной среды PATH.

При вызове функции Windows API, как в предыдущем примере, задавать папку не нужно, потому что соответствующая библиотека DLL всегда находится в системной папке Windows. В данном примере функция ExitWindowsEx() находится в файле user32.dll.

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

Многие значения, используемые в функциях Windows API, могут быть сведены к простым числам с помощью типа long, как показано ниже. Можно запомнить жестко закодированные числа и применять их в коде. Но есть лучший подход: определите числа как константы и перечисления. Например, функция ExitWindowsEx() поддерживает ряд значений аргумента uFlags, которые можно определить в перечислении:

[Flags]
public enum ExitWindows : uint
{
        // Выход пользователя из системы без перезагрузки компьютера
        LogOff = 0x00,

        // Завершение работы операционной системы
        ShutDown = 0x01,
        
        // Перезагрузка
        Reboot = 0x02,

        // Принудительное завершение работы операционной системы, 
        // даже если пользователь пытается отменить его
        Force = 0x04,
        ForceIfHung = 0x10,
}

Необходимо немного изменить объявление функции:

[DllImport("user32.dll")]
static extern bool ExitWindowsEx(ExitWindowsFlags uFlags, long dwReason);

Теперь при вызове функции ExitWindowsEx() можно использовать хорошо запоминающиеся значения перечисления:

ExitWindowsEx(ExitWindowsFlags.LogOff, 0);

Когда приложение выполняет эту инструкцию, Windows начинает немедленно закрывать все открытые программы. Некоторые программы могут попросить пользователя подтвердить что-нибудь, например сохранение изменений в документе. Щелчок на кнопке Отмена приведет к остановке процесса выхода из системы, потому что при вызове применен флажок Logoff, а не Force.

При использовании функции ExitWindowsEx() в реальных задачах нужно немного модифицировать данный пример. Параметр dwReason означает причину закрытия системы. Желательно присвоить ему что-либо отличное от нуля. Информацию о доступных значениях dwReason можно найти на сайте pinvoke.net, где есть каталог функций Windows API с кодами .NET, которые можно использовать для объявления функций в приложении. Коды написаны для стандартных приложений .NET, но большинство из них работоспособно и в Silverlight.

Пройди тесты
Лучший чат для C# программистов