国产一级a片免费看高清,亚洲熟女中文字幕在线视频,黄三级高清在线播放,免费黄色视频在线看

打開APP
userphoto
未登錄

開通VIP,暢享免費電子書等14項超值服

開通VIP
InfoQ: 文章:RichClient/RIA原則與實踐(上)

文章:RichClient/RIA原則與實踐(上)

作者 陳金洲 發(fā)布于 2009年3月16日 上午8時42分

主題 RIA, 富客戶端/桌面 標簽 原則

Web領(lǐng)域的經(jīng)驗在過去十多年的不斷的 使用和錘煉中,整個開發(fā)領(lǐng)域的技術(shù)、理念、缺陷已經(jīng)趨于成熟,它豐富的積累使得開發(fā)者逐漸將更多的精力投入到應用本身。但是,目前仍然沒有比較深入的實踐 性文章來介紹企業(yè)環(huán)境下RichClient開發(fā),而只是偏向于小規(guī)模特性介紹,但在大規(guī)模的企業(yè)應用中,這些小的技巧對于架構(gòu)決策往往幫助很小。作者在 加入ThoughtWorks之后,參加了多個不同的RichClient項目的開發(fā)工作,使用/嘗試過的語言包括Java Swing、Flex/Adobe Air、.NET WinForm/.NET WPF,對于不同平臺之間的種種有些體會。在本文中,作者將這些實踐和原則進行了總結(jié)。

在講述“一切皆異步”這條原則時,作者說到:

所有耗時的操作都應當異步進行。這是第一條、也是最重要的原則,違背了這條原則將會導致你的應用完全不可用。

考慮這樣的一個功能:點擊一個"更新股票信息"按鈕,系統(tǒng)會從股票市場(第三方應用)獲得最新的股票信息,并將信息更新到主界面。絲毫不考慮用戶體驗的寫法:

void updateStockDataButton_clicked() {
    stockData = stockDataService.getLatest(); // 從遠程獲取股票信息
    updateUI(stockData); // 這個方法會更新界面
}

那么,當用戶點擊updateStockDataButton的時候,會有什么反應?難說。如果是一個無限帶寬、無限計算資源的世界,這段代碼直觀又易 懂,而且工作的非常好:它會從第三方股票系統(tǒng)讀到股票數(shù)據(jù),并且更新到界面上??上Р皇?。這段代碼在現(xiàn)實世界工作的時候,當用戶點擊這個按鈕,整個界面會 凍結(jié)──知道那種感覺嗎?就是點完這個按鈕,界面不動了;如果你在使用Windows, 然后嘗試拽住窗口到處移動,你會發(fā)現(xiàn)這個窗口經(jīng)過的地方都是白的。你的客戶不會理解你的程序?qū)嶋H上在很努力的從股票市場獲得數(shù)據(jù),他們只會很憤怒的說,這 個東西把我的機器弄死了!他們的思路被打斷了。于是他們不再使用你的程序,你們的合作沒了。你沒錢了。你的狗也跑了。

在談到“視圖生命周期管理”時,作者將Web開發(fā)和RichClient開發(fā)進行了對比:

在WEB開發(fā)中,視圖的生命周期很短:在進入頁面的時候創(chuàng)建,在離開頁面的時候銷毀。一不小心頁面被弄糟了,或者不能按照預期的渲染了,點下刷新按鈕,整個世界一片清凈。

WEB下的視圖導航也是如此自然?;诔溄拥姆绞?,每點擊一次,就能夠打開一個新的頁面,舊的頁面被瀏覽器銷毀,新的頁面誕生。(這里不考慮AJAX或者其他JavaScript特效)

如果把這種想法帶入到RichClient開發(fā),后果會很糟糕。每當點擊按鈕或者進行其他操作需要導航到新的窗口,你不加任何限制的創(chuàng)建新窗口或者新的視 圖。然而CPU不是無限的。創(chuàng)建一個新的視圖通常是很耗CPU和內(nèi)存的。系統(tǒng)響應會變慢。用戶會抱怨,拒絕付錢,于是因為饑餓,你的狗再次離開了你。

每次新創(chuàng)建視圖產(chǎn)生的嚴重后果并不僅僅是非功能性的,還包括功能性的缺失。如果你用過Skype,當你在給張三通話的時候,再次點擊張三并且進行通話,你 會發(fā)現(xiàn)剛剛的通話界面會彈出來,而不是開啟新窗口。在我們的一個項目中,有一個功能:點擊軟件界面上的電話號碼就能開啟一個新窗口,并直接連到桌上的電話 撥號通話。可以想象,如果每次都會彈出新的窗口,軟件的邏輯是根本錯誤的。

3 事件管理

事件管理應當是整個RichClient/RIA開發(fā)中的最難以把握的部分。這部分控制的好,你的程序用起來將如行云流水,用戶的思維不會被打斷。任何一 個做RichClient開發(fā)的程序員,可以對其他方面毫無所知,但這部分應當非常熟悉。事件是RichClient的核心,是“一切皆異步”的終極實現(xiàn)。前面所說的例子,實際上可以被抽象為事件,例如第一個,獲取股票數(shù)據(jù),從事件的觀點看,應該是:

         開始獲取股票數(shù)據(jù)
  • 正在獲取股票數(shù)據(jù)
  • 獲取數(shù)據(jù)完成
  • 獲取數(shù)據(jù)失敗

看起來相當復雜。然而這樣去考慮的時候,你可以將執(zhí)行計算與界面展現(xiàn)清晰的分開。界面只需要響應事件,運算可以在另外的地方 悄悄的進行,并當任務完成或者失敗的是時候報告相應的事件。從經(jīng)驗看來,往往同樣的數(shù)據(jù)會在不同的地方進行不同的展示,例如skype在通話的時候這個人 的頭像會顯示為占線,而具體的通話窗口中又是另外不同的展現(xiàn);MSN的個人簽名在好友列表窗口中顯示為一個點擊可以編輯控件,而同時在聊天窗口顯示為一個 不能點擊只能看的標簽。這是RichClient的特性,你永遠不知道同一份數(shù)據(jù)會以什么形式來展現(xiàn),更要命的是,當數(shù)據(jù)在一個地方更新的時候,其他所有 能展現(xiàn)的地方都需要同時做相應的更新。如果我們?nèi)匀灰缘谝徊糠值睦?,簡單采?code>runInAnoterThread是完全不能解決這個問題的。

我們曾經(jīng)犯過一些很嚴重的錯誤,導致最終即便重構(gòu)都積重難返。無視事件的抽象帶來的影響是架構(gòu)級別的,小修小補將無濟于事。

事件的實現(xiàn)方式可以有很多種。對于沒有事件支持的語言,接口或者干脆某一個約束的方法就可以。有事件支持的語言能夠享受到好處,但仍然是語法級別的,根本 是一樣的。觀察者模式在這里很好用。仍然以股票為例,被觀察的對象就是獲取股票數(shù)據(jù)對象StockDataRetriver,觀察的就是StockWindow

StockDataRetriver {observers: []retrieve() {try {theData = ...// 從遠程獲取數(shù)據(jù)observers.each {|o| o.stockDataReady(theData)}  // 觸發(fā)數(shù)據(jù)獲取成功事件} catch {observers.each { |o| o.stockDataFailed() }  // 觸發(fā)事件獲取失敗事件}}}StockDataRetriver.observers.add(StockWindow)  // 將StockWindow加入到觀察者隊列StockWindow {stockDataReady(theData) {showDataInUIThread(); // 在UI線程顯示數(shù)據(jù)}stockDataFailed() {showErrorInUIThread(); // 在UI線程顯示錯誤}}

你會發(fā)現(xiàn)代碼變得簡單。UI與計算之間的耦合被事件解開,并且區(qū)分UI線程與運算線程之間也變得容易。當嘗試以事件的視角去觀察整個應用程序的時候,你會更關(guān)注于用戶與界面之間的交互。

讓我們繼續(xù)抽象。如果把“獲取股票數(shù)據(jù)”這個按鈕點擊,讓StockDataRetriver去獲取數(shù)據(jù)當作事件來處理,應該怎么寫呢?將按鈕作為被觀察 者,StockDataRetriver作為觀察者顯然不好,好不容易分開的耦合又黏在一起。引入一個中間的Events看起來不錯:

Events {listeners: {}register(eventId, listener) {listeners[eventId].add(listener)}broadcast(eventId) {listeners[eventId].observers.each{|o| o.doSomething(); }}}

Events中維護了一個listeners的列表,它是一個簡單的Hash結(jié)構(gòu),key是eventId,value是observer的列表;它提供了兩個方法,用來注冊事件監(jiān)聽以及通知事件產(chǎn)生。對于上面的案例,可以先注冊StockDataRetriver為一個觀察者,觀察start_retrive_stock_data事件:

Events.register('start_retrive_stock_data', StockDataRetriever)

當點擊“獲取股票數(shù)據(jù)”按鈕的時候,可以是這樣:

Events.broadcast('start_retrive_stock_data')

你會發(fā)現(xiàn)StockDataRetriver能夠老老實實的開始獲取數(shù)據(jù)了。

需要注意的是,并非將所有事件定義為全局事件是一個好的實踐。在更大規(guī)模的系統(tǒng)中,將事件進行有效整理和分級是有好處的。在強類型的語言(如 Java/C#)中,抽象出強類型的EventId,能夠幫助理解系統(tǒng)和進行編程,避免到處進行強制類型轉(zhuǎn)換。例如,StockEvent

StockDataLoadedEvent {StockData theData;StockDataLoadedEvent(StockData theData);}Event.broadcast(new StockDataLoadedEvent(loadedData))

這個事件的監(jiān)聽者能夠不加類型轉(zhuǎn)換的獲得StockData數(shù)據(jù)。上面的例子是不支持事件的語言,C#語言支持自定義強類型的事件,用起來要自然一些:

delegate void StockDataLoaded(StockData theData)

事件管理原則我相信并不難理解。然而困難的是具體實現(xiàn)。對一個新的UI框架不熟悉的時候,我們經(jīng)常在“代碼的優(yōu)美”與“界面提供的特性”之間徘徊。實現(xiàn)這 樣的一個事件架構(gòu)需要在項目一開始就稍具雛形,并且所有的事件都有良好的命名和管理。避免在命名、使用事件的時候的隨意性,對于讓代碼可讀、應用穩(wěn)定有非 常大的意義。一個好的事件管理、通知機制是一個良好RichClient應用的根本基礎(chǔ)。一般說來,你正在使用的編程平臺如Swing/WinForm /WPF/Flex等能夠提供良好的事件響應機制,即監(jiān)聽事件、onXXX等,但一般沒有統(tǒng)一的事件的監(jiān)聽和管理機制。對于架構(gòu)師,對于要使用的編程平臺 對于這些的原生支持要了熟于心,在編寫這樣的事件架構(gòu)的時候也能兼顧這些語言、平臺提供給你的支持。

采用了事件的事件后,你不得不同時實踐“線程管理”,因為事件一般來說意味著將耗時的操作放到別的地方完成,當完成的時候進行事件通知。簡單的模式下,你可以在所有需要進行異步運算的地方,將運算放到另外一個線程,如ThreadPool.QueueUserWorkItem, 在運算完成的時候通知事件。但從資源的角度考慮,將這些線程資源有效的管理也是很重要的,在“線程管理”部分有詳細的闡述。另外,如果能將你的應用轉(zhuǎn)變?yōu)?數(shù)據(jù)驅(qū)動的,你需要關(guān)注“緩存以及本地存儲”。

4 線程管理

在WEB開發(fā)幾乎無需考慮線程,所有的頁面渲染由瀏覽器完成,瀏覽器會異步的進行文字和圖片的渲染。我們只需要寫界面和JavaScript就好。如果你認同“一切皆異步”,你一定得考慮線程管理。

毫無管理的線程處理是這樣的:凡是需要進行異步調(diào)用的地方,都新起一個線程來進行運算,例如前面提到的runInThread的實現(xiàn)。這種方式如果托管在 在“事件管理”之下,問題不大,只會給測試帶來一些麻煩:你不得不wait一段時間來確定是否耗時操作完成。這種方式很山寨,也無法實現(xiàn)更高級功能。更好 的的方式是將這些線程資源進行統(tǒng)籌管理。

線程的管理的核心功能是用來統(tǒng)一化所有的耗時操作,最簡單的TaskExecutor如下:

TaskExecutor {void pendTask(task) { //task: 耗時操作任務runInThread {task.run(); // 運行任務}}}RetrieveStockDataTask extends Task {void run() {theData = ... // 直接獲取遠程數(shù)據(jù),不用在另外線程中執(zhí)行Events.broadcast(new StockDataLoadedEvent(theData)) // 廣播事件}}

需要進行這個操作的時候,只需要執(zhí)行類似于下面的代碼:

TaskExecutor.pendTask(new RetrieveStockDataTask())

好處很明顯。通過引入TaskExecutor,所有線程管理放在同一個地方,耗時操作不需要自行維護線程的生命周期。你可以在TaskExecutor中靈活定義線程策略實現(xiàn)一些有趣的效果,如暫停執(zhí)行,監(jiān)控任務狀況等,如果你愿意,為了更好的進行調(diào)試跟蹤,你甚至可以將所有的任務以同步的方式執(zhí)行。

耗時任務的定義與執(zhí)行被分開,使得在任務內(nèi)部能夠按照正常的方式進行編碼。測試也很容易寫了。

不同的語言平臺會提供不同的線程管理能力。.NET2.0提供了BackgroundWorker, 提供了一序列對多線程調(diào)用的封裝,事件如開始調(diào)用,調(diào)用,跨線程返回值,報告運算進度等等。它內(nèi)部也實現(xiàn)了對線程的調(diào)度處理。在你要開始實現(xiàn)類似的TaskExecutor時,參考一下它的API設(shè)計會有參考價值。Java 6提供的Executor也不錯。

一個完善的TaskExecutor可以包含如下功能:

  • Task的定義:一個通用的任務定義。最簡單的就是run(),復雜的可以加上生命周期的管理:start()、end()、success()fail()..取決于要控制到多么細致的粒度。
  • pendTask,將任務放入運算線程中
  • reportStatus,報告運算狀態(tài)
  • 事件:任務完成
  • 事件:任務失敗

寫這樣的一個線程管理的不難。最簡單的實現(xiàn)就是每當pendTask的時候新開線程,當運算結(jié)束的時候報告狀態(tài)。或者使用像BackgroundWorker或者Executor這樣的高級API。對于像ActionScript/JavaScript這樣的,只能用偽線程, 或者干脆將無法拆解的任務扔到服務器端完成。

5 緩存與本地存儲

純粹的B/S結(jié)構(gòu),瀏覽器不持有任何數(shù)據(jù),包括基本不變的界面和實際展現(xiàn)的數(shù)據(jù)。RichClient的一大進步是將界面部分本地持有,與服務器只作數(shù)據(jù)通訊,從而降低數(shù)據(jù)流量。像《魔獸世界》10多G的超大型客戶端,在普通的撥號網(wǎng)絡(luò)都可以順暢的游戲。

緩存與本地存儲之間的差別在于,前者是在線模式下,將一段時間不變的數(shù)據(jù)緩存,最少的與服務器進行交互,更快的響應客戶;后者是在離線模式下,應用仍然能 夠完成某些功能。一般來說,凡是需要類似于“查看XXX歷史”功能的,需要“點擊列表查看詳細信息”的,都會存在本地存儲的必要,無論這個功能是否需要向 用戶開放。

無論是緩存還是本地存儲,最需要處理的問題如何處理本地數(shù)據(jù)與服務器數(shù)據(jù)之間的更新機制。當新數(shù)據(jù)來的時候,當舊數(shù)據(jù)更新的時候,當數(shù)據(jù)被刪除的時候,等 等。一般來說,引入這個實踐,最好也實現(xiàn)基于數(shù)據(jù)變化的“事件管理”。如果能夠?qū)崿F(xiàn)“客戶機-服務器數(shù)據(jù)交互模式”那就更完美了。

我們犯過這樣一個錯誤。系統(tǒng)啟動的時候,將當前用戶的聯(lián)系人列表讀取出來,放到內(nèi)存中。當用戶雙擊這個聯(lián)系人的時候,彈出這個聯(lián)系人的詳細信息窗口。由于 沒有本地存儲,由于采用了Navigator方式的導航,于是很自然的采用了Navigator.goTo('ContactDetailWindow', theContactInfo)。由于列表頁面一般是不變的,因此顯示出來的永遠是那份舊的數(shù)據(jù)。后來有了編輯聯(lián)系人信息的功能,為了總是顯示更新的數(shù) 據(jù),我們將調(diào)用更改為Navigator.goTo('ContactDetailWindow', 'contactId'),然后在ContactDetailWindow中按照contactId把聯(lián)系人信息重新讀取一次。遠在南非的用戶抱怨慢。還 好我沒養(yǎng)狗,沒有狗離開我。后來我們慢慢的實現(xiàn)了本地存儲,所有的數(shù)據(jù)讀取都從這個地方獲得。當數(shù)據(jù)需要更新的時候,直接更新這個本地存儲。

本地存儲會在根本上影響RichClient程序的架構(gòu)。除非本地不保存任何信息,否則本地存儲一定需要優(yōu)先考慮。某些編程平臺需要你在本地存儲界面和數(shù) 據(jù),如Google Gears的本地存儲,置于Adobe Air的AJAX應用等,某些編程平臺只需要存儲數(shù)據(jù),因為界面完全是本地繪制的,如Java/JavaFX/WinForm/WPF等。緩存界面與緩存 數(shù)據(jù)在實現(xiàn)上差別很大。

本地存儲的存儲機制最好是采用某一種基于文件的關(guān)系數(shù)據(jù)庫,如SQLite、H2(HypersonicSQL)、Firebird等。一旦確定要采用本地存儲,就從成熟的數(shù)據(jù)庫中選擇一個,而不要嘗試著自己寫基于文件的某種緩存機制。你會發(fā)現(xiàn)到最后你實現(xiàn)了一個山寨版的數(shù)據(jù)庫。

在沒有考慮本地存儲之前,與遠端的數(shù)據(jù)訪問是直接連接的:

我們上面的例子說明,一旦考慮使用本地存儲,就不能直接訪問遠程服務器,那么就需要一個中間的數(shù)據(jù)層:

數(shù)據(jù)層的主要職責是維護本地存儲與遠程服務器之間的數(shù)據(jù)同步,并提供與應用相關(guān)的數(shù)據(jù)緩存、更新機制。數(shù)據(jù)更新機制有兩種,一種是Proxy(代理)模式,一種是自動同步模式。

代理模式比較容易理解。每當需要訪問數(shù)據(jù)的時候,將請求發(fā)送到這個代理。這個代理會檢查本地是否可用,如果可用,如緩存處于有效期,那么直接從本地讀取數(shù) 據(jù),否則它會真正去訪問遠端服務器,獲取數(shù)據(jù),更新緩存并返回數(shù)據(jù)。這種手工處理同步的方式簡單并且容易控制。當應用處于離線模式的時候仍然可以工作的很 好。

自動同步模式下,客戶端變成都針對本地數(shù)據(jù)層。有一個健壯的自動同步機制與服務器的保持長連接,保證數(shù)據(jù)一直都是更新的。這種方式在應用需要完全本地可運行的時候工作的非常好。如果設(shè)計得好,自動同步方式健壯的話,這種方式會給編程帶來極大的便利。

說到同步,很多人會考慮數(shù)據(jù)庫自帶的自動同步機制。我完全不推薦數(shù)據(jù)庫自帶的機制。他們的設(shè)計初衷本身是為了數(shù)據(jù)庫備份,以及可擴展性 (Scalability)的考慮。在應用層面,數(shù)據(jù)庫的同步機制往往不知道具體應用需要進行哪些數(shù)據(jù)的同步,同步周期等等。更致命的是,這種機制或多或 少會要求客戶端與服務器端具備類似的數(shù)據(jù)庫表結(jié)構(gòu),遷就這樣的設(shè)計會給客戶端的緩存表設(shè)計帶來很大的局限。另外,它對客戶機-服務器連接也存在一定的局限 性,例如需要開放特定端口,特定服務等等。對于純粹的Internet應用,這種方式更是完全不可行的,你根本不知道遠程數(shù)據(jù)庫的結(jié)構(gòu),例如 Flickr, Google Docs.

當本地存儲+自動同步機制與“事件管理”都實現(xiàn)的時候,應用會是一種全新的架構(gòu):基于數(shù)據(jù)驅(qū)動的事件結(jié)構(gòu)。對于所有本地數(shù)據(jù)的增刪改都定義為事件,將關(guān)心 這些數(shù)據(jù)的視圖都注冊為響應的觀察者,徹底將數(shù)據(jù)的變化于展現(xiàn)隔離。界面永遠只是被動的響應數(shù)據(jù)的變化,在我看來,這是最極致的方式。

結(jié)尾

限于篇幅,這篇文章并沒有很深入的討論每一種原則/實踐。同時還有一些在RichClient中需要考慮的東西我們并沒有討論:

  • 純Internat應用離線模式的實現(xiàn)。像AdobeAir/Google Gears都有離線模式和本地存儲的支持,他們的特點是緩存的不僅僅是數(shù)據(jù),還包括界面。雖然常規(guī)的企業(yè)應用不太可能包含這些特性,但也具備借鑒意義。
  • 狀態(tài)的控制。例如管理員能夠看到編輯按鈕而普通用戶無法看見,例如不同操作系統(tǒng)下的快捷鍵不同。簡單情況下,通過if-else或者對應編程平臺下提供的綁定能夠完成,然而涉及到更復雜的情況時,特別是網(wǎng)絡(luò)游戲中大量互斥狀態(tài)時,一個設(shè)計良好的分層狀態(tài)機模型能夠解決這些問題。如何定義、分析這些狀態(tài)之間的互斥、并行關(guān)系,也是處理超復雜
  • 測試性。如何對RichClient進行測試?特別是像WPF、JavaFX、Adobe Air等用Runtime+編程實現(xiàn)的框架。它們控制了視圖的創(chuàng)建過程,并且傾向于綁定來進行界面更新。采用傳統(tǒng)的MVP/MVC方式會帶來巨大的不必要的工作量(我們這么做過!),而且測試帶來的價值并沒有想象那么高。
  • 客戶機-服務器數(shù)據(jù)交互模式。如何進行客戶機服務器之間的數(shù)據(jù)交互?最簡單的方式是類似于Http Request/Response。這種方式對于單用戶程序工作得很好,但當用戶之間需要進行交互的時候,會面臨巨大挑戰(zhàn)。例如,股票代理人關(guān)注亞洲銀行板塊,剛好有一篇新的關(guān)于這方面的評論出現(xiàn),股票代理人需要在最多5分鐘內(nèi)知道這個消息。如果是Http Request/Response, 你不得不做每隔5分鐘刷一次的蠢事,雖然大多數(shù)時候都不會給你數(shù)據(jù)。項目一旦開始,就應當仔細考慮是否存在這樣的需求來選擇如何進行交互。這部分與本地存儲也有密切的關(guān)系。
  • 部署方式。RichClient與B/S 直接最大的差異就是,它需要本地安裝。如何進行版本檢測以及自動升級?如何進行分發(fā)?在大規(guī)模訪問的時候如何進行服務器端分布式部署?這些問題有些被新技術(shù)解決了,例如Adobe Air以及Google Gears,但仍然存在考慮的空間。如果是一個安全要求較高的應用,還需要考慮兩端之間的安全加密以及客戶端正確性驗證。新的UI框架層出不窮。開始一個新的RichClient項目的時候,作為架構(gòu)師/Tech Lead首先應當關(guān)注的不是華麗的界面和效果,應當觀察如何將上述原則和時間華麗的界面框架結(jié)合起來。就像我們開始一個web項目就會考慮domain 層、持久層、服務層、web層的技術(shù)選型一樣,這些原則和實踐也是項目一開始就考慮的問題。

感謝

感謝我的同事周小強、付瑩在我寫作過程中提供的無私的建議和幫助。小強推薦了介紹Google Gears架構(gòu)的鏈接,讓我能夠?qū)懽?#8220;本地存儲”部分有了更深的體會。

這篇文章是我近兩年來在RichClient工作、網(wǎng)絡(luò)游戲、WebGame眾多思考的一個集合。我嘗試過JavaFX/WPF/AdobAir 以及相關(guān)的文章,然而大多數(shù)的例子都是從華麗的界面入手,沒有實踐相關(guān)的內(nèi)容。有意思的反而是《大型多人在線游戲開發(fā)》這本書,給了我在企業(yè) RichClient開發(fā)很多啟發(fā)。我們曾經(jīng)犯了很多錯誤,也獲得了許多經(jīng)驗,以后我們應當能做得更好。

本站僅提供存儲服務,所有內(nèi)容均由用戶發(fā)布,如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊舉報。
打開APP,閱讀全文并永久保存 查看更多類似文章
猜你喜歡
類似文章
RichClient/RIA原則與實踐
MYSQL 邏輯架構(gòu)
1招教你怎么清理蘋果手機垃圾,快來!
極簡簡歷如何清空本地數(shù)據(jù)緩存
先刪緩存還是先刪數(shù)據(jù)庫
線程的使用——為datagridView添加數(shù)據(jù)行
更多類似文章 >>
生活服務
分享 收藏 導長圖 關(guān)注 下載文章
綁定賬號成功
后續(xù)可登錄賬號暢享VIP特權(quán)!
如果VIP功能使用有故障,
可點擊這里聯(lián)系客服!

聯(lián)系客服