Welcome to Pete Brown's 10rem.net

First time here? If you are a developer or are interested in Microsoft tools and technology, please consider subscribing to the latest posts.

You may also be interested in my blog archives, the articles section, or some of my lab projects such as the C64 emulator written in Silverlight.

(hide this)

Enumerating Printers using PInvoke in Silverlight 5

Pete Brown - 27 September 2011

Today I got a question on twitter asking how to enumerate printers in Silverlight, so I put together this quick blog post.

Silverlight lacks the PrinterSettings.InstalledPrinters collection. So, how do you go about getting a list of installed printers in Silverlight 5? PInvoke to the Win32 API, of course.

Alexandra Rusina on the Silverlight team wrote a great article on PInvoke. If you're interested in how PInvoke in Silverlight compares to PInvoke in .NET in general, stop right now and read her post. Be sure to subscribe to the blog while you're there.

Turning to my trusty friend PInvoke.net, I dug up the API reference for EnumPrinters. They were even so helpful as to provide a nice C# example. Unfortunately, some of the functions don't work exactly the same, so you need to do a bit of tweaking. My thanks to Alexandra for helping with some tips about how to refactor this code to work around missing Marshal overloads in Silverlight. I also made a number of changes to make it more Silverlight-friendly (observable collection, for example).

When done, here's what the application will look like:

image

Nothing fancy, but it shows the list of printers on my system in beautiful highlighter yellow.

Now let's look at how it's constructed. Warning, lots of Win32 API cruft in here. Of course, that's the good part too :)

First of all, make sure you create the application as an elevated trust application. PInvoke requires that. I did mine as out-of-browser, but in-browser should work fine if you prefer that.

image

image

I named my application "SilverlightEnumPrinters". Now, to the structure of the app.

The PrinterInformation Class

The sample needs something to hold the information for a single printer. This class is my friendly version of the PRINTER_INFO_2 structure used by the Win32 API. I removed the fields I wasn't using (including those that required another call to marshal an aggregated structure, as I didn't want to fill the code with that), and made the remaining fields into actual properties that could be used for binding. The actual PRINTER_INFO_2 class is also in the same file. Note that it is a class, not a structure. That's to allow Marshal.PtrToStructure to work (thanks to Alexandra for help on that one).

Marshal.PtrToStructure in Silverlight doesn't include the overload to return the instance of the type. So, the sample on the pinvoke site which looks like this:

printerInfo2[i] = (PRINTER_INFO_2)Marshal.PtrToStructure(new IntPtr(offset), type);

Must be changed to look like this:

Marshal.PtrToStructure(new IntPtr(offset), printerInfo2[i]);

Note that you don't pass a type, you pass an actual instance of the object you want to fill. I made some additional changes which eliminate the use of the array, so the code below will look slightly different. The rest of the code follows.

using System;
using System.Runtime.InteropServices;

namespace SilverlightEnumPrinters
{
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
internal class PRINTER_INFO_2
{
[MarshalAs(UnmanagedType.LPTStr)]
public string pServerName;
[MarshalAs(UnmanagedType.LPTStr)]
public string pPrinterName;
[MarshalAs(UnmanagedType.LPTStr)]
public string pShareName;
[MarshalAs(UnmanagedType.LPTStr)]
public string pPortName;
[MarshalAs(UnmanagedType.LPTStr)]
public string pDriverName;
[MarshalAs(UnmanagedType.LPTStr)]
public string pComment;
[MarshalAs(UnmanagedType.LPTStr)]
public string pLocation;
public IntPtr pDevMode;
[MarshalAs(UnmanagedType.LPTStr)]
public string pSepFile;
[MarshalAs(UnmanagedType.LPTStr)]
public string pPrintProcessor;
[MarshalAs(UnmanagedType.LPTStr)]
public string pDatatype;
[MarshalAs(UnmanagedType.LPTStr)]
public string pParameters;
public IntPtr pSecurityDescriptor;
public uint Attributes;
public uint Priority;
public uint DefaultPriority;
public uint StartTime;
public uint UntilTime;
public uint Status;
public uint cJobs;
public uint AveragePPM;
}


public class PrinterInformation
{
public string ServerName { get; set; }
public string PrinterName { get; set; }
public string ShareName { get; set; }
public string PortName { get; set; }
public string DriverName { get; set; }
public string Comment { get; set; }
public string Location { get; set; }
public string SeparatorPageFileName { get; set; }
public string PrintProcessor { get; set; }
public string DefaultPrintProcessorParameters { get; set; }
public PrinterAttributes Attributes { get; set; }
public uint Priority { get; set; }
public uint DefaultPriority { get; set; }
public uint StartTime { get; set; }
public uint UntilTime { get; set; }
public PrinterStatus Status { get; set; }
public uint QueuedJobCount { get; set; }
public uint AveragePagesPerMinute { get; set; }
}
}

Printer Attributes

The printer attributes enum is a bunch of flag constants repackaged into an enum.

using System;

namespace SilverlightEnumPrinters
{
[Flags]
public enum PrinterAttributes
{
Queued = 0x1,
Direct = 0x2,
Default = 0x4,
Shared = 0x8,
Network = 0x10,
Hidden = 0x20,
Local = 0x40,
WorkOffline = 0x400,
EnableBidi = 0x800
}
}

Printer Status

Similarly, PrinterStatus is also a repackaging of constants

using System;

namespace SilverlightEnumPrinters
{
[Flags]
public enum PrinterStatus
{
Busy = 0x200,
DoorOpen = 0x400000,
Error = 0x2,
Initializing = 0x8000,
IOActive = 0x100,
ManualFeed = 0x20,
NoToner = 0x40000,
NotAvailable = 0x1000,
Offline = 0x80,
OutOfMemory = 0x200000,
OutputBinFull = 0x800,
PagePunt = 0x80000,
PaperJam = 0x8,
PaperOut = 0x10,
PaperProblem = 0x40,
Paused = 0x1,
PendingDeletion = 0x4,
Printing = 0x400,
Processing = 0x4000,
TonerLow = 0x20000,
UserIntervention = 0x100000,
Waiting = 0x2000,
WarmingUp = 0x10000
}
}

Finally, it's time to create the function which actually enumerates the printers.

Printer Service

The PrinterService class is where everything happens. It exposes the collection of printers, and includes a public method which loads the collection using the EnumPrinters API call. It even includes my ugly L2R code to transform a PRINTER_INFO_2 into a PrinterInformation object.

using System;
using System.Runtime.InteropServices;
using System.Collections.ObjectModel;

namespace SilverlightEnumPrinters
{
[Flags]
public enum PrinterEnumFlags
{
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
}

public class PrinterService
{
[Flags]
private enum LocalMemoryFlags
{
LMEM_FIXED = 0x0000,
LMEM_MOVEABLE = 0x0002,
LMEM_NOCOMPACT = 0x0010,
LMEM_NODISCARD = 0x0020,
LMEM_ZEROINIT = 0x0040,
LMEM_MODIFY = 0x0080,
LMEM_DISCARDABLE = 0x0F00,
LMEM_VALID_FLAGS = 0x0F72,
LMEM_INVALID_HANDLE = 0x8000,
LHND = (LMEM_MOVEABLE | LMEM_ZEROINIT),
LPTR = (LMEM_FIXED | LMEM_ZEROINIT),
NONZEROLHND = (LMEM_MOVEABLE),
NONZEROLPTR = (LMEM_FIXED)
}
[DllImport("winspool.drv", CharSet = CharSet.Auto, SetLastError = true)]
private static extern bool EnumPrinters(PrinterEnumFlags Flags, string Name, uint Level, IntPtr pPrinterEnum, uint cbBuf, ref uint pcbNeeded, ref uint pcReturned);

[DllImport("kernel32.dll", EntryPoint = "LocalAlloc")]
private static extern IntPtr LocalAlloc_NoSafeHandle(LocalMemoryFlags uFlags, IntPtr sizetdwBytes);

[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr LocalFree(IntPtr hMem);

private const int ERROR_INSUFFICIENT_BUFFER = 122;


private ObservableCollection<PrinterInformation> _printers = new ObservableCollection<PrinterInformation>();
public ObservableCollection<PrinterInformation> Printers
{
get { return _printers; }
}

public void LoadPrinters(PrinterEnumFlags Flags)
{
Printers.Clear();

uint cbNeeded = 0;
uint cReturned = 0;
if (EnumPrinters(Flags, null, 2, IntPtr.Zero, 0, ref cbNeeded, ref cReturned))
{
// nothing
return;
}

int lastWin32Error = Marshal.GetLastWin32Error();
if (lastWin32Error == ERROR_INSUFFICIENT_BUFFER)
{
IntPtr pAddr = LocalAlloc_NoSafeHandle(LocalMemoryFlags.LMEM_FIXED, (IntPtr)cbNeeded);

if (EnumPrinters(Flags, null, 2, pAddr, cbNeeded, ref cbNeeded, ref cReturned))
{
int offset = pAddr.ToInt32();
int increment = Marshal.SizeOf(typeof(PRINTER_INFO_2));
for (int i = 0; i < cReturned; i++)
{
PrinterInformation printer = new PrinterInformation();
PRINTER_INFO_2 pinfo = new PRINTER_INFO_2();

Marshal.PtrToStructure(new IntPtr(offset), pinfo);

printer.Attributes = (PrinterAttributes)pinfo.Attributes;
printer.AveragePagesPerMinute = pinfo.AveragePPM;
printer.Comment = pinfo.pComment;
printer.DefaultPrintProcessorParameters = pinfo.pParameters;
printer.DefaultPriority = pinfo.Priority;
printer.DriverName = pinfo.pDriverName;
printer.Location = pinfo.pLocation;
printer.PortName = pinfo.pPortName;
printer.PrinterName = pinfo.pPrinterName;
printer.PrintProcessor = pinfo.pPrintProcessor;
printer.Priority = pinfo.Priority;
printer.QueuedJobCount = pinfo.cJobs;
printer.SeparatorPageFileName = pinfo.pSepFile;
printer.ServerName = pinfo.pServerName;
printer.ShareName = pinfo.pShareName;
printer.StartTime = pinfo.StartTime; // should xform this
printer.Status = (PrinterStatus)pinfo.Status;
printer.UntilTime = pinfo.UntilTime; // should xform this

Printers.Add(printer);

offset += increment;
}

LocalFree(pAddr);
}
else
{
lastWin32Error = Marshal.GetLastWin32Error();
throw new Exception("Win32 Error: " + lastWin32Error);
}
}
}
}
}

The code is pretty straight-forward. Make an API call to see if there are any printers. If so, allocate some memory for the buffer, get the list of printers into that buffer, and start reading the printers out of it. Once complete, free the allocated buffer.

With all that in place, it's time to create the UI.

Main Page

The main page has a simple ListBox with a really basic template used to show the properties of the printers. I only surface a few properties; you could certainly add more.

<UserControl x:Class="SilverlightEnumPrinters.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="400">

<Grid x:Name="LayoutRoot" Background="White">
<ListBox x:Name="PrinterList"
Margin="10">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid Margin="10" Background="Yellow">
<StackPanel>
<TextBlock Text="{Binding ServerName}" />
<TextBlock Text="{Binding ShareName}" />
<TextBlock Text="{Binding PrinterName}" />
<TextBlock Text="{Binding DriverName}" />
<TextBlock Text="{Binding Location}" />
<TextBlock Text="{Binding Comment}" />
</StackPanel>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>

</Grid>
</UserControl>

Code-behind

The code-behind instantiates the service and binds the ListBox on the UI.

namespace SilverlightEnumPrinters
{
public partial class MainPage : UserControl
{
public MainPage()
{
InitializeComponent();

Loaded += new RoutedEventHandler(MainPage_Loaded);
}

void MainPage_Loaded(object sender, RoutedEventArgs e)
{
var service = new PrinterService();
PrinterList.ItemsSource = service.Printers;

service.LoadPrinters(PrinterEnumFlags.Local);
}
}
}

That's it! Run it and it should rattle off the list of printers attached to your system. If you want to change from, say, local to network printers, just change the parameter you pass into LoadPrinters.

Full source code attached. Warning: Works on my machine (Windows 7 x64, one attached printer).

     

Source Code and Related Media

Download /media/78175/silverlightenumprinters.zip
posted by Pete Brown on Tuesday, September 27, 2011
filed under:      

18 comments for “Enumerating Printers using PInvoke in Silverlight 5”

  1. Keith Udomonsays:
    Hi,
    You seem to know alot on how to use SL5......I was wondering if you have an example on how to use SL5 to write/read data from a COM port using the PIinvoke.
    Thanks
  2. Ganapatsasays:
    HI,

    I tried the above example and found that when
    if (EnumPrinters(Flags, null, 2, IntPtr.Zero, 0, ref cbNeeded, ref cReturned))
    {
    // nothing
    return;
    }

    when the if condition executes there is an MethodAccess Exception raised.

    Have done all the settings for the Out of Browser as mentioned.
    How ever I do not see the "Requires elevated privileges in IN BROWSER" mode setting in the project settings page.

    Require help in order to use PInvoke in Silverlight 5 Application.

    Thanks and regards
    Ganapqatsa
  3. Petesays:
    @Ganapatsa

    1. Make sure your project is actually targeting Silverlight 5 and not Silverlght 4. (Check the project property pages)

    2. Make sure you have the Silverlight 5 RC installed (see silverlight.net to download) and not an earlier version

    3. Make sure you're actually running it out-of-browser and not in-browser (since you made the changes in OOB page).

    Pete
  4. Petesays:
    @josh

    No, but full-trust in-browser is really only useful inside a corporate environment. It requires policy/registry settings. If that's your situation, then you can use this there.

    Pete
  5. joshsays:
    thanks for the info,

    My app needs to stay IN browser, and the only permissions my app needs are, enumprinters but also acess to the framework 4 client profile (not SL) print objects for a more flexible print dialog and/or silent printing. The SL print objects are limited and very bulky when they render images large amount of overhead get created that do not exist when using the FW4 print objects.

    It sounds like just those two things would still require the mentioned policy settings.

    josh
  6. Petesays:
    @josh

    Yep, you're out of the sandbox there.

    Also, Silverlight doesn't natively have access to the .NET client profile, although there have been some ugly COM automation hacks to kind of get you there.

    You can consider using a WPF XBAP but those are generally disabled by browsers these days.

    The things you want to do just aren't browser-friendly. The browser isn't the general application host everyone treats it as; it's a heavily sandboxed intERnet-oriented host. If possible, see if you can get the browser requirement lifted. :)

    Pete
  7. Stevesays:
    Hi Pete,

    Im starting to follow your MS post amoung others - thanks for listing - after I googled "Marshal.PtrToStructure Silverlight 5" because I couldnt understand why the MSDN listed the 'Object' return, not the 'void' return but VS kept reporting an error.

    Anyway I will adapt my code thanks to your article :)
    Steve

    PS. Am a embedded engineer/programmer who is planning on using Silverlight to talk to my company's bluetooth (just a virtual com port lols) AVR based device for car comms. Was quite striaght forward to make a 'SerialPort' drop in replacement for Silverlight -even with GetPortNames().

    Anyway enough with my boasting, I've work to do. Thanks again & will add a community comment in MSDN!!

  8. Manfredsays:
    Hello Pete,
    Thanks for the great post. The project is working fine. The only problem I have. It does not display all network printers as listed in the Control Panel. Do you have a tip for me how can I solve this?

    Regards
    Manfred
  9. varunsays:
    Hi,,

    i am using SL5 and your code is running in-browser for me. the only thing i did was i didnt used your code as such and i copied the files in a fresh solution.. When i run it from your solution file.. it gives me same error same - method access execption.. I think there some prob occurs when i click on enable out of browser checkbox and uncheck it again..
  10. Jorge Escobarsays:
    Hi,

    this is really awsome, I've implemented and it works fine, but I've a problem getting the actual status and attributes of the printer. For the Attributes I'm receiving different values

    for:
    "Microsoft Shared Fax Driver" i get 16448
    "Microsoft XPS Document Writer v4" i get 576

    Also it's not listing the unconnected Network printers

    Have you had similar behavior?

    Keep up the good work !

    Best Regards !

    Jorge
  11. Mattiassays:
    Thanks for great code.

    About the network printers, I get them when using printerService.LoadPrinters(PrinterEnumFlags.Connections);

    So I make one pass with the Local flag and then another with the Connections flag to get them all.

Comment on this Post

Remember me