當 DCOM 在若干年前登上歷史舞台時用於演示其功能的最常用示例之一就是遠程剪貼板管理器通過使用 DCOM 編程模型一個組件能夠讀取和寫入存儲在另一台計算機上但連接到同一網絡的剪貼板內容(當然只有在安全設置允許的情況下才能生效)
但是當 DCOM 提供基礎結構以構建到系統組件(例如剪貼板)的遠程訪問時無論是 Windows 還是 DCOM 都無法提供能夠直接對剪貼板進行遠程訪問的 API開發人員可以利用的技巧是以本地代理與遠程存根之間的交互為基礎的應用程序調入本地代理進而在網絡傳輸層上序列化該調用並將其傳送到遠程主機然後該應用程序宿主將剪貼板處理程序組件的一個本地副本實例化以對 Windows 本地副本的剪貼板進行讀取和寫入操作
MicrosoftNET Framework 提供了 Clipboard 類來包裝系統剪貼板上的主要操作該 Clipboard 類作為 Windows 窗體基礎結構的一部分在 SystemWindowsForms 命名空間中進行聲明該類上的方法允許您在單個應用程序的上下文中獲得並設置剪貼板的當前內容
我的一位客戶要構建一個不使用剪貼板的 ASPNET 電子商務站點然而該團隊中的一名開發人員意識到負責填寫後端表格的人員需要持續不斷地在計算機間傳送大量數據(大部分為純文本)他們找到的最快方法是創建能夠跨網絡共享的臨時文本文件盡管可以接受這個特定技巧但整個過程都不太智能特別是文本首先會在 Microsoft Word 或 Microsoft Internet Explorer 中突出顯示然後被復制到剪貼板接著再粘貼到一個新的 Notepad 文檔中最後該文檔被拖放到網絡文件夾中在具備了良好的意志和新的 DCOM 存儲器後有悟性的開發人員會想到一個定制的遠程剪貼板查看器和管理器可以更快更有效地完成工作
圖 剪貼板查看器
剪貼板查看器(請參見圖 )是一個舊的 Windows 附件它不再出現在開始菜單中但是依然可以作為 clipbrdexe 從 System 文件夾中使用剪貼板查看器充當所有連接到網絡的計算機上的剪貼板的管理器如果當前的安全設置允許您就可以連接到 Windows 的一個遠程實例並監視計算機的剪貼板雖然查看器只是一個查看器(它顯示當前內容並可讓您刪除內容)但是它不提供向剪貼板中輸入新文本的用戶界面此外剪貼板查看器基於分布式技術(十幾年前的舊技術)— 網絡動態數據交換(或縮寫為 NetDDE)所以我的客戶決定編寫一個自定義版本的剪貼板查看器以作為實際的分布式應用程序因為他使用的是 NET Framework所以他在計劃時就想到了使用 NET Remoting 來設計實用工具
安裝遠程組件
NET Remoting 是一種機制能夠實現在不同 AppDomains 中運行的組件之間的通訊所有基於 NET Framework 的應用程序至少由一個(主)AppDomain 構成但是更多的 AppDomains 可以通過編程方式創建AppDomain 代表一個托管子進程該子進程存在於由操作系統和 CPU 管理的物理進程上下文中公共語言運行庫 (CLR) 可確保不能從一個 AppDomain 訪問包含在另一個 AppDomain 中的任何數據無論兩個應用程序域是位於同一個應用程序中同一計算機的兩個截然不同的應用程序中還是運行於物理分隔的計算機上分離機制都是完全相同的
從體系結構上講NET Remoting 的角色取決於系統將調用上下文從客戶端封送到服務器然後將結果發送回客戶端的能力遠程組件是一個具有公共方法的類它可以直接或間接地來自 MarshalByRefObject該類被編譯為程序集它部署在服務器計算機上並通過宿主應用程序與客戶端進行交互宿主應用程序負責偵聽傳入調用的特定端口將它接收到的參數轉換為對象的本地調用並將返回值封送回調用方圖 顯示了訪問網絡計算機剪貼板的遠程組件的體系結構
圖 訪問剪貼板的遠程組件
客戶端(假設運行在 Machine 上)發出一個對服務器端對象(假設駐留在 Machine 上)的遠程調用該調用由偵聽協議端口的應用程序宿主接收然後再進行處理該調用上下文包括要調用的遠程方法的名稱及其參數通過圖 中所示的代碼可以實現封裝剪貼板函數的組件marshalbyref 類將公開一個 Copy 方法以使客戶端能夠將文本寫入遠程剪貼板以及一個 Paste 方法以將遠程剪貼板的內容粘貼到本地上下文中圖 中的代碼和基本模式應該比較易於理解特別是如果您閱讀了我在 年 月刊上發表的 NET Remoting 介紹文章(請參閱 NET Remoting Design and Develop Seamless Distributed Applications for the Common Language Runtime)則更是如此截至目前一切都還不錯然而您可能已經注意到Copy 和 Paste 方法的主體都是空的起初我認為這部分代碼的內容無足輕重但是我大錯特錯了我解決問題的方式只是對 Win? 和 Visual Studio? 進行了一些改善
NET 剪貼板 API
正如先前提到的那樣基於 NET Framework 的應用程序使用 SystemWindowsForms 命名空間中的 Clipboard 類來管理剪貼板該類(不一定要實例化)包含一對靜態方法 — GetDataObject 和 SetDataObjectGetDataObject 檢索當前存儲在系統剪貼板中的數據SetDataObject 將指定的數據對象置於系統內存中
剪貼板支持多種數據格式包括用戶定義的格式Win 剪貼板 API 定義一組預先定義的格式並用助記鍵常量(一個整數值例如 CF_TEXT 或 CF_BITMAP)對其進行標識
由於該剪貼板是系統組件因此表示它的 NET Framework 類只為一組低級別的 API 函數提供包裝可能會使您感到驚訝的是剪貼板的 NET Framework 實現不是基於原始 Win 函數和消息的……NET Framework 中的 Clipboard 類通過 OLE 剪貼板協議執行操作您是說 OLE 是的沒錯!本專欄打算回顧一些早期技術它們是 Windows 發展過程中的重要裡程碑
大約在 年前Microsoft 引入了 OLE 作為一種全包含組件技術引用 Kraig Brockschmidt 在 Inside OLE(Microsoft Press 年)一書中的話OLE 被定義為一個基於對象服務的統一環境與使用 Win 不同您無法只使用 OLE(或者是它的繼任者 COM)來編寫一個完整的應用程序但是OLE 采用了某些現有的系統功能並以一種更通用更廣泛的方式來公開它們那麼什麼是 OLE 剪貼板協議呢?
從本質上說OLE 剪貼板是一組接口旨在泛化基於 Windows 的應用程序與剪貼板之間發生的數據交換Platform SDK 只定義幾個基本的數據類型(多數為文本和位圖)OLE 剪貼板協議通過提供對任意數據格式和存儲介質的支持來擴展模型只要應用程序所需的數據格式未超出 Platform SDK 提供的范圍那麼對於基於 Windows 的應用程序來說OLE 剪貼板協議就不是必需的基於 OLE 的協議比 Win 剪貼板 API 的功能更強大並且在 NET Framework 中構建剪貼板支持時OLE 是一個相當合理的選擇
在 OLE 和 NET Framework 中執行剪貼板操作的關鍵接口是 IDataObject這兩個接口具有不同的方法集但都扮演著同樣的角色IDataObject 提供一種獨立於格式的機制以傳輸數據並且由 Clipboard 類在拖放操作中使用(在 NET Framework 中OLE IDataObject 接口被重命名為 IOleDataObject)圖 列出了在 IDataObject 接口上定義的方法
以下代碼片段顯示了一個基於 NET Framework 的應用程序如何將純文本復制到剪貼板
ClipboardSetDataObject(text)
SetDataObject 方法提取一個對象並將它作為 IDataObject 實例復制到剪貼板中如果該參數是一個已實現 IDataObject 接口的對象則直接進行復制否則該方法將對象打包到一個動態創建的DataObject 類實例中如圖 所示
DataObject 類可實現 IDataObject 和 IOleDataObject 接口剪貼板的 SetDataObject 方法接受普通的對象參數
public static void SetDataObject(object)
public static void SetDataObject(object bool)
這段代碼允許您將簡單數據(例如文本和位圖)作為原生對象傳入而將數據改寫的重擔轉嫁給內置的基礎結構SetDataObject 方法還具有一個要求額外 Boolean 參數的重載該參數指示在當前應用程序終止之後置於剪貼板中的數據是否應該保持可用如果您使用一個參數的重載則剪貼板的內容會在退出時刷新
與將數據寫入剪貼板相比從剪貼板讀取數據更明了些這是因為數據推斷不能委托給 NET Framework以下代碼片段顯示了托管應用程序如何從剪貼板中進行讀取
IDataObject data
data = ClipboardGetDataObject()
GetDataObject 方法返回一個 IDataObject 數據包應用程序必須將其解包
// Verify that the data object contains plain text
if (dataGetDataPresent(DataFormatsText))
{
// Extrapolate and display the text
string text = dataGetData(DataFormatsText);
MessageBoxShow(text);
}
IDataObject 接口上的 GetDataPresent 方法(請參見圖 )采用一個參數來識別數據類型(例如純文本)如果該數據對象的內容與指定的類型相匹配則該方法會返回真請注意無論是存儲對象的原生類型相匹配還是對象的類型能夠轉換為所需類型該方法都會返回真例如如果數據對象包含 HTML 文本但是用戶要求純文本那麼 GetDataPresent 會返回真
從基於 NET Framework 的應用程序中使用剪貼板 API 不費吹灰之力但是如果您試圖從遠程對象來使用它就不那麼簡單了
構建遠程剪貼板處理程序
Windows 窗體應用程序通常以單線程單元 (STA) 模式運行通過將 [STAThread] 屬性添加到應用程序的 Main 例程C# 項目將這一模式清晰地呈現在您面前在 Visual Basic NET 中該設置是隱式的對於許多 GUI 應用程序而言STA 模式絕對是必要的因為這些應用程序依賴於由並不始終支持純多線程環境的操作系統所公開的服務這是典型的 OLE 和 COM 服務例如剪貼板和拖放操作
事實上您不能在來自 MTA 池的線程中使用剪貼板對象請試驗下面的小型控制台應用程序
using SystemWindowsForms;
class Test {
[MTAThread]
static void Main() {
ClipboardSetDataObject(MSDNMag);
}
}
這段代碼會拋出一個線程狀態異常如圖 所示
圖 線程狀態異常
能夠在基於 NET Framework 的應用程序中進行 OLE 調用之前程序員必須確保當前線程以 STA 模式運行實際上托管對象負責以一種線程安全的方式來公開它們的共享數據……NET Framework 支持線程單元只為獲得向後兼容性相反COM 組件使用線程單元出於該原因CLR 需要在與 COM 對象發生任何交互之前先創建一個線程單元STAThread 和 MTAThread 屬性都是聲明性編程接口用於為應用程序選擇線程模型在上面的代碼片段中MTAThread 屬性設置了控制台應用程序以創建一個托管線程並對其進行配置以輸入一個 MTA 單元只要應用程序調入 OLE/COM 的內容就可以檢測到單元沖突並引發一個異常通常控制台和 Windows 窗體應用程序以 STA 模式運行(除非指定其他模式)
當我第一次構建 marshalbyref 對象以將文本復制到遠程剪貼板時我保留了默認設置但是只要執行流到達 Clipboard 類就會引發線程異常請記住遠程調用始終是通過 MTA 線程解決的
在調入 Win OLE 方法之前Clipboard 類會根據當前線程的單元模式執行預備檢查這通過發出一個對 ApplicationOleRequired 方法的調用來完成該方法檢驗 OLE 是否在當前線程上進行初始化或者親自進行初始化(如果需要)該方法從列出三種可行值(TASTA 和 Unknown)的 ApartmentState 枚舉中返回一個值您可以通過下面的代碼檢查當前的線程模型
ConsoleWriteLine(ApplicationOleRequired()ToString())
調入遠程對象的客戶端應用程序是一個單線程的 Windows 窗體應用程序……NET Remoting 宿主是一個顯式標記為 STA 的控制台應用程序但是用於遠程調用的線程的單元狀態是 MTA您在圖 中看到的錯誤消息其實是以前的結論
對於如何解決 NET Remoting 文檔和可用參考資料(包括出色的 )中的問題我還沒有找到理想的解決方法所以我采用了 Kraig Brockschmidt 在有關 OLE 剪貼板的章節中給出的建議Kraig 指出只有當 OLE 剪貼板能夠為您帶來附加值時才應該使用它(如 NET Framework 所做的那樣)在只需要交換文本和位圖時您可以堅持使用 Win 剪貼板 API作為回報這樣做會減少線程之爭圖 顯示了一個 Win DLL它導出一對公共函數以將純文本復制並粘貼到剪貼板
當編碼 Win 方式時您必須首先打開並清空剪貼板您需要一個窗口句柄來打開剪貼板這是因為剪貼板只能屬於一個窗口對象OpenClipboard 是要調用的 API 函數EmptyClipboard 函數會釋放存儲在剪貼板中的全局數據的所有句柄之後當前讓剪貼板打開的窗口便成為新的所有者要將數據復制到剪貼板您必須分配一塊最好以 GHND 標志標記的共用內存該標志表示其可移動並可初始化為零復制到剪貼板或從中讀取的任何數據都被打包到共用內存的句柄中在 Win 級別上可以打包到存儲介質中的數據格式種類限制為幾種例如 HTML純文本或獨立於設備的位圖如果您使用 OLE則可以使用更多格式和存儲介質
雖然將 Win DLL 和 P/Invoke 平台用於 Win 交互操作會限制剪貼板只能使用幾種格式但是不需要更改線程模型並且也可以通過遠程使用圖 顯示了遠程剪貼板處理程序對象的最終代碼兩個 DLL 公共函數映射到 marshalbyref 類的靜態外部成員CopyToClipboard 的簽名不難轉換為 CLR 類型系統利用字符串更改 LPCTSTR 並完成操作導入 PasteFromClipboard 函數需要一點技巧在圖 中通過引用聲明該函數接受一個字符串並返回一個布爾值
將類似簽名轉換為 NET 代碼的有效方式涉及到 StringBuilder 對象的使用
[DllImport(clipdll)]
private static extern bool PasteFromClipboard(StringBuilder text)
您首先將對字符串的引用替換為初始化的 StringBuilder 對象然後使用 StringBuilder 類上的 ToString 方法來獲取該字符串
StringBuilder buf = new StringBuilder()
PasteFromClipboard(buf)return bufToString()
應當注意的是StringBuilder 對象與 String 對象不同前者是可增長的對象而後者是傳統的字符串它在本質上不會改變換言之在您串聯兩個字符串時NET Framework 會創建一個新的字符串其大小為二者之和在您將字符串添加到 StringBuilder 對象時只是將輸入文本追加到現有緩沖區而已
遠程剪貼板客戶端
遠程剪貼板客戶端由兩個元素組成即客戶端和遠程組件的主服務器要部署該客戶端您必須將主服務器(請參見圖 )和帶有遠程組件的程序集一起復制到要到達的任何計算機上您必須將客戶端應用程序復制到您希望在網絡中從其進行復制或粘貼的所有計算機上
圖 遠程剪貼板應用程序
NET Remoting 基礎結構不會自動啟動服務器端主機來接收對遠程組件的傳入調用用戶負責啟動並運行這種 stub 程序您可以對該任務使用 Microsoft Internet 信息服務 (IIS)或編寫自己的應用程序使用 IIS 會受到一些限制(需要 HTTP 通道)但是該方法可讓您不必管理遠程處理宿主或者您可以編寫自定義應用程序該應用程序只需注冊一條服務器通道(TCPHTTP 或自定義類型)並通過服務器 URI 將遠程類型與已知類型相關聯
在圖 中宿主是為端口 創建 TCP 通道並將 MsdnMagClipboardHandler 類型(請參見圖 )標記為已知的控制台應用程序用於調用對象的 URI 是 ClipboardHandler您必須手動啟動和終止控制台應用程序您也可以決定將其編寫為 GUI 應用程序並提供一個用戶界面來暫停或終止端口監視要暫停監視您可以取消注冊通道如果您不希望編寫控制台應用程序則可以選擇創建一個 Windows 服務該服務提供相同的功能而不需要手動處理啟動/停止操作
NET Remoting 組件的客戶端必須完成一項基本任務即使遠程類型可由其余的應用程序識別該任務通過使用 RemotingConfiguration 類的其中一個靜態成員(RegisterWellKnownClientType 方法)來執行以下代碼片段顯示了如何將類型注冊為已知
RemotingConfigurationRegisterWellKnownClientType(
typeof(MsdnMagClipboardHandler)
tcp://expotwo/ClipboardHandler)
RegisterWellKnownClientType 方法使用兩個參數第一個是待定的類型第二個是一個 URI它包括用於封送的傳輸協議服務器名稱或 IP 地址要使用的端口以及遠程對象的昵稱如之前的代碼所示當然該端口必須與遠程主機在其上進行偵聽的端口相匹配雖然已知類型的概念易於理解但不能在本地定義遠程類型並且對任何方法或屬性的任何引用都必須以不同的方式進行處理在每個調用前面編譯器都必須生成一些普通的代碼以將調用封送到遠程服務器並返回出於這個原因必須識別並標記源代碼中對遠程對象的所有引用以便實時 (JIT) 編譯器可以為其正確生成動態代碼
RegisterWellKnownClientType 方法的內部實現並不復雜它可緩存類型信息並將其添加到已知類型的全局哈希表它將類型用作密鑰而將 URI 字符串用作成對的值已知類型的編程接口決不允許您取消注冊類型如果您試圖將一個已知類型重定向到另一個 URI則會引發一個異常
正如您看到的那樣RegisterWellKnownClientType(以及類似方法如 RegisterActivatedClientType)通過在類型和服務器 URI 之間建立一對一的關系來運行如果應用程序需要從不同的服務器調用相同的類型應該怎麼做呢?好這就是在構建遠程剪貼板處理程序時要面對的下一個問題
如圖 所示客戶端應用程序必須多次注冊相同的遠程類型為每台連接的服務器注冊一次遠程剪貼板客戶端必須能夠對網絡上可用的所有計算機的剪貼板進行讀取和寫入操作這些計算機都會運行同一宿主和同一類型的實例即圖 中的 MsdnMagClipboardHandler 類如何多次將同一類型注冊到不同的 URI 您有兩個選擇第一個要求跳過內置的配置機制您不用將遠程類型標記為已知而只需使用 ActivatorGetObject 方法的一個重載來創建並獲取對象的遠程實例
string uri = @tcp\\expotwo\ClipboardHandler
object o = ActivatorGetObject(typeof(MsdnMagClipboardHandler) uri)
MsdnMagClipboardHandler clip
clip = (MsdnMagClipboardHandler) o
GetObject 方法為指定類型和 URL 所指示的對象獲取或創建代理該解決方案為您提供了極大的靈活性因為它並不依賴於服務器的數量及其位置
第二個選擇是如果您明確知道客戶端始終與之協同工作的服務器則可使用更為嚴格的方法將傳統的已知類型方法擴展到多台服務器的情況其思想是通過派生幾乎完全相同的新類來重命名遠程類型在計算機名之後的命名空間中將新類設為根(盡管這是任意的)
圖 中的代碼顯示了如何使用繼承來重命名 MsdnMagClipboardHandler 類如果重新編譯則 ExpoTwo 和 ExpoStar 命名空間中的新類將不會添加額外的代碼並且不需要系統開銷要支持新服務器您必須添加新的命名空間聲明應當注意的是該解決方案缺乏靈活性因為它對服務器的名稱進行硬編碼並且任何更改(例如將新服務器添加到列表)都要求重新編譯以下代碼說明了來自客戶端的遠程調用
ExpoTwoRemoteClipboardHandler rc
rc = new
ExpoTwoRemoteClipboardHandler()
rcCopy(TextToCopyText)
在調用已知類型上的方法時NET Remoting 基礎結構會驗證宿主應用程序是否已啟動並處於運行狀態如果不是則會引發一個套接字異常引發的特定異常是 SocketException該類屬於 SystemNetSockets 命名空間客戶端應用程序的示例項目引用了包含遠程對象的組件
盡管其功能不會引發異常但該方法或許不是實際情況中的最佳選擇版本控制類依賴項甚至大小都是確定備選方法的理由例如您可以通過為所有公共方法甚至接口定義帶有空實現的基類來避免鏈接原始程序集盡管在後一種情況中您不能在客戶端上只使用 new operator 來獲取遠程對象的實例實際上按照設計您無法在接口上調用 new您可以使用 ActivatorGetObject 來檢索在指定 URI 處實現給定接口的對象的實例有關該特定點的詳細信息以及與 NET Remoting 相關的工作的有價值資源請參閱
小結
圖 中所示的客戶端應用程序提供了在特定計算機上復制和粘貼純文本的按鈕通過使用低級別 API 調用的 Win DLL 來完成系統剪貼板上的物理操作對於跨網絡中的計算機共享剪貼板這一問題 上的文章提供了更為有用的方法
圖 復制和粘貼到特定計算機
對於我的客戶端用途而言Win 基本格式就足夠了希望將來沒有這些限制我的解決方案將模擬 ASPNET 會話的分布式體系結構引入充當宿主的 Windows 服務並添加在 STA 線程上以遠程方式操縱剪貼板的 marshalbyref 類最後我會將一對重載添加到現有 Clipboard 類的方法中
請將給 Dino 的問題和意見發送至 c
Dino Esposito 是一位講師兼顧問現居住在意大利的羅馬他著有 Building Web Solutions with ASPNET and ADONET 和 Applied XML Programming for NET這兩本書均出版自 Microsoft Press他的大部分時間都用於講授有關 ASPNET 的課程以及會議演講Dino 最近為 Microsoft Press 出版了 Programming ASPNET 一書您可以通過 與 Dino 取得聯系
From:http://tw.wingwit.com/Article/os/xtgl/201311/9235.html