Работа с устройствами печати в C# на примере реализации виртуального принтера

в 0:08, , рубрики: .net, alloc, allocate, allocation, api, C#, code, develop, developing, driver, error, escorp, exomode, extern, intptr, invoke, malloc, marshal, marshalling, memory, native, PInvoke, pointer, port, print, printing, programming, technologies, technology, virtual, WinAPI, больше, виртуальный, выделение, Драйвер, код, маршалинг, маршалирование, монитор, натив, нужно, ооп, ошибка, память, печать, принтер, Программирование, разработка под windows, тегов., указатель

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

Как и в прошлый раз, статья будет полезна для ознакомления разработчикам младшего и среднего звена. В процессе изучения материала, Вы узнаете как можно обращаться к низкоуровневым DLL WinAPI в C# с помощью P/Invoke, как установить, настроить и удалить из системы мониторы печати, драйвера принтера, само устройство печати, открыть и связать порт для перенаправления входных данных с устройства печати на монитор, познакомитесь с ключевыми моментами применения маршалирования. Так же мы на практическом примере разберёмся, как с помощью нашего API можно удобно манипулировать устройствами печати в системе, узнаем как можно перехватить обработанные данные после печати с принтера и, например, отправить их на сервер.

Постановка задачи

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

  • Возможность управлять полным циклом установки устройства печати в системе;
  • Возможность управлять полным циклом удаления устройства печати из системы;
  • Возможность конфигурировать устройство печати;
  • По возможности, оптимизировать повторное использование кода в проекте, а так же сделать структуру API максимально функциональной и удобной;
  • Максимально возможно повысить отказоустойчивость API, но при этом сохранить доступ к низкоуровневым исключениям Win32;
  • Обеспечить совместимость с системами, начиная с Windows XP и заканчивая Windows 10;
  • Возможность обработки данных печати в коде, использующем наш API.

В рамках статьи мы будем использовать уже реализованый итальянским разработчиком Lorenzo Monti монитор печати mfilemon.dll, а в качестве драйвера для принтера используем официальный драйвер Microsoft PostScript Printer Driver. По сути, монитор и драйвер могут быть любыми, в зависимости от требований вашей задачи.

Немного теории

Я не буду рассматривать в данной статье теоретическую базу по печати документов, различиям между принтерами с поддержкой PCL/PostScript и принтерами GDI и прочую базу. Всю необходимую информацию по данной тематике можно в избытке найти на просторах сети. Но кое-что знать всё же необходимо, чтобы лучше понимать как реализовать поставленные перед нами задачи.

Начнём с самого главного — процессом печати в Windows рулит служба Spooler. В директории C:/Windows/System32/ имеется её собственный низкоуровневый драйвер winspool.drv, в котором заложено множество точек входа для обращения к службе печати и выполнения целого ряда действий, от получения системной директории драйверов печати и имени установленного по умолчанию принтера в системе до манипуляций с очередью задач на печать. Для тех, кто хочет написать собственный монитор (о чём, возможно, я когда-нибудь так же расскажу в одной из будущих статей), помимо winspool.h ещё понадобится winsplp.h из DDK, представляющий дополнительный функционал для сборки драйвера в соответствии спецификации Spooler.

Далее, полнофункциональное устройство печати в Windows, говоря простым языком, состоит из монитора печати, открытого на мониторе порта, собственно устройства печати (принтера) и предварительно установленного драйвера к нему (рис. 1). На одном мониторе может быть открыто сразу несколько портов, к одному порту может быть привязано сразу несколько принтеров. Это важно учитывать при удалении того или иного компонента из системы, если, например, Вы захотите удалить определённый порт, предварительно нужно будет снести и все устройства печати, которые к нему привязаны.

Рисунок 1

P/Invoke

Platform Invocation Services (PInvoke) — платформа для обращения к точкам входа DLL (функциям), написанным в неуправляемом коде (C/C++ и WinAPI), из кода управляемого (C#/VB и .NET). Для того, чтобы обратиться к низкоуровневому коду из .NET, нам нужно описать External-методы из DLL, а так же структуры, которые в них используются. Рассмотрим каждый из сучаев по порядку на примерах.

Пример 1. Вызов метода GetPrinterDriverDirectory из драйвера winspool.drv.
Первым делом, нам нужно знать, что возвращает метод и что принимает в аргументах при вызове. Для этого лезем в документацию и читаем описание сигнатуры метода. Обратите внимание, в дальнейшем к документации низкоуровневого API нам придётся обращаться сплошь и рядом, далее по ходу статьи я не буду больше указывать о необходимости этого действия при реализации тех или иных методов/структур, т.к. они требуются по умолчанию.

BOOL GetPrinterDriverDirectory(
  _In_  LPTSTR  pName,
  _In_  LPTSTR  pEnvironment,
  _In_  DWORD   Level,
  _Out_ LPBYTE  pDriverDirectory,
  _In_  DWORD   cbBuf,
  _Out_ LPDWORD pcbNeeded
);

Описание каждого параметра функции так же можно найти в документации. Здесь важно понимать, что параметры могут быть только входными (In), только выходными (Out), как входными, так и выходными одновременно (In/Out), а так же опциональными (либо входными, либо выходными, в зависимости от других параметров). Так же нам нужно знать, какой тип данных в .NET нужно сопоставить WDT-типу (здесь, в большинстве случаев, работает правило «размеры выделяемой памяти значимым типам .NET соответствуют размерам выделяемой памяти базовым типам C++, для остальных есть IntPtr»).

Теперь, опираясь на полученные сведения, опишем метод в управляемом коде на C#:

[DllImport("winspool.drv", CharSet = CharSet.Auto, SetLastError = true)]
extern bool GetPrinterDriverDirectory(string serverName, string environment, uint level, [Out] StringBuilder driverDirectory, uint bufferSize, ref uint bytesNeeded);

Атрибут DllImportAttribute позволяет нам указать параметры обращения к низкоуровневой точке входа. В WinAPI большинство функций написаны с учётом двух основных кодировок — Unicode и ANSI, функции имеют окончания W и A соответственно. Если нам нужно обратиться к конкретному методу в кодировке, но при этом мы не хотим рефакторить основное имя описываемого метода, мы можем указать имя точки входа явно, передав его в соответствующий аргумент атрибута (например, GetPrinterDriverDirectoryW). Так же при этом не забываем указать аргумент CharSet = CharSet.Unicode (в нашем случае, кодировка определяется автоматически). По всем другим полезным атрибутам можно найти информацию в официальной документации.

Атрибут InAttribute в большинстве случаев можно опустить, т.к. аргументы в методах C# по умолчанию передаются по значению. Атрибут OutAttribute мы указываем в тех случаях, когда тип передаваемого аргумента — ссылочный, но данные должны быть выходными. Для выходных данных аргументов значимых типов мы указываем ref, т.е. передаём аргумент по ссылке.

Пример 2. Вызов метода AddMonitor из драйвера winspool.drv.
В данном примере используется структура данных MONITOR_INFO_2, которую нам нужно будет предварительно описать в коде C#.

typedef struct _MONITOR_INFO_2 {
  LPTSTR pName;
  LPTSTR pEnvironment;
  LPTSTR pDLLName;
} MONITOR_INFO_2, *PMONITOR_INFO_2;

BOOL AddMonitor(
  _In_ LPTSTR pName,
  _In_ DWORD  Level,
  _In_ LPBYTE pMonitors
);

Здесь важно понимать, что структуры, говоря «народным» языком, — это набор выделенных участков памяти определенного размера, хранящих значения. Каждый из таких участков представляет собой поле данных определённого типа. Ни о каких именах полей и их атрибутах речи не идёт, никаких метаданных, присущих изменяемым типам (классам), только размеры выделяемой памяти для хранения данных. Это означает, что имена членов структуры, как и само имя структуры, могут быть любыми, важно соблюсти соответствие выделяемых размеров памяти каждому члену. Для таких целей существует атрибут StructLayoutAttribute, позволяющий управлять физическим размещением полей данных класса или структуры в памяти. Есть множество способов управления размещением данных полей в памяти, можно явно задавать смещение полей, указывать абсолютный размер структуры, задавать кодировку для способа маршалирования, упаковку, размещать сегменты полей в порядке очерёдности друг за другом и т.д. Примеры реализаций этих способов можно найти здесь. Конкретно для нашей задачи отлично подойдёт последний способ, который мы указываем через LayoutKind.Sequential.

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
struct MonitorInfo
{
    [MarshalAs(UnmanagedType.LPTStr)] public string Name;
    [MarshalAs(UnmanagedType.LPTStr)] public string Environment;
    [MarshalAs(UnmanagedType.LPTStr)] public string DllName;
}

[DllImport("winspool.drv", CharSet = CharSet.Auto, SetLastError = true)]
static extern bool AddMonitor(string serverName, uint level, ref MonitorInfo monitor);

Как это работает: мы объявили структуру, указали в атрибуте размещение полей в памяти с помощью LayoutKind.Sequential, указали типы данных полей, для WinAPI типы данных в структурах — значимые, а значит их размер нам известен, в неуправляемом коде это sizeof(), в управляемом — Marshal.SizeOf(). Всё.

Маршалирование

Понятие «маршалирование» (оно же «маршалинг», оно же «Marshalling») описывает процесс преобразования данных, хранимых в памяти, из одного представления в другое. В случае с .NET в целом и P/Invoke в частности — это преобразование типов от неуправляемого кода к CLR. Маршалирование позволяет облегчить процесс работы с памятью в управляемом коде. Для этих целей предусмотрены два главных класса — Marshal и MarshalAsAttribute. Атрибут MarshalAsAttribute позволяет явно задать соответствие типов для преобразования из неуправляемого кода в управляемый (подобно мапированию при сериализации типов). Он может применяться только к полям типа, к параметрам метода с указанием уточнения через param:, а так же к возвращаемому значению через return:. Класс Marshal содержит множество полезных статических методов для работы с указателями, выделения памяти, определения размеров, смещения и прочего. Так же нам пригодится атрибут FlagsAttribute, позволяющий настроить преобразование низкоуровневых битовых флагов в enum'ы C#.

Архитектура будущего API

С теорией разобрались, пора продумать архитектуру нашего будущего API. Здесь нет какой-либо конкретной или оптимальной философии, каждый сам выбирает нужные паттерны проектирования кода в зависимости от условий решаемой задачи. Для нашего случая я решил поступить следующим образом: весь код будущей библиотеки будет состоять из двух основных модулей — «фабрики» классов и интерфейсов, реализующих эти классы. Публичная реализация будет давать возможность получить список всех установленных компонентов в системе, установить/удалить компонент и прочее. Внутренняя реализация будет работать с маршалированием и P/Invoke. Для специфических случаев мы сможем создавать экземпляры наших классов и вызывать их методы, для базовых случаев будем обращаться к нашей «фабрике». Визуально это всё можно представить примерно следующим образом (рис. 2):
Работа с устройствами печати в C# на примере реализации виртуального принтера - 2
Для решения задачи в рамках статьи нам понадобятся реализации IMonitor, IPort, IDriver и IPrinter, собственно сам класс фабрики PrintingApi, а так же вспомагательные флаги. Остальное пока что опустим.

Кодовая база

Первым делом, давайте напишем базовый интерфейс для всех наших компонентов печати:

/// <summary>
/// Представляет базовый интерфейс для реализации устройств, связанных с печатью (мониторы печати, порты принтеров, принтеры).
/// </summary>
public interface IPrintableDevice
{
    /// <summary>
    /// Наименование устройства печати.
    /// </summary>
    string Name { get; }

    /// <summary>
    /// Устанавливает устройство печати на удалённой машине.
    /// </summary>
    /// <param name="serverName">Имя сервера.</param>
    void Install(string serverName);

    /// <summary>
    /// Удаляет устройство печати на удалённой машине.
    /// </summary>
    /// <param name="serverName">Имя сервера.</param>
    void Uninstall(string serverName);
}

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

Теперь напишем основу для нашей фабрики:

class PrintingApi

/// <summary>
/// Представляет API для работы со службой печати.
/// </summary>
public class PrintingApi
{
    /// <summary>
    /// Вспомагательный делегат для оптимизации кода в P/Invoke.
    /// </summary>
    /// <param name="serverName">Имя сервера.</param>
    /// <param name="level">Уровень структуры данных.</param>
    /// <param name="structs">Указатель на структуру данных.</param>
    /// <param name="bufferSize">Размер буфера.</param>
    /// <param name="bytesNeeded">Число требуемых байт для выделения памяти.</param>
    /// <param name="bufferReturnedLength">Число выделенных байт памяти.</param>
    /// <returns></returns>
    internal delegate bool EnumInfo(string serverName, uint level, IntPtr structs, uint bufferSize, ref uint bytesNeeded, ref uint bufferReturnedLength);

    /// <summary>
    /// Вспомагательный делегат для оптимизации кода в P/Invoke.
    /// </summary>
    /// <param name="serverName">Имя сервера.</param>
    /// <param name="environment">Окружение.</param>
    /// <param name="level">Уровень структуры данных.</param>
    /// <param name="structs">Указатель на структуру данных.</param>
    /// <param name="bufferSize">Размер буфера.</param>
    /// <param name="bytesNeeded">Число требуемых байт для выделения памяти.</param>
    /// <param name="bufferReturnedLength">Число выделенных байт памяти.</param>
    /// <returns></returns>
    internal delegate bool EnumInfo2(string serverName, string environment, uint level, IntPtr structs, uint bufferSize, ref uint bytesNeeded, ref uint bufferReturnedLength);

    /// <summary>
    /// Фабрика классов для <see cref="PrintingApi"/>.
    /// </summary>
    public static PrintingApi Factory { get; protected set; }

    /// <summary>
    /// Статический инициализатор класса <see cref="PrintingApi"/>.
    /// </summary>
    static PrintingApi()
    {
        Factory = new PrintingApi();
    }

    /// <summary>
    /// Получает коллекцию нативных структур Spooler API. Вспомагательный метод для оптимизации кода в P/Invoke.
    /// </summary>
    /// <typeparam name="T">Тип структуры.</typeparam>
    /// <param name="handler">Делегат-обработчик нативного метода из Spooler API.</param>
    /// <param name="serverName">Имя сервера.</param>
    /// <param name="level">Уровень структуры.</param>
    /// <returns>Коллекция нативных структур Spooler API.</returns>
    internal static T[] GetInfo<T>(EnumInfo handler, string serverName, uint level) where T : struct
    {
        uint bytesNeeded = 0;
        uint bufferReturnedLength = 0;

        if (handler(serverName, level, IntPtr.Zero, 0, ref bytesNeeded, ref bufferReturnedLength)) return null;

        int lastWin32Error = Marshal.GetLastWin32Error();

        if (lastWin32Error != PrintingException.ErrorInsufficientBuffer) throw new PrintingException(lastWin32Error);

        IntPtr pointer = Marshal.AllocHGlobal((int)bytesNeeded);

        try
        {
            if (handler(serverName, level, pointer, bytesNeeded, ref bytesNeeded, ref bufferReturnedLength))
            {
                IntPtr currentPointer = pointer;
                T[] dataCollection = new T[bufferReturnedLength];
                Type type = typeof(T);

                for (int i = 0; i < bufferReturnedLength; i++)
                {
                    dataCollection[i] = (T)Marshal.PtrToStructure(currentPointer, type);
                    currentPointer = (IntPtr)(currentPointer.ToInt64() + Marshal.SizeOf(type));
                }

                return dataCollection;
            }

            throw new PrintingException(Marshal.GetLastWin32Error());
        }
        catch (Exception e)
        {
            throw new PrintingException(e.Message, e);
        }
        finally
        {
            Marshal.FreeHGlobal(pointer);
        }
    }

    /// <summary>
    /// Получает коллекцию нативных структур Spooler API. Вспомагательный метод для оптимизации кода в P/Invoke.
    /// </summary>
    /// <typeparam name="T">Тип структуры.</typeparam>
    /// <param name="handler">Делегат-обработчик нативного метода из Spooler API.</param>
    /// <param name="serverName">Имя сервера.</param>
    /// <returns>Коллекция нативных структур Spooler API.</returns>
    internal static T[] GetInfo<T>(EnumInfo handler, string serverName) where T : struct => GetInfo<T>(handler, serverName, 2);

    /// <summary>
    /// Получает коллекцию нативных структур Spooler API. Вспомагательный метод для оптимизации кода в P/Invoke.
    /// </summary>
    /// <typeparam name="T">Тип структуры.</typeparam>
    /// <param name="handler">Делегат-обработчик нативного метода из Spooler API.</param>
    /// <param name="level">Уровень структуры.</param>
    /// <returns>Коллекция нативных структур Spooler API.</returns>
    internal static T[] GetInfo<T>(EnumInfo handler, uint level) where T : struct => GetInfo<T>(handler, null, level);

    /// <summary>
    /// Получает коллекцию нативных структур Spooler API. Вспомагательный метод для оптимизации кода в P/Invoke.
    /// </summary>
    /// <typeparam name="T">Тип структуры.</typeparam>
    /// <param name="handler">Делегат-обработчик нативного метода из Spooler API.</param>
    /// <returns>Коллекция нативных структур Spooler API.</returns>
    internal static T[] GetInfo<T>(EnumInfo handler) where T : struct => GetInfo<T>(handler, null);

    /// <summary>
    /// Получает коллекцию нативных структур Spooler API. Вспомагательный метод для оптимизации кода в P/Invoke.
    /// </summary>
    /// <typeparam name="T">Тип структуры.</typeparam>
    /// <param name="handler">Делегат-обработчик нативного метода из Spooler API.</param>
    /// <param name="serverName">Имя сервера.</param>
    /// <param name="arg">Дополнительный аргумент делегата.</param>
    /// <param name="level">Уровень структуры.</param>
    /// <returns>Коллекция нативных структур Spooler API.</returns>
    internal static T[] GetInfo<T>(EnumInfo2 handler, string serverName, string arg, uint level) where T : struct
    {
        uint bytesNeeded = 0;
        uint bufferReturnedLength = 0;

        if (handler(serverName, arg, level, IntPtr.Zero, 0, ref bytesNeeded, ref bufferReturnedLength)) return null;

        int lastWin32Error = Marshal.GetLastWin32Error();

        if (lastWin32Error != PrintingException.ErrorInsufficientBuffer) throw new PrintingException(lastWin32Error);

        IntPtr pointer = Marshal.AllocHGlobal((int)bytesNeeded);

        try
        {
            if (handler(serverName, arg, level, pointer, bytesNeeded, ref bytesNeeded, ref bufferReturnedLength))
            {
                IntPtr currentPointer = pointer;
                T[] dataCollection = new T[bufferReturnedLength];
                Type type = typeof(T);

                for (int i = 0; i < bufferReturnedLength; i++)
                {
                    dataCollection[i] = (T)Marshal.PtrToStructure(currentPointer, type);
                    currentPointer = (IntPtr)(currentPointer.ToInt64() + Marshal.SizeOf(type));
                }

                return dataCollection;
            }

            throw new PrintingException(Marshal.GetLastWin32Error());
        }
        catch (Exception e)
        {
            throw new PrintingException(e.Message, e);
        }
        finally
        {
            Marshal.FreeHGlobal(pointer);
        }
    }

    /// <summary>
    /// Получает коллекцию нативных структур Spooler API. Вспомагательный метод для оптимизации кода в P/Invoke.
    /// </summary>
    /// <typeparam name="T">Тип структуры.</typeparam>
    /// <param name="handler">Делегат-обработчик нативного метода из Spooler API.</param>
    /// <param name="serverName">Имя сервера.</param>
    /// <param name="arg">Дополнительный аргумент делегата.</param>
    /// <returns>Коллекция нативных структур Spooler API.</returns>
    internal static T[] GetInfo<T>(EnumInfo2 handler, string serverName, string arg) where T : struct => GetInfo<T>(handler, serverName, arg, 2);

    /// <summary>
    /// Получает коллекцию нативных структур Spooler API. Вспомагательный метод для оптимизации кода в P/Invoke.
    /// </summary>
    /// <typeparam name="T">Тип структуры.</typeparam>
    /// <param name="handler">Делегат-обработчик нативного метода из Spooler API.</param>
    /// <param name="arg">Дополнительный аргумент делегата.</param>
    /// <param name="level">Уровень структуры.</param>
    /// <returns>Коллекция нативных структур Spooler API.</returns>
    internal static T[] GetInfo<T>(EnumInfo2 handler, string arg, uint level) where T : struct => GetInfo<T>(handler, null, arg, level);

    /// <summary>
    /// Получает коллекцию нативных структур Spooler API. Вспомагательный метод для оптимизации кода в P/Invoke.
    /// </summary>
    /// <typeparam name="T">Тип структуры.</typeparam>
    /// <param name="handler">Делегат-обработчик нативного метода из Spooler API.</param>
    /// <param name="arg">Дополнительный аргумент делегата.</param>
    /// <returns>Коллекция нативных структур Spooler API.</returns>
    internal static T[] GetInfo<T>(EnumInfo2 handler, string arg) where T : struct => GetInfo<T>(handler, null, arg);

    /// <summary>
    /// Получает коллекцию нативных структур Spooler API. Вспомагательный метод для оптимизации кода в P/Invoke.
    /// </summary>
    /// <typeparam name="T">Тип структуры.</typeparam>
    /// <param name="handler">Делегат-обработчик нативного метода из Spooler API.</param>
    /// <param name="level">Уровень структуры.</param>
    /// <returns>Коллекция нативных структур Spooler API.</returns>
    internal static T[] GetInfo<T>(EnumInfo2 handler, uint level) where T : struct => GetInfo<T>(handler, null, level);

    /// <summary>
    /// Получает коллекцию нативных структур Spooler API. Вспомагательный метод для оптимизации кода в P/Invoke.
    /// </summary>
    /// <typeparam name="T">Тип структуры.</typeparam>
    /// <param name="handler">Делегат-обработчик нативного метода из Spooler API.</param>
    /// <returns>Коллекция нативных структур Spooler API.</returns>
    internal static T[] GetInfo<T>(EnumInfo2 handler) where T : struct => GetInfo<T>(handler, null);

    /// <summary>
    /// Получает коллекцию нативных структур Spooler API и в случае успеха возвращает True. Вспомагательный метод для оптимизации кода в P/Invoke.
    /// </summary>
    /// <typeparam name="T">Тип структуры.</typeparam>
    /// <param name="handler">Делегат-обработчик нативного метода из Spooler API.</param>
    /// <param name="serverName">Имя сервера.</param>
    /// <param name="level">Уровень структуры.</param>
    /// <param name="dataCollection">Коллекция нативных структур Spooler API.</param>
    /// <param name="e">Исключение, возникшее во время операции.</param>
    /// <returns>True, если операция прошла успешно, иначе False.</returns>
    internal static bool TryGetInfo<T>(EnumInfo handler, string serverName, uint level, out T[] dataCollection, out PrintingException e) where T : struct
    {
        dataCollection = null;
        e = null;

        try
        {
            dataCollection = GetInfo<T>(handler, serverName, level);
            return true;
        }
        catch (PrintingException ex)
        {
            e = ex;
        }

        return false;
    }

    /// <summary>
    /// Получает коллекцию нативных структур Spooler API и в случае успеха возвращает True. Вспомагательный метод для оптимизации кода в P/Invoke.
    /// </summary>
    /// <typeparam name="T">Тип структуры.</typeparam>
    /// <param name="handler">Делегат-обработчик нативного метода из Spooler API.</param>
    /// <param name="serverName">Имя сервера.</param>
    /// <param name="level">Уровень структуры.</param>
    /// <param name="dataCollection">Коллекция нативных структур Spooler API.</param>
    /// <returns>True, если операция прошла успешно, иначе False.</returns>
    internal static bool TryGetInfo<T>(EnumInfo handler, string serverName, uint level, out T[] dataCollection) where T : struct
        => TryGetInfo(handler, serverName, level, out dataCollection, out PrintingException e);

    /// <summary>
    /// Получает коллекцию нативных структур Spooler API и в случае успеха возвращает True. Вспомагательный метод для оптимизации кода в P/Invoke.
    /// </summary>
    /// <typeparam name="T">Тип структуры.</typeparam>
    /// <param name="handler">Делегат-обработчик нативного метода из Spooler API.</param>
    /// <param name="serverName">Имя сервера.</param>
    /// <param name="dataCollection">Коллекция нативных структур Spooler API.</param>
    /// <param name="e">Исключение, возникшее во время операции.</param>
    /// <returns>True, если операция прошла успешно, иначе False.</returns>
    internal static bool TryGetInfo<T>(EnumInfo handler, string serverName, out T[] dataCollection, out PrintingException e) where T : struct
        => TryGetInfo(handler, serverName, 2, out dataCollection, out e);

    /// <summary>
    /// Получает коллекцию нативных структур Spooler API и в случае успеха возвращает True. Вспомагательный метод для оптимизации кода в P/Invoke.
    /// </summary>
    /// <typeparam name="T">Тип структуры.</typeparam>
    /// <param name="handler">Делегат-обработчик нативного метода из Spooler API.</param>
    /// <param name="serverName">Имя сервера.</param>
    /// <param name="dataCollection">Коллекция нативных структур Spooler API.</param>
    /// <returns>True, если операция прошла успешно, иначе False.</returns>
    internal static bool TryGetInfo<T>(EnumInfo handler, string serverName, out T[] dataCollection) where T : struct
        => TryGetInfo(handler, serverName, 2, out dataCollection, out PrintingException e);

    /// <summary>
    /// Получает коллекцию нативных структур Spooler API и в случае успеха возвращает True. Вспомагательный метод для оптимизации кода в P/Invoke.
    /// </summary>
    /// <typeparam name="T">Тип структуры.</typeparam>
    /// <param name="handler">Делегат-обработчик нативного метода из Spooler API.</param>
    /// <param name="level">Уровень структуры.</param>
    /// <param name="dataCollection">Коллекция нативных структур Spooler API.</param>
    /// <param name="e">Исключение, возникшее во время операции.</param>
    /// <returns>True, если операция прошла успешно, иначе False.</returns>
    internal static bool TryGetInfo<T>(EnumInfo handler, uint level, out T[] dataCollection, out PrintingException e) where T : struct
        => TryGetInfo(handler, null, level, out dataCollection, out e);

    /// <summary>
    /// Получает коллекцию нативных структур Spooler API и в случае успеха возвращает True. Вспомагательный метод для оптимизации кода в P/Invoke.
    /// </summary>
    /// <typeparam name="T">Тип структуры.</typeparam>
    /// <param name="handler">Делегат-обработчик нативного метода из Spooler API.</param>
    /// <param name="level">Уровень структуры.</param>
    /// <param name="dataCollection">Коллекция нативных структур Spooler API.</param>
    /// <returns>True, если операция прошла успешно, иначе False.</returns>
    internal static bool TryGetInfo<T>(EnumInfo handler, uint level, out T[] dataCollection) where T : struct
        => TryGetInfo(handler, null, level, out dataCollection, out PrintingException e);

    /// <summary>
    /// Получает коллекцию нативных структур Spooler API и в случае успеха возвращает True. Вспомагательный метод для оптимизации кода в P/Invoke.
    /// </summary>
    /// <typeparam name="T">Тип структуры.</typeparam>
    /// <param name="handler">Делегат-обработчик нативного метода из Spooler API.</param>
    /// <param name="dataCollection">Коллекция нативных структур Spooler API.</param>
    /// <param name="e">Исключение, возникшее во время операции.</param>
    /// <returns>True, если операция прошла успешно, иначе False.</returns>
    internal static bool TryGetInfo<T>(EnumInfo handler, out T[] dataCollection, out PrintingException e) where T : struct
        => TryGetInfo(handler, null, out dataCollection, out e);

    /// <summary>
    /// Получает коллекцию нативных структур Spooler API и в случае успеха возвращает True. Вспомагательный метод для оптимизации кода в P/Invoke.
    /// </summary>
    /// <typeparam name="T">Тип структуры.</typeparam>
    /// <param name="handler">Делегат-обработчик нативного метода из Spooler API.</param>
    /// <param name="dataCollection">Коллекция нативных структур Spooler API.</param>
    /// <returns>True, если операция прошла успешно, иначе False.</returns>
    internal static bool TryGetInfo<T>(EnumInfo handler, out T[] dataCollection) where T : struct
        => TryGetInfo(handler, null, out dataCollection, out PrintingException e);

    /// <summary>
    /// Получает коллекцию нативных структур Spooler API и в случае успеха возвращает True. Вспомагательный метод для оптимизации кода в P/Invoke.
    /// </summary>
    /// <typeparam name="T">Тип структуры.</typeparam>
    /// <param name="handler">Делегат-обработчик нативного метода из Spooler API.</param>
    /// <param name="serverName">Имя сервера.</param>
    /// <param name="arg">Дополнительный аргумент делегата.</param>
    /// <param name="level">Уровень структуры.</param>
    /// <param name="dataCollection">Коллекция нативных структур Spooler API.</param>
    /// <param name="e">Исключение, возникшее во время операции.</param>
    /// <returns>True, если операция прошла успешно, иначе False.</returns>
    internal static bool TryGetInfo<T>(EnumInfo2 handler, string serverName, string arg, uint level, out T[] dataCollection, out PrintingException e) where T : struct
    {
        dataCollection = null;
        e = null;

        try
        {
            dataCollection = GetInfo<T>(handler, serverName, arg, level);
            return true;
        }
        catch (PrintingException ex)
        {
            e = ex;
        }

        return false;
    }

    /// <summary>
    /// Получает коллекцию нативных структур Spooler API и в случае успеха возвращает True. Вспомагательный метод для оптимизации кода в P/Invoke.
    /// </summary>
    /// <typeparam name="T">Тип структуры.</typeparam>
    /// <param name="handler">Делегат-обработчик нативного метода из Spooler API.</param>
    /// <param name="serverName">Имя сервера.</param>
    /// <param name="arg">Дополнительный аргумент делегата.</param>
    /// <param name="level">Уровень структуры.</param>
    /// <param name="dataCollection">Коллекция нативных структур Spooler API.</param>
    /// <returns>True, если операция прошла успешно, иначе False.</returns>
    internal static bool TryGetInfo<T>(EnumInfo2 handler, string serverName, string arg, uint level, out T[] dataCollection) where T : struct
        => TryGetInfo(handler, serverName, arg, level, out dataCollection, out PrintingException e);

    /// <summary>
    /// Получает коллекцию нативных структур Spooler API и в случае успеха возвращает True. Вспомагательный метод для оптимизации кода в P/Invoke.
    /// </summary>
    /// <typeparam name="T">Тип структуры.</typeparam>
    /// <param name="handler">Делегат-обработчик нативного метода из Spooler API.</param>
    /// <param name="serverName">Имя сервера.</param>
    /// <param name="arg">Дополнительный аргумент делегата.</param>
    /// <param name="dataCollection">Коллекция нативных структур Spooler API.</param>
    /// <param name="e">Исключение, возникшее во время операции.</param>
    /// <returns>True, если операция прошла успешно, иначе False.</returns>
    internal static bool TryGetInfo<T>(EnumInfo2 handler, string serverName, string arg, out T[] dataCollection, out PrintingException e) where T : struct
        => TryGetInfo(handler, serverName, arg, 2, out dataCollection, out e);

    /// <summary>
    /// Получает коллекцию нативных структур Spooler API и в случае успеха возвращает True. Вспомагательный метод для оптимизации кода в P/Invoke.
    /// </summary>
    /// <typeparam name="T">Тип структуры.</typeparam>
    /// <param name="handler">Делегат-обработчик нативного метода из Spooler API.</param>
    /// <param name="serverName">Имя сервера.</param>
    /// <param name="arg">Дополнительный аргумент делегата.</param>
    /// <param name="dataCollection">Коллекция нативных структур Spooler API.</param>
    /// <returns>True, если операция прошла успешно, иначе False.</returns>
    internal static bool TryGetInfo<T>(EnumInfo2 handler, string serverName, string arg, out T[] dataCollection) where T : struct
        => TryGetInfo(handler, serverName, arg, 2, out dataCollection, out PrintingException e);

    /// <summary>
    /// Получает коллекцию нативных структур Spooler API и в случае успеха возвращает True. Вспомагательный метод для оптимизации кода в P/Invoke.
    /// </summary>
    /// <typeparam name="T">Тип структуры.</typeparam>
    /// <param name="handler">Делегат-обработчик нативного метода из Spooler API.</param>
    /// <param name="arg">Дополнительный аргумент делегата.</param>
    /// <param name="level">Уровень структуры.</param>
    /// <param name="dataCollection">Коллекция нативных структур Spooler API.</param>
    /// <param name="e">Исключение, возникшее во время операции.</param>
    /// <returns>True, если операция прошла успешно, иначе False.</returns>
    internal static bool TryGetInfo<T>(EnumInfo2 handler, string arg, uint level, out T[] dataCollection, out PrintingException e) where T : struct
        => TryGetInfo(handler, null, arg, level, out dataCollection, out e);

    /// <summary>
    /// Получает коллекцию нативных структур Spooler API и в случае успеха возвращает True. Вспомагательный метод для оптимизации кода в P/Invoke.
    /// </summary>
    /// <typeparam name="T">Тип структуры.</typeparam>
    /// <param name="handler">Делегат-обработчик нативного метода из Spooler API.</param>
    /// <param name="arg">Дополнительный аргумент делегата.</param>
    /// <param name="level">Уровень структуры.</param>
    /// <param name="dataCollection">Коллекция нативных структур Spooler API.</param>
    /// <returns>True, если операция прошла успешно, иначе False.</returns>
    internal static bool TryGetInfo<T>(EnumInfo2 handler, string arg, uint level, out T[] dataCollection) where T : struct
        => TryGetInfo(handler, null, arg, level, out dataCollection, out PrintingException e);

    /// <summary>
    /// Получает коллекцию нативных структур Spooler API и в случае успеха возвращает True. Вспомагательный метод для оптимизации кода в P/Invoke.
    /// </summary>
    /// <typeparam name="T">Тип структуры.</typeparam>
    /// <param name="handler">Делегат-обработчик нативного метода из Spooler API.</param>
    /// <param name="arg">Дополнительный аргумент делегата.</param>
    /// <param name="dataCollection">Коллекция нативных структур Spooler API.</param>
    /// <param name="e">Исключение, возникшее во время операции.</param>
    /// <returns>True, если операция прошла успешно, иначе False.</returns>
    internal static bool TryGetInfo<T>(EnumInfo2 handler, string arg, out T[] dataCollection, out PrintingException e) where T : struct
        => TryGetInfo(handler, null, arg, out dataCollection, out e);

    /// <summary>
    /// Получает коллекцию нативных структур Spooler API и в случае успеха возвращает True. Вспомагательный метод для оптимизации кода в P/Invoke.
    /// </summary>
    /// <typeparam name="T">Тип структуры.</typeparam>
    /// <param name="handler">Делегат-обработчик нативного метода из Spooler API.</param>
    /// <param name="arg">Дополнительный аргумент делегата.</param>
    /// <param name="dataCollection">Коллекция нативных структур Spooler API.</param>
    /// <returns>True, если операция прошла успешно, иначе False.</returns>
    internal static bool TryGetInfo<T>(EnumInfo2 handler, string arg, out T[] dataCollection) where T : struct
        => TryGetInfo(handler, null, arg, out dataCollection, out PrintingException e);

    /// <summary>
    /// Получает коллекцию нативных структур Spooler API и в случае успеха возвращает True. Вспомагательный метод для оптимизации кода в P/Invoke.
    /// </summary>
    /// <typeparam name="T">Тип структуры.</typeparam>
    /// <param name="handler">Делегат-обработчик нативного метода из Spooler API.</param>
    /// <param name="level">Уровень структуры.</param>
    /// <param name="dataCollection">Коллекция нативных структур Spooler API.</param>
    /// <param name="e">Исключение, возникшее во время операции.</param>
    /// <returns>True, если операция прошла успешно, иначе False.</returns>
    internal static bool TryGetInfo<T>(EnumInfo2 handler, uint level, out T[] dataCollection, out PrintingException e) where T : struct
        => TryGetInfo(handler, null, level, out dataCollection, out e);
        
    /// <summary>
    /// Получает коллекцию нативных структур Spooler API и в случае успеха возвращает True. Вспомагательный метод для оптимизации кода в P/Invoke.
    /// </summary>
    /// <typeparam name="T">Тип структуры.</typeparam>
    /// <param name="handler">Делегат-обработчик нативного метода из Spooler API.</param>
    /// <param name="level">Уровень структуры.</param>
    /// <param name="dataCollection">Коллекция нативных структур Spooler API.</param>
    /// <returns>True, если операция прошла успешно, иначе False.</returns>
    internal static bool TryGetInfo<T>(EnumInfo2 handler, uint level, out T[] dataCollection) where T : struct
        => TryGetInfo(handler, null, level, out dataCollection, out PrintingException e);

    /// <summary>
    /// Получает коллекцию нативных структур Spooler API и в случае успеха возвращает True. Вспомагательный метод для оптимизации кода в P/Invoke.
    /// </summary>
    /// <typeparam name="T">Тип структуры.</typeparam>
    /// <param name="handler">Делегат-обработчик нативного метода из Spooler API.</param>
    /// <param name="dataCollection">Коллекция нативных структур Spooler API.</param>
    /// <param name="e">Исключение, возникшее во время операции.</param>
    /// <returns>True, если операция прошла успешно, иначе False.</returns>
    internal static bool TryGetInfo<T>(EnumInfo2 handler, out T[] dataCollection, out PrintingException e) where T : struct
        => TryGetInfo(handler, null, out dataCollection, out e);

    /// <summary>
    /// Получает коллекцию нативных структур Spooler API и в случае успеха возвращает True. Вспомагательный метод для оптимизации кода в P/Invoke.
    /// </summary>
    /// <typeparam name="T">Тип структуры.</typeparam>
    /// <param name="handler">Делегат-обработчик нативного метода из Spooler API.</param>
    /// <param name="dataCollection">Коллекция нативных структур Spooler API.</param>
    /// <returns>True, если операция прошла успешно, иначе False.</returns>
    internal static bool TryGetInfo<T>(EnumInfo2 handler, out T[] dataCollection) where T : struct
        => TryGetInfo(handler, null, out dataCollection, out PrintingException e);
}

Думаю, из документированных комментариев понятно что здесь к чему. Синглтоном создаём новый статический экземпляр класса в статическом конструкторе, описываем два делегата EnumInfo и EnumInfo2 для вызова нативных методов получения данных в наших будущих классах, описываем методы-хэлперы над нативными методами.

В большинстве случаев, весь процесс работы с нативными методами будет сводиться к следующей последовательности действий:

  • Вызвать метод в первый раз, чтобы он вернул false и код нативной ошибки 122 («неинициализированный буфер»). В аргументах при этом мы указываем дефолтные значения аргументов (нулевые) для обработки буфера. Это нужно для того, чтобы метод вернул нам изменённые значения инициализации буфера и передал их в наши переменные по ссылке, после того как метод отработает, у нас будет необходимая информация для вызова метода с уже корректными значениями аргументов для инициализации буфера;
  • Дальше нам нужно получить указатель на наш буфер. Делается это с помощью метода Marshal.AllocHGlobal(), который выделит память в соответствии указанному нами размеру байт (которые мы уже получили ранее по ссылке при первом вызове нативного метода) и вернёт нам экземпляр IntPtr;
  • Теперь можно обрабатывать наш буфер. Либо преобразуем указатель на буфер в структуру с помощью Marshal.PtrToStructure(), либо делаем обратное действие с помощью Marshal.StructureToPtr и передаём указатель на структуру в метод, в зависимости от задачи и сигнатуры метода;
  • Не забываем за перехват возможных ошибок Win32 с помощью Marshal.GetLastWin32Error(), а так же освободить память, выделенную ранее для нашего буфера, с помощью Marshal.FreeHGlobal().

Для работы с буферами char** (массивы строк) я рекомендую использовать StringBuilder. У него есть уже готовые перегрузки, работающие с указателями, а так же реализована вся необходимая функциональность по маршалированию.

Для перехвата и генерации исключений в нашем API предусмотрим отдельный класс:

class PrintingException

/// <summary>
/// Представляет ошибку менеджера печати.
/// </summary>
[Serializable]
public class PrintingException : Win32Exception
{
    #region Error Codes
    /// <summary>
    /// Код ошибки "Файл не найден".
    /// </summary>
    public const int ErrorFileNotFound = 2;

    /// <summary>
    /// Код ошибки "Неинициализированный буфер".
    /// </summary>
    public const int ErrorInsufficientBuffer = 122;

    /// <summary>
    /// Код ошибки "Модуль не найден".
    /// </summary>
    public const int ErrorModuleNotFound = 126;

    /// <summary>
    /// Код ошибки "Имя принтера задано неверно".
    /// </summary>
    public const int ErrorInvalidPrinterName = 1801;

    /// <summary>
    /// Код ошибки "Указан неизвестный монитор печати".
    /// </summary>
    public const int ErrorMonitorUnknown = 3000;

    /// <summary>
    /// Код ошибки "Указанный драйвер принтера занят".
    /// </summary>
    public const int ErrorPrinterDriverIsReadyUsed = 3001;

    /// <summary>
    /// Код ошибки "Не найден файл диспетчера очереди".
    /// </summary>
    public const int ErrorPrinterJobFileNotFound = 3002;

    /// <summary>
    /// Код ошибки "Не был произведен вызов StartDocPrinter".
    /// </summary>
    public const int ErrorStartDocPrinterNotCalling = 3003;

    /// <summary>
    /// Код ошибки "Не был произведен вызов AddJob".
    /// </summary>
    public const int ErrorAddJobNotCalling = 3004;

    /// <summary>
    /// Код ошибки "Указанный процессор печати уже установлен".
    /// </summary>
    public const int ErrorPrinterProcessorAlreadyInstalled = 3005;

    /// <summary>
    /// Код ошибки "Указанный монитор печати уже установлен".
    /// </summary>
    public const int ErrorMonitorAlreadyInstalled = 3006;

    /// <summary>
    /// Код ошибки "Указанный монитор печати не имеет требуемых функций".
    /// </summary>
    public const int ErrorInvalidMonitor = 3007;

    /// <summary>
    /// Код ошибки "Указанный монитор печати сейчас уже используется".
    /// </summary>
    public const int ErrorMonitorIsReadyUsed = 3008;
    #endregion

    /// <summary>
    /// Инициализирует новый экземпляр класса <see cref="PrintingException"/>.
    /// </summary>
    public PrintingException() : base() { }

    /// <summary>
    /// Инициализирует новый экземпляр класса <see cref="PrintingException"/>.
    /// </summary>
    /// <param name="nativeErrorCode">Код ошибки Win32.</param>
    public PrintingException(int nativeErrorCode) : base(nativeErrorCode) { }

    /// <summary>
    /// Инициализирует новый экземпляр класса <see cref="PrintingException"/>.
    /// </summary>
    /// <param name="message">Сообщение об ошибке.</param>
    public PrintingException(string message) : base(message) { }

    /// <summary>
    /// Инициализирует новый экземпляр класса <see cref="PrintingException"/>.
    /// </summary>
    /// <param name="nativeErrorCode">Код ошибки Win32.</param>
    /// <param name="message">Сообщение об ошибке.</param>
    public PrintingException(int nativeErrorCode, string message) : base(nativeErrorCode, message) { }

    /// <summary>
    /// Инициализирует новый экземпляр класса <see cref="PrintingException"/>.
    /// </summary>
    /// <param name="message">Сообщение об ошибке.</param>
    /// <param name="innerException"></param>
    public PrintingException(string message, Exception innerException) : base(message, innerException) { }

    /// <summary>
    /// Инициализирует новый экземпляр класса <see cref="PrintingException"/>.
    /// </summary>
    /// <param name="info">Данные для сериализации.</param>
    /// <param name="context">Контекст потока сериализации.</param>
    public PrintingException(SerializationInfo info, StreamingContext context) : base(info, context) { }
}

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

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

/// <summary>
/// Окружение системы.
/// </summary>
public enum Environment
{
    /// <summary>
    /// Текущее окружение системы.
    /// </summary>
    Current,
    /// <summary>
    /// Windows NT x86.
    /// </summary>
    X86,
    /// <summary>
    /// Windows x64.
    /// </summary>
    X64,
    /// <summary>
    /// Windows IA64.
    /// </summary>
    IA64,
}

/// <summary>
/// Тип порта для монитора печати.
/// </summary>
[Flags]
public enum PortType
{
    /// <summary>
    /// Запись данных.
    /// </summary>
    Write = 0x1,
    /// <summary>
    /// Чтение данных.
    /// </summary>
    Read = 0x2,
    /// <summary>
    /// Перенаправление данных.
    /// </summary>
    Redirected = 0x4,
    /// <summary>
    /// Отправка данных на сервер.
    /// </summary>
    NetAttached = 0x8,
}

/// <summary>
/// Протокол данных печати.
/// </summary>
public enum DataType : uint
{
    RAW = 1,
    LPR = 2,
}

Для преобразования Environment в строку и наоборот, реализуем два метода расширения:

/// <summary>
/// Представляет статический класс расширений типов.
/// </summary>
public static class PrintingExtensions
{
    /// <summary>
    /// Возвращает имя окружение системы, совместимое с WinAPI.
    /// </summary>
    /// <param name="environment">Окружение системы.</param>
    /// <returns>Строковое представление имени окружения системы.</returns>
    internal static string GetEnvironmentName(this Environment environment)
    {
        switch (environment)
        {
            default: return null;
            case Environment.X86: return "Windows x86";
            case Environment.X64: return "Windows x64";
            case Environment.IA64: return "Windows IA64";
        }
    }

    /// <summary>
    /// Возвращает <see cref="Environment"/>, эквивалентный входной строке имени окружения.
    /// </summary>
    /// <param name="environmentString">Входная строка имени окружения.</param>
    /// <returns><see cref="Environment"/>, эквивалентный входной строке имени окружения.</returns>
    internal static Environment GetEnvironment(this string environmentString)
    {
        environmentString = environmentString.ToLower();

        if (environmentString.Contains("x86")) return Environment.X86;
        if (environmentString.Contains("x64")) return Environment.X64;
        if (environmentString.Contains("ia64")) return Environment.IA64;

        return Environment.Current;
    }
}

Вместо двух методов расширения и enum Environment можно обойтись обычными строковыми константами, я пошёл таким путём просто потому, что хотел изначально запретить передавать в имени окружения невалидную отсебятину, но сохранить при этом возможность выбирать из заранее заданного ограниченного набора строк.

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

abstract class PrintableDevice

/// <summary>
/// Представляет базовый класс для всех компонентов устройства печати.
/// </summary>
public abstract class PrintableDevice : IPrintableDevice
{
    /// <summary>
    /// Наименование компонента устройства печати.
    /// </summary>
    public virtual string Name { get; protected set; }

    /// <summary>
    /// Инициализирует новый экземпляр класса <see cref="PrintableDevice"/>.
    /// </summary>
    /// <param name="name"></param>
    /// <exception cref="ArgumentNullException"/>
    public PrintableDevice(string name)
    {
        if (string.IsNullOrEmpty(name)) throw new ArgumentNullException("name");

        Name = name;
    }

    /// <summary>
    /// Устанавливает компонента устройства печати на удалённой машине.
    /// </summary>
    /// <param name="serverName">Имя сервера.</param>
    /// <exception cref="FileNotFoundException" />
    /// <exception cref="PrintingException" />
    public abstract void Install(string serverName);

    /// <summary>
    /// Устанавливает компонента устройства печати на локальной машине.
    /// </summary>
    /// <exception cref="FileNotFoundException" />
    /// <exception cref="PrintingException" />
    public void Install() => Install(null);

    /// <summary>
    /// Устанавливает компонента устройства печати на удалённой машине.
    /// </summary>
    /// <param name="serverName">Имя сервера.</param>
    /// <param name="e">Исключение, возникшее в процессе установки.</param>
    /// <returns>True, если процедура установки прошла успешно, иначе False.</returns>
    public bool TryInstall(string serverName, out PrintingException e)
    {
        e = null;

        try
        {
            Install(serverName);
        }
        catch (PrintingException ex)
        {
            e = ex;
            return false;
        }

        return true;
    }

    /// <summary>
    /// Устанавливает компонента устройства печати на удалённой машине.
    /// </summary>
    /// <param name="serverName">Имя сервера.</param>
    /// <returns>True, если процедура установки прошла успешно, иначе False.</returns>
    public bool TryInstall(string serverName) => TryInstall(serverName, out PrintingException e);

    /// <summary>
    /// Устанавливает компонента устройства печати на локальной машине.
    /// </summary>
    /// <param name="e">Исключение, возникшее в процессе установки.</param>
    /// <returns>True, если процедура установки прошла успешно, иначе False.</returns>
    public bool TryInstall(out PrintingException e) => TryInstall(null, out e);

    /// <summary>
    /// Устанавливает компонента устройства печати на локальной машине.
    /// </summary>
    /// <returns>True, если процедура установки прошла успешно, иначе False.</returns>
    public bool TryInstall() => TryInstall(out PrintingException e);

    /// <summary>
    /// Удалает компонента устройства печати на удалённой машине.
    /// </summary>
    /// <param name="serverName">Имя сервера.</param>
    /// <exception cref="PrintingException" />
    public abstract void Uninstall(string serverName);

    /// <summary>
    /// Удаляет компонента устройства печати на локальной машине.
    /// </summary>
    /// <exception cref="PrintingException" />
    public void Uninstall() => Uninstall(null);

    /// <summary>
    /// Удалает компонента устройства печати на удалённой машине.
    /// </summary>
    /// <param name="serverName">Имя сервера.</param>
    /// <param name="e">Исключение, возникшее в процессе удаления.</param>
    /// <returns>True, если процедура удаления прошла успешно, иначе False.</returns>
    public bool TryUninstall(string serverName, out PrintingException e)
    {
        e = null;

        try
        {
            Uninstall(serverName);
        }
        catch (PrintingException ex)
        {
            e = ex;
            return false;
        }

        return true;
    }

    /// <summary>
    /// Удалает компонента устройства печати на удалённой машине.
    /// </summary>
    /// <param name="serverName">Имя сервера.</param>
    /// <returns>True, если процедура удаления прошла успешно, иначе False.</returns>
    public bool TryUninstall(string serverName) => TryUninstall(serverName, out PrintingException e);

    /// <summary>
    /// Удалает компонента устройства печати на локальной машине.
    /// </summary>
    /// <param name="e">Исключение, возникшее в процессе удаления.</param>
    /// <returns>True, если процедура удаления прошла успешно, иначе False.</returns>
    public bool TryUninstall(out PrintingException e) => TryUninstall(null, out e);

    /// <summary>
    /// Удалает компонента устройства печати на локальной машине.
    /// </summary>
    /// <returns>True, если процедура удаления прошла успешно, иначе False.</returns>
    public bool TryUninstall() => TryUninstall(out PrintingException e);
}

Монитор печати

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

struct MonitorInfo

/// <summary>
/// Представляет структуру для хранения информации о мониторе принтера.
/// </summary>
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
internal struct MonitorInfo
{
    /// <summary>
    /// Наименование монитора.
    /// </summary>
    [MarshalAs(UnmanagedType.LPTStr)]
    public string Name;

    /// <summary>
    /// Окружение, для которого был написан драйвер (например, Windows NT x86, Windows IA64 или Windows x64).
    /// </summary>
    [MarshalAs(UnmanagedType.LPTStr)]
    public string Environment;

    /// <summary>
    /// Имя файла *.dll монитора.
    /// </summary>
    [MarshalAs(UnmanagedType.LPTStr)]
    public string DllName;
}

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

interface IMonitor

/// <summary>
/// Представляет базовый интерфейс для реализации мониторов печати.
/// </summary>
public interface IMonitor : IPrintableDevice
{
    /// <summary>
    /// Окружение, для которого был написан драйвер (например, Windows NT x86, Windows IA64 или Windows x64).
    /// </summary>
    Environment Environment { get; }

    /// <summary>
    /// Имя файла *.dll монитора.
    /// </summary>
    string Dll { get; }
}

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

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

class Monitor

/// <summary>
/// Представляет монитор печати для открытия портов.
/// </summary>
public class Monitor : PrintableDevice, IMonitor
{
    /// <summary>
    /// Окружение, для которого был написан драйвер (например, Windows NT x86, Windows IA64 или Windows x64).
    /// </summary>
    public virtual Environment Environment { get; protected set; }

    /// <summary>
    /// Имя файла *.dll монитора.
    /// </summary>
    public virtual string Dll { get; protected set; }

    /// <summary>
    /// Возвращает список всех установленных в системе мониторов печати.
    /// </summary>
    public static Monitor[] All
    {
        get
        {
            if (!PrintingApi.TryGetInfo(EnumMonitors, out MonitorInfo[] monitorInfo)) return null;

            Monitor[] monitors = new Monitor[monitorInfo.Length];

            for (int i = 0; i < monitorInfo.Length; i++)
                monitors[i] = new Monitor(monitorInfo[i].Name, monitorInfo[i].DllName, monitorInfo[i].Environment.GetEnvironment());

            return monitors;
        }
    }

    /// <summary>
    /// Инициализирует новый экземпляр класса <see cref="Monitor"/>.
    /// </summary>
    /// <param name="name">Наименование монитора печати.</param>
    /// <param name="dll">Имя файла *.dll монитора.</param>
    /// <param name="environment">Окружение, для которого был написан драйвер (например, Windows NT x86, Windows IA64 или Windows x64).</param>
    /// <exception cref="ArgumentNullException" />
    public Monitor(string name, string dll, Environment environment) : base(name)
    {
        if (string.IsNullOrEmpty(dll)) throw new ArgumentNullException("dll");

        Environment = environment;
        Dll = dll;
    }

    /// <summary>
    /// Инициализирует новый экземпляр класса <see cref="Monitor"/>.
    /// </summary>
    /// <param name="name">Наименование монитора печати.</param>
    /// <param name="dll">Имя файла *.dll монитора.</param>
    /// <exception cref="ArgumentNullException" />
    public Monitor(string name, string dll) : this(name, dll, Environment.Current) { }

    /// <summary>
    /// Устанавливает монитор печати на удалённой машине.
    /// </summary>
    /// <param name="serverName">Имя сервера.</param>
    /// <exception cref="FileNotFoundException"/>
    /// <exception cref="PrintingException"/>
    public override void Install(string serverName)
    {
        try
        {
            if (!File.Exists(Dll)) throw new FileNotFoundException("Не удалось найти файл монитора печати", Dll);

            string dllName = Path.GetFileName(Dll);
            string dllPath = Path.Combine(System.Environment.SystemDirectory, dllName);

            File.Copy(Dll, dllPath, true);

            MonitorInfo monitorInfo = new MonitorInfo
            {
                Name = Name,
                Environment = Environment.GetEnvironmentName(),
                DllName = File.Exists(dllPath) ? dllName : Dll,
            };

            if (AddMonitor(serverName, 2, ref monitorInfo)) return;

            if (Marshal.GetLastWin32Error() == PrintingException.ErrorMonitorAlreadyInstalled && TryUninstall(serverName)
                && AddMonitor(serverName, 2, ref monitorInfo))
                return;
            else
                throw new PrintingException(Marshal.GetLastWin32Error());
        }
        catch (Exception e)
        {
            throw new PrintingException(e.Message, e);
        }
    }

    /// <summary>
    /// Удалает монитор печати на удалённой машине.
    /// </summary>
    /// <param name="serverName">Имя сервера.</param>
    /// <exception cref="PrintingException"/>
    public override void Uninstall(string serverName)
    {
        try
        {
            if (!All.Select(m => m.Name).Contains(Name)) return;

            /// TODO: Добавить удаление октрытых на мониторе портов.

            if (DeleteMonitor(serverName, Environment.GetEnvironmentName(), Name)) return;
            if (Marshal.GetLastWin32Error() == PrintingException.ErrorMonitorUnknown) return;
            if (DeleteMonitor(serverName, Environment.GetEnvironmentName(), Name)) return;

            throw new PrintingException(Marshal.GetLastWin32Error());
        }
        catch (Exception e)
        {
            throw new PrintingException(e.Message, e);
        }
    }

    #region Native
    /// <summary>
    /// Производит установку монитора принтера в систему.
    /// </summary>
    /// <param name="serverName">Имя сервера, на который необходимо произвести установку. Если равно null - устанавливает на локальную машину.</param>
    /// <param name="level">Номер версии структуры. Должен быть равен 2.</param>
    /// <param name="monitor">Экземпляр структуры <see cref="MonitorInfo"/>.</param>
    /// <returns>True, если операция выполнена успешно, иначе False.</returns>
    [DllImport("winspool.drv", CharSet = CharSet.Auto, SetLastError = true)]
    internal static extern bool AddMonitor(string serverName, uint level, ref MonitorInfo monitor);

    /// <summary>
    /// Возвращает указатель на буфер экземпляров структур <see cref="MonitorInfo"/>.
    /// </summary>
    /// <param name="serverName">Имя сервера, с которого требуется получить список мониторов. Если равно null - получает на локальной машине.</param>
    /// <param name="level">Номер версии структуры. Должен быть равен 1 или 2.</param>
    /// <param name="monitors">Указатель на буфер экземпляров структур <see cref="MonitorInfo"/>.</param>
    /// <param name="bufferSize">Размер буфера экземпляров структур <see cref="MonitorInfo"/> (в байтах).</param>
    /// <param name="bytesNeeded">Число полученных байт размера буфера.</param>
    /// <param name="bufferReturnedLength">Число экземпляров структур.</param>
    /// <returns>True, если операция выполнена успешно, иначе False.</returns>
    [DllImport("winspool.drv", CharSet = CharSet.Auto, SetLastError = true)]
    internal static extern bool EnumMonitors(string serverName, uint level, IntPtr monitors, uint bufferSize, ref uint bytesNeeded, ref uint bufferReturnedLength);

    /// <summary>
    /// Производит удаление монитора принтера из системы.
    /// </summary>
    /// <param name="serverName">Имя сервера, на который необходимо произвести установку. Если равно null - устанавливает на локальную машину.</param>
    /// <param name="environment">Окружение, для которого был написан драйвер (например, Windows x86, Windows IA64 или Windows x64).</param>
    /// <param name="monitorName">Имя удаляемого монитора.</param>
    /// <returns>True, если операция выполнена успешно, иначе False.</returns>
    [DllImport("winspool.drv", CharSet = CharSet.Auto, SetLastError = true)]
    internal static extern bool DeleteMonitor(string serverName, string environment, string monitorName);
    #endregion
}

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

Расширяем функционал класса PrintingApi

/// <summary>
/// Возвращает коллекцию всех установленных мониторов в системе.
/// </summary>
public static Monitor[] Monitors => Monitor.All;

/// <summary>
/// Создаёт новый монитор печати в системе.
/// </summary>
/// <param name="name">Наименование монитора печати.</param>
/// <param name="dll">Путь к файлу dll монитора печати.</param>
/// <param name="environment">Окружение, для которого был написан монитор печати.</param>
/// <param name="serverName">Наименование сервера, на котором производится установка монитора печати.</param>
/// <returns>Экземпляр монитора печати.</returns>
public Monitor CreateMonitor(string name, string dll, Environment environment, string serverName)
{
	Monitor monitor = new Monitor(name, dll, environment);
	monitor.TryInstall(serverName);

	return monitor;
}

/// <summary>
/// Создаёт новый монитор печати в системе.
/// </summary>
/// <param name="name">Наименование монитора печати.</param>
/// <param name="dll">Путь к файлу dll монитора печати.</param>
/// <param name="environment">Окружение, для которого был написан монитор печати.</param>
/// <returns>Экземпляр монитора печати.</returns>
public Monitor CreateMonitor(string name, string dll, Environment environment) => CreateMonitor(name, dll, environment, null);

/// <summary>
/// Создаёт новый монитор печати в системе.
/// </summary>
/// <param name="name">Наименование монитора печати.</param>
/// <param name="dll">Путь к файлу dll монитора печати.</param>
/// <param name="serverName">Наименование сервера, на котором производится установка монитора печати.</param>
/// <returns>Экземпляр монитора печати.</returns>
public Monitor CreateMonitor(string name, string dll, string serverName) => CreateMonitor(name, dll, Environment.Current, null);

/// <summary>
/// Создаёт новый монитор печати в системе.
/// </summary>
/// <param name="name">Наименование монитора печати.</param>
/// <param name="dll">Путь к файлу dll монитора печати.</param>
/// <returns>Экземпляр монитора печати.</returns>
public Monitor CreateMonitor(string name, string dll) => CreateMonitor(name, dll, null);

Убедимся в работоспособности нашего кода. Добавляем Unit-тест, прописываем константы имени и путей к dll монитора для удобства, реализуем тестовые методы, покрывающие основные сегменты кода:

Unit-тест для монитора печати

/// <summary>
/// Представляет тестовый модуль класса <see cref="Monitor"/>.
/// </summary>
[TestClass]
public class MonitorTests
{
    /// <summary>
    /// Наименование монитора.
    /// </summary>
    protected const string MonitorName = "Test Monitor";

    /// <summary>
    /// Путь к dll монитора.
    /// </summary>
    protected const string MonitorDll = "D:/Printing Tests/mfilemon.dll";

    /// <summary>
    /// Неправильный путь к dll монитора.
    /// </summary>
    protected const string FailedMonitorDll = "noexist.dll";

    /// <summary>
    /// Тест локальной установки монитора.
    /// </summary>
    [TestMethod]
    public void InstallTest()
    {
        Monitor monitor = new Monitor(MonitorName, MonitorDll);
        monitor.Install();

        Assert.IsTrue(Monitor.All.Select(m => m.Name).Contains(MonitorName));
    }

    /// <summary>
    /// Тест локального удаления монитора.
    /// </summary>
    [TestMethod]
    public void UninstallTest()
    {
        Monitor monitor = new Monitor(MonitorName, MonitorDll);
        monitor.Uninstall();

        Assert.IsFalse(Monitor.All.Select(m => m.Name).Contains(MonitorName));
    }

    /// <summary>
    /// Тест локальной установки монитора с перехватом состояния установки.
    /// </summary>
    [TestMethod]
    public void TryInstallTest()
    {
        Monitor monitor = new Monitor(MonitorName, MonitorDll);
        bool f = monitor.TryInstall();

        Assert.IsTrue(f);
        Assert.IsTrue(Monitor.All.Select(m => m.Name).Contains(MonitorName));
    }

    /// <summary>
    /// Тест локального удаления монитора с перехватом состояния удаления.
    /// </summary>
    [TestMethod]
    public void TryUninstallTest()
    {
        Monitor monitor = new Monitor(MonitorName, MonitorDll);
        bool f = monitor.TryUninstall();

        Assert.IsTrue(f);
        Assert.IsFalse(Monitor.All.Select(m => m.Name).Contains(MonitorName));
    }

    /// <summary>
    /// Тест неправильной локальной установки монитора.
    /// </summary>
    [TestMethod]
    [ExpectedException(typeof(PrintingException))]
    public void InstallFailedTest()
    {
        Monitor monitor = new Monitor(MonitorName, FailedMonitorDll);
        monitor.Install();

        Assert.IsFalse(Monitor.All.Select(m => m.Name).Contains(MonitorName));
    }
        
    /// <summary>
    /// Тест неправильной локальной установки монитора с перехватом состояния установки.
    /// </summary>
    [TestMethod]
    public void TryInstallFailedTest()
    {
        Monitor monitor = new Monitor(MonitorName, FailedMonitorDll);
        bool f = monitor.TryInstall();

        Assert.IsFalse(f);
        Assert.IsFalse(Monitor.All.Select(m => m.Name).Contains(MonitorName));
    }

Не забываем закинуть в тестовый каталог mfilemon.dll монитора печати.

Порт

С портом проделываем аналогичные действия. Сперва нам понадобится IPort:

interface IPort

/// <summary>
/// Представляет базовый интерфейс для реализации портов печати.
/// </summary>
public interface IPort : IPrintableDevice
{
    /// <summary>
    /// Монитор, на котором открыт порт.
    /// </summary>
    IMonitor Monitor { get; }

    /// <summary>
    /// Описание порта.
    /// </summary>
    string Description { get; }

    /// <summary>
    /// Тип порта.
    /// </summary>
    PortType Type { get; }
}

Теперь описываем нативную структуру данных порта:

struct PortInfo

/// <summary>
/// представляет информацию о порте монитора принтера.
/// </summary>
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public struct PortInfo
{
    /// <summary>
    /// Наименование поддерживаемого порта (например, "LPT1:").
    /// </summary>
    [MarshalAs(UnmanagedType.LPTStr)]
    public string PortName;

    /// <summary>
    /// Наименование установленного монитора принтера (например, "PJL monitor"). Может быть равно null.
    /// </summary>
    [MarshalAs(UnmanagedType.LPTStr)]
    public string MonitorName;

    /// <summary>
    /// Описание порта (например, если <see cref="PortName"/> равен "LPT1:", <see cref="Description"/> будет равен "printer port"). Может быть равно null.
    /// </summary>
    [MarshalAs(UnmanagedType.LPTStr)]
    public string Description;

    /// <summary>
    /// Тип порта.
    /// </summary>
    public PortType Type;

    /// <summary>
    /// Зарезервировано. Должен быть равен 0.
    /// </summary>
    internal uint Reserved;
}

Spooler предусматривает два способа открытия и закрытия порта: первый — использовать базовые методы AddPort/DeletePort, второй — использовать средства XcvData и различные хэлперы над ним. Второй вариант для нас предпочтительнее, т.к. в первом случае понадобится указатель на диалоговое окно процесса установки, что нам отнюдь не нужно. Для XCV нам дополнительно понадобятся:

enum PrinterAccess - права доступа к данным принтера

/// <summary>
/// Права доступа к принтеру.
/// </summary>
internal enum PrinterAccess
{
    /// <summary>
    /// Полный доступ к данным принтера.
    /// </summary>
    ServerAdmin = 0x01,
    /// <summary>
    /// Доступ к чтению данных принтера.
    /// </summary>
    ServerEnum = 0x02,
    /// <summary>
    /// Полный доступ к использованию принтера.
    /// </summary>
    PrinterAdmin = 0x04,
    /// <summary>
    /// Ограниченный доступ к использованию принтера.
    /// </summary>
    PrinterUse = 0x08,
    /// <summary>
    /// Полный доступ к данным очереди печати.
    /// </summary>
    JobAdmin = 0x10,
    /// <summary>
    /// Чтение данных очереди печати.
    /// </summary>
    JobRead = 0x20,
    /// <summary>
    /// Стандартные права доступа.
    /// </summary>
    StandardRightsRequired = 0x000F0000,
    /// <summary>
    /// Самый полный доступ.
    /// </summary>
    PrinterAllAccess = (StandardRightsRequired | PrinterAdmin | PrinterUse),
}

struct PrinterDefaults - установки принтера для XcvData

/// <summary>
/// Представляет установки принтера для <see cref="Port.XcvData(IntPtr, string, IntPtr, uint, IntPtr, uint, out uint, out uint)"/>.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
internal struct PrinterDefaults
{
    /// <summary>
    /// Тип данных (по умолчанию равен null).
    /// </summary>
    public IntPtr DataType;

    /// <summary>
    /// Режим работы (по умолчанию равен null).
    /// </summary>
    public IntPtr DevMode;

    /// <summary>
    /// Права доступа к принтеру.
    /// </summary>
    public PrinterAccess DesiredAccess;
}

struct PortData - структура данных порта для XcvData

/// <summary>
/// Представляет данные принтера для <see cref="Port.XcvData(System.IntPtr, string, System.IntPtr, uint, System.IntPtr, uint, out uint, out uint)"/>.
/// </summary>
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
internal struct PortData
{
    /// <summary>
    /// Наименование порта.
    /// </summary>
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)]
    public string PortName;

    /// <summary>
    /// Номер версии (по умолчанию равен 1).
    /// </summary>
    public uint Version;

    /// <summary>
    /// Протокол.
    /// </summary>
    public DataType Protocol;

    /// <summary>
    /// Размер буфера данных.
    /// </summary>
    public uint BufferSize;

    /// <summary>
    /// Размер зарезервированного буфера.
    /// </summary>
    public uint ReservedSize;

    /// <summary>
    /// Адрес хоста.
    /// </summary>
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 49)]
    public string HostAddress;

    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 33)]
    public string SNMPCommunity;

    public uint DoubleSpool;

    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 33)]
    public string Queue;

    /// <summary>
    /// IP-адрес.
    /// </summary>
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 16)]
    public string IPAddress;

    /// <summary>
    /// Зарезервированный буфер.
    /// </summary>
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 540)]
    public byte[] Reserved;

    /// <summary>
    /// Номер порта.
    /// </summary>
    public uint PortNumber;

    public uint SNMPEnabled;

    public uint SNMPDevIndex;
}

enum XcvDataType - тип операции для XcvData

/// <summary>
/// Тип операции для <see cref="Port.XcvData(IntPtr, string, IntPtr, uint, IntPtr, uint, out uint, out uint)"/>.
/// </summary>
internal enum XcvDataType
{
    /// <summary>
    /// Добавить новый порт.
    /// </summary>
    AddPort,
    /// <summary>
    /// Удалить существующий порт.
    /// </summary>
    DeletePort,
}

Отлично, теперь у нас есть всё что нужно для реализации класса порта:

class Port

/// <summary>
/// Представляет порт для запуска принтера.
/// </summary>
public class Port : PrintableDevice, IPort
{
    /// <summary>
    /// Монитор, на котором открыт порт.
    /// </summary>
    public virtual IMonitor Monitor { get; protected set; }

    /// <summary>
    /// Описание порта.
    /// </summary>
    public virtual string Description { get; protected set; }

    /// <summary>
    /// Тип порта.
    /// </summary>
    public virtual PortType Type { get; protected set; }

    /// <summary>
    /// Возвращает список всех установленных в системе портов печати.
    /// </summary>
    public static Port[] All
    {
        get
        {
            if (!PrintingApi.TryGetInfo(EnumPorts, out PortInfo[] portInfo)) return null;

            Port[] ports = new Port[portInfo.Length];

            for (int i = 0; i < portInfo.Length; i++)
                ports[i] = new Port(portInfo[i].PortName, portInfo[i].Description, portInfo[i].Type, portInfo[i].MonitorName);

            return ports;
        }
    }

    /// <summary>
    /// Инициализирует новый экземпляр класса <see cref="Port"/>.
    /// </summary>
    /// <param name="name">Наименование порта.</param>
    /// <param name="description">Описание порта.</param>
    /// <param name="type">Тип порта.</param>
    /// <param name="monitorName">Наименование монитора печати, на котором открыт порт.</param>
    /// <exception cref="ArgumentNullException"/>
    /// <exception cref="PrintingException"/>
    public Port(string name, string description, PortType type, string monitorName) : base(name)
    {
        Description = description;
        Type = type;

        Monitor[] monitors = PrintingApi.Monitors;

        if (monitors.Select(m => m.Name).Contains(monitorName)) Monitor = monitors.Where(m => m.Name == monitorName).FirstOrDefault();
    }

    /// <summary>
    /// Инициализирует новый экземпляр класса <see cref="Port"/>.
    /// </summary>
    /// <param name="name">Наименование порта.</param>
    /// <param name="description">Описание порта.</param>
    /// <param name="type">Тип порта.</param>
    /// <param name="monitor">Монитор печати, на котором открыт порт.</param>
    /// <exception cref="ArgumentNullException"/>
    /// <exception cref="PrintingException"/>
    public Port(string name, string description, PortType type, IMonitor monitor) : this(name, description, type, monitor?.Name) { }

    /// <summary>
    /// Инициализирует новый экземпляр класса <see cref="Port"/>.
    /// </summary>
    /// <param name="name">Наименование порта.</param>
    /// <param name="description">Описание порта.</param>
    /// <param name="type">Тип порта.</param>
    /// <exception cref="ArgumentNullException"/>
    /// <exception cref="PrintingException"/>
    public Port(string name, string description, PortType type) : this(name, description, type, null as string) { }

    /// <summary>
    /// Инициализирует новый экземпляр класса <see cref="Port"/>.
    /// </summary>
    /// <param name="name">Наименование порта.</param>
    /// <param name="description">Описание порта.</param>
    /// <param name="monitorName">Наименование монитора печати, на котором открыт порт.</param>
    /// <exception cref="ArgumentNullException"/>
    /// <exception cref="PrintingException"/>
    public Port(string name, string description, string monitorName) : this(name, description, PortType.Redirected, monitorName) { }

    /// <summary>
    /// Инициализирует новый экземпляр класса <see cref="Port"/>.
    /// </summary>
    /// <param name="name">Наименование порта.</param>
    /// <param name="description">Описание порта.</param>
    /// <param name="monitor">Монитор печати, на котором открыт порт.</param>
    /// <exception cref="ArgumentNullException"/>
    /// <exception cref="PrintingException"/>
    public Port(string name, string description, IMonitor monitor) : this(name, description, PortType.Redirected, monitor) { }

    /// <summary>
    /// Инициализирует новый экземпляр класса <see cref="Port"/>.
    /// </summary>
    /// <param name="name">Наименование порта.</param>
    /// <param name="description">Описание порта.</param>
    /// <exception cref="ArgumentNullException"/>
    /// <exception cref="PrintingException"/>
    public Port(string name, string description) : this(name, description, null as string) { }

    /// <summary>
    /// Инициализирует новый экземпляр класса <see cref="Port"/>.
    /// </summary>
    /// <param name="name">Наименование порта.</param>
    /// <param name="monitor">Монитор печати, на котором открыт порт.</param>
    /// <exception cref="ArgumentNullException"/>
    /// <exception cref="PrintingException"/>
    public Port(string name, IMonitor monitor) : this(name, null, monitor) { }

    /// <summary>
    /// Инициализирует новый экземпляр класса <see cref="Port"/>.
    /// </summary>
    /// <param name="name">Наименование порта.</param>
    /// <param name="type">Тип порта.</param>
    /// <exception cref="ArgumentNullException"/>
    /// <exception cref="PrintingException"/>
    public Port(string name, PortType type) : this(name, null, type) { }

    /// <summary>
    /// Инициализирует новый экземпляр класса <see cref="Port"/>.
    /// </summary>
    /// <param name="name">Наименование порта.</param>
    /// <exception cref="ArgumentNullException"/>
    /// <exception cref="PrintingException"/>
    public Port(string name) : this(name, null as string) { }

    /// <summary>
    /// Открывает порт на удалённой машине.
    /// </summary>
    /// <param name="serverName">Имя сервера.</param>
    /// <exception cref="FileNotFoundException"/>
    /// <exception cref="PrintingException"/>
    public override void Install(string serverName)
    {
        try
        {
            if (All.Select(p => p.Name).Contains(Name)) Uninstall(serverName);

            PrinterDefaults defaults = new PrinterDefaults { DesiredAccess = PrinterAccess.ServerAdmin };

            if (!OpenPrinter($",XcvMonitor {Monitor.Name}", out IntPtr printerHandle, ref defaults)) throw new PrintingException(Marshal.GetLastWin32Error());

            PortData portData = new PortData
            {
                Version = 1,
                Protocol = DataType.RAW,
                PortNumber = 9100,  // 9100 = RAW, 515 = LPR.
                ReservedSize = 0,
                PortName = Name,
                IPAddress = serverName,
                SNMPCommunity = "public",
                SNMPEnabled = 1,
                SNMPDevIndex = 1,
            };

            uint size = (uint)Marshal.SizeOf(portData);
            portData.BufferSize = size;
            IntPtr pointer = Marshal.AllocHGlobal((int)size);
            Marshal.StructureToPtr(portData, pointer, true);

            try
            {
                IntPtr outputData = IntPtr.Zero;
                uint outputDataSize = 0;

                if (!XcvData(printerHandle, Enum.GetName(typeof(XcvDataType), XcvDataType.AddPort), pointer, size, outputData, outputDataSize, out uint outputNeeded, out uint status))
                    throw new PrintingException(Marshal.GetLastWin32Error());
            }
            catch (Exception e)
            {
                throw new PrintingException(e.Message, e);
            }
            finally
            {
                Marshal.FreeHGlobal(pointer);
                ClosePrinter(printerHandle);
            }
        }
        catch (Exception e)
        {
            throw new PrintingException(e.Message, e);
        }
    }

    /// <summary>
    /// Закрывает порт на удалённой машине.
    /// </summary>
    /// <param name="serverName">Имя сервера.</param>
    /// <exception cref="FileNotFoundException"/>
    /// <exception cref="PrintingException"/>
    public override void Uninstall(string serverName)
    {
        try
        {
            if (!All.Select(p => p.Name).Contains(Name)) return;

            /// TODO: Удалить все принтеры, привязанные к порту.
                
            PrinterDefaults defaults = new PrinterDefaults { DesiredAccess = PrinterAccess.ServerAdmin };

            if (!OpenPrinter($",XcvPort {Name}", out IntPtr printerHandle, ref defaults)) throw new PrintingException(Marshal.GetLastWin32Error());

            PortData portData = new PortData
            {
                Version = 1,
                Protocol = DataType.RAW,
                PortNumber = 9100,
                ReservedSize = 0,
                PortName = Name,
                IPAddress = serverName,
                SNMPCommunity = "public",
                SNMPEnabled = 1,
                SNMPDevIndex = 1,
            };

            uint size = (uint)Marshal.SizeOf(portData);
            portData.BufferSize = size;
            IntPtr pointer = Marshal.AllocHGlobal((int)size);
            Marshal.StructureToPtr(portData, pointer, true);

            try
            {
                IntPtr outputData = IntPtr.Zero;
                uint outputDataSize = 0;

                if (!XcvData(printerHandle, Enum.GetName(typeof(XcvDataType), XcvDataType.DeletePort), pointer, size, outputData, outputDataSize, out uint outputNeeded, out uint status))
                    throw new PrintingException(Marshal.GetLastWin32Error());
            }
            catch (Exception e)
            {
                throw new PrintingException(e.Message, e);
            }
            finally
            {
                Marshal.FreeHGlobal(pointer);
                ClosePrinter(printerHandle);
            }
        }
        catch (Exception e)
        {
            throw new PrintingException(e.Message, e);
        }
    }

    #region Native
    /// <summary>
    /// Получает указатель на принтер.
    /// </summary>
    /// <param name="printerName">Имя принтера.</param>
    /// <param name="printer">Указатель на принтер.</param>
    /// <param name="printerDefaults">Установки для <see cref="XcvData(IntPtr, string, IntPtr, uint, IntPtr, uint, out uint, out uint)"/>.</param>
    /// <returns>True, если операция выполнена успешно, иначе False.</returns>
    [DllImport("winspool.drv", CharSet = CharSet.Auto, SetLastError = true)]
    internal static extern bool OpenPrinter(string printerName, out IntPtr printer, ref PrinterDefaults printerDefaults);

    /// <summary>
    /// Освобождает ресурсы принтера.
    /// </summary>
    /// <param name="printer">Указатель принтера.</param>
    /// <returns>True, если операция выполнена успешно, иначе False.</returns>
    [DllImport("winspool.drv", CharSet = CharSet.Auto, SetLastError = true)]
    internal static extern bool ClosePrinter(IntPtr printer);

    /// <summary>
    /// Производит оперции с принтером.
    /// </summary>
    /// <param name="printer">Указатель на принтер.</param>
    /// <param name="dataType">Тип операции.</param>
    /// <param name="inputData">Входные данные.</param>
    /// <param name="inputDataSize">Размер буфера входных данных.</param>
    /// <param name="outputData">Выходные данные.</param>
    /// <param name="outputDataSize">Размер буфера выходных данных.</param>
    /// <param name="outputNeeded">Размер указателя на выходные данные.</param>
    /// <param name="status">Статус операции.</param>
    /// <returns>True, если операция выполнена успешно, иначе False.</returns>
    [DllImport("winspool.drv", CharSet = CharSet.Auto, SetLastError = true)]
    internal static extern bool XcvData(IntPtr printer, string dataType, IntPtr inputData, uint inputDataSize, IntPtr outputData, uint outputDataSize,
        out uint outputNeeded, out uint status);

    /// <summary>
    /// Возвращает указатель на буфер экземпляров структур <see cref="PortInfo"/>.
    /// </summary>
    /// <param name="serverName">Имя сервера, с которого требуется получить список портов. Если равно null - получает на локальной машине.</param>
    /// <param name="level">Номер версии структуры. Должен быть равен 1 или 2.</param>
    /// <param name="ports">Указатель на буфер экземпляров структур <see cref="PortInfo"/>.</param>
    /// <param name="bufferSize">Размер буфера экземпляров структур <see cref="PortInfo"/> (в байтах).</param>
    /// <param name="bytesNeeded">Число полученных байт размера буфера.</param>
    /// <param name="bufferReturnedLength">Число экземпляров структур.</param>
    /// <returns>True, если операция выполнена успешно, иначе False.</returns>
    [DllImport("winspool.drv", CharSet = CharSet.Auto, SetLastError = true)]
    internal static extern bool EnumPorts(string serverName, uint level, IntPtr ports, uint bufferSize, ref uint bytesNeeded, ref uint bufferReturnedLength);
    #endregion
}

Теперь расширим наш PrintingApi за счёт внесения функционала для работы с портами:

Добавляем методы работы с портами в PrintingApi

/// <summary>
/// Возвращает коллекцию всех октрытых портов печати в системе.
/// </summary>
public static Port[] Ports => Port.All;

/// <summary>
/// Открывает новый порт печати в системе.
/// </summary>
/// <param name="name">Наименование порта.</param>
/// <param name="description">Описание порта.</param>
/// <param name="type">Тип порта.</param>
/// <param name="monitor">Монитор печати, на котором открывается порт.</param>
/// <param name="serverName">Имя сервера.</param>
/// <returns>Экземпляр порта печати.</returns>
public Port OpenPort(string name, string description, PortType type, Monitor monitor, string serverName)
{
    Port port = new Port(name, description, type, monitor);
    monitor.TryInstall(serverName);

    return port;
}

/// <summary>
/// Открывает новый порт печати в системе.
/// </summary>
/// <param name="name">Наименование порта.</param>
/// <param name="description">Описание порта.</param>
/// <param name="type">Тип порта.</param>
/// <param name="monitor">Монитор печати, на котором открывается порт.</param>
/// <returns>Экземпляр порта печати.</returns>
public Port OpenPort(string name, string description, PortType type, Monitor monitor) => OpenPort(name, description, type, monitor, null);

/// <summary>
/// Открывает новый порт печати в системе.
/// </summary>
/// <param name="name">Наименование порта.</param>
/// <param name="description">Описание порта.</param>
/// <param name="type">Тип порта.</param>
/// <param name="serverName">Имя сервера.</param>
/// <returns>Экземпляр порта печати.</returns>
public Port OpenPort(string name, string description, PortType type, string serverName) => OpenPort(name, description, type, null, serverName);

/// <summary>
/// Открывает новый порт печати в системе.
/// </summary>
/// <param name="name">Наименование порта.</param>
/// <param name="description">Описание порта.</param>
/// <param name="type">Тип порта.</param>
/// <returns>Экземпляр порта печати.</returns>
public Port OpenPort(string name, string description, PortType type) => OpenPort(name, description, type, null as string);

/// <summary>
/// Открывает новый порт печати в системе.
/// </summary>
/// <param name="name">Наименование порта.</param>
/// <param name="description">Описание порта.</param>
/// <param name="monitor">Монитор печати, на котором открывается порт.</param>
/// <param name="serverName">Имя сервера.</param>
/// <returns>Экземпляр порта печати.</returns>
public Port OpenPort(string name, string description, Monitor monitor, string serverName)
    => OpenPort(name, description, PortType.Redirected, monitor, serverName);

/// <summary>
/// Открывает новый порт печати в системе.
/// </summary>
/// <param name="name">Наименование порта.</param>
/// <param name="description">Описание порта.</param>
/// <param name="monitor">Монитор печати, на котором открывается порт.</param>
/// <returns>Экземпляр порта печати.</returns>
public Port OpenPort(string name, string description, Monitor monitor) => OpenPort(name, description, monitor, null);

/// <summary>
/// Открывает новый порт печати в системе.
/// </summary>
/// <param name="name">Наименование порта.</param>
/// <param name="monitor">Монитор печати, на котором открывается порт.</param>
/// <param name="serverName">Имя сервера.</param>
/// <returns>Экземпляр порта печати.</returns>
public Port OpenPort(string name, Monitor monitor, string serverName)
    => OpenPort(name, null, monitor, serverName);

/// <summary>
/// Открывает новый порт печати в системе.
/// </summary>
/// <param name="name">Наименование порта.</param>
/// <param name="monitor">Монитор печати, на котором открывается порт.</param>
/// <returns>Экземпляр порта печати.</returns>
public Port OpenPort(string name, Monitor monitor) => OpenPort(name, monitor, null);

/// <summary>
/// Открывает новый порт печати в системе.
/// </summary>
/// <param name="name">Наименование порта.</param>
/// <param name="type">Тип порта.</param>
/// <param name="serverName">Имя сервера.</param>
/// <returns>Экземпляр порта печати.</returns>
public Port OpenPort(string name, PortType type, string serverName)
    => OpenPort(name, null, type, serverName);

/// <summary>
/// Открывает новый порт печати в системе.
/// </summary>
/// <param name="name">Наименование порта.</param>
/// <param name="type">Тип порта.</param>
/// <returns>Экземпляр порта печати.</returns>
public Port OpenPort(string name, PortType type) => OpenPort(name, type, null);

/// <summary>
/// Открывает новый порт печати в системе.
/// </summary>
/// <param name="name">Наименование порта.</param>
/// <param name="description">Описание порта.</param>
/// <param name="serverName">Имя сервера.</param>
/// <returns>Экземпляр порта печати.</returns>
public Port OpenPort(string name, string description, string serverName) => OpenPort(name, description, null, serverName);

/// <summary>
/// Открывает новый порт печати в системе.
/// </summary>
/// <param name="name">Наименование порта.</param>
/// <param name="description">Описание порта.</param>
/// <returns>Экземпляр порта печати.</returns>
public Port OpenPort(string name, string description) => OpenPort(name, description, null as string);

/// <summary>
/// Открывает новый порт печати в системе.
/// </summary>
/// <param name="name">Наименование порта.</param>
/// <returns>Экземпляр порта печати.</returns>
public Port OpenPort(string name) => OpenPort(name, null as string);

Так же не забываем добавить проверку в метод удаления монитора вместо TODO:

IEnumerable<Port> openPorts = Port.All.Where(p => p.Monitor?.Name == Name);
foreach (Port openPort in openPorts) openPort.Uninstall(serverName);

Теперь убедимся в работоспособности нашего кода и можно приступать к следующему этапу:

Unit-тест для работы с портами печати

/// <summary>
/// Представляет тестовый модуль класса <see cref="Port"/>.
/// </summary>
[TestClass]
public class PortTests
{
    /// <summary>
    /// Наименование порта.
    /// </summary>
    protected const string PortName = "TESTPORT:";

    /// <summary>
    /// Описание порта.
    /// </summary>
    protected const string PortDescription = "Description for " + PortName;

    /// <summary>
    /// Наименование монитора.
    /// </summary>
    protected const string MonitorName = "mfilemon";

    /// <summary>
    /// Наименование несуществующего монитора.
    /// </summary>
    protected const string FailedMonitorName = "noexist";

    /// <summary>
    /// Тест локальной установки порта.
    /// </summary>
    [TestMethod]
    public void InstallTest()
    {
        Port port = new Port(PortName, PortDescription, MonitorName);
        port.Install();

        Assert.IsTrue(Port.All.Select(p => p.Name).Contains(PortName));
    }

    /// <summary>
    /// Тест локального удаления порта.
    /// </summary>
    [TestMethod]
    public void UninstallTest()
    {
        Port port = new Port(PortName, PortDescription, MonitorName);
        port.Uninstall();

        Assert.IsFalse(Port.All.Select(p => p.Name).Contains(PortName));
    }

    /// <summary>
    /// Тест локальной установки порта с перехватом состояния установки.
    /// </summary>
    [TestMethod]
    public void TryInstallTest()
    {
        Port port = new Port(PortName, PortDescription, MonitorName);
        bool f = port.TryInstall();

        Assert.IsTrue(f);
        Assert.IsTrue(Port.All.Select(p => p.Name).Contains(PortName));
    }

    /// <summary>
    /// Тест локального удаления порта с перехватом состояния удаления.
    /// </summary>
    [TestMethod]
    public void TryUninstallTest()
    {
        Port port = new Port(PortName, PortDescription, MonitorName);
        bool f = port.TryUninstall();

        Assert.IsTrue(f);
        Assert.IsFalse(Port.All.Select(p => p.Name).Contains(PortName));
    }

    /// <summary>
    /// Тест неправильной локальной установки порта.
    /// </summary>
    [TestMethod]
    [ExpectedException(typeof(PrintingException))]
    public void InstallFailedTest()
    {
        Port port = new Port(PortName, PortDescription, MonitorName);
        port.Install();

        Assert.IsFalse(Port.All.Select(p => p.Name).Contains(PortName));
    }

    /// <summary>
    /// Тест неправильной локальной установки порта с перехватом состояния установки.
    /// </summary>
    [TestMethod]
    public void TryInstallFailedTest()
    {
        Port port = new Port(PortName, PortDescription, FailedMonitorName);
        bool f = port.TryUninstall();

        Assert.IsTrue(f);
        Assert.IsFalse(Port.All.Select(p => p.Name).Contains(PortName));
    }
}

Драйвер принтера

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

struct DriverInfo

/// <summary>
/// Представляет структуру для хранения информации о драйвере устройства принтера.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct DriverInfo
{
    /// <summary>
    /// Номер версии операционной системы, для которой был написан драйвер. Поддерживаемые значения - 3 и 4 (V3 и V4 номера версий драйвера соответственно).
    /// </summary>
    public uint Version;

    /// <summary>
    /// Имя драйвера (например, "QMS 810").
    /// </summary>
    [MarshalAs(UnmanagedType.LPTStr)]
    public string Name;

    /// <summary>
    /// Окружение, для которого был написан драйвер (например, Windows x86, Windows IA64 или Windows x64).
    /// </summary>
    [MarshalAs(UnmanagedType.LPTStr)]
    public string Environment;

    /// <summary>
    /// Полный или относительный путь к файлу драйвера устройства (например, "C:DRIVERSPscript.dll").
    /// </summary>
    [MarshalAs(UnmanagedType.LPTStr)]
    public string DriverPath;

    /// <summary>
    /// Полный или относительный путь к файлу данных драйвера (например, "C:DRIVERSQms810.ppd").
    /// </summary>
    [MarshalAs(UnmanagedType.LPTStr)]
    public string DataFile;

    /// <summary>
    /// Полный или относительный путь к dll данных конфигурации драйвера (например, "C:DRIVERSPscriptui.dll").
    /// </summary>
    [MarshalAs(UnmanagedType.LPTStr)]
    public string ConfigFile;

    /// <summary>
    /// Полный или относительный путь к dll данных HLP-файла драйвера (например, "C:DRIVERSPscript.hlp").
    /// </summary>
    [MarshalAs(UnmanagedType.LPTStr)]
    public string HelpFile;

    /// <summary>
    /// Зависимые файлы.
    /// </summary>
    [MarshalAs(UnmanagedType.LPTStr)]
    public string DependentFiles;

    /// <summary>
    /// Наименование монитора.
    /// </summary>
    [MarshalAs(UnmanagedType.LPTStr)]
    public string MonitorName;

    /// <summary>
    /// Тип данных принтера по умолчанию.
    /// </summary>
    [MarshalAs(UnmanagedType.LPTStr)]
    public string DefaultDataType;
}

interface IDriver

/// <summary>
/// Представляет интерфейс для реализации сущностей драйверов печати.
/// </summary>
public interface IDriver : IPrintableDevice
{
    /// <summary>
    /// Монитор печати.
    /// </summary>
    IMonitor Monitor { get; }

    /// <summary>
    /// Номер версии операционной системы, для которой был написан драйвер. Поддерживаемые значения - 3 и 4 (V3 и V4 номера версий драйвера соответственно).
    /// </summary>
    uint Version { get; }

    /// <summary>
    /// Окружение, для которого был написан драйвер (например, Windows x86, Windows IA64 или Windows x64).
    /// </summary>
    Environment Environment { get; }

    /// <summary>
    /// Полный или относительный путь к файлу драйвера устройства (например, "C:DRIVERSPscript.dll").
    /// </summary>
    string Dll { get; }

    /// <summary>
    /// Полный или относительный путь к файлу данных драйвера (например, "C:DRIVERSQms810.ppd").
    /// </summary>
    string DataFile { get; }

    /// <summary>
    /// Полный или относительный путь к dll данных конфигурации драйвера (например, "C:DRIVERSPscriptui.dll").
    /// </summary>
    string ConfigFile { get; }

    /// <summary>
    /// Полный или относительный путь к dll данных HLP-файла драйвера (например, "C:DRIVERSPscript.hlp").
    /// </summary>
    string HelpFile { get; }

    /// <summary>
    /// Зависимые файлы.
    /// </summary>
    string DependentFiles { get; }

    /// <summary>
    /// Тип данных принтера по умолчанию.
    /// </summary>
    DataType DefaultDataType { get; }
}

class Driver

/// <summary>
/// Представляет данные для работы с драйвером печати.
/// </summary>
public class Driver : PrintableDevice, IDriver
{
    /// <summary>
    /// Монитор печати.
    /// </summary>
    public virtual IMonitor Monitor { get; protected set; }

    /// <summary>
    /// Номер версии операционной системы, для которой был написан драйвер. Поддерживаемые значения - 3 и 4 (V3 и V4 номера версий драйвера соответственно).
    /// </summary>
    public virtual uint Version { get; protected set; }

    /// <summary>
    /// Окружение, для которого был написан драйвер.
    /// </summary>
    public virtual Environment Environment { get; protected set; }

    /// <summary>
    /// Полный или относительный путь к файлу драйвера устройства.
    /// </summary>
    public virtual string Dll { get; protected set; }

    /// <summary>
    /// Полный или относительный путь к файлу данных драйвера.
    /// </summary>
    public virtual string DataFile { get; protected set; }

    /// <summary>
    /// Полный или относительный путь к dll данных конфигурации драйвера.
    /// </summary>
    public virtual string ConfigFile { get; protected set; }

    /// <summary>
    /// Полный или относительный путь к dll данных HLP-файла драйвера.
    /// </summary>
    public virtual string HelpFile { get; protected set; }

    /// <summary>
    /// Зависимые файлы.
    /// </summary>
    public virtual string DependentFiles { get; protected set; }
        
    /// <summary>
    /// Тип данных принтера по умолчанию.
    /// </summary>
    public virtual DataType DefaultDataType { get; protected set; }

    /// <summary>
    /// Путь к системному каталогу драйверов печати.
    /// </summary>
    public static string Directory { get; protected set; }

    /// <summary>
    /// Возвращает коллекцию всех установленных драйверов печати в системе.
    /// </summary>
    public static Driver[] All
    {
        get
        {
            if (!PrintingApi.TryGetInfo(EnumPrinterDrivers, 3, out DriverInfo[] driverInfo)) return null;

            Driver[] drivers = new Driver[driverInfo.Length];

            for (int i = 0; i < driverInfo.Length; i++)
                drivers[i] = new Driver(driverInfo[i].Name, driverInfo[i].DriverPath, driverInfo[i].DataFile, driverInfo[i].ConfigFile, driverInfo[i].HelpFile,
                    driverInfo[i].Version, driverInfo[i].Environment.GetEnvironment(), (DataType)Enum.Parse(typeof(DataType), driverInfo[i].DefaultDataType ?? "RAW", true),
                    driverInfo[i].DependentFiles, driverInfo[i].MonitorName);

            return drivers;
        }
    }

    /// <summary>
    /// Инициализирует новый экземпляр класса <see cref="Driver"/>.
    /// </summary>
    /// <param name="name">Наименование драйвера.</param>
    /// <param name="dll">Полный или относительный путь к файлу драйвера устройства (например, "C:DRIVERSPscript.dll").</param>
    /// <param name="dataFile">Полный или относительный путь к файлу данных драйвера (например, "C:DRIVERSQms810.ppd").</param>
    /// <param name="configFile">Полный или относительный путь к dll данных конфигурации драйвера (например, "C:DRIVERSPscriptui.dll").</param>
    /// <param name="helpFile">Полный или относительный путь к dll данных HLP-файла драйвера (например, "C:DRIVERSPscript.hlp").</param>
    /// <param name="version">Номер версии операционной системы, для которой был написан драйвер. Поддерживаемые значения - 3 и 4 (V3 и V4 номера версий драйвера соответственно).</param>
    /// <param name="environment">Окружение, для которого был написан драйвер (например, Windows x86, Windows IA64 или Windows x64).</param>
    /// <param name="defaultDataType">Тип данных принтера по умолчанию.</param>
    /// <param name="dependentFiles">Зависимые файлы.</param>
    /// <param name="monitorName">Наименование монитора печати.</param>
    public Driver(string name, string dll, string dataFile, string configFile, string helpFile, uint version, Environment environment, DataType defaultDataType,
        string dependentFiles, string monitorName) : base(name)
    {
        Dll = dll;
        DataFile = dataFile;
        ConfigFile = configFile;
        HelpFile = helpFile;
        Version = version;
        Environment = environment;
        DefaultDataType = defaultDataType;
        DependentFiles = dependentFiles;

        Monitor[] monitors = PrintingApi.Monitors;

        if (monitors.Select(m => m.Name).Contains(monitorName)) Monitor = monitors.Where(m => m.Name == monitorName).FirstOrDefault();
    }

    /// <summary>
    /// Инициализирует новый экземпляр класса <see cref="Driver"/>.
    /// </summary>
    /// <param name="name">Наименование драйвера.</param>
    /// <param name="dll">Полный или относительный путь к файлу драйвера устройства (например, "C:DRIVERSPscript.dll").</param>
    /// <param name="dataFile">Полный или относительный путь к файлу данных драйвера (например, "C:DRIVERSQms810.ppd").</param>
    /// <param name="configFile">Полный или относительный путь к dll данных конфигурации драйвера (например, "C:DRIVERSPscriptui.dll").</param>
    /// <param name="helpFile">Полный или относительный путь к dll данных HLP-файла драйвера (например, "C:DRIVERSPscript.hlp").</param>
    /// <param name="version">Номер версии операционной системы, для которой был написан драйвер. Поддерживаемые значения - 3 и 4 (V3 и V4 номера версий драйвера соответственно).</param>
    /// <param name="environment">Окружение, для которого был написан драйвер (например, Windows x86, Windows IA64 или Windows x64).</param>
    /// <param name="defaultDataType">Тип данных принтера по умолчанию.</param>
    /// <param name="dependentFiles">Зависимые файлы.</param>
    /// <param name="monitor">Монитор печати.</param>
    public Driver(string name, string dll, string dataFile, string configFile, string helpFile, uint version, Environment environment, DataType defaultDataType,
        string dependentFiles, IMonitor monitor)
        : this(name, dll, dataFile, configFile, helpFile, version, environment, defaultDataType, dependentFiles, monitor?.Name) { }

    /// <summary>
    /// Инициализирует новый экземпляр класса <see cref="Driver"/>.
    /// </summary>
    /// <param name="name">Наименование драйвера.</param>
    /// <param name="dll">Полный или относительный путь к файлу драйвера устройства (например, "C:DRIVERSPscript.dll").</param>
    /// <param name="dataFile">Полный или относительный путь к файлу данных драйвера (например, "C:DRIVERSQms810.ppd").</param>
    /// <param name="configFile">Полный или относительный путь к dll данных конфигурации драйвера (например, "C:DRIVERSPscriptui.dll").</param>
    /// <param name="helpFile">Полный или относительный путь к dll данных HLP-файла драйвера (например, "C:DRIVERSPscript.hlp").</param>
    /// <param name="version">Номер версии операционной системы, для которой был написан драйвер. Поддерживаемые значения - 3 и 4 (V3 и V4 номера версий драйвера соответственно).</param>
    /// <param name="environment">Окружение, для которого был написан драйвер (например, Windows x86, Windows IA64 или Windows x64).</param>
    /// <param name="defaultDataType">Тип данных принтера по умолчанию.</param>
    /// <param name="monitorName">Наименование монитора печати.</param>
    public Driver(string name, string dll, string dataFile, string configFile, string helpFile, uint version, Environment environment, DataType defaultDataType,
        string monitorName)
        : this(name, dll, dataFile, configFile, helpFile, version, environment, defaultDataType, null, monitorName) { }

    /// <summary>
    /// Инициализирует новый экземпляр класса <see cref="Driver"/>.
    /// </summary>
    /// <param name="name">Наименование драйвера.</param>
    /// <param name="dll">Полный или относительный путь к файлу драйвера устройства (например, "C:DRIVERSPscript.dll").</param>
    /// <param name="dataFile">Полный или относительный путь к файлу данных драйвера (например, "C:DRIVERSQms810.ppd").</param>
    /// <param name="configFile">Полный или относительный путь к dll данных конфигурации драйвера (например, "C:DRIVERSPscriptui.dll").</param>
    /// <param name="helpFile">Полный или относительный путь к dll данных HLP-файла драйвера (например, "C:DRIVERSPscript.hlp").</param>
    /// <param name="version">Номер версии операционной системы, для которой был написан драйвер. Поддерживаемые значения - 3 и 4 (V3 и V4 номера версий драйвера соответственно).</param>
    /// <param name="environment">Окружение, для которого был написан драйвер (например, Windows x86, Windows IA64 или Windows x64).</param>
    /// <param name="defaultDataType">Тип данных принтера по умолчанию.</param>
    /// <param name="monitor">Монитор печати.</param>
    public Driver(string name, string dll, string dataFile, string configFile, string helpFile, uint version, Environment environment, DataType defaultDataType,
        IMonitor monitor)
        : this(name, dll, dataFile, configFile, helpFile, version, environment, defaultDataType, monitor?.Name) { }

    /// <summary>
    /// Инициализирует новый экземпляр класса <see cref="Driver"/>.
    /// </summary>
    /// <param name="name">Наименование драйвера.</param>
    /// <param name="dll">Полный или относительный путь к файлу драйвера устройства (например, "C:DRIVERSPscript.dll").</param>
    /// <param name="dataFile">Полный или относительный путь к файлу данных драйвера (например, "C:DRIVERSQms810.ppd").</param>
    /// <param name="configFile">Полный или относительный путь к dll данных конфигурации драйвера (например, "C:DRIVERSPscriptui.dll").</param>
    /// <param name="helpFile">Полный или относительный путь к dll данных HLP-файла драйвера (например, "C:DRIVERSPscript.hlp").</param>
    /// <param name="version">Номер версии операционной системы, для которой был написан драйвер. Поддерживаемые значения - 3 и 4 (V3 и V4 номера версий драйвера соответственно).</param>
    /// <param name="environment">Окружение, для которого был написан драйвер (например, Windows x86, Windows IA64 или Windows x64).</param>
    /// <param name="monitorName">Наименование монитора печати.</param>
    public Driver(string name, string dll, string dataFile, string configFile, string helpFile, uint version, Environment environment, string monitorName)
        : this(name, dll, dataFile, configFile, helpFile, version, environment, DataType.RAW, monitorName) { }

    /// <summary>
    /// Инициализирует новый экземпляр класса <see cref="Driver"/>.
    /// </summary>
    /// <param name="name">Наименование драйвера.</param>
    /// <param name="dll">Полный или относительный путь к файлу драйвера устройства (например, "C:DRIVERSPscript.dll").</param>
    /// <param name="dataFile">Полный или относительный путь к файлу данных драйвера (например, "C:DRIVERSQms810.ppd").</param>
    /// <param name="configFile">Полный или относительный путь к dll данных конфигурации драйвера (например, "C:DRIVERSPscriptui.dll").</param>
    /// <param name="helpFile">Полный или относительный путь к dll данных HLP-файла драйвера (например, "C:DRIVERSPscript.hlp").</param>
    /// <param name="version">Номер версии операционной системы, для которой был написан драйвер. Поддерживаемые значения - 3 и 4 (V3 и V4 номера версий драйвера соответственно).</param>
    /// <param name="environment">Окружение, для которого был написан драйвер (например, Windows x86, Windows IA64 или Windows x64).</param>
    /// <param name="monitor">Монитор печати.</param>
    public Driver(string name, string dll, string dataFile, string configFile, string helpFile, uint version, Environment environment, IMonitor monitor)
        : this(name, dll, dataFile, configFile, helpFile, version, environment, monitor?.Name) { }

    /// <summary>
    /// Инициализирует новый экземпляр класса <see cref="Driver"/>.
    /// </summary>
    /// <param name="name">Наименование драйвера.</param>
    /// <param name="dll">Полный или относительный путь к файлу драйвера устройства (например, "C:DRIVERSPscript.dll").</param>
    /// <param name="dataFile">Полный или относительный путь к файлу данных драйвера (например, "C:DRIVERSQms810.ppd").</param>
    /// <param name="configFile">Полный или относительный путь к dll данных конфигурации драйвера (например, "C:DRIVERSPscriptui.dll").</param>
    /// <param name="helpFile">Полный или относительный путь к dll данных HLP-файла драйвера (например, "C:DRIVERSPscript.hlp").</param>
    /// <param name="version">Номер версии операционной системы, для которой был написан драйвер. Поддерживаемые значения - 3 и 4 (V3 и V4 номера версий драйвера соответственно).</param>
    /// <param name="monitorName">Наименование монитора печати.</param>
    public Driver(string name, string dll, string dataFile, string configFile, string helpFile, uint version, string monitorName)
        : this(name, dll, dataFile, configFile, helpFile, version, Environment.Current, monitorName) { }

    /// <summary>
    /// Инициализирует новый экземпляр класса <see cref="Driver"/>.
    /// </summary>
    /// <param name="name">Наименование драйвера.</param>
    /// <param name="dll">Полный или относительный путь к файлу драйвера устройства (например, "C:DRIVERSPscript.dll").</param>
    /// <param name="dataFile">Полный или относительный путь к файлу данных драйвера (например, "C:DRIVERSQms810.ppd").</param>
    /// <param name="configFile">Полный или относительный путь к dll данных конфигурации драйвера (например, "C:DRIVERSPscriptui.dll").</param>
    /// <param name="helpFile">Полный или относительный путь к dll данных HLP-файла драйвера (например, "C:DRIVERSPscript.hlp").</param>
    /// <param name="version">Номер версии операционной системы, для которой был написан драйвер. Поддерживаемые значения - 3 и 4 (V3 и V4 номера версий драйвера соответственно).</param>
    /// <param name="monitor">Монитор печати.</param>
    public Driver(string name, string dll, string dataFile, string configFile, string helpFile, uint version, IMonitor monitor)
        : this(name, dll, dataFile, configFile, helpFile, version, monitor?.Name) { }

    /// <summary>
    /// Инициализирует новый экземпляр класса <see cref="Driver"/>.
    /// </summary>
    /// <param name="name">Наименование драйвера.</param>
    /// <param name="dll">Полный или относительный путь к файлу драйвера устройства (например, "C:DRIVERSPscript.dll").</param>
    /// <param name="dataFile">Полный или относительный путь к файлу данных драйвера (например, "C:DRIVERSQms810.ppd").</param>
    /// <param name="configFile">Полный или относительный путь к dll данных конфигурации драйвера (например, "C:DRIVERSPscriptui.dll").</param>
    /// <param name="helpFile">Полный или относительный путь к dll данных HLP-файла драйвера (например, "C:DRIVERSPscript.hlp").</param>
    /// <param name="monitorName">Наименование монитора печати.</param>
    public Driver(string name, string dll, string dataFile, string configFile, string helpFile, string monitorName)
        : this(name, dll, dataFile, configFile, helpFile, 3, monitorName) { }

    /// <summary>
    /// Инициализирует новый экземпляр класса <see cref="Driver"/>.
    /// </summary>
    /// <param name="name">Наименование драйвера.</param>
    /// <param name="dll">Полный или относительный путь к файлу драйвера устройства (например, "C:DRIVERSPscript.dll").</param>
    /// <param name="dataFile">Полный или относительный путь к файлу данных драйвера (например, "C:DRIVERSQms810.ppd").</param>
    /// <param name="configFile">Полный или относительный путь к dll данных конфигурации драйвера (например, "C:DRIVERSPscriptui.dll").</param>
    /// <param name="helpFile">Полный или относительный путь к dll данных HLP-файла драйвера (например, "C:DRIVERSPscript.hlp").</param>
    /// <param name="monitor">Монитор печати.</param>
    public Driver(string name, string dll, string dataFile, string configFile, string helpFile, IMonitor monitor)
        : this(name, dll, dataFile, configFile, helpFile, monitor?.Name) { }

    /// <summary>
    /// Инициализирует новый экземпляр класса <see cref="Driver"/>.
    /// </summary>
    /// <param name="name">Наименование драйвера.</param>
    /// <param name="dll">Полный или относительный путь к файлу драйвера устройства (например, "C:DRIVERSPscript.dll").</param>
    /// <param name="dataFile">Полный или относительный путь к файлу данных драйвера (например, "C:DRIVERSQms810.ppd").</param>
    /// <param name="configFile">Полный или относительный путь к dll данных конфигурации драйвера (например, "C:DRIVERSPscriptui.dll").</param>
    /// <param name="monitorName">Наименование монитора печати.</param>
    public Driver(string name, string dll, string dataFile, string configFile, string monitorName)
        : this(name, dll, dataFile, configFile, null, monitorName) { }

    /// <summary>
    /// Инициализирует новый экземпляр класса <see cref="Driver"/>.
    /// </summary>
    /// <param name="name">Наименование драйвера.</param>
    /// <param name="dll">Полный или относительный путь к файлу драйвера устройства (например, "C:DRIVERSPscript.dll").</param>
    /// <param name="dataFile">Полный или относительный путь к файлу данных драйвера (например, "C:DRIVERSQms810.ppd").</param>
    /// <param name="configFile">Полный или относительный путь к dll данных конфигурации драйвера (например, "C:DRIVERSPscriptui.dll").</param>
    /// <param name="monitor">Монитор печати.</param>
    public Driver(string name, string dll, string dataFile, string configFile, IMonitor monitor)
        : this(name, dll, dataFile, configFile, monitor?.Name) { }

    /// <summary>
    /// Инициализирует новый экземпляр класса <see cref="Driver"/>.
    /// </summary>
    /// <param name="name">Наименование драйвера.</param>
    /// <param name="dll">Полный или относительный путь к файлу драйвера устройства (например, "C:DRIVERSPscript.dll").</param>
    /// <param name="dataFile">Полный или относительный путь к файлу данных драйвера (например, "C:DRIVERSQms810.ppd").</param>
    /// <param name="configFile">Полный или относительный путь к dll данных конфигурации драйвера (например, "C:DRIVERSPscriptui.dll").</param>
    public Driver(string name, string dll, string dataFile, string configFile)
        : this(name, dll, dataFile, configFile, null as string) { }

    /// <summary>
    /// Статический инициализатор класса <see cref="Driver"/>.
    /// </summary>
    static Driver()
    {
        uint length = 1024;
        StringBuilder driverDirectory = new StringBuilder((int)length);
        uint bytesNeeded = 0;

        if (!GetPrinterDriverDirectory(null, null, 1, driverDirectory, length, ref bytesNeeded)) throw new PrintingException(Marshal.GetLastWin32Error());

        Directory = driverDirectory.ToString();
    }

    /// <summary>
    /// Устанавливает драйвер удалённо в системе.
    /// </summary>
    /// <param name="serverName">Имя сервера.</param>
    public override void Install(string serverName)
    {
        try
        {
            if (!File.Exists(Dll)) throw new PrintingException($"Не удалось найти файл драйвера '{Dll}'");
            if (!File.Exists(DataFile)) throw new PrintingException($"Не удалось найти файл драйвера '{DataFile}'");
            if (!File.Exists(ConfigFile)) throw new PrintingException($"Не удалось найти файл драйвера '{ConfigFile}'");

            if (All.Select(d => d.Name).Contains(Name)) Uninstall(serverName);
                
            string systemDriverPath = Path.Combine(Directory, Path.GetFileName(Dll));
            string systemDataPath = Path.Combine(Directory, Path.GetFileName(DataFile));
            string systemConfigPath = Path.Combine(Directory, Path.GetFileName(ConfigFile));
            string systemHelpPath = Path.Combine(Directory, Path.GetFileName(HelpFile));

            File.Copy(Dll, systemDriverPath, true);
            File.Copy(DataFile, systemDataPath, true);
            File.Copy(ConfigFile, systemConfigPath, true);
                
            if (File.Exists(HelpFile)) File.Copy(HelpFile, systemHelpPath, true);

            DriverInfo driverInfo = new DriverInfo
            {
                Version = Version,
                Name = Name,
                Environment = Environment.GetEnvironmentName(),
                DriverPath = File.Exists(systemDriverPath) ? systemDriverPath : Dll,
                DataFile = File.Exists(systemDataPath) ? systemDataPath : DataFile,
                ConfigFile = File.Exists(systemConfigPath) ? systemConfigPath : ConfigFile,
                HelpFile = File.Exists(systemHelpPath) ? systemHelpPath : HelpFile,
                DependentFiles = DependentFiles,
                MonitorName = Monitor?.Name,
                DefaultDataType = Enum.GetName(typeof(DataType), DefaultDataType),
            };

            if (AddPrinterDriver(serverName, Version, ref driverInfo)) return;

            int lastWin32ErrorCode = Marshal.GetLastWin32Error();

            if (lastWin32ErrorCode == 0) return;

            throw new PrintingException(lastWin32ErrorCode);
        }
        catch (Exception e)
        {
            throw new PrintingException(e.Message, e);
        }
    }

    /// <summary>
    /// Удаляет драйвер удалённо в системе.
    /// </summary>
    /// <param name="serverName">Имя сервера.</param>
    public override void Uninstall(string serverName)
    {
        try
        {
            if (!All.Select(d => d.Name).Contains(Name)) return;

            /// TODO: Удалить все принтеры, использующие драйвер.

            if (DeletePrinterDriver(serverName, Environment.GetEnvironmentName(), Name)) return;

            throw new PrintingException(Marshal.GetLastWin32Error());
        }
        catch (Exception e)
        {
            throw new PrintingException(e.Message, e);
        }
    }

    #region Native
    /// <summary>
    /// Возвращает путь к системной директории с установленными драйверами принтера.
    /// </summary>
    /// <param name="serverName">Имя сервера, с которого требуется получить путь к директории драйвера принтера. Если равно null - получает локальный путь.</param>
    /// <param name="environment">Окружение, для которого был написан драйвер (например, Windows x86, Windows IA64 или Windows x64).</param>
    /// <param name="level">Номер версии структуры. Должен быть равен 1.</param>
    /// <param name="driverDirectory">Путь к драйверу принтера.</param>
    /// <param name="bufferSize">Размер буфера для вывода пути.</param>
    /// <param name="bytesNeeded">Число полученных байт пути.</param>
    /// <returns>True, если операция прошла успешно, иначе False.</returns>
    [DllImport("winspool.drv", CharSet = CharSet.Auto, SetLastError = true)]
    internal static extern bool GetPrinterDriverDirectory(string serverName, string environment, uint level, [Out] StringBuilder driverDirectory, uint bufferSize,
        ref uint bytesNeeded);

    /// <summary>
    /// Добавляет драйвер в систему.
    /// </summary>
    /// <param name="serverName">Имя сервера.</param>
    /// <param name="level">Номер версии структуры. Должен быть равен 1, 2, 3, 4, 5, 6 или 8.</param>
    /// <param name="driverInfo">Данные драйвера.</param>
    /// <returns>True, если операция прошла успешно, иначе False.</returns>
    [DllImport("winspool.drv", CharSet = CharSet.Auto, SetLastError = true)]
    internal static extern bool AddPrinterDriver(string serverName, uint level, ref DriverInfo driverInfo);

    /// <summary>
    /// Возвращает указатель на буфер экземпляров структур <see cref="DriverInfo"/>.
    /// </summary>
    /// <param name="serverName">Имя сервера, с которого требуется получить список портов. Если равно null - получает на локальной машине.</param>
    /// <param name="environment">Окружение (например, Windows x86, Windows IA64, Windows x64, или Windows NT R4000). Если параметр равен null,
    /// используется вызываемое окружение клиента (не сервера). Если параметр равен "all", метод верёт список драйверов для всех платформ,
    /// для которых они были установлены на сервере.</param>
    /// <param name="level">Номер версии структуры. Должен быть равен 1, 2, 3, 4, 5, 6 или 8.</param>
    /// <param name="drivers">Указатель на буфер экземпляров структур <see cref="DriverInfo"/>.</param>
    /// <param name="bufferSize">Размер буфера экземпляров структур <see cref="DriverInfo"/> (в байтах).</param>
    /// <param name="bytesNeeded">Число полученных байт размера буфера.</param>
    /// <param name="bufferReturnedLength">Число экземпляров структур.</param>
    /// <returns>True, если операция прошла успешно, иначе False.</returns>
    [DllImport("winspool.drv", CharSet = CharSet.Auto, SetLastError = true)]
    internal static extern bool EnumPrinterDrivers(string serverName, string environment, uint level, IntPtr drivers, uint bufferSize, ref uint bytesNeeded,
        ref uint bufferReturnedLength);

    /// <summary>
    /// Удаляет драйвер из системы.
    /// </summary>
    /// <param name="serverName">Имя сервера.</param>
    /// <param name="environment">Окружение (например, Windows x86, Windows IA64, Windows x64, или Windows NT R4000). Если параметр равен null,
    /// используется вызываемое окружение клиента (не сервера). Если параметр равен "all", метод верёт список драйверов для всех платформ,
    /// для которых они были установлены на сервере.</param>
    /// <param name="driverName">Наименование драйвера.</param>
    /// <returns>True, если операция прошла успешно, иначе False.</returns>
    [DllImport("winspool.drv", CharSet = CharSet.Auto, SetLastError = true)]
    internal static extern bool DeletePrinterDriver(string serverName, string environment, string driverName);
    #endregion
}

Расширяем PrintingApi методами работы с драйверами

/// <summary>
/// Возвращает коллекцию всех установленных драйверов печати в системе.
/// </summary>
public static Driver[] Drivers => Driver.All;

/// <summary>
/// Устанавливает новый драйвер в системе.
/// </summary>
/// <param name="name">Наименование драйвера.</param>
/// <param name="dll">Полный или относительный путь к файлу драйвера устройства (например, "C:DRIVERSPscript.dll").</param>
/// <param name="dataFile">Полный или относительный путь к файлу данных драйвера (например, "C:DRIVERSQms810.ppd").</param>
/// <param name="configFile">Полный или относительный путь к dll данных конфигурации драйвера (например, "C:DRIVERSPscriptui.dll").</param>
/// <param name="helpFile">Полный или относительный путь к dll данных HLP-файла драйвера (например, "C:DRIVERSPscript.hlp").</param>
/// <param name="version">Номер версии операционной системы, для которой был написан драйвер. Поддерживаемые значения - 3 и 4 (V3 и V4 номера версий драйвера соответственно).</param>
/// <param name="environment">Окружение, для которого был написан драйвер (например, Windows x86, Windows IA64 или Windows x64).</param>
/// <param name="defaultDataType">Тип данных принтера по умолчанию.</param>
/// <param name="dependentFiles">Зависимые файлы.</param>
/// <param name="monitor">Монитор печати.</param>
/// <param name="serverName">Имя сервера.</param>
/// <returns>Экземпляр драйвера печати.</returns>
public Driver InstallDriver(string name, string dll, string dataFile, string configFile, string helpFile, uint version, Environment environment,
    DataType defaultDataType, string dependentFiles, Monitor monitor, string serverName)
{
    Driver driver = new Driver(name, dll, dataFile, configFile, helpFile, version, environment, defaultDataType, dependentFiles, monitor);
    driver.TryInstall(serverName);

    return driver;
}

/// <summary>
/// Устанавливает новый драйвер в системе.
/// </summary>
/// <param name="name">Наименование драйвера.</param>
/// <param name="dll">Полный или относительный путь к файлу драйвера устройства (например, "C:DRIVERSPscript.dll").</param>
/// <param name="dataFile">Полный или относительный путь к файлу данных драйвера (например, "C:DRIVERSQms810.ppd").</param>
/// <param name="configFile">Полный или относительный путь к dll данных конфигурации драйвера (например, "C:DRIVERSPscriptui.dll").</param>
/// <param name="helpFile">Полный или относительный путь к dll данных HLP-файла драйвера (например, "C:DRIVERSPscript.hlp").</param>
/// <param name="version">Номер версии операционной системы, для которой был написан драйвер. Поддерживаемые значения - 3 и 4 (V3 и V4 номера версий драйвера соответственно).</param>
/// <param name="environment">Окружение, для которого был написан драйвер (например, Windows x86, Windows IA64 или Windows x64).</param>
/// <param name="defaultDataType">Тип данных принтера по умолчанию.</param>
/// <param name="dependentFiles">Зависимые файлы.</param>
/// <param name="monitor">Монитор печати.</param>
/// <returns>Экземпляр драйвера печати.</returns>
public Driver InstallDriver(string name, string dll, string dataFile, string configFile, string helpFile, uint version, Environment environment,
    DataType defaultDataType, string dependentFiles, Monitor monitor)
    => InstallDriver(name, dll, dataFile, configFile, helpFile, version, environment, defaultDataType, dependentFiles, monitor, null);

/// <summary>
/// Устанавливает новый драйвер в системе.
/// </summary>
/// <param name="name">Наименование драйвера.</param>
/// <param name="dll">Полный или относительный путь к файлу драйвера устройства (например, "C:DRIVERSPscript.dll").</param>
/// <param name="dataFile">Полный или относительный путь к файлу данных драйвера (например, "C:DRIVERSQms810.ppd").</param>
/// <param name="configFile">Полный или относительный путь к dll данных конфигурации драйвера (например, "C:DRIVERSPscriptui.dll").</param>
/// <param name="helpFile">Полный или относительный путь к dll данных HLP-файла драйвера (например, "C:DRIVERSPscript.hlp").</param>
/// <param name="version">Номер версии операционной системы, для которой был написан драйвер. Поддерживаемые значения - 3 и 4 (V3 и V4 номера версий драйвера соответственно).</param>
/// <param name="environment">Окружение, для которого был написан драйвер (например, Windows x86, Windows IA64 или Windows x64).</param>
/// <param name="defaultDataType">Тип данных принтера по умолчанию.</param>
/// <param name="dependentFiles">Зависимые файлы.</param>
/// <param name="serverName">Имя сервера.</param>
/// <returns>Экземпляр драйвера печати.</returns>
public Driver InstallDriver(string name, string dll, string dataFile, string configFile, string helpFile, uint version, Environment environment,
    DataType defaultDataType, string dependentFiles, string serverName)
    => InstallDriver(name, dll, dataFile, configFile, helpFile, version, environment, defaultDataType, dependentFiles, null, serverName);

/// <summary>
/// Устанавливает новый драйвер в системе.
/// </summary>
/// <param name="name">Наименование драйвера.</param>
/// <param name="dll">Полный или относительный путь к файлу драйвера устройства (например, "C:DRIVERSPscript.dll").</param>
/// <param name="dataFile">Полный или относительный путь к файлу данных драйвера (например, "C:DRIVERSQms810.ppd").</param>
/// <param name="configFile">Полный или относительный путь к dll данных конфигурации драйвера (например, "C:DRIVERSPscriptui.dll").</param>
/// <param name="helpFile">Полный или относительный путь к dll данных HLP-файла драйвера (например, "C:DRIVERSPscript.hlp").</param>
/// <param name="version">Номер версии операционной системы, для которой был написан драйвер. Поддерживаемые значения - 3 и 4 (V3 и V4 номера версий драйвера соответственно).</param>
/// <param name="environment">Окружение, для которого был написан драйвер (например, Windows x86, Windows IA64 или Windows x64).</param>
/// <param name="defaultDataType">Тип данных принтера по умолчанию.</param>
/// <param name="dependentFiles">Зависимые файлы.</param>
/// <returns>Экземпляр драйвера печати.</returns>
public Driver InstallDriver(string name, string dll, string dataFile, string configFile, string helpFile, uint version, Environment environment,
    DataType defaultDataType, string dependentFiles)
    => InstallDriver(name, dll, dataFile, configFile, helpFile, version, environment, defaultDataType, dependentFiles, null as string);

/// <summary>
/// Устанавливает новый драйвер в системе.
/// </summary>
/// <param name="name">Наименование драйвера.</param>
/// <param name="dll">Полный или относительный путь к файлу драйвера устройства (например, "C:DRIVERSPscript.dll").</param>
/// <param name="dataFile">Полный или относительный путь к файлу данных драйвера (например, "C:DRIVERSQms810.ppd").</param>
/// <param name="configFile">Полный или относительный путь к dll данных конфигурации драйвера (например, "C:DRIVERSPscriptui.dll").</param>
/// <param name="helpFile">Полный или относительный путь к dll данных HLP-файла драйвера (например, "C:DRIVERSPscript.hlp").</param>
/// <param name="version">Номер версии операционной системы, для которой был написан драйвер. Поддерживаемые значения - 3 и 4 (V3 и V4 номера версий драйвера соответственно).</param>
/// <param name="environment">Окружение, для которого был написан драйвер (например, Windows x86, Windows IA64 или Windows x64).</param>
/// <param name="defaultDataType">Тип данных принтера по умолчанию.</param>
/// <returns>Экземпляр драйвера печати.</returns>
public Driver InstallDriver(string name, string dll, string dataFile, string configFile, string helpFile, uint version, Environment environment,
    DataType defaultDataType)
    => InstallDriver(name, dll, dataFile, configFile, helpFile, version, environment, defaultDataType, null);

/// <summary>
/// Устанавливает новый драйвер в системе.
/// </summary>
/// <param name="name">Наименование драйвера.</param>
/// <param name="dll">Полный или относительный путь к файлу драйвера устройства (например, "C:DRIVERSPscript.dll").</param>
/// <param name="dataFile">Полный или относительный путь к файлу данных драйвера (например, "C:DRIVERSQms810.ppd").</param>
/// <param name="configFile">Полный или относительный путь к dll данных конфигурации драйвера (например, "C:DRIVERSPscriptui.dll").</param>
/// <param name="helpFile">Полный или относительный путь к dll данных HLP-файла драйвера (например, "C:DRIVERSPscript.hlp").</param>
/// <param name="version">Номер версии операционной системы, для которой был написан драйвер. Поддерживаемые значения - 3 и 4 (V3 и V4 номера версий драйвера соответственно).</param>
/// <param name="environment">Окружение, для которого был написан драйвер (например, Windows x86, Windows IA64 или Windows x64).</param>
/// <param name="serverName">Имя сервера.</param>
/// <returns>Экземпляр драйвера печати.</returns>
public Driver InstallDriver(string name, string dll, string dataFile, string configFile, string helpFile, uint version, Environment environment,
    string serverName)
    => InstallDriver(name, dll, dataFile, configFile, helpFile, version, environment, DataType.RAW, serverName);

/// <summary>
/// Устанавливает новый драйвер в системе.
/// </summary>
/// <param name="name">Наименование драйвера.</param>
/// <param name="dll">Полный или относительный путь к файлу драйвера устройства (например, "C:DRIVERSPscript.dll").</param>
/// <param name="dataFile">Полный или относительный путь к файлу данных драйвера (например, "C:DRIVERSQms810.ppd").</param>
/// <param name="configFile">Полный или относительный путь к dll данных конфигурации драйвера (например, "C:DRIVERSPscriptui.dll").</param>
/// <param name="helpFile">Полный или относительный путь к dll данных HLP-файла драйвера (например, "C:DRIVERSPscript.hlp").</param>
/// <param name="version">Номер версии операционной системы, для которой был написан драйвер. Поддерживаемые значения - 3 и 4 (V3 и V4 номера версий драйвера соответственно).</param>
/// <param name="environment">Окружение, для которого был написан драйвер (например, Windows x86, Windows IA64 или Windows x64).</param>
/// <returns>Экземпляр драйвера печати.</returns>
public Driver InstallDriver(string name, string dll, string dataFile, string configFile, string helpFile, uint version, Environment environment)
    => InstallDriver(name, dll, dataFile, configFile, helpFile, version, environment, null);

/// <summary>
/// Устанавливает новый драйвер в системе.
/// </summary>
/// <param name="name">Наименование драйвера.</param>
/// <param name="dll">Полный или относительный путь к файлу драйвера устройства (например, "C:DRIVERSPscript.dll").</param>
/// <param name="dataFile">Полный или относительный путь к файлу данных драйвера (например, "C:DRIVERSQms810.ppd").</param>
/// <param name="configFile">Полный или относительный путь к dll данных конфигурации драйвера (например, "C:DRIVERSPscriptui.dll").</param>
/// <param name="helpFile">Полный или относительный путь к dll данных HLP-файла драйвера (например, "C:DRIVERSPscript.hlp").</param>
/// <param name="version">Номер версии операционной системы, для которой был написан драйвер. Поддерживаемые значения - 3 и 4 (V3 и V4 номера версий драйвера соответственно).</param>
/// <param name="serverName">Имя сервера.</param>
/// <returns>Экземпляр драйвера печати.</returns>
public Driver InstallDriver(string name, string dll, string dataFile, string configFile, string helpFile, uint version, string serverName)
    => InstallDriver(name, dll, dataFile, configFile, helpFile, version, Environment.Current, serverName);

/// <summary>
/// Устанавливает новый драйвер в системе.
/// </summary>
/// <param name="name">Наименование драйвера.</param>
/// <param name="dll">Полный или относительный путь к файлу драйвера устройства (например, "C:DRIVERSPscript.dll").</param>
/// <param name="dataFile">Полный или относительный путь к файлу данных драйвера (например, "C:DRIVERSQms810.ppd").</param>
/// <param name="configFile">Полный или относительный путь к dll данных конфигурации драйвера (например, "C:DRIVERSPscriptui.dll").</param>
/// <param name="helpFile">Полный или относительный путь к dll данных HLP-файла драйвера (например, "C:DRIVERSPscript.hlp").</param>
/// <param name="version">Номер версии операционной системы, для которой был написан драйвер. Поддерживаемые значения - 3 и 4 (V3 и V4 номера версий драйвера соответственно).</param>
/// <returns>Экземпляр драйвера печати.</returns>
public Driver InstallDriver(string name, string dll, string dataFile, string configFile, string helpFile, uint version)
    => InstallDriver(name, dll, dataFile, configFile, helpFile, version, null);

/// <summary>
/// Устанавливает новый драйвер в системе.
/// </summary>
/// <param name="name">Наименование драйвера.</param>
/// <param name="dll">Полный или относительный путь к файлу драйвера устройства (например, "C:DRIVERSPscript.dll").</param>
/// <param name="dataFile">Полный или относительный путь к файлу данных драйвера (например, "C:DRIVERSQms810.ppd").</param>
/// <param name="configFile">Полный или относительный путь к dll данных конфигурации драйвера (например, "C:DRIVERSPscriptui.dll").</param>
/// <param name="helpFile">Полный или относительный путь к dll данных HLP-файла драйвера (например, "C:DRIVERSPscript.hlp").</param>
/// <param name="serverName">Имя сервера.</param>
/// <returns>Экземпляр драйвера печати.</returns>
public Driver InstallDriver(string name, string dll, string dataFile, string configFile, string helpFile, string serverName)
    => InstallDriver(name, dll, dataFile, configFile, helpFile, 3, serverName);

/// <summary>
/// Устанавливает новый драйвер в системе.
/// </summary>
/// <param name="name">Наименование драйвера.</param>
/// <param name="dll">Полный или относительный путь к файлу драйвера устройства (например, "C:DRIVERSPscript.dll").</param>
/// <param name="dataFile">Полный или относительный путь к файлу данных драйвера (например, "C:DRIVERSQms810.ppd").</param>
/// <param name="configFile">Полный или относительный путь к dll данных конфигурации драйвера (например, "C:DRIVERSPscriptui.dll").</param>
/// <param name="helpFile">Полный или относительный путь к dll данных HLP-файла драйвера (например, "C:DRIVERSPscript.hlp").</param>
/// <returns>Экземпляр драйвера печати.</returns>
public Driver InstallDriver(string name, string dll, string dataFile, string configFile, string helpFile)
    => InstallDriver(name, dll, dataFile, configFile, helpFile, null);

/// <summary>
/// Устанавливает новый драйвер в системе.
/// </summary>
/// <param name="name">Наименование драйвера.</param>
/// <param name="dll">Полный или относительный путь к файлу драйвера устройства (например, "C:DRIVERSPscript.dll").</param>
/// <param name="dataFile">Полный или относительный путь к файлу данных драйвера (например, "C:DRIVERSQms810.ppd").</param>
/// <param name="configFile">Полный или относительный путь к dll данных конфигурации драйвера (например, "C:DRIVERSPscriptui.dll").</param>
/// <returns>Экземпляр драйвера печати.</returns>
public Driver InstallDriver(string name, string dll, string dataFile, string configFile) => InstallDriver(name, dll, dataFile, configFile, null);

Добавляем удаление драйверов, привязанных к монитору печати в методе удаления монитора

IEnumerable<Driver> drivers = Driver.All.Where(d => d.Monitor?.Name == Name);
foreach (Driver driver in drivers) driver.Uninstall(serverName);

Тестируем с помощью Unit-тестов

/// <summary>
/// Представляет тестовый модуль класса <see cref="Driver"/>.
/// </summary>
[TestClass]
public class DriverTests
{
    /// <summary>
    /// Наименование драйвера.
    /// </summary>
    protected const string DriverName = "Test Driver";

    /// <summary>
    /// Наименование монитора.
    /// </summary>
    protected const string MonitorName = "mfilemon";

    /// <summary>
    /// Наименование несуществующего монитора.
    /// </summary>
    protected const string FailedMonitorName = "noexist";

    protected const string DllPath = "D:/Printing Tests/pscript.dll";
    protected const string DataPath = "D:/Printing Tests/testprinter.ppd";
    protected const string ConfigPath = "D:/Printing Tests/pscriptui.dll";
    protected const string HelpPath = "D:/Printing Tests/pscript.hlp";

    /// <summary>
    /// Тест локальной установки драйвера.
    /// </summary>
    [TestMethod]
    public void InstallTest()
    {
        Driver driver = new Driver(DriverName, DllPath, DataPath, ConfigPath, HelpPath, MonitorName);
        driver.Install();

        Assert.IsTrue(Driver.All.Select(d => d.Name).Contains(DriverName));
    }

    /// <summary>
    /// Тест локального удаления драйвера.
    /// </summary>
    [TestMethod]
    public void UninstallTest()
    {
        Driver driver = new Driver(DriverName, DllPath, DataPath, ConfigPath, HelpPath, MonitorName);
        driver.Uninstall();

        Assert.IsFalse(Driver.All.Select(d => d.Name).Contains(DriverName));
    }

    /// <summary>
    /// Тест локальной установки драйвера с перехватом состояния установки.
    /// </summary>
    [TestMethod]
    public void TryInstallTest()
    {
        Driver driver = new Driver(DriverName, DllPath, DataPath, ConfigPath, HelpPath, MonitorName);
        bool f = driver.TryInstall();

        Assert.IsTrue(f);
        Assert.IsTrue(Driver.All.Select(d => d.Name).Contains(DriverName));
    }

    /// <summary>
    /// Тест локального удаления драйвера с перехватом состояния удаления.
    /// </summary>
    [TestMethod]
    public void TryUninstallTest()
    {
        Driver driver = new Driver(DriverName, DllPath, DataPath, ConfigPath, HelpPath, MonitorName);
        bool f = driver.TryUninstall();

        Assert.IsTrue(f);
        Assert.IsFalse(Driver.All.Select(d => d.Name).Contains(DriverName));
    }

    /// <summary>
    /// Тест неправильной локальной установки драйвера.
    /// </summary>
    [TestMethod]
    [ExpectedException(typeof(PrintingException))]
    public void InstallFailedTest()
    {
        Driver driver = new Driver(DriverName, DllPath + "failed", DataPath, ConfigPath, HelpPath, FailedMonitorName);
        driver.Install();

        Assert.IsFalse(Driver.All.Select(d => d.Name).Contains(DriverName));
    }

    /// <summary>
    /// Тест неправильной локальной установки драйвера с перехватом состояния установки.
    /// </summary>
    [TestMethod]
    public void TryInstallFailedTest()
    {
        Driver driver = new Driver(DriverName, DllPath + "failed", DataPath, ConfigPath, HelpPath, FailedMonitorName);
        bool f = driver.TryInstall();

        Assert.IsTrue(f);
        Assert.IsFalse(Driver.All.Select(d => d.Name).Contains(DriverName));
    }
}

Устройство печати

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

struct PrinterInfo

/// <summary>
/// Представляет структуру данных принтера.
/// </summary>
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public struct PrinterInfo
{
    /// <summary>
    /// Имя сервера.
    /// </summary>
    [MarshalAs(UnmanagedType.LPTStr)]
    public string ServerName;

    /// <summary>
    /// Наименование принтера.
    /// </summary>
    [MarshalAs(UnmanagedType.LPTStr)]
    public string PrinterName;

    /// <summary>
    /// Публичное наименование принтера.
    /// </summary>
    [MarshalAs(UnmanagedType.LPTStr)]
    public string ShareName;

    /// <summary>
    /// Наименование порта, привязанного к принтеру.
    /// </summary>
    [MarshalAs(UnmanagedType.LPTStr)]
    public string PortName;

    /// <summary>
    /// Наименование драйвера принтера.
    /// </summary>
    [MarshalAs(UnmanagedType.LPTStr)]
    public string DriverName;

    /// <summary>
    /// Описание принтера.
    /// </summary>
    [MarshalAs(UnmanagedType.LPTStr)]
    public string Comment;

    /// <summary>
    /// Местоположение принтера.
    /// </summary>
    [MarshalAs(UnmanagedType.LPTStr)]
    public string Location;

    public IntPtr DevMode;

    [MarshalAs(UnmanagedType.LPTStr)]
    public string SepFile;

    /// <summary>
    /// Процессор печати, связанный с принтером.
    /// </summary>
    [MarshalAs(UnmanagedType.LPTStr)]
    public string PrintProcessor;

    /// <summary>
    /// Тип данных печати принтера.
    /// </summary>
    [MarshalAs(UnmanagedType.LPTStr)]
    public string DataType;

    [MarshalAs(UnmanagedType.LPTStr)]
    public string Parameters;

    public IntPtr SecurityDescriptor;

    public uint Attributes;
    public uint Priority;
    public uint DefaultPriority;
    public uint StartTime;
    public uint UntilTime;
    public uint Status;
    public uint cJobs;
    public uint AveragePPM;
}

Для получения списка установленных принтеров нам так же понадобится флаг:

enum PrinterEnumFlag

/// <summary>
/// Флаги выборки принтеров при получении их списка.
/// </summary>
[Flags]
internal enum PrinterEnumFlag
{
    Default = 0x00000001,
    Local = 0x00000002,
    Connections = 0x00000004,
    Favorite = 0x00000004,
    Name = 0x00000008,
    Remote = 0x00000010,
    Shared = 0x00000020,
    Network = 0x00000040,
    Expand = 0x00004000,
    Container = 0x00008000,
    IconMask = 0x00ff0000,
    Icon1 = 0x00010000,
    Icon2 = 0x00020000,
    Icon3 = 0x00040000,
    Icon4 = 0x00080000,
    Icon5 = 0x00100000,
    Icon6 = 0x00200000,
    Icon7 = 0x00400000,
    Icon8 = 0x00800000,
    Hide = 0x01000000,
    All = 0x02000000,
    Category3D = 0x04000000,
}

interface IPrinter

/// <summary>
/// Представляет интерфейс для реализации принтеров.
/// </summary>
public interface IPrinter : IPrintableDevice
{
    /// <summary>
    /// Порт, к которому привязан принтер.
    /// </summary>
    IPort Port { get; }

    /// <summary>
    /// Драйвер, который связан с принтером.
    /// </summary>
    IDriver Driver { get; }

    /// <summary>
    /// Публичное наименование принтера.
    /// </summary>
    string ShareName { get; }

    /// <summary>
    /// Имя сервера, на котором запущен принтер.
    /// </summary>
    string ServerName { get; }

    /// <summary>
    /// Описание устройства принтера.
    /// </summary>
    string Description { get; }

    /// <summary>
    /// Расположение принтера.
    /// </summary>
    string Location { get; }
        
    string SepFile { get; }

    /// <summary>
    /// Параметры принтера.
    /// </summary>
    string Parameters { get; }

    /// <summary>
    /// Тип данных печати.
    /// </summary>
    DataType DataType { get; }
}

class Printer

/// <summary>
/// Представляет устройство принтера.
/// </summary>
public class Printer : PrintableDevice, IPrinter
{
    /// <summary>
    /// Порт, к которому привязан принтер.
    /// </summary>
    public virtual IPort Port { get; protected set; }

    /// <summary>
    /// Драйвер, который связан с принтером.
    /// </summary>
    public virtual IDriver Driver { get; protected set; }

    /// <summary>
    /// Публичное наименование принтера.
    /// </summary>
    public virtual string ShareName { get; protected set; }

    /// <summary>
    /// Описание устройства принтера.
    /// </summary>
    public virtual string Description { get; protected set; }

    /// <summary>
    /// Тип данных печати.
    /// </summary>
    public virtual DataType DataType { get; protected set; }

    /// <summary>
    /// Процессор очереди печати.
    /// </summary>
    public virtual string Processor { get; protected set; }

    /// <summary>
    /// Имя сервера, на котором запущен принтер.
    /// </summary>
    public virtual string ServerName { get; protected set; }

    /// <summary>
    /// Расположение принтера.
    /// </summary>
    public virtual string Location { get; protected set; }

    /// <summary>
    /// Параметры принтера.
    /// </summary>
    public virtual string Parameters { get; protected set; }

    public virtual string SepFile { get; protected set; }

    /// <summary>
    /// Задаёт или возвращает принтер по умолчанию.
    /// </summary>
    public static Printer Default
    {
        get
        {
            uint length = 0;

            if (GetDefaultPrinter(null, ref length)) return null;

            int lastWin32Error = Marshal.GetLastWin32Error();

            if (lastWin32Error != PrintingException.ErrorInsufficientBuffer) throw new PrintingException(lastWin32Error);

            StringBuilder printerName = new StringBuilder((int)length);

            if (!GetDefaultPrinter(printerName, ref length)) throw new PrintingException(Marshal.GetLastWin32Error());

            string name = printerName.ToString();

            return All.Where(p => p.Name == name).FirstOrDefault();
        }

        set
        {
            if (!SetDefaultPrinter(value?.Name)) throw new PrintingException(Marshal.GetLastWin32Error());
        }
    }

    /// <summary>
    /// Список всех установленных принтеров в системе.
    /// </summary>
    public static Printer[] All
    {
        get
        {
            uint bytesNeeded = 0;
            uint bufferReturnedLength = 0;
            uint level = 2;
            PrinterEnumFlag flags = PrinterEnumFlag.Local | PrinterEnumFlag.Network;

            if (EnumPrinters(flags, null, level, IntPtr.Zero, 0, ref bytesNeeded, ref bufferReturnedLength)) return null;

            int lastWin32Error = Marshal.GetLastWin32Error();

            if (lastWin32Error != PrintingException.ErrorInsufficientBuffer) throw new PrintingException(lastWin32Error);

            IntPtr printersPtr = Marshal.AllocHGlobal((int)bytesNeeded);

            try
            {
                if (EnumPrinters(flags, null, level, printersPtr, bytesNeeded, ref bytesNeeded, ref bufferReturnedLength))
                {
                    IntPtr currentPrinterPtr = printersPtr;
                    PrinterInfo[] printerInfo = new PrinterInfo[bufferReturnedLength];
                    Printer[] printers = new Printer[bufferReturnedLength];
                    Type type = typeof(PrinterInfo);

                    for (int i = 0; i < bufferReturnedLength; i++)
                    {
                        printerInfo[i] = (PrinterInfo)Marshal.PtrToStructure(currentPrinterPtr, type);
                        currentPrinterPtr = (IntPtr)(currentPrinterPtr.ToInt64() + Marshal.SizeOf(type));

                        printers[i] = new Printer(printerInfo[i].PrinterName, printerInfo[i].PortName, printerInfo[i].DriverName, printerInfo[i].PrintProcessor,
                            printerInfo[i].ShareName, printerInfo[i].ServerName, printerInfo[i].Comment,
                            (DataType)Enum.Parse(typeof(DataType), printerInfo[i].DataType), printerInfo[i].Location, printerInfo[i].Parameters,
                            printerInfo[i].SepFile);
                    }

                    return printers;
                }

                throw new PrintingException(Marshal.GetLastWin32Error());
            }
            catch
            {
                return null;
            }
            finally
            {
                Marshal.FreeHGlobal(printersPtr);
            }
        }
    }

    /// <summary>
    /// Инициализирует новый экземпляр класса <see cref="Printer"/>.
    /// </summary>
    /// <param name="name">Наименование принтера.</param>
    /// <param name="portName">Наименование порта.</param>
    /// <param name="driverName">Наименование драйвера.</param>
    /// <param name="processorName">Наименование процессора печати.</param>
    /// <param name="shareName">Публичное наименование принтера.</param>
    /// <param name="serverName">Имя сервера.</param>
    /// <param name="description">Описание принтера.</param>
    /// <param name="dataType">Тип данных печати.</param>
    /// <param name="location">Местоположение принтера.</param>
    /// <param name="parameters">Параметры принтера.</param>
    /// <param name="sepFile"></param>
    public Printer(string name, string portName, string driverName, string processorName, string shareName, string serverName, string description, DataType dataType,
        string location, string parameters, string sepFile) : base(name)
    {
        Port[] ports = PrintingApi.Ports;
        Driver[] drivers = PrintingApi.Drivers;

        if (ports.Select(p => p.Name).Contains(portName)) Port = ports.Where(p => p.Name == portName).FirstOrDefault();
        if (drivers.Select(d => d.Name).Contains(driverName)) Driver = drivers.Where(d => d.Name == driverName).FirstOrDefault();

        Processor = processorName;
        ShareName = shareName;
        ServerName = serverName;
        Description = description;
        DataType = dataType;
        Location = location;
        Parameters = parameters;
        SepFile = sepFile;
    }

    /// <summary>
    /// Инициализирует новый экземпляр класса <see cref="Printer"/>.
    /// </summary>
    /// <param name="name">Наименование принтера.</param>
    /// <param name="port">Порт, к которому привязан принтер.</param>
    /// <param name="driver">Драйвер, который связан с принтером.</param>
    /// <param name="processorName">Наименование процессора печати.</param>
    /// <param name="shareName">Публичное наименование принтера.</param>
    /// <param name="serverName">Имя сервера.</param>
    /// <param name="description">Описание принтера.</param>
    /// <param name="dataType">Тип данных печати.</param>
    /// <param name="location">Местоположение принтера.</param>
    /// <param name="parameters">Параметры принтера.</param>
    /// <param name="sepFile"></param>
    public Printer(string name, IPort port, IDriver driver, string processorName, string shareName, string serverName, string description, DataType dataType,
        string location, string parameters, string sepFile)
        : this(name, port?.Name, driver?.Name, processorName, shareName, serverName, description, dataType, location, parameters, sepFile) { }

    /// <summary>
    /// Инициализирует новый экземпляр класса <see cref="Printer"/>.
    /// </summary>
    /// <param name="name">Наименование принтера.</param>
    /// <param name="portName">Наименование порта.</param>
    /// <param name="driverName">Наименование драйвера.</param>
    /// <param name="processorName">Наименование процессора печати.</param>
    /// <param name="shareName">Публичное наименование принтера.</param>
    /// <param name="serverName">Имя сервера.</param>
    /// <param name="description">Описание принтера.</param>
    /// <param name="dataType">Тип данных печати.</param>
    /// <param name="location">Местоположение принтера.</param>
    /// <param name="parameters">Параметры принтера.</param>
    public Printer(string name, string portName, string driverName, string processorName, string shareName, string serverName, string description, DataType dataType,
        string location, string parameters)
        : this(name, portName, driverName, processorName, shareName, serverName, description, dataType, location, parameters, null) { }

    /// <summary>
    /// Инициализирует новый экземпляр класса <see cref="Printer"/>.
    /// </summary>
    /// <param name="name">Наименование принтера.</param>
    /// <param name="port">Порт, к которому привязан принтер.</param>
    /// <param name="driver">Драйвер, который связан с принтером.</param>
    /// <param name="processorName">Наименование процессора печати.</param>
    /// <param name="shareName">Публичное наименование принтера.</param>
    /// <param name="serverName">Имя сервера.</param>
    /// <param name="description">Описание принтера.</param>
    /// <param name="dataType">Тип данных печати.</param>
    /// <param name="location">Местоположение принтера.</param>
    /// <param name="parameters">Параметры принтера.</param>
    public Printer(string name, IPort port, IDriver driver, string processorName, string shareName, string serverName, string description, DataType dataType,
        string location, string parameters)
        : this(name, port?.Name, driver?.Name, processorName, shareName, serverName, description, dataType, location, parameters) { }

    /// <summary>
    /// Инициализирует новый экземпляр класса <see cref="Printer"/>.
    /// </summary>
    /// <param name="name">Наименование принтера.</param>
    /// <param name="portName">Наименование порта.</param>
    /// <param name="driverName">Наименование драйвера.</param>
    /// <param name="processorName">Наименование процессора печати.</param>
    /// <param name="shareName">Публичное наименование принтера.</param>
    /// <param name="serverName">Имя сервера.</param>
    /// <param name="description">Описание принтера.</param>
    /// <param name="dataType">Тип данных печати.</param>
    /// <param name="location">Местоположение принтера.</param>
    public Printer(string name, string portName, string driverName, string processorName, string shareName, string serverName, string description, DataType dataType,
        string location)
        : this(name, portName, driverName, processorName, shareName, serverName, description, dataType, location, null) { }

    /// <summary>
    /// Инициализирует новый экземпляр класса <see cref="Printer"/>.
    /// </summary>
    /// <param name="name">Наименование принтера.</param>
    /// <param name="port">Порт, к которому привязан принтер.</param>
    /// <param name="driver">Драйвер, который связан с принтером.</param>
    /// <param name="processorName">Наименование процессора печати.</param>
    /// <param name="shareName">Публичное наименование принтера.</param>
    /// <param name="serverName">Имя сервера.</param>
    /// <param name="description">Описание принтера.</param>
    /// <param name="dataType">Тип данных печати.</param>
    /// <param name="location">Местоположение принтера.</param>
    public Printer(string name, IPort port, IDriver driver, string processorName, string shareName, string serverName, string description, DataType dataType,
        string location)
        : this(name, port?.Name, driver?.Name, processorName, shareName, serverName, description, dataType, location) { }

    /// <summary>
    /// Инициализирует новый экземпляр класса <see cref="Printer"/>.
    /// </summary>
    /// <param name="name">Наименование принтера.</param>
    /// <param name="portName">Наименование порта.</param>
    /// <param name="driverName">Наименование драйвера.</param>
    /// <param name="processorName">Наименование процессора печати.</param>
    /// <param name="shareName">Публичное наименование принтера.</param>
    /// <param name="serverName">Имя сервера.</param>
    /// <param name="description">Описание принтера.</param>
    /// <param name="dataType">Тип данных печати.</param>
    public Printer(string name, string portName, string driverName, string processorName, string shareName, string serverName, string description, DataType dataType)
        : this(name, portName, driverName, processorName, shareName, serverName, description, dataType, null) { }

    /// <summary>
    /// Инициализирует новый экземпляр класса <see cref="Printer"/>.
    /// </summary>
    /// <param name="name">Наименование принтера.</param>
    /// <param name="port">Порт, к которому привязан принтер.</param>
    /// <param name="driver">Драйвер, который связан с принтером.</param>
    /// <param name="processorName">Наименование процессора печати.</param>
    /// <param name="shareName">Публичное наименование принтера.</param>
    /// <param name="serverName">Имя сервера.</param>
    /// <param name="description">Описание принтера.</param>
    /// <param name="dataType">Тип данных печати.</param>
    public Printer(string name, IPort port, IDriver driver, string processorName, string shareName, string serverName, string description, DataType dataType)
        : this(name, port?.Name, driver?.Name, processorName, shareName, serverName, description, dataType) { }

    /// <summary>
    /// Инициализирует новый экземпляр класса <see cref="Printer"/>.
    /// </summary>
    /// <param name="name">Наименование принтера.</param>
    /// <param name="portName">Наименование порта.</param>
    /// <param name="driverName">Наименование драйвера.</param>
    /// <param name="processorName">Наименование процессора печати.</param>
    /// <param name="shareName">Публичное наименование принтера.</param>
    /// <param name="serverName">Имя сервера.</param>
    /// <param name="description">Описание принтера.</param>
    public Printer(string name, string portName, string driverName, string processorName, string shareName, string serverName, string description)
        : this(name, portName, driverName, processorName, shareName, serverName, description, DataType.RAW) { }

    /// <summary>
    /// Инициализирует новый экземпляр класса <see cref="Printer"/>.
    /// </summary>
    /// <param name="name">Наименование принтера.</param>
    /// <param name="port">Порт, к которому привязан принтер.</param>
    /// <param name="driver">Драйвер, который связан с принтером.</param>
    /// <param name="processorName">Наименование процессора печати.</param>
    /// <param name="shareName">Публичное наименование принтера.</param>
    /// <param name="serverName">Имя сервера.</param>
    /// <param name="description">Описание принтера.</param>
    public Printer(string name, IPort port, IDriver driver, string processorName, string shareName, string serverName, string description)
        : this(name, port?.Name, driver?.Name, processorName, shareName, serverName, description) { }

    /// <summary>
    /// Инициализирует новый экземпляр класса <see cref="Printer"/>.
    /// </summary>
    /// <param name="name">Наименование принтера.</param>
    /// <param name="portName">Наименование порта.</param>
    /// <param name="driverName">Наименование драйвера.</param>
    /// <param name="processorName">Наименование процессора печати.</param>
    /// <param name="shareName">Публичное наименование принтера.</param>
    /// <param name="serverName">Имя сервера.</param>
    public Printer(string name, string portName, string driverName, string processorName, string shareName, string serverName)
        : this(name, portName, driverName, processorName, shareName, serverName, null) { }

    /// <summary>
    /// Инициализирует новый экземпляр класса <see cref="Printer"/>.
    /// </summary>
    /// <param name="name">Наименование принтера.</param>
    /// <param name="port">Порт, к которому привязан принтер.</param>
    /// <param name="driver">Драйвер, который связан с принтером.</param>
    /// <param name="processorName">Наименование процессора печати.</param>
    /// <param name="shareName">Публичное наименование принтера.</param>
    /// <param name="serverName">Имя сервера.</param>
    public Printer(string name, IPort port, IDriver driver, string processorName, string shareName, string serverName)
        : this(name, port?.Name, driver?.Name, processorName, shareName, serverName) { }

    /// <summary>
    /// Инициализирует новый экземпляр класса <see cref="Printer"/>.
    /// </summary>
    /// <param name="name">Наименование принтера.</param>
    /// <param name="portName">Наименование порта.</param>
    /// <param name="driverName">Наименование драйвера.</param>
    /// <param name="processorName">Наименование процессора печати.</param>
    /// <param name="shareName">Публичное наименование принтера.</param>
    public Printer(string name, string portName, string driverName, string processorName, string shareName)
        : this(name, portName, driverName, processorName, shareName, null) { }

    /// <summary>
    /// Инициализирует новый экземпляр класса <see cref="Printer"/>.
    /// </summary>
    /// <param name="name">Наименование принтера.</param>
    /// <param name="port">Порт, к которому привязан принтер.</param>
    /// <param name="driver">Драйвер, который связан с принтером.</param>
    /// <param name="processorName">Наименование процессора печати.</param>
    /// <param name="shareName">Публичное наименование принтера.</param>
    public Printer(string name, IPort port, IDriver driver, string processorName, string shareName) : this(name, port?.Name, driver?.Name, processorName, shareName)
    { }

    /// <summary>
    /// Инициализирует новый экземпляр класса <see cref="Printer"/>.
    /// </summary>
    /// <param name="name">Наименование принтера.</param>
    /// <param name="portName">Наименование порта.</param>
    /// <param name="driverName">Наименование драйвера.</param>
    /// <param name="processorName">Наименование процессора печати.</param>
    public Printer(string name, string portName, string driverName, string processorName) : this(name, portName, driverName, processorName, null) { }

    /// <summary>
    /// Инициализирует новый экземпляр класса <see cref="Printer"/>.
    /// </summary>
    /// <param name="name">Наименование принтера.</param>
    /// <param name="port">Порт, к которому привязан принтер.</param>
    /// <param name="driver">Драйвер, который связан с принтером.</param>
    /// <param name="processorName">Наименование процессора печати.</param>
    public Printer(string name, IPort port, IDriver driver, string processorName) : this(name, port?.Name, driver?.Name, processorName) { }

    /// <summary>
    /// Инициализирует новый экземпляр класса <see cref="Printer"/>.
    /// </summary>
    /// <param name="name">Наименование принтера.</param>
    /// <param name="portName">Наименование порта.</param>
    /// <param name="driverName">Наименование драйвера.</param>
    public Printer(string name, string portName, string driverName) : this(name, portName, driverName, "WinPrint") { }

    /// <summary>
    /// Инициализирует новый экземпляр класса <see cref="Printer"/>.
    /// </summary>
    /// <param name="name">Наименование принтера.</param>
    /// <param name="port">Порт, к которому привязан принтер.</param>
    /// <param name="driver">Драйвер, который связан с принтером.</param>
    public Printer(string name, IPort port, IDriver driver) : this(name, port?.Name, driver?.Name) { }

    /// <summary>
    /// Устанавливает принтер в системе.
    /// </summary>
    /// <param name="serverName">Имя сервера.</param>
    public override void Install(string serverName)
    {
        try
        {
            if (All.Select(p => p.Name).Contains(Name)) Uninstall(serverName);

            PrinterInfo printerInfo = new PrinterInfo
            {
                ServerName = serverName,
                PrinterName = Name,
                ShareName = ShareName,
                PortName = Port?.Name,
                DriverName = Driver?.Name,
                Comment = Description,
                Location = Location,
                DevMode = new IntPtr(0),
                SepFile = SepFile,
                PrintProcessor = Processor,
                DataType = Enum.GetName(typeof(DataType), DataType),
                Parameters = Parameters,
                SecurityDescriptor = new IntPtr(0),
            };

            if (AddPrinter(serverName, 2, ref printerInfo)) return;

            int lastWin32ErrorCode = Marshal.GetLastWin32Error();

            if (lastWin32ErrorCode == 0) return;

            throw new PrintingException(lastWin32ErrorCode);
        }
        catch (Exception e)
        {
            throw new PrintingException(e.Message, e);
        }
    }

    /// <summary>
    /// Удаляет принтер из системы.
    /// </summary>
    /// <param name="serverName">Имя сервера.</param>
    public override void Uninstall(string serverName)
    {
        try
        {
            if (!All.Select(p => p.Name).Contains(Name)) return;

            PrinterDefaults defaults = new PrinterDefaults { DesiredAccess = PrinterAccess.PrinterAllAccess };

            if (!NET.Port.OpenPrinter(Name, out IntPtr printerHandle, ref defaults)) throw new PrintingException(Marshal.GetLastWin32Error());

            if (!DeletePrinter(printerHandle))
            {
                int lastWin32ErrorCode = Marshal.GetLastWin32Error();

                if (lastWin32ErrorCode == PrintingException.ErrorInvalidPrinterName) return;

                throw new PrintingException(lastWin32ErrorCode);
            }

            NET.Port.ClosePrinter(printerHandle);
        }
        catch (Exception e)
        {
            throw new PrintingException(e.Message, e);
        }
    }

    #region Native
    /// <summary>
    /// Устанавливает принтер в системе.
    /// </summary>
    /// <param name="serverName">Имя сервера.</param>
    /// <param name="level">Уровень структуры.</param>
    /// <param name="printerInfo">Структура данных.</param>
    /// <returns>True, если операция прошла успешно, иначе False.</returns>
    [DllImport("winspool.drv", CharSet = CharSet.Auto, SetLastError = true, CallingConvention = CallingConvention.StdCall)]
    internal static extern bool AddPrinter(string serverName, uint level, [In] ref PrinterInfo printerInfo);

    /// <summary>
    /// Возвращает имя принтера, установленного в системе по умолчанию.
    /// </summary>
    /// <param name="printerName">Имя принтера.</param>
    /// <param name="bytesNeeded">Размер символьного буфера имени.</param>
    /// <returns>True, если операция прошла успешно, иначе False.</returns>
    [DllImport("winspool.drv", CharSet = CharSet.Auto, SetLastError = true)]
    internal static extern bool GetDefaultPrinter([Out] StringBuilder printerName, ref uint bytesNeeded);

    /// <summary>
    /// Устанавливает имя принтера по умолчанию.
    /// </summary>
    /// <param name="printerName">Имя устанавливаемого по умолчанию принтера.</param>
    /// <returns>True, если операция прошла успешно, иначе False.</returns>
    [DllImport("winspool.drv", CharSet = CharSet.Auto, SetLastError = true)]
    internal static extern bool SetDefaultPrinter(string printerName);

    /// <summary>
    /// Получает список установленных в системе принтеров.
    /// </summary>
    /// <param name="flags">Флаги для выборки результатов.</param>
    /// <param name="serverName">Имя сервера.</param>
    /// <param name="level">Уровень структуры.</param>
    /// <param name="printers">Указатель на буфер структур.</param>
    /// <param name="bufferSize">Размер буфера.</param>
    /// <param name="bytesNeeded">Число требуемых байт для выделения памяти под буфер.</param>
    /// <param name="bufferReturnedLength">Размер полученного буфера.</param>
    /// <returns>True, если операция прошла успешно, иначе False.</returns>
    [DllImport("winspool.drv", CharSet = CharSet.Auto, SetLastError = true)]
    internal static extern bool EnumPrinters(PrinterEnumFlag flags, string serverName, uint level, IntPtr printers, uint bufferSize, ref uint bytesNeeded,
        ref uint bufferReturnedLength);

    /// <summary>
    /// Удаляет принтер из системы.
    /// </summary>
    /// <param name="printer">Указатель на принтер.</param>
    /// <returns>True, если операция прошла успешно, иначе False.</returns>
    [DllImport("winspool.drv", CharSet = CharSet.Auto, SetLastError = true, CallingConvention = CallingConvention.StdCall)]
    internal static extern bool DeletePrinter(IntPtr printer);
    #endregion
}

Расширяем PrintingApi

/// <summary>
/// Возвращает коллекцию всех установленных устройств печати в системе.
/// </summary>
public static Printer[] Printers => Printer.All;

/// <summary>
/// Инициализирует новый экземпляр класса <see cref="Printer"/>.
/// </summary>
/// <param name="name">Наименование принтера.</param>
/// <param name="port">Порт, к которому привязан принтер.</param>
/// <param name="driver">Драйвер, который связан с принтером.</param>
/// <param name="processorName">Наименование процессора печати.</param>
/// <param name="shareName">Публичное наименование принтера.</param>
/// <param name="serverName">Имя сервера.</param>
/// <param name="description">Описание принтера.</param>
/// <param name="dataType">Тип данных печати.</param>
/// <param name="location">Местоположение принтера.</param>
/// <param name="parameters">Параметры принтера.</param>
/// <param name="sepFile"></param>
/// <returns>Экземпляр устройства печати.</returns>
public Printer RunPrinter(string name, Port port, Driver driver, string processorName, string shareName, string serverName, string description,
    DataType dataType, string location, string parameters, string sepFile)
{
    Printer printer = new Printer(name, port, driver, processorName, shareName, serverName, description, dataType, location, parameters, sepFile);
    printer.TryInstall(serverName);

    return printer;
}

/// <summary>
/// Инициализирует новый экземпляр класса <see cref="Printer"/>.
/// </summary>
/// <param name="name">Наименование принтера.</param>
/// <param name="port">Порт, к которому привязан принтер.</param>
/// <param name="driver">Драйвер, который связан с принтером.</param>
/// <param name="processorName">Наименование процессора печати.</param>
/// <param name="shareName">Публичное наименование принтера.</param>
/// <param name="serverName">Имя сервера.</param>
/// <param name="description">Описание принтера.</param>
/// <param name="dataType">Тип данных печати.</param>
/// <param name="location">Местоположение принтера.</param>
/// <param name="parameters">Параметры принтера.</param>
/// <returns>Экземпляр устройства печати.</returns>
public Printer RunPrinter(string name, Port port, Driver driver, string processorName, string shareName, string serverName, string description,
    DataType dataType, string location, string parameters)
    => RunPrinter(name, port, driver, processorName, shareName, serverName, description, dataType, location, parameters, null);

/// <summary>
/// Инициализирует новый экземпляр класса <see cref="Printer"/>.
/// </summary>
/// <param name="name">Наименование принтера.</param>
/// <param name="port">Порт, к которому привязан принтер.</param>
/// <param name="driver">Драйвер, который связан с принтером.</param>
/// <param name="processorName">Наименование процессора печати.</param>
/// <param name="shareName">Публичное наименование принтера.</param>
/// <param name="serverName">Имя сервера.</param>
/// <param name="description">Описание принтера.</param>
/// <param name="dataType">Тип данных печати.</param>
/// <param name="location">Местоположение принтера.</param>
/// <returns>Экземпляр устройства печати.</returns>
public Printer RunPrinter(string name, Port port, Driver driver, string processorName, string shareName, string serverName, string description,
    DataType dataType, string location)
    => RunPrinter(name, port, driver, processorName, shareName, serverName, description, dataType, location, null);

/// <summary>
/// Инициализирует новый экземпляр класса <see cref="Printer"/>.
/// </summary>
/// <param name="name">Наименование принтера.</param>
/// <param name="port">Порт, к которому привязан принтер.</param>
/// <param name="driver">Драйвер, который связан с принтером.</param>
/// <param name="processorName">Наименование процессора печати.</param>
/// <param name="shareName">Публичное наименование принтера.</param>
/// <param name="serverName">Имя сервера.</param>
/// <param name="description">Описание принтера.</param>
/// <param name="dataType">Тип данных печати.</param>
/// <returns>Экземпляр устройства печати.</returns>
public Printer RunPrinter(string name, Port port, Driver driver, string processorName, string shareName, string serverName, string description,
    DataType dataType)
    => RunPrinter(name, port, driver, processorName, shareName, serverName, description, dataType, null);

/// <summary>
/// Инициализирует новый экземпляр класса <see cref="Printer"/>.
/// </summary>
/// <param name="name">Наименование принтера.</param>
/// <param name="port">Порт, к которому привязан принтер.</param>
/// <param name="driver">Драйвер, который связан с принтером.</param>
/// <param name="processorName">Наименование процессора печати.</param>
/// <param name="shareName">Публичное наименование принтера.</param>
/// <param name="serverName">Имя сервера.</param>
/// <param name="description">Описание принтера.</param>
/// <returns>Экземпляр устройства печати.</returns>
public Printer RunPrinter(string name, Port port, Driver driver, string processorName, string shareName, string serverName, string description)
    => RunPrinter(name, port, driver, processorName, shareName, serverName, description, DataType.RAW);

/// <summary>
/// Инициализирует новый экземпляр класса <see cref="Printer"/>.
/// </summary>
/// <param name="name">Наименование принтера.</param>
/// <param name="port">Порт, к которому привязан принтер.</param>
/// <param name="driver">Драйвер, который связан с принтером.</param>
/// <param name="processorName">Наименование процессора печати.</param>
/// <param name="shareName">Публичное наименование принтера.</param>
/// <param name="serverName">Имя сервера.</param>
/// <returns>Экземпляр устройства печати.</returns>
public Printer RunPrinter(string name, Port port, Driver driver, string processorName, string shareName, string serverName)
    => RunPrinter(name, port, driver, processorName, shareName, serverName, null);

/// <summary>
/// Инициализирует новый экземпляр класса <see cref="Printer"/>.
/// </summary>
/// <param name="name">Наименование принтера.</param>
/// <param name="port">Порт, к которому привязан принтер.</param>
/// <param name="driver">Драйвер, который связан с принтером.</param>
/// <param name="processorName">Наименование процессора печати.</param>
/// <param name="shareName">Публичное наименование принтера.</param>
/// <returns>Экземпляр устройства печати.</returns>
public Printer RunPrinter(string name, Port port, Driver driver, string processorName, string shareName)
    => RunPrinter(name, port, driver, processorName, shareName, null);

/// <summary>
/// Инициализирует новый экземпляр класса <see cref="Printer"/>.
/// </summary>
/// <param name="name">Наименование принтера.</param>
/// <param name="port">Порт, к которому привязан принтер.</param>
/// <param name="driver">Драйвер, который связан с принтером.</param>
/// <param name="processorName">Наименование процессора печати.</param>
/// <returns>Экземпляр устройства печати.</returns>
public Printer RunPrinter(string name, Port port, Driver driver, string processorName) => RunPrinter(name, port, driver, processorName, null);

/// <summary>
/// Инициализирует новый экземпляр класса <see cref="Printer"/>.
/// </summary>
/// <param name="name">Наименование принтера.</param>
/// <param name="port">Порт, к которому привязан принтер.</param>
/// <param name="driver">Драйвер, который связан с принтером.</param>
/// <returns>Экземпляр устройства печати.</returns>
public Printer RunPrinter(string name, Port port, Driver driver) => RunPrinter(name, port, driver, "WinPrint");

Добавляем удаление всех привязанных принтеров к удаляемому порту и драйверу

IEnumerable<Printer> printers = Printer.All.Where(p => p.Driver?.Name == Name);
foreach (Printer printer in printers) printer.Uninstall(serverName);

Тестируем

/// <summary>
/// Представляет тестовый модуль класса <see cref="Printer"/>.
/// </summary>
[TestClass]
public class PrinterTests
{
    /// <summary>
    /// Наименование принтера.
    /// </summary>
    protected const string PrinterName = "Test Printer";

    /// <summary>
    /// Наименование порта.
    /// </summary>
    protected const string PortName = "TESTPORT:";
        
    /// <summary>
    /// Наименование драйвера.
    /// </summary>
    protected const string DriverName = "Test Driver";
        
    /// <summary>
    /// Тест локальной установки принтера.
    /// </summary>
    [TestMethod]
    public void InstallTest()
    {
        Printer printer = new Printer(PrinterName, PortName, DriverName);
        printer.Install();

        Assert.IsTrue(Printer.All.Select(p => p.Name).Contains(PrinterName));
    }

    /// <summary>
    /// Тест локального удаления принтера.
    /// </summary>
    [TestMethod]
    public void UninstallTest()
    {
        Printer printer = new Printer(PrinterName, PortName, DriverName);
        printer.Uninstall();

        Assert.IsFalse(Printer.All.Select(p => p.Name).Contains(PrinterName));
    }

    /// <summary>
    /// Тест локальной установки принтера с перехватом состояния установки.
    /// </summary>
    [TestMethod]
    public void TryInstallTest()
    {
        Printer printer = new Printer(PrinterName, PortName, DriverName);
        bool f = printer.TryInstall();

        Assert.IsTrue(f);
        Assert.IsTrue(Printer.All.Select(p => p.Name).Contains(PrinterName));
    }

    /// <summary>
    /// Тест локального удаления принтера с перехватом состояния удаления.
    /// </summary>
    [TestMethod]
    public void TryUninstallTest()
    {
        Printer printer = new Printer(PrinterName, PortName, DriverName);
        bool f = printer.TryUninstall();

        Assert.IsTrue(f);
        Assert.IsFalse(Printer.All.Select(p => p.Name).Contains(PrinterName));
    }
}

Для того, чтобы изменения в системе вступили в силу, после установки принтера нам понадобится перезапустить службу печати вручную. Напишем статический метод в классе PrintingApi, который будет запускать/перезапускать Spooler. Это так же актуально для случаев, когда служба печати на компьютере была изначально остановлена:

/// <summary>
/// Перезагружает службу печати.
/// </summary>
/// <returns>True, если операция прошла успешно, иначе False.</returns>
public static bool TryRestart()
{
    int tryCount = 5;

    while (tryCount > 0)
    {
        try
        {
            ServiceController sc = new ServiceController("Spooler");

            if (sc.Status != ServiceControllerStatus.Stopped || sc.Status != ServiceControllerStatus.StopPending)
            {
                sc.Stop();
                sc.WaitForStatus(ServiceControllerStatus.Stopped);
            }

            sc.Start();
            sc.WaitForStatus(ServiceControllerStatus.Running);

            return sc.Status == ServiceControllerStatus.Running;
        }
        catch
        {
            tryCount--;
        }
    }

    return false;
}

Необходимо будет подключить в проект ссылку на System.ServiceProcess.dll. Здесь всё просто: запускаем контроллер службы, проверяем статус, если запущена — останавливаем, ждём пока остановится, затем запускаем и ждём пока статус поменяется на «запущено», в случае ошибки (например, если служба в данный момент занята) пытаемся повторить процедуру ещё четыре раза.

На этом базовый функционал для работы с устройствами печати можно считать готовым. Итак, что умеет наш API на данный момент:

  • Получать коллекцию установленных в системе мониторов, драйверов, открытых портов и запущеных принтеров;
  • Устанавливать новый монитор. В случае, если такой монитор уже установлен — переустанавливать его;
  • Удалять монитор. В случае, если на мониторе открыты порты или привязаны драйвера — предварительно удалять и их тоже;
  • Открывать новый порт. В случае, если такой порт уже открыт — переоткрывать его;
  • Закрывать порт. В случае, если к порту привязаны принтеры — предварительно отключать их;
  • Устанавливать драйвера принтера. В случае, если драйвер уже установлен — переустанавливать его;
  • Удалять драйвера принтера. В случае, если драйвер используется принтерами — предварительно отключать их;
  • Запускать принтер. В случае, если принтер с заданным именем уже запущен — перезапускать его;
  • Отключать принтер;
  • Отлавливать любые ошибки CLR и Win32;
  • Перезапускать службу печати.

Делаем последний общий тест для класса PrintingApi и переходим к заключительной части статьи:

Unit-тест для проверки последовательной установки всех компонентов устройства печати

[TestClass]
public class PrintingApiTests
{
    protected const string MonitorName = "mfilemon";
    protected const string PortName = "TESTPORT:";
    protected const string DriverName = "Test Driver";
    protected const string PrinterName = "Test Printer";

    protected const string MonitorFile = "D:/Printing Tests/mfilemon.dll";
    protected const string DriverFile = "D:/Printing Tests/pscript5.dll";
    protected const string DriverDataFile = "D:/Printing Tests/testprinter.ppd";
    protected const string DriverConfigFile = "D:/Printing Tests/ps5ui.dll";
    protected const string DriverHelpFile = "D:/Printing Tests/pscript.hlp";

    [TestMethod]
    public void PrinterInstallationTest()
    {
        PrintingApi.TryRestart();

        Monitor monitor = PrintingApi.Factory.CreateMonitor(MonitorName, MonitorFile);
        Port port = PrintingApi.Factory.OpenPort(PortName, monitor);
        Driver driver = PrintingApi.Factory.InstallDriver(DriverName, DriverFile, DriverDataFile, DriverConfigFile, DriverHelpFile, 3, Environment.Current, DataType.RAW, null, monitor);
        Printer printer = PrintingApi.Factory.RunPrinter(PrinterName, port, driver);

        PrintingApi.TryRestart();

        Assert.IsNotNull(printer);
    }
}

Работа с устройствами печати в C# на примере реализации виртуального принтера - 3
Работа с устройствами печати в C# на примере реализации виртуального принтера - 4
Обратите внимание, практически все нативные методы Spooler блокируют поток, в котором они вызываются, не забывайте проводить операции с нашим API в асинхронном режиме, дабы избежать подвисания главного STA-потока UI.

Работа с данными

После установки виртуального принтера в систему, необходимо сконфигурировать монитор. Здесь всё зависит от спецификации монитора, для этого нужно изучать его документацию. Конкретно в нашем случае, mfilemon.dll конфигурируется с помощью рееста:

string monitorName = "mfilemon";
string portName = "TESTPORT:";
string keyName = $"SYSTEM\CurrentControlSet\Control\Print\Monitors\{monitorName}\{portName}";

Registry.LocalMachine.CreateSubKey(keyName);

using (RegistryKey regKey = Registry.LocalMachine.OpenSubKey(keyName, true))
{
	regKey.SetValue("OutputPath", "D:/Printing Tests/", RegistryValueKind.String);
	regKey.SetValue("FilePattern", "%r_%c_%u_%Y%m%d_%H%n%s_%j.ps", RegistryValueKind.String);
	regKey.SetValue("Overwrite", 0, RegistryValueKind.DWord);
	regKey.SetValue("UserCommand", string.Empty, RegistryValueKind.String);
	regKey.SetValue("ExecPath", string.Empty, RegistryValueKind.String);
	regKey.SetValue("WaitTermination", 0, RegistryValueKind.DWord);
	regKey.SetValue("PipeData", 0, RegistryValueKind.DWord);
}

Теперь в каталоге «D:/Printing Tests/» будут появляться уже сформированные PostScript-файлы описания страниц, мы можем делать с ними что угодно, хоть преобразовать в текстовый формат или PDF, распарсив по старинке (упаси Боже) или воспользовавшись средствами GhostScript, хоть переслать на сервер, хоть загрузить в память. Осталось только перехватить момент создания файла после печати, для этого в System.IO предусмотрен класс FileSystemWatcher, который следит за изменением состояния файловой системы и может вызывать наши обработчики событий:

// Инициализируем, указываем наш каталог для слежения за изменением состояния, указываем файловую маску, отфильтровываем ненужные уведомления об изменении состояния директории.
FileSystemWatcher fileSystemWatcher = new FileSystemWatcher("D:/Printing Tests/", "*.ps")
{
    NotifyFilter = NotifyFilters.DirectoryName
};

fileSystemWatcher.NotifyFilter = fileSystemWatcher.NotifyFilter | NotifyFilters.FileName;
fileSystemWatcher.NotifyFilter = fileSystemWatcher.NotifyFilter | NotifyFilters.Attributes;

fileSystemWatcher.Created += new FileSystemEventHandler(PrinterHandler);    // Подписываемся на событие создания файла.

try
{
    fileSystemWatcher.EnableRaisingEvents = true;    // Активируем события.
}
catch (ArgumentException e) { }

Обработчик события выглядит примерно следующим образом:

void PrinterHandler(object sender, FileSystemEventArgs e)
{
	// Проверяем тип изменения состояния.
	switch (e.ChangeType)
	{
		// Событие создания файла. В этом ветвлении так же можно задать и другие события, при необходимости.
		case WatcherChangeTypes.Created:
			try
			{
				// TODO: Здесь желательно сделать ожидание разблокировки файла, если он по каким-либо причинам занят (повысит отказоустойчивость).

				byte[] fileData = File.ReadAllBytes(e.FullPath);	// У нас есть полный путь к нашему файлу, а значит здесь мы можем делать с ним что захотим.

				// Здесь мы можем обработать полученные данные.
				
				File.Delete(e.FullPath);	// По завершению мы можем тут же удалить файл, если он больше не нужен.
			}
			catch (Exception ex) { }
			break;
	}
}

Собственно, на этом нашу задачу можно считать полностью решённой.

Заключение

Как и всегда, этой статьёй я ни на что не претендую. Сегодня я попытался объяснить «на пальцах» решение довольно специфической (но, как показал опыт последних пары лет — всё ещё актуальной) задачи средствами языка C#, настолько доходчиво и подробно, насколько это было для меня возможно. Надеюсь, статья была для Вас полезной и Вы не потратили время попусту.

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

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

Благодарю за внимание!

Автор: Exomode

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js