终端服务的剪贴板的缺陷,导致WPF调用Clipboard.SetText() 失败
这是一个在实际项目中遇到的问题,在VPN和远程桌面中,WPF程序对系统剪贴板进行操作的时候,发生CLIPBRD_E_CANT_OPEN异常。从异常本身来看,很明显,是COM有问题。
代码很简单 Clipboard.SetText(mSelection); 但是注意,这个是WPF的窗口,所以调用的是 System.Windows.Clipboard,而不是WinForm的System.Windows.Forms.Clipboard。
经过一番搜索,找到了根源:
http://stackoverflow.com/questions/68666/clipbrd-e-cant-open-error-when-setting-the-clipboard-from-net
http://blogs.microsoft.co.il/blogs/tamir/archive/2007/10/24/clipboard-setdata-getdata-troubles-with-vpc-and-ts.aspx
原因是微软的Terminal Service 的Clipboard有一个bug,解决方法就是用try-catch包一下函数调用,然后多调用几次,每次之后呢,Sleep一点时间片,代码看上去就是:
for (int i = 0; i < 10; i++) { try { Clipboard.SetText(str); return; } catch { } System.Threading.Thread.Sleep(10); }
而有人也指出,在.Net 2.0 SP1的时候,微软对Winform的剪切板做了修正,内部就做了这个处理,但是WPF没有!好,那就来看看WinForm和WPF的代码:
WinForm的SetText()最终会调用SetDataObject(data, copy, 10, 100),这个是SetDataObject()的签名:
[UIPermission(SecurityAction.Demand, Clipboard=UIPermissionClipboard.OwnClipboard)] public static void SetDataObject(object data, bool copy, int retryTimes, int retryDelay) { if (Application.OleRequired() != ApartmentState.STA) { throw new ThreadStateException(SR.GetString("ThreadMustBeSTA")); } if (data == null) { throw new ArgumentNullException("data"); } if (retryTimes < 0) { object[] args = new object[] { "retryTimes", retryTimes.ToString(CultureInfo.CurrentCulture), 0.ToString(CultureInfo.CurrentCulture) }; throw new ArgumentOutOfRangeException("retryTimes", SR.GetString("InvalidLowBoundArgumentEx", args)); } if (retryDelay < 0) { object[] objArray2 = new object[] { "retryDelay", retryDelay.ToString(CultureInfo.CurrentCulture), 0.ToString(CultureInfo.CurrentCulture) }; throw new ArgumentOutOfRangeException("retryDelay", SR.GetString("InvalidLowBoundArgumentEx", objArray2)); } DataObject obj2 = null; if (!(data is IDataObject)) { obj2 = new DataObject(data); } bool flag = false; try { IntSecurity.ClipboardRead.Demand(); } catch (SecurityException) { flag = true; } if (flag) { if (obj2 == null) { obj2 = data as DataObject; } if (!IsFormatValid(obj2)) { throw new SecurityException(SR.GetString("ClipboardSecurityException")); } } if (obj2 != null) { obj2.RestrictedFormats = flag; } int num2 = retryTimes; IntSecurity.UnmanagedCode.Assert(); try { int num; do { if (data is IDataObject) { num = UnsafeNativeMethods.OleSetClipboard((IDataObject) data); } else { num = UnsafeNativeMethods.OleSetClipboard(obj2); } if (num != 0) { if (num2 == 0) { ThrowIfFailed(num); } num2--; Thread.Sleep(retryDelay); } } while (num != 0); if (copy) { num2 = retryTimes; do { num = UnsafeNativeMethods.OleFlushClipboard(); if (num != 0) { if (num2 == 0) { ThrowIfFailed(num); } num2--; Thread.Sleep(retryDelay); } } while (num != 0); } } finally { CodeAccessPermission.RevertAssert(); } }
而WPF的SetText(),虽然签名一样,但是具有完全不同的实现:
[SecurityCritical] public static void SetDataObject(object data, bool copy) { SecurityHelper.DemandAllClipboardPermission(); CriticalSetDataObject(data, copy); } [FriendAccessAllowed, SecurityCritical] internal static void CriticalSetDataObject(object data, bool copy) { IDataObject obj2; if (data == null) { throw new ArgumentNullException("data"); } if (data is DataObject) { obj2 = (DataObject) data; } else if (data is IDataObject) { SecurityHelper.DemandUnmanagedCode(); obj2 = (IDataObject) data; } else { obj2 = new DataObject(data); } int num2 = 10; while (true) { int hr = OleServicesContext.CurrentOleServicesContext.OleSetClipboard(obj2); if (NativeMethods.Succeeded(hr)) { break; } if (--num2 == 0) { Marshal.ThrowExceptionForHR(hr); } Thread.Sleep(100); } if (copy) { Thread.Sleep(10); Flush(); } }