I have a simple application to copy the properties of the printer devmode and restore them after a manual change.
I read through topics the whole day and can't understand why my code isn't working. I compared the output of two different printer settings against each other and can confirm that they are different, so my guess is that something with restoring these settings isn't working.
I also tried thisĀ applicationĀ but found some strange behavior. When I export the devmode data, change the properties manually and load the save again, the data only shows changed if I call the printer property dialog directly with the button in the application itself. If I go to windows settings and navigate to the printer properties myself, the data isn't changed.
Here is the code, got it from this thread
public static class PrinterSettingsExtensions
{
public static byte[] GetDevModeData(this PrinterSettings settings)
{
//Contract.Requires(settings != null);
byte[] devModeData;
RuntimeHelpers.PrepareConstrainedRegions();
try
{
// cer since hDevMode is not a SafeHandle
}
finally
{
var hDevMode = settings.GetHdevmode();
try
{
IntPtr pDevMode = NativeMethods.GlobalLock(hDevMode);
try
{
var devMode = (NativeMethods.DEVMODE)Marshal.PtrToStructure(
pDevMode, typeof(NativeMethods.DEVMODE));
var devModeSize = devMode.dmSize + devMode.dmDriverExtra;
devModeData = new byte[devModeSize];
Marshal.Copy(pDevMode, devModeData, 0, devModeSize);
}
finally
{
NativeMethods.GlobalUnlock(hDevMode);
}
}
finally
{
Marshal.FreeHGlobal(hDevMode);
}
}
return devModeData;
}
public static void SetDevModeData(this PrinterSettings settings, byte[] data)
{
//Contract.Requires(settings != null);
//Contract.Requires(data != null);
//Contract.Requires(data.Length >= Marshal.SizeOf(typeof(NativeMethods.DEVMODE)));
RuntimeHelpers.PrepareConstrainedRegions();
try
{
// cer since AllocHGlobal does not return SafeHandle
}
finally
{
var pDevMode = Marshal.AllocHGlobal(data.Length);
try
{
// we don't have to worry about GlobalLock since AllocHGlobal only uses LMEM_FIXED
Marshal.Copy(data, 0, pDevMode, data.Length);
var devMode = (NativeMethods.DEVMODE)Marshal.PtrToStructure(
pDevMode, typeof(NativeMethods.DEVMODE));
// The printer name must match the original printer, otherwise an AV will be thrown
settings.PrinterName = devMode.dmDeviceName;
// SetHDevmode creates a copy of the devmode, so we don't have to keep ours around
settings.SetHdevmode(pDevMode);
}
finally
{
Marshal.FreeHGlobal(pDevMode);
}
}
}
}
static class NativeMethods
{
private const string Kernel32 = "kernel32.dll";
[DllImport(Kernel32, SetLastError = true, ExactSpelling = true, CharSet = CharSet.Auto)]
public static extern IntPtr GlobalLock(IntPtr handle);
[DllImport(Kernel32, SetLastError = true, ExactSpelling = true, CharSet = CharSet.Auto)]
public static extern bool GlobalUnlock(IntPtr handle);
[StructLayout(LayoutKind.Sequential, Pack = 1, CharSet = CharSet.Auto)]
public struct DEVMODE
{
private const int CCHDEVICENAME = 32;
private const int CCHFORMNAME = 32;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCHDEVICENAME)]
public string dmDeviceName;
public short dmSpecVersion;
public short dmDriverVersion;
public short dmSize;
public short dmDriverExtra;
public int dmFields;
public int dmPositionX;
public int dmPositionY;
public int dmDisplayOrientation;
public int dmDisplayFixedOutput;
public short dmColor;
public short dmDuplex;
public short dmYResolution;
public short dmTTOption;
public short dmCollate;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCHFORMNAME)]
public string dmFormName;
public short dmLogPixels;
public int dmBitsPerPel;
public int dmPelsWidth;
public int dmPelsHeight;
public int dmDisplayFlags;
public int dmDisplayFrequency;
public int dmICMMethod;
public int dmICMIntent;
public int dmMediaType;
public int dmDitherType;
public int dmReserved1;
public int dmReserved2;
public int dmPanningWidth;
public int dmPanningHeight;
}
}
I'm using .Net Core 6 and Windows 11 22000.795 if this helps.
DevMode is what I am working on at the moment. Using .net 4.8.1 unfortunately but I am using Heap functions and I am pulling the default settings using p/Invoke.
My goal is to modify settings and then stream the changed settings to another API. So, I'll look into this and see what I can find.
Several steps could be giving you problems with settings. Mostly around the printer/document/settings/defaults/drivers themselves. Once you make your settings and pass need to make sure your end test is picking up the changes. But, as you suggest, most likely an invalid format due to each driver being unique.
Any progress?
Appears for the data to show it has been changed, you need to flag the fields that have been modified in public int dmFields; (Says it is defined as a long).
dmFields
Specifies bit flags identifying which of the following DEVMODEW members are in use. For example, the DM_ORIENTATION flag is set when the dmOrientation member contains valid data. The DM_XXX flags are defined in wingdi.h.
Related
I'm trying to catch VIP and PID from USB device:
public const int WM_DEVICECHANGE = 0x219;
public const int DBT_DEVTYP_VOLUME = 0x00000002;
public const int DBT_DEVICEARRIVAL = 0x8000;
[StructLayout(LayoutKind.Sequential)]
internal class DEV_BROADCAST_HDR
{
public int dbch_size;
public int dbch_devicetype;
public int dbch_reserved;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct DEV_BROADCAST_DEVICEINTERFACE
{
public int dbcc_size;
public int dbcc_devicetype;
public int dbcc_reserved;
[MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 16)]
public byte[] dbcc_classguid;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 256)]
public char[] dbcc_name;
}
public void WndProc(ref Message m)
{
if (m.Msg == WM_DEVICECHANGE) //Device state has changed
{
switch (m.WParam.ToInt32())
{
case DBT_DEVICEARRIVAL: //New device arrives
DEV_BROADCAST_HDR hdr;
hdr = (DEV_BROADCAST_HDR)Marshal.PtrToStructure(m.LParam, typeof(DEV_BROADCAST_HDR));
if (hdr.dbch_devicetype == DBT_DEVTYP_VOLUME) //If it is a USB Mass Storage or Hard Drive
{
//Save Device name
DEV_BROADCAST_DEVICEINTERFACE deviceInterface;
string deviceName = "";
deviceInterface = (DEV_BROADCAST_DEVICEINTERFACE)Marshal.PtrToStructure(m.LParam, typeof(DEV_BROADCAST_DEVICEINTERFACE));
deviceName = new string(deviceInterface.dbcc_name).Trim();
}
}
}
}
But deviceName always returns a string with non sense characters. I have change CharSet in DEV_BROADCAST_DEVICEINTERFACE structure and declare dbcc.name as string but the result is the same.
I would like to avoid reading from registry, and among all I have read, I have seen that it is possible to cast a DEV_BROADCAST_HEADER to a DEV_BROADCAST_DEVICEINTERFACE only if dbch_devicetype==DBT_DEVTYP_DEVICEINTERFACE. In my case, dbch_devicetype is 2, not 5, and I am using some common USB Mass Storage devices. What am I doing wrong? Thanks in advance!
Maybe, there is a more elegant way to resolve this question, but at least, and after a long search, this seems to be working.
On one hand, I register the app in order to receive the correct info to cast it as DEV_BROADCAST_DEVICEINTERFACE structure (DEV_BROADCAST_HEADER.dbch_devicetype is 5 in this case). So, I am able to retrieve VID and PID info. On the other hand, I keep first Windows message WndProc receives to retrieve the volume that Windows assings when I connect USB devices (DEV_BROADCAST_HEADER.dbch_devicetype is 2). Then, I receive two messages.
In code:
public const int WM_DEVICECHANGE = 0x219;
public const int DBT_DEVTYP_VOLUME = 0x00000002;
public const int DBT_DEVICEARRIVAL = 0x8000;
public const int DBT_DEVTYP_DEVICEINTERFACE = 0x00000005;
private IntPtr notificationHandle;
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr RegisterDeviceNotification(IntPtr recipient, IntPtr notificationFilter, int flags);
[StructLayout(LayoutKind.Sequential)]
internal class DEV_BROADCAST_HDR
{
public int dbch_size;
public int dbch_devicetype;
public int dbch_reserved;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct DEV_BROADCAST_DEVICEINTERFACE
{
public int dbcc_size;
public int dbcc_devicetype;
public int dbcc_reserved;
public Guid dbcc_classguid;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 256)]
public char[] dbcc_name;
}
public void WndProc(ref Message m)
{
if (m.Msg == WM_DEVICECHANGE) //Device state has changed
{
switch (m.WParam.ToInt32())
{
case DBT_DEVICEARRIVAL: //New device arrives
DEV_BROADCAST_HDR hdr;
hdr = (DEV_BROADCAST_HDR)Marshal.PtrToStructure(m.LParam, typeof(DEV_BROADCAST_HDR));
if (hdr.dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE) //If it is a USB Mass Storage or Hard Drive
{
//Save Device name
DEV_BROADCAST_DEVICEINTERFACE deviceInterface = (DEV_BROADCAST_DEVICEINTERFACE)Marshal.PtrToStructure(m.LParam, typeof(DEV_BROADCAST_DEVICEINTERFACE));
deviceName = new string(deviceInterface.dbcc_name);
deviceNameFiltered = deviceName.Substring(0, deviceName.IndexOf('{'));
vid = GetVid(deviceName);
pid = GetPid(deviceName);
}
if (hdr.dbch_devicetype == DBT_DEVTYP_VOLUME)
{
DEV_BROADCAST_VOLUME volume;
volume = (DEV_BROADCAST_VOLUME)Marshal.PtrToStructure(m.LParam, typeof(DEV_BROADCAST_VOLUME));
//Translate mask to device letter
driveLetter = DriveMaskToLetter(volume.dbcv_unitmask);
}
}
}
To register app in order to receive the correct info to cast it as DEV_BROADCAST_DEVICEINTERFACE structure, it is necessary to call to this last RegisterUsbDeviceNotification method from Form class with its window handler as argument.
public void RegisterUsbDeviceNotification(IntPtr windowHandle)
{
DEV_BROADCAST_DEVICEINTERFACE deviceInterface = new DEV_BROADCAST_DEVICEINTERFACE
{
dbcc_classguid = GuidDevinterfaceUSBDevice,
dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE,
dbcc_reserved = 0,
};
deviceInterface.dbcc_size = Marshal.SizeOf(deviceInterface);
IntPtr buffer = Marshal.AllocHGlobal(deviceInterface.dbcc_size);
Marshal.StructureToPtr(deviceInterface, buffer, true);
notificationHandle = RegisterDeviceNotification(windowHandle, buffer, 0);
}
I want to use API from Omron V4KU, the documentation described like this :
Original c# code :
const string DllLocation = #"..\..\Libs\Omron\OMCR.dll";
[DllImport(DllLocation)]
public static extern LPCOMCR OMCR_OpenDevice(string lpcszDevice, LPCOMCR_OPTION lpcOption);
public void Start()
{
var lpcOption = new LPCOMCR_OPTION();
var result = OMCR_OpenDevice(null, lpcOption); // error method's type signature is not pinvoke compatible
}
[StructLayout(LayoutKind.Sequential)]
public struct LPCOMCR
{
public string lpcszDevice;
public IntPtr hDevice;
public uint lpcDevice;
}
[StructLayout(LayoutKind.Sequential)]
public struct LPCOMCR_OPTION
{
public uint dwReserved0;
public uint dwReserved1;
public uint dwReserved2;
public uint dwReserved3;
}
if I missed or wrong in writing code?
sorry, my english is bad. thanks for help.
Start by defining the union structure correctly:
// OMCR_OPTION.COM
[StructLayout(LayoutKind.Sequential)]
public struct OmcrCom
{
public IntPtr Reserved0;
public uint BaudRate;
public uint Reserved1;
public uint Reserved2;
public uint Reserved3;
public IntPtr Reserved1;
public IntPtr Reserved2;
}
// OMCR_OPTION.USB
[StructLayout(LayoutKind.Sequential)]
public struct OmcrUsb
{
public uint Reserved0;
public uint Reserved1;
public uint Reserved2;
public uint Reserved3;
}
// OMCR_OPTION (union of COM and USB)
[StructLayout(LayoutKind.Explicit)]
public struct OmcrOptions
{
[FieldOffset(0)]
public OmcrCom Com;
[FieldOffset(0)]
public OmcrUsb Usb;
}
// OMCR
[StructLayout(LayoutKind.Sequential)]
public struct OmcrDevice
{
public string Device;
public IntPtr DeviceHandle;
public IntPtr DevicePointer;
}
[DllImport(dllName: DllLocation, EntryPoint = "OMCR_OpenDevice"]
public static extern IntPtr OmcrOpenDevice(string type, ref OmcrOptions options);
And then call the method, something like:
var options = new OmcrOptions();
options.Com.BaudRate = 115200; // or whatever you need to set
var type = "COM"; // is this USB/COM? not sure
OmcrDevice device;
var devicePtr = OmcrOpenDevice(type, ref options);
if (devicePtr == IntPtr.Zero)
device = (OmcrDevice)Marshal.PtrToStructure(devicePtr, typeof(OmcrDevice));
Well, for one, the documentation is asking you to pass LPCOMCR_OPTION as a pointer - you're passing it as a value. Using ref should help. There's another problem, though, and that's the return value - again, you're trying to interpret it as a value, while the docs say it's a pointer. However, this is a lot trickier than the first error - as far as I'm aware, your only options are using a C++/CLI interop library, or expecting IntPtr as a return value. In any case, you need to handle proper deallocation of the memory you get this way.
You need to do it like this:
[StructLayout(LayoutKind.Sequential)]
public struct OMCR
{
[MarshalAs(UnmanagedType.LPStr)]
public string lpcszDevice;
public IntPtr hDevice;
public IntPtr lpcDevice;
}
[StructLayout(LayoutKind.Sequential)]
public struct OMCR_OPTION
{
public uint dwReserved0;
public uint dwReserved1;
public uint dwReserved2;
public uint dwReserved3;
}
[DllImport(DllLocation, CallingConvention = CallingConvention.???,
SetLastError = true)]
public static extern IntPtr OMCR_OpenDevice(string lpcszDevice,
ref OMCR_OPTION lpcOption);
You need to replace CallingConvention.??? with the appropriate calling convention. We cannot tell from the question what that is. You will have to find out by reading the header file.
The return value is a pointer to OMCR. You need to hold on to this pointer and pass it to OMCR_CloseDevice when you are finished with it.
In order to obtain an OMCR value you would do the following:
OMCR_OPTION Option = new OMCR_OPTION(); // not sure how to initialize this
IntPtr DevicePtr = OMCR_OpenDevice(DeviceType, ref Option);
if (DevicePtr == IntPtr.Zero)
throw new Win32Exception();
OMCR Device = (OMCR)Marshal.PtrToStructure(DevicePtr, typeof(OMCR));
I'm trying to get back this struct from a native C library through P/Invoke:
struct ndb_mgm_cluster_state
{
int no_of_nodes;
struct ndb_mgm_node_state node_states[1];
};
where ndb_mgm_node_state is:
struct ndb_mgm_node_state {
int node_id;
enum ndb_mgm_node_type node_type;
enum ndb_mgm_node_status node_status;
int start_phase;
int dynamic_id;
int node_group;
int version;
int connect_count;
char connect_address[sizeof("000.000.000.000")+1 ];
int mysql_version;
};
The method's signature is:
ndb_mgm_cluster_state* WINAPI wrap_ndb_mgm_get_status(HANDLE handle);
All of this is provided by a 3rd party library, so nothing can be changed.
In C# i have the follow definitions:
[DllImport("Ndb_CWrapper.dll", CharSet = CharSet.Ansi)]
private static extern IntPtr wrap_ndb_mgm_get_status(IntPtr handle);
the structures are:
[StructLayoutAttribute(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct ndb_mgm_cluster_state {
public int no_of_nodes;
public IntPtr node_states;
};
[StructLayoutAttribute(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct ndb_mgm_node_state
{
public int node_id;
public ndb_mgm_node_type node_type;
public ndb_mgm_node_status node_status;
public int start_phase;
public int dynamic_id;
public int node_group;
public int version;
public int connect_count;
[MarshalAsAttribute(UnmanagedType.ByValTStr, SizeConst = 17)]
public string connect_address;
public int mysql_version;
};
I tried to unmarshal the results without success (i recieve an oddly error (not an Exception) Fatal error execution, can be a CLR bug or a miscall P/invoke.
Obviously the reason is a problem in my P/Invoke call
I tried in this way:
First of all i unmarshalled the ndb_mgm_cluster_state structure:
var res=(ndb_mgm_cluster_state)Marshal.PtrToStructure(
tmpPtr, typeof(ndb_mgm_cluster_state));
where IntPtr is the result of the native call.
Up to this step everything "seems" to be done right, but when i try to unmarshall the node_states i get the error:
ndb_mgm_node_state tmpNode = (ndb_mgm_node_state)Marshal.PtrToStructure(
status.node_states, typeof(ndb_mgm_node_state));
What can be the problem? I supposed is something related to the strange declaration of ndb_mgm_cluster_state because is defined an array of 1 element, but it contain several elements. (the number of elements is in no_of_nodes)
WORKAROUND:
The only way to let everything work i found is to change a bit the signature, in this way:
ndb_mgm_node_state* WINAPI wrap_ndb_mgm_get_status(HANDLE handle,int* length);
in C#:
[DllImport("Ndb_CWrapper.dll", CharSet = CharSet.Ansi)]
private static extern IntPtr wrap_ndb_mgm_get_status(IntPtr handle,out int length);
where length contains no_of_nodes
the unmarshall will be in this way:
IntPtr tmpPtr = wrap_ndb_mgm_get_status(raw,out length);
ndb_mgm_cluster_state tmpRes = new ndb_mgm_cluster_state();
tmpRes.no_of_nodes = length;
tmpRes.node_states = new ndb_mgm_node_state[length];
int step=0;
for (int i = 0; i < tmpRes.no_of_nodes; i++)
{
tmpRes.node_states[i] = (ndb_mgm_node_state)Marshal.PtrToStructure(
tmpPtr+(step*i), typeof(ndb_mgm_node_state));
step = Marshal.SizeOf(tmpRes.node_states[i]);
}
I know the step calculation is odd at the monent, but its not that the point.
there is no way to let the thing work returning directly the ndb_mgm_cluster_state struct instead of do all of this?
I'm working on a windows mobile project using compact framework.
One thing I have to do is log when users perform actions, this can mean any action from pressing a button to using the barcode scanner. The time it happened also needs to be logged.
My plan is to override all controls to include logging functionality built into them but this might not be the right way to go about it, seems like a very tedious thing to do..
Is there a better way?
I would go with IL Weaving. Here is a library that I would recommend: http://www.sharpcrafters.com/aop.net/msil-injection What it does is that you mark your class with an attribute and you can intercept all function calls. In this interception you would put in your logging logic.
I'd say it depends greatly on the definition of "action". I'd be highly inclined to see if the (undocumented) QASetWindowsJournalHook API would work. It's probably going to grab most of what you want, with not a lot of code required. A native example of usage can be found on Codeproject here.
SetWindowsHook with WH_JOURNALRECORD might also be worth a look. Yeah, I know it's "unsupported" but it works just fine, and it's unlikely to be removed from a device you've got fielded (plus it's been in the OS for at least 10 years).
Some P/Invoke declarations, all derived from pwinuser.h, for them both are as follows:
[StructLayout(LayoutKind.Sequential)]
public struct JournalHookStruct
{
public int message { get; set; }
public int paramL { get; set; }
public int paramH { get; set; }
public int time { get; set; }
public IntPtr hwnd { get; set; }
}
internal enum HookType
{
JournalRecord = 0,
JournalPlayback = 1,
KeyboardLowLevel = 20
}
internal enum HookCode
{
Action = 0,
GetNext = 1,
Skip = 2,
NoRemove = 3,
SystemModalOn = 4,
SystemModalOff = 5
}
public const int HC_ACTION = 0;
public const int LLKHF_EXTENDED = 0x1;
public const int LLKHF_INJECTED = 0x10;
public const int LLKHF_ALTDOWN = 0x20;
public const int LLKHF_UP = 0x80;
public const int VK_TAB = 0x9;
public const int VK_CONTROL = 0x11;
public const int VK_ESCAPE = 0x1B;
public const int VK_DELETE = 0x2E;
[DllImport("coredll.dll", SetLastError = true)]
public static extern IntPtr SetWindowsHookEx(HookType idHook, HookProc lpfn, IntPtr hMod, int
[DllImport("coredll.dll", SetLastError = true)]
public static extern bool UnhookWindowsHookEx(IntPtr hhk);
[DllImport("coredll.dll", SetLastError = true)]
public static extern int CallNextHookEx(IntPtr hhk, HookCode nCode, IntPtr wParam, IntPtr
[DllImport("coredll.dll", SetLastError = true)]
public static extern IntPtr QASetWindowsJournalHook(HookType nFilterType, HookProc pfnFilterProc, ref JournalHookStruct pfnEventMsg);
Would writing these messages to a log file not solve your problem?
#if PocketPC
private static string _appPath = Environment.GetFolderPath(Environment.SpecialFolder.Personal);
#else
private static string _appPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), Application.CompanyName);
#endif
public const int KILOBYTE = 1024;
public static string ErrorFile { get { return _appPath + #"\error.log"; } }
public static void Log(string message)
{
if (String.IsNullOrEmpty(message)) return;
using (FileStream stream = File.Open(ErrorFile, FileMode.Append, FileAccess.Write))
{
using (StreamWriter sw = new StreamWriter(stream, Encoding.UTF8, KILOBYTE))
{
sw.WriteLine(string.Format("{0:MM/dd/yyyy HH:mm:ss} - {1}", DateTime.Now, message));
}
}
}
You could have issues though if you have threading going on and multiple routines try to write at the same time. In that case, you could add additional logic to lock the routine while it is in use.
That's how I do it, anyway.
By the #if regions, you can see this is also used by my Windows PC applications.
Programmatic solution of course...
http://www.daveamenta.com/2008-05/c-delete-a-file-to-the-recycle-bin/
From above:
using Microsoft.VisualBasic;
string path = #"c:\myfile.txt";
FileIO.FileSystem.DeleteDirectory(path,
FileIO.UIOption.OnlyErrorDialogs,
RecycleOption.SendToRecycleBin);
You need to delve into unmanaged code. Here's a static class that I've been using:
public static class Recycle
{
private const int FO_DELETE = 3;
private const int FOF_ALLOWUNDO = 0x40;
private const int FOF_NOCONFIRMATION = 0x0010;
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto, Pack = 1)]
public struct SHFILEOPSTRUCT
{
public IntPtr hwnd;
[MarshalAs(UnmanagedType.U4)]
public int wFunc;
public string pFrom;
public string pTo;
public short fFlags;
[MarshalAs(UnmanagedType.Bool)]
public bool fAnyOperationsAborted;
public IntPtr hNameMappings;
public string lpszProgressTitle;
}
[DllImport("shell32.dll", CharSet = CharSet.Auto)]
static extern int SHFileOperation(ref SHFILEOPSTRUCT FileOp);
public static void DeleteFileOperation(string filePath)
{
SHFILEOPSTRUCT fileop = new SHFILEOPSTRUCT();
fileop.wFunc = FO_DELETE;
fileop.pFrom = filePath + '\0' + '\0';
fileop.fFlags = FOF_ALLOWUNDO | FOF_NOCONFIRMATION;
SHFileOperation(ref fileop);
}
}
Addendum:
Tsk tsk # Jeff for "using Microsoft.VisualBasic" in C# code.
Tsk tsk # MS for putting all the goodies in VisualBasic namespace.
The best way I have found is to use the VB function FileSystem.DeleteFile.
Microsoft.VisualBasic.FileIO.FileSystem.DeleteFile(file.FullName,
Microsoft.VisualBasic.FileIO.UIOption.OnlyErrorDialogs,
Microsoft.VisualBasic.FileIO.RecycleOption.SendToRecycleBin);
It requires adding Microsoft.VisualBasic as a reference, but this is part of the .NET framework and so isn't an extra dependency.
Alternate solutions require a P/Invoke to SHFileOperation, as well as defining all the various structures/constants. Including Microsoft.VisualBasic is much neater by comparison.