Click here for the English article.
Ausgangslage
Ein physischer Druck muss serverseitig erfolgen, wenn der Drucker direkt am Server bzw. im Servernetzwerk eingerichtet ist und der Client keinen unmittelbaren Zugriff auf diesen Drucker hat. Hierbei gilt es generell zu unterscheiden, ob clientseitig (lokal im Browser des Anwenders) oder serverseitig gedruckt werden soll.
Beispiel: Nach Eingang einer Bestellung druckt die ASP.NET-Anwendung automatisch einen Lieferschein auf dem Netzwerkdrucker im Lager, unabhängig davon, welcher Benutzer im Büro die Bestellung im Browser auslöst.
Was ist zu beachten?
Eine .NET-Webanwendung unter IIS wird in der Regel in einem Worker-Prozess ausgeführt und läuft standardmäßig unter einer eigenen Application-Pool-Identität, bspw. IIS APPPOOL\<MyAppName>.
Damit für den Druck der gewünschte Drucker auf dem Server verwendet werden kann, muss dieser für die ausführende Identität sichtbar und zugreifbar sein. IIS führt jeden Worker-Prozess unter der konfigurierten Application-Pool-Identität aus. Das bedeutet in der Praxis, dass die folgenden Voraussetzungen sichergestellt und berücksichtigt werden müssen:
- Drucker möglichst systemweit auf dem Server installieren.
- Bei Netzwerkdruckern der Application-Pool-Identität die erforderlichen Druckberechtigungen erteilen.
- Benutzerbezogene Druckerverbindungen eines angemeldeten Benutzers sind für IIS häufig nicht sichtbar.
- Falls nötig, den Application Pool unter einem dedizierten Dienstkonto betreiben und die Drucker für dieses Konto einrichten.
- Eine zuverlässige Möglichkeit, ausschließlich physische Drucker zu ermitteln, existiert nicht. Daher müssen beispielsweise virtuelle Drucker anhand von Treiber, Name oder Port ausgefiltert werden. Diese können etwa einen Speichern-unter-Dialog auslösen und dadurch den Worker-Prozess blockieren. In einer kontrollierten Serverumgebung kann daher eine explizite Negativliste nicht zugelassener Druckernamen hilfreich sein.
Die Ermittlung der verfügbaren Drucker erfolgt somit serverseitig. So erhält man die Drucker des Windows-Servers, die für die Identität des Application Pools sichtbar sind – nicht die clientseitig verfügbaren Drucker des Anwenders.
Dabei liefert GetPhysicalPrinters (siehe unten) zunächst alle über Win32_Printer ermittelten Drucker und damit auch virtuelle Geräte wie beispielsweise Microsoft Print to PDF. Für die Ermittlung relevanter Druckereigenschaften eignet sich Win32_Printer. Die verwendete Windows-Klasse liefert unter anderem Name, Treiber, Port sowie Informationen darüber, ob ein Drucker lokal oder als Netzwerkdrucker eingebunden ist.
Ermittlung der verfügbaren physischen Drucker
...
public sealed class ServerPrinterModel
{
public string Name { get; init; } = "";
public string Server { get; init; } = "";
public string DriverName { get; init; } = "";
public string PortName { get; init; } = "";
public bool IsLocal { get; init; }
public bool IsNetwork { get; init; }
public bool IsOffline { get; init; }
public bool IsVirtual { get; set; }
}
...
Die zentrale Funktion zur Ermittlung der Drucker sieht wie folgt aus:
...
public IReadOnlyList<ServerPrinterModel> GetPhysicalPrinters()
{
const string searchQuery = @"SELECT * FROM Win32_Printer";
using var searcher = new ManagementObjectSearcher(@"root\CIMV2", searchQuery);
using var results = searcher.Get();
var printers = new List<ServerPrinterModel>();
foreach (ManagementObject printer in results)
{
var item = new ServerPrinterModel
{
Name = GetString(printer, "Name"),
Server = GetString(printer, "ServerName"),
PortName = GetString(printer, "PortName"),
DriverName = GetString(printer, "DriverName"),
IsLocal = GetBool(printer, "Local"),
IsNetwork = GetBool(printer, "Network"),
IsOffline = GetBool(printer, "WorkOffline")
};
item.IsVirtual = IsVirtualPrinter(item);
printers.Add(item);
}
return printers
.OrderBy(p => p.Name)
.ToList();
}
Unterstützende Funktionen bei der Ermittlung der Drucker:
...
private static bool IsVirtualPrinter(ServerPrinterModel printer)
{
var description = $"{printer.Name} {printer.DriverName}".ToUpperInvariant();
string[] virtualPrinterTerms =
{
"MICROSOFT PRINT TO PDF",
"MICROSOFT XPS",
"ONENOTE",
"FAX",
"PDFCREATOR",
"PDF24",
"ADOBE PDF",
"PDF-XCHANGE",
"PDF ARCHITECT",
"SEND TO ONENOTE"
};
if (virtualPrinterTerms.Any(description.Contains))
return true;
var port = printer.PortName.ToUpperInvariant();
return port == "FILE:"
|| port == "PORTPROMPT:"
|| port == "NUL"
|| port == "NUL:";
}
private static string GetString(ManagementObject printer, string propertyName)
{
return Convert.ToString(printer[propertyName]) ?? "";
}
private static bool GetBool(ManagementObject printer, string propertyName)
{
return printer[propertyName] is bool value && value;
}
...
Bestimmten Drucker für den List & Label-Druck festlegen
Nun muss für den Druckvorgang mit List & Label noch explizit der gewünschte Drucker festgelegt werden, auf dem der Ausdruck erfolgen soll. Das ListLabel-Objekt wird dafür wie folgt vorbereitet:
...
private enum PrinterActionMode
{
DOM,
PFile,
Export
}
...
using (ListLabel LL = new ListLabel())
{
LL.LicensingInfo = "<MyLicensingInfo>";
// define important options for server-side printing
LL.Core.LlSetOption(LlOption.ProhibitUserInteraction, 1); // web server mode -> no interaction
LL.Core.LlSetOption(LlOption.NoPrinterPathCheck, 1); // do not validate printer paths
// define general print/export options like FileRepository and DataSource
LL.FileRepository = <MyRepository>;
LL.DataSource = <MyDataSource>;
LL.AutoProjectFile = "<MyProjectToPrint>";
LL.AutoProjectType = <ProjectTypeToPrint>;
// use one of the following methods here for physical printing...
switch (printerActionMode)
{
//...
}
}
...
Dafür gibt es grundsätzlich folgende Möglichkeiten:
PrinterActionMode.DOM
Während des Druckvorgangs wird im Hintergrund die zu druckende Projektdatei mit Hilfe der DOM-API geladen. Dabei wird der gewünschte Drucker temporär für alle Regions gesetzt:
...
// temporarily set the printer for all regions in the project
case PrinterActionMode.DOM:
{
LL.AutoDestination = LlPrintMode.Normal;
LL.Print(printerName);
}
break;
...
PrinterActionMode.PFile
Es wird eine P-Datei verwendet bzw. erzeugt, die den gewünschten Drucker in den PrinterSettings enthält und anschließend für den Druck verwendet wird:
...
// using/creating the P-File with the mentioned printer name
case PrinterActionMode.PFile:
{
LL.Core.LlSetPrinterInPrinterFile(
LL.AutoProjectType,
LL.AutoProjectFile,
printerName);
LL.Print();
}
break;
...
PrinterActionMode.Export
Der Druck erfolgt in zwei Stufen, indem zunächst eine Vorschaudatei (*.ll) erzeugt und diese anschließend auf dem gewünschten Drucker ausgegeben wird:
...
case PrinterActionMode.Export:
{
// optional: define P-File also, to be sure the designer-variable
// LL.Device.PrinterName is set to the correct printer name
// LL.Core.LlSetPrinterInPrinterFile(
// LL.AutoProjectType,
// LL.AutoProjectFile,
// printerName);
// 1. generate the preview file
var previewFileName = Path.Combine(
Path.GetTempPath(),
$"tempoutput-{Guid.NewGuid():N}.ll");
try
{
LL.Export(new ExportConfiguration(
LlExportTarget.Preview,
previewFileName,
LL.AutoProjectFile));
// 2. open and print the preview file
using (var previewFile = new PreviewFile(previewFileName, true))
{
previewFile.Print(printerName, printerName);
}
// 3. the temporary preview file can also be archived
// or processed further before it is deleted
}
finally
{
if (System.IO.File.Exists(previewFileName))
System.IO.File.Delete(previewFileName);
}
}
break;
...
Tipp: Wenn auch physische Drucker auf anderen Serversystemen verwendet werden sollen, kann hierfür eine User Impersonation eingesetzt werden, um auf Drucker in anderen Netzwerkumgebungen bzw. auf anderen Systemen zuzugreifen. Voraussetzung ist, dass die erforderlichen Zugangsdaten sowohl bei der Ermittlung als auch bei der Verwendung des Druckers verfügbar sind.