2014年1月31日 星期五

Agile Contract Type (III) Comparison

在前兩篇整理了幾種合約類型,最後一篇是整理比較表。對於合約,就像「好吃」的定義一樣,每個人都可以講一套自己的理論,所以小弟找了一個甲方資深同學,請他加上甲方對各類合約的想法,還好這篇在 Blog 發表,不是論文,要不然就只好叫他對自己的內容 "出來面對" ╮(╯_╰)╭。除了甲方的想法外,加上自己的一些想法,合約的目的是促進甲乙方的合作,對於可能發生的風險可以有一個雙方都可以接受的方案,而不是造成甲乙雙方互不信任,各位捧有,Agile Manifesto 講的 "與客戶合作重於合約協商 Customer Collaboration over Contract Negotiation"  說的真是有道理阿~~

合約類型 小弟 Comment 資深甲方's Comment
固定價格合約(Fixed price) 範圍夠明確的專案才比較適合。
現在台灣的軟體專案大部分都簽這種類型的,但是台灣的甲方(特別是私人企業端的)對「完成」的定義會無限上綱,一不小心就是相看兩厭..甲方覺得乙方難配合,乙方寧願不收驗收期的款項,儘早開始往下一個乙方專案。
至於固定價格合約(Fixed price) 我的心得 就是合約階段 一定要跟老闆想要的階段 整合在一起(老闆想要的就是我們這種小小承辦人績效來源)
有一次有一個一年大案子 基本上分 三個階段 要完成 但第一階段 佔50%工作量 卻被要求 2個月內要完工 意思就是合理工期至少6 個月的 工期 卻被要求縮短30% 完工, 我只好使出殺手鐧: 我跟廠商說你幫我做到 我一次直接先給你2個階段費用 ) 反正後面還有第三階段 二三階段 廠商看在第三階段款 也不太可能不做。那一次經驗 果然廠商在我 [威脅利誘]下 又早2天完成第一階段上線…。
時間與材料合約/有上限的時間與材料合約( T&M / T&M with cap) 適用一定範圍內的解決問題
解決個Bug、改個什麼確定的程式功能,這種就滿適合的。看甲方是時間優先還是預算優先,高手貴一點,資淺的便宜一點,有時候解決問題比錢重要...
至於時間與材料合約/有上限的時間與材料合約( T&M / T&M with cap)跟固定價格合約(Fixed price)  我通常運用在 不同性致的廠商或專案上:

如果這個需求非常急 就是那種 7/25 跟你說 8/1 一定要上,如果不上公司會遭受多少莫大損失,而手上廠商對需求範圍熟悉度很低,根本時間不讓我做到最簡單的需求確認 /SA 分析/SD分析 /CODING 測試 ….我就會直接選擇時間與材料合約/有上限的時間與材料合約( T&M / T&M with cap) 直接跟對方談 我跟你買5天人CODING 人力 你派人來 我直接跟你說改哪邊 做什麼?(用嘴巴作完需求確認 /SA 分析/SD分析….)  基本上我還會多利用目標成本合約精神,我會跟廠商說 我會至少買5 天 ,三天你的人就做完 那是你的本事 不會少給你2 天。 那一次廠商 就派個高手來 4HR 作完5 天要做的事情!!!!
漸進式交付合約(Incremental Delivery) 長時間、大範圍的專案應該都考慮用這種專案
事實上大範圍應該先切分階段個別簽約,對雙方都會比較好,先有一個小專案磨合比較容易瞭解,但是有時候就一定要一次發一大包的話,那應該切多個小階段,每個階段做完考慮還需不需要繼續。如果只為了消化預算而繼續開發價值低(使用頻率低 或是 為了未來一兩年後可能發生需求..)的系統,那才真的是浪費錢...
 一般軟體外包客製和約,買方公司不太可能簽漸進式交付合約(Incremental Delivery 因為如果這個需求 至少要買5箱蘋果 你不可能跟老闆說 我們先跟對方簽 10買10箱蘋果  然後再看看要用到幾箱 因為老闆絕對會質疑承辦人 阿 你是不是 搞不清楚需求 不然 幹嘛抓不準?(承辦人寧願買貴 不願買錯…買錯是要負政治責任!!!!)
目標成本合約(Target Cost Contract) 如果甲乙雙方已經建立信任的基礎,對於軟體專案,應該多多考慮採用這種合約,激勵甲方也讓乙方提早使用到有價值的軟體。 目標成本合約(Target Cost Contract) 也不太可能 同樣道理 如果只要花 100 元就可以買到 5 箱蘋果 老闆不會允許 你 如果提前一個月收到蘋果 少付對方 20元,老闆只會要求: 你就是給我提前一個月辦完事情 而且我只願出80 元 ,(小弟 曾經有一次還遇到 老闆不只80 不願出 只願給 50 元  因為 老闆的朋友跟他說 你買貴了…直接硬砍50%)



總結小弟甲乙方都做過,個人觀察,這幾年,台灣的甲方因為陸續導入ERP,被外商訓練過,現在對軟體專案的進行方式都比較熟悉,不會像以前一樣只用價低者得標、又愛凹、結案又愛挑東挑西不給結案,現在比較能接受不同方式。
如果看官你是甲方承辦人,小弟建議你可以依照不同的專案內容,選擇適合的合約方式;
如果看官你是乙方,為了專案執行順利,請多想想可以用什麼合約方式可以讓乙方可以在對的時間獲得價值!
說來說去好像還是在說錢,錢在採購合約上絕對是關鍵,但絕對不是最大關鍵, 比如我們公司所有承辦人就很討厭跟大軟體公司合作 尤其那種在台成立20幾年的…..因為1他不缺錢 2.溝通都派業務 (業務都直接找老闆搞定我們這種小承辦員) 業務都年輕貌美說話甜 3. 作事太沒彈性,沒彈性出事時等於逼承辦人什麼都自己扛,等於逼承辦人去跳淡水河…..。4.如果真不幸,大公司還養打官司法務,如果案子搞到打官司 那承辦人 早就跳N次淡水河。

實務上其實很難 一招打片天下無敵手,不同CASE要運用的手法往往不同,良好的買賣關係,其實就跟人際關係一樣要有點黏又不能太黏, 而有些關係還得花時間花力氣去培養,比如如果配合還不錯公司,我會選擇犧牲一時換得長久信任(出小事要幫廠商扛 不要什麼事都要廠商吃下來 總一天 他會要你整個吞回去…….)。



各位看官如果有你自己一套的採購原則或是經驗、案例的分享,也歡迎留言或寄信給我。累積越多合約案例


小弟的敏捷式專案的採購領域整理到這邊,算是已經解答了自己的疑問,再來的主題,是怎麼樣做都不對的績效考評,在敏捷專案領域又怎麼看..











2013年7月29日 星期一

Agile Contract Type (II) - Incremental and Target Cost

接續著上一篇的主題,這一篇則整理另外 2種Agile合約,在國外的討論或是書上會看到的,這兩種在台灣就不是這麼常聽到了:
  • 漸進式交付合約(Incremental Delivery) (1)(2):這種合約會設置好幾個檢查點,每個檢查點可以讓甲方決定還要不要繼續下去,不繼續的話,已經完成的功能也是能運作的軟體,而不是一堆做到一半的程式碼。這種方式是比較適合Agile Project Management,因為每個 Iteration 交出的成品都是可運作的軟體。譬如一年的專案,可以在每一季設一個檢查點,如果乙方的Agile Team每個 Iteration 是 2 週,每個 Iteration 都交付一些重要性高的、可以用的軟體功能,6個 Iteration 時(也就是3個月),甲方就可以決定還要不要繼續下去,如果想要更經常的檢視完成品,也可以雙方討論後,修改檢查點的時間。
  • 目標成本合約(Target Cost Contract) (3 對細節有興趣的朋友,可以花點時間看):這種合約是 Toyota 跟供應商長期簽訂的合約類型,上課的時候也是講說 Agile Project Management 建議簽這樣的合約類型。這種合約先由甲乙雙方決定一個目標金額,專案開始後,則依照比例甲方付款給乙方,而如果乙方提前完成目標,則兩方共享節省的金額,而如果超過目標金額,則兩方共同承擔懲罰。而獎賞或懲罰的比例則事先訂定下來,如果想拿大比例的獎賞,那就要承擔大比例的懲罰。這種合約類型用圖解釋比較清楚:


    • 專案的基本數字:
      • 專案總金額:500 萬,額外準備100萬,總專案的最高費用(Cap)是600萬
      • 簽約時的付款:不先付任何款
      • 結案的付款:結案時給50萬
      • 每月工資:50萬
      • 預計工時:10個月
    • 專案 3種狀況(正常完成、提前、延誤)的付款進度如圖:



    • 正常進度:紅線,每月固定50萬,最後第10個月後到500萬,沒有獎勵也沒有懲罰。
    • 提前:綠色的線,假設提前在第8個月做完的話,可以領到
      • 8個月的400萬
      • 結案時可以領的50萬
      • 激勵金額 (目標的500萬-完工400萬)*50%=50萬,全部加起來也是500萬。在這個情況下,甲方可以提早開始使用系統帶來的效益,而乙方可以早領錢、早結案,專案人員可以慶祝(在台灣軟體開發專案能夠提前,真是難得啊...)並準備投入下一個專案。
    • 延後:在到 10th個月後,11th個月起,甲乙方各付一半(因為獎金也是甲乙各一半),所以11th個月起,每月只能算25萬,一直到550萬為止,所以到12th月後,累計就已經550萬,加上結案可以收到的50萬,就已經到頂(600萬)了,這時候乙方就必須不領錢直到做完為止,雖然看起來只有乙方在suffer,但是實際上甲方一直沒系統可用,這樣造成的損失也是甲方要承擔的懲罰。
    • 因為Target Cost Contract 比較特殊,可能有一些實行上的細節要考慮,除了看 (3) 以外,也可以看這篇 Agile Journal 2005年的 Selling agile: target-cost contracts ,講到非常實務上的細節。
小弟在整理這些合約類型的過程中,如果是乙方,可能需要先取得甲方的信任,在第2次、第3次的專案中,再開始用新的合約類型,比較不會引起甲方的過度懷疑,小弟認為做專案就是信任大考驗,在雙方互信的狀況下,怎麼樣都好談,要是雙方無信任,那就很難再繼續下去....
下一篇則要來個整理表了,比較各專案類型的適用對象,與陰謀論的想一下甲乙方會承擔的風險。

參考

2013年7月27日 星期六

Agile Contract Type (I) - Fixed Price and T&M


這個圖是你的合約關係嗎?

在上Agile Project Management的課程時,一直想跟實際工作情形連起來,連到採購的時候,心中有的大問號:現在一般常見的系統開發合約(開發一個系統,分3或4期付款:簽約、系統設計完成、開發完成、結案) ,跟以 Iteration 為進行方式的Agile Project,要怎麼對應的起來? 用Agile 開發的話,不把全部的SA/SD文件都寫完啊,那要怎麼付款跟請款 ?
上完課後,為了一解心中的疑惑,開始蒐集 Agile Contract 的類型,這一系列分3篇,算是對整理後的筆記,先整理目前常見的合約類型共4種,最後再加上一個比較表,說明適用的時機與缺點。

這一篇先講兩種常見的固定價格合約(Fixed Price and Fixed Scope)與工時與材料(Time and Material)類型的合約:
  • 固定價格合約(Fixed price):是目前最常見的軟體專案發包方式,這種合約通常有固定的總金額與專案範圍,通常付款是分為:
    • (1)合約簽約付一筆
    • (2)中間階段0~N次付款:通常可以分 "系統分析文件交付"、"系統設計文件交付"、"系統開發完成交付"、"系統通過使用者驗收測試(UAT)" 等等不同階段
    • (3)專案結案付一筆
    • 譬如一個中型專案,乙方為了公司營運金流的順暢,希望分 4 次付款,所以請款的時候會是 "合約簽約"、"系統分析、系統設計文件交付與確認"、"通過UAT"、"專案結案",這樣的合約配合瀑布式開發方式是很合適的,各階段都能搭配上。
    • 在Agile Project Management時,並不會一開始完整的分析整個系統,用 固定價格的合約 就會覺得有點卡卡,譬如甲方要求 "SA, SD文件確認" 付款時,乙方不知道要怎麼產出 超大本的SA/SD文件 去給甲方確認,所以出現其他比較符合Agile的合約類型。但是,如果甲方說,只能發包這樣的合約(組織要求或...),乙方是可以應用一些觀念,讓合約比較能符合Agile開發方式,譬如偷偷引入漸進式交付合約的想法(Incremental Delivery),把Release當作階段 而不把SA/SD文件完成當作階段 (完成子系統#1,2當作第一階段、完成子系統#3,4,5 當作第二階段)。
    • Scrum 大師Jeff Sutherland 提出的 2個重要的合約的觀念 (1) ,如果必須要承接/發包固定價格的合約,可以加入到合約中,對甲乙雙方都會有幫助的 (小弟不喜歡用"雙贏", 講出雙贏的人通常都不會說自己贏比較多...)
    • Money for nothing 金錢無用 :甲方可以在任一個 Iteration 後結束專案。評量的標準是, 如果甲方認為後續要做的價值不如已經完成的價值(i.e 已經完成的功能已經能達到所需要,因為Agile 是從高優先順序開始開發) 甲方將會付給乙方剩下合約的20%的金額。( 金錢無用 -> 不想付錢買不會用到的功能..) 
    • Change for free 改變免費:雖然講免費,但實際是,當有改變的時候,Product Owner 在 Iteration Planning 前或中提出新需求,Agile Team 評估 Story Point,確定要做的話,Product Owner把不重要的功能或相等Story Point的功能的移出 Product Backlog,做一些 TradeOff,甲方的高優先需求得以滿足,乙方一樣做那麼多Story Point 的工作,以這個方式來歡迎改變!

  • 時間與材料合約/有上限的時間與材料合約( T&M / T&M with cap):跟台灣俗稱的的"買人力"的想法很接近,可以想成"買整組人"在甲方駐點(通常還是要駐一下, out of sight out of mind, XD )開發所需要的系統,這種方式就是做多久就付多少金額。因為時間都是乙方估計,所以風險都在甲方,也因此有變種的,是加上上限的T&M(Capped T&M)合約,這個上限是雙方同意後的金額,通常甲方應該要加上鼓勵條款,鼓勵乙方早日完成。(不知為何,在台灣的甲方,能夠準時驗收與準時付款,對乙方就是一種激勵...) (2)

下一篇將整理的是漸進式的與目標成本的合約。

參考

2013年7月13日 星期六

[Engineering Practice] 軟體開發的測試-單元測試 / Software Development Testing - Unit Testing

以前只要講到系統功能要測試,通常就會想到找 PM、SA或直接給客戶進行人體測試,結果就會發生一使用功能就 Error 的悲劇... 。今年去上 Agile Project Management 時, 講到強調軟體開發的Extreme Programming時,特別強調測試,在 "XP 規則" ( The Rules of Extreme Programming原文 )中,有一組規則是講測試的,貼上來給大家震撼一下:
  • All code must have unit tests. 所有程式碼都要有單元測試 !!!!!
  • All code must pass all unit tests before it  can be released. 所有程式碼通過單元測試後才能release !
  • When a bug is found tests are created. 如果有發現 bug 時,就要回去寫測試 !!!
  • Acceptance tests are run often and the score is published.  經常執行驗收測試,並且必須公布量化指標!!
對XP Unit Test 有興趣的朋友,可以再看這一篇: Unit Test ..

既然單元測試是這麼重要,在上完Agile的課後,小弟開始學習在.Net Framework 要怎麼做單元測試,這一篇算是介紹,也是對自己學習的筆記,我使用的工具是 C# 與 Visual Studio 2012,使用 Java 的朋友,你一定能找到對應的工具與平台,這年頭, M$ 會推出某個功能,都是被 Java Community 逼出來的..ㄎㄎ... 廢話不多說,我用問題與回答來說明單元測試:

1. 單元測試到底是指甚麼?  =>預期結果等於測試結果
  • 簡單說,單元測試是對程式的最基本的單位進行測試 ( 最基本單位是要看你自己認定,小弟是非常程序導向的開發人員 i.e. old ,我的通常是 Function ),確認程式能夠如預期的執行。小弟的心得是,因為要單元測試,會強迫自己開始規劃自己的程式,讓 function 真正能比較像一個 function 該做的事,不會通通塞到一起才來拼命 debug line by line (還記得以前老師教的高聚合低耦合嗎?... )
  • 在單元測試會做的三件事:這跟人體測試時的邏輯是滿相同的,假設你要呼叫一個兩數相除的 function,會有3 步驟
    • Step (1) 先準備呼叫的資料,如 100除以5,所以你會把100跟5先準備好,這個步驟就是 "安排測試資料(Arrange)"
    • Step (2) 開始呼叫Function,並取得結果,譬如這個function可正常呼叫並回傳20,這個步驟就是 "執行測試(Act)"
    • Step (3) "判斷結果(Assert)其實在提供資料去測試時,你會有預期的結果,所以從第2步取得結果後,就會跟你預期的結果進行比對,以判斷兩數相除的Function是否如果預期執行,譬如 100/5=20,function回傳結果也是20,所以你就可以判斷function是否運作正常了!
    • 下圖是我寫測試一個上傳檔案的 Function 是否正確的單元測試程式,其實跟真正要呼叫該 Function 的程式會很類似,所以其實單元測試寫完,就可以copy/paste到正是用的程式,一魚兩吃阿!


2. 要如何開始做單元測試 ? =>打開你的好朋友(?!) Visual Studio
  • 這個問題跟使用的程式語言與開發工具非常相關了,我用Visual Studio 2012來當範例,基本操作在網路上已經很多教學怎麼做了 (如果跟我一樣從未接觸過,可以參考這一篇 MSDN,照著做一次會有感覺...) ,步驟大致是:
    • (1)在你目前開發的"方案"中,加入一個 "單元測試專案"
    • (2)在新的單元測試專案中,增加"參考" 開發的專案
    • (3)在新的單元測試專案中,開始寫Arrange, Act, Assert的程式。 請注意的是,在一個單元測試中,可以放多筆測試資料,預設是只要一個測試未通過,就會當作不成功,可以多放幾筆確定 function 真的跟預期的結果相同 (該有回傳就回傳,該有Exception 就會有 Exception...)
  • 寫完後,可以直接按 "測試總管" 的 "全部執行",就會知道是否通過單元測試了! 在測試的過程中,再回去改程式,我覺得這個過程是最大的收穫 !

3. 要如何確認自己做夠多的單元測試? => 計算程式碼涵蓋範圍
  • 還記得剛才的XP Test Rule "所有程式碼都要有單元測試 !",所以要如何判斷自己有涵蓋夠多的程式? 你可以使用Visual Studio 2012的  "分析程式碼涵蓋範圍"功能 ,馬上就可以看到涵蓋結果。至於你的目標要設為多少,可能要由你的 developer team 自行在definition of done 中定義。 
  • 如果你有引用Web Service 或 WCF,預設在計算程式碼涵蓋範圍時,會把這些外部參考也會當作範圍,這樣分母就會很大,涵蓋率就會變低,看了就不爽! 這時你可以自己加上一個設定檔(參考這一篇 Customizing Code Coverage Analysis ,最下方有範例檔案),並且在裡面增加排除Web Service/WCF,這樣就不會把外部的算進去了! 想要直接下載的可以到這邊下載,加到你的測試專案中,並在 "測試"->"測試設定"->"選取測試執行檔"使用這設定。
    • 加入後設定檔案後,要自己指定


    • 設定檔案中,如果不想納入Web Service/WCF,這邊要加上排外的設定。設定完再執行一次 "分析程式碼涵蓋範圍" 就會看到變化囉 !



4. 要如何準備單元測試的多筆測試資料? => 資料驅動的測試方式
  • 如果你的developer team有專職的測試人員或是你的Product Owner/Customer 願意準備測試資料,這時你就可以使用 資料驅動的測試方式 (Data-driven test) 。這方式可以用 CSV, EXCEL, Database 當作資料來源,而在單元測試時,就可以取得這些資料,進行多次的測試,太棒啦,測試可也切成資料與邏輯 !  
  • 使用 data-driven 測試有幾個眉角需要注意的
    • (1)資料上,準備時要有輸入的參數,還有預期的結果,譬如大家都熟悉的 Excel ,我有一個function要驗證帳號密碼是否正確,所以有3個欄位:帳號,密碼,預期結果,類似下圖

    • (2)資料上,檔案要自己加入倒測試專案中,並且在 "複製到輸出目錄" 要選 "永遠複製"
    • (3)程式上,要自己加上 using System.Data 
    • (4)程式上,要加上 private TestContext testContextInstance; 的一段
    • (5)程式上,要加上 Data Source,Connection String 可以參考這篇 Data Driven Testing with Visual Studio 2012 – Coded UI Test ,已經整理好了,改改檔名就可以。
    • 這段的程式是這樣的,其實很單純,但是  Visual Studio 2012 就是不會自己做.


5. 要如何把自己的 Function 跟別的 Interface/Library 隔離?  => Fake 機制
  • 有時候測試的時候會希望跟別的 Interface/Library 切割的比較清楚,譬如拿到輸入資料後不寫入資料庫,只要判斷程式邏輯正確,這時要怎麼切割呢? 在 Visual Studio 2012 時加上了 Fake 的機制,請一定要參考這一篇 VS 2012 的 Unit Test與測試總管 的"建立 Fake 組件"  與這一篇 Isolating Code Under Test with Microsoft Fakes ,Fake機制裡面還可以分為 Stub 與 Shim ,各有各的適用對象。以下小弟舉一個簡單的範例,使用的是Shim給大家參考:我要做的是,不去資料庫檢查帳密,我只要確定帳號密碼不一致就可以,所以我用Shim 隔離我的測試程式與實際的程式。


6. 要如何自動Build 自動單元測試?  => TFS 現在5人內免費!
  • 如果你是 one person team (獵人的磊扎,就是下圖那一位,1個人可以當5個人),那你可以不用考慮這個了 "持續整合/持續集成"這個議題,你自己應該會持續的 build,並且你的開發環境上就是 releasable build...

  • 如果你不是 one person team,一定會遇到版本控制、自動整合Build、自動測試的議題。在這部分,我是使用免費的tfs,現在是5人以內免費,適合small team (M$可能不知道台灣軟體廠商 >5個工程師的專案是很大的專案 哈...)。這一段的內容我是參考這一本書 軟體測試實戰 Visual Studio & Team Foundation Server (推! 雖然是visual studio 2010 ,但是還是很有參考價值,不知道會不會改版..) 書中的 "10-03 自動化組件與測試" 。使用方式大致是這樣:
    • (1) Check in 最新的程式版本到 tfs,Solution 應該也包含測試的專案!
    • (2)建立新的 組建定義,設定一下組建方式等等,譬如指定星期一到五中午12:30 tfs自己build自己測試...
    • (3)每天跑完後回去看結果,因為組建設定中,已經包含執行自動測試,所以在build完成後,tfs也會測試,測試的結果也在報告中,下圖是我前幾天的測試結果,可以看到單元測試錯誤! 



後記
  前前後後約花了約 1個月在Survey、熟悉這些工具的操作,並且在目前開發的系統加入單元測試。在開始加入單元測試後,發現了一些自己的盲點 (function執行的結果與預期不一樣),修改後也增加了程式的 robustness, Unit Test 好物!


參考資料

2013年2月7日 星期四

.Net and SAP Integration: shared function RFC_READ_TABLE

  • 前言:RFC_READ_TABLE 是 SAP 提供的 Read SAP Table 的公用程式,對於需要取得SAP資料的.Net Programmer而言,經常會使用到這個程式,因此適合開發成共用層。這一篇則是將RFC_READ_TABLE 包裝成 WCF 提供服務,SAP端則是以 ECC6 Unicode 版本為目的地。對於本篇的內容,歡迎建議與討論!
  • 使用工具:
    • VS 2010、.Net Framework 4.0
    • SAP NCO 3.0 for .Net Framework 4.0:請自行到SAP網站下載,建議取得最新版,SAP更新的滿快的,舊版曾經發生一些奇怪的問題,安裝好的目錄通常在:C:\Program Files (x86)\SAP 底下,會需要 sapnco.dll 與 sapnco_utils.dll。另外,位元版本也需要注意:
      • Winform: 與專案版本相同,如果專案是64bit,則使用64bit
      • Web application、Web Service、WCF: 都使用32bit
    • 黃昭仁大大的"IRfcTable、DataTable、DataSet 資料轉換模組"(http://vsqa.blogspot.tw/2011/08/irfctabledatatabledataset.html):轉換 NCO 回傳的Table資料結構轉換成.Net Programmer適合使用的 Data Table, DataSet,非常好用。
    • SAP RFC_READ_TABLE:要用的好,必須先對這個function 的用法,詳細可以開 SAP GUI 先試試看。建議在開發前,先在SAP確認好資料都正確,SAP RFC_READ_TABLE 正確,在外部呼叫一定都會正確。
  • WCF 運作方式:WCF主要就是提供 RFC_READ_TABLE去設計,服務的input跟SAP相同,只是我沒有放RowCount,個人認為是不需要,各位看官如果覺得有需要,請再加上去就好。
    • 服務的宣告:
      [OperationContract]
      SAPMessageType SAP_RFC_READ_TABLE_UNICODE(string strDest, string strTable, string[] strOptions, string[] strFields);

      參數說明如下:
      > strDest:SAP目的地
      > strTable: 要查詢的SAP Table
      > strOptions: SAP Options 也就是 Where 的條件,每一行有長度限制是72
      > strFields: SAP Fileds 要查詢SAP Table哪一些欄位
      其中,回傳的資料結構(合約)我自行定義的,因為通常Export會回傳執行的狀態,而 Table 則存放實際的資料,所以把這些合成一種資料結構
      [DataContract]
      public class SAPMessageType
      {
          [DataMember]
          public DataTable SAPTable { get; set; }
          [DataMember]
          public string strMessage { get; set; }
          [DataMember]
          public string strMessageCode { get; set; }
      }
      
    • WCF 執行過程:邏輯上與資料庫連結是一樣的,主要分5區,
      • 第一區 宣告區:主要是宣告會使用到的變數等等,小弟比較老一點,所以會先宣告要用到的變數
      • 第二區 連線區:從連線的class取得連線參數,並開始連線
      • 第三區 傳入參數區:指定SAP Function(RFC_READ_TABLE),傳入Fileds, Options
      • 第四區 回傳參數與處理:取得回傳的SAP 資料,並根據Fields所指定的開始切割
      • 第五區 結束區
        [PrincipalPermission(SecurityAction.Demand, Authenticated = true)]
        public SAPMessageType SAP_RFC_READ_TABLE_UNICODE(string strDest, string strTable, string strOptions, string[] strFields)
        {
            //
            //第一區 宣告區: logger 是使用nlog
            //
            logger.Info("SAP_RFC_READ_TABLE START UNICODE Version Start");
            SAPMessageType aData = new SAPMessageType();
            string strMessage = "";
            string strMessageCode = "";

            DataTable SAPDataTable = null;
            RfcDestination destination = null;

            try
            {
                //
                //第二區 連線區,連線內容在 SAPSystemConnect.cs 中取出
                //
                SAPSystemConnect aSapCfg = new SAPSystemConnect();
                RfcConfigParameters parameters = aSapCfg.GetParameters(strDest);

                if (parameters == null)
                {
                    //指定的SAP Client不存在
                    strMessageCode = "E";
                    strMessage = "指定的SAP Client不存在";
                }
                else
                {
                    destination = RfcDestinationManager.GetDestination(parameters);
                    RfcSessionManager.BeginContext(destination);

                    destination.Ping();
                    IRfcFunction function = null;

                    //
                    //第三區 傳入參數區
                    //

                    function = destination.Repository.CreateFunction("RFC_READ_TABLE");
                    function.SetValue("QUERY_TABLE", strTable);

                    //OPTIONS
                    if (strOptions != "")
                    {
                        IRfcTable tableOPTIONS = function["OPTIONS"].GetTable();
                        tableOPTIONS.Append();
                        tableOPTIONS.SetValue(0, strOptions);
                    }

                    IRfcTable tableFIELDS = function["FIELDS"].GetTable();
                    if (strFields.Length != 0)
                    {
                        foreach (string fd in strFields)
                        {
                            tableFIELDS.Append();
                            tableFIELDS.SetValue("FIELDNAME", fd);
                        }
                    }

                    function.Invoke(destination);

                    //
                    //第四區 回傳參數與處理
                    //

                    IRfcTable tableRead = function.GetTable("DATA");
                    DataTable dtRealTable = new DataTable(strTable);
                    ArrayList listFields = new ArrayList();
                    foreach (IRfcStructure row in tableFIELDS)
                    {
                        RFC_Table_Schema aRowSchema = new RFC_Table_Schema();
                        aRowSchema.fdOffset = Convert.ToInt16(row.GetString("OFFSET"));
                        aRowSchema.fdLength = Convert.ToInt16(row.GetString("LENGTH"));
                        aRowSchema.fdName = row.GetString("FIELDNAME");
                        aRowSchema.fdType = row.GetString("Type");
                        listFields.Add(aRowSchema);

                        //準備存放整理後資料的DataTable
                        dtRealTable.Columns.Add(row.GetString("FIELDNAME"), typeof(String));
                    }

                    foreach (IRfcStructure row in tableRead)
                    {
                        DataRow realRow = dtRealTable.NewRow();
                        //根據實際資料切Table內容, WA是為分割前的資料
                        String strWA = row.GetString("WA");

                        //unicode
                        #region Unicode切割方式

                        string anewString;

                        foreach (RFC_Table_Schema aRowSchema in listFields)
                        {
                            anewString = "";
                            //根據長度切割
                            //如果自SAP取得資訊,發現後面都是空白,SAP就會自己Trim字串,資料會與Fields提供的資訊不同
                            if ((aRowSchema.fdOffset + aRowSchema.fdLength) > strWA.Length)
                            {
                                if ((strWA.Length - aRowSchema.fdOffset) > 0)
                                {
                                    anewString = strWA.Substring(aRowSchema.fdOffset, (strWA.Length - aRowSchema.fdOffset));
                                }
                            }
                            else
                            {
                                anewString = strWA.Substring(aRowSchema.fdOffset, aRowSchema.fdLength);
                            }

                            realRow[aRowSchema.fdName] = anewString.Replace("\0", "").Trim();
                        }
                        #endregion

                        dtRealTable.Rows.Add(realRow);

                    }

                    //
                    //第五區 結束區
                    //

                    SAPDataTable = dtRealTable;
                    strMessageCode = "S";
                    strMessage = "成功讀取";

                    RfcSessionManager.EndContext(destination);
                    destination = null;
                }

            }
            catch (Exception ex)
            {
                logger.Error(ex.ToString());

                strMessageCode = "E";
                strMessage = ex.ToString();
                if (destination != null)
                {
                    RfcSessionManager.EndContext(destination);
                    destination = null;
                }
            }

            aData.SAPTable = SAPDataTable;
            aData.strMessage = strMessage;
            aData.strMessageCode = strMessageCode;
            logger.Info("SAP_RFC_READ_TABLE START UNICODE Version END");
            return aData;
        }



  • Service 使用端:呼叫時,先引用WCF,依照這樣呼叫:
  • //使用WCF
    SAPServiceReference.SAPServiceClient aClient = new SAPServiceReference.SAPServiceClient();
    SAPServiceReference.SAPMessageType aClientData = null;
    
    //需要的欄位
    string[] strFds2 = new string[] { "BUKRS", "BUTXT", "LAND1" };
    //Where條件
    string[] strOps = new string[] { "LAND1 <> 'TW'", "AND MANDT = '500' " };
    //取得 T001 公司主檔
    aClientData = aClient.SAP_RFC_READ_TABLE_UNICODE("DEV500", "T001", strOps, strFds2);
    aClient.Close();
    
    
    //因為回傳 Datatable,所以可以直接用
    GridView4.DataSource = aClientData.SAPTable;
    GridView4.DataBind();
    //可以有一個Label說明執行的結果
    lbNewErr.Text = aClientData.strMessageCode + ":" + aClientData.strMessage;
    

如果有需要程式參考的大大,請到這邊:

  • https://dl.dropbox.com/u/3330791/Sharecode/SAPWcfService.rar 
    • 程式目前會無法執行,因為要請自行下載SAP NCO 3.0,並加入 sapnco.dll、sapnco_utils.dll

參考資料:

  • SAP NCO 手冊: http://help.sap.com/saphelp_crm700_ehp02/helpdata/EN/4a/097b0543f4088ce10000000a421937/content.htm 
  • 黃昭仁大大的 Blog: http://vsqa.blogspot.tw/ 
  • How-To Use SAP Nco 3 Connector | .Net 4 | Visual Studio 2010: http://klanguedoc.hubpages.com/hub/How-To-Use-SAP-Nco-3-Net-4-Visual-Studio-2010

#異質資料介接

2012年11月26日 星期一

Develop Google Maps API v3 Map Pages with ASP.NET AJAX


  • 前言:Google Maps API 提供服務以來,由於容易開發、API功能多更新快並且又穩定,只要不踩到一些地雷,就不會收錢,所以一直是地圖開發者喜歡用的工具API。這一篇是要介紹,從資料庫取得點位資料套疊到地圖上。我使用的工具是 ASP.Net 使用 Google Map API v3 ,並且用AJAX,是因為如果用標準的asp.net button,會submit/postback 回 server,如果使用者在submit之前就做了一些地圖操作(放大、移動..),就會因為submit to server 會回到初始畫面,這樣的操作模式是滿令人不悅的,所以才需要AJAX方式來處理使用者對地圖的操作。
  • 網頁功能:取得使用者目前座標,繪製地點與精確度的範圍。並根據使用者點選的圖層類型,帶入對應的點位。
  • 瀏覽器限制:因為有用到HTML5,IE8,9確定不行,用 Chrome Ok, IPhone Safari OK
  • 使用元件:
    • Google Map API V3 :必要
    • Visual Studio 2012:必要
    • JQuery mobile:非必要,放著只為了可以在行動裝置上看。
    • HTML5:必要,為了取得目前座標。所以..那個 IE
  • 網頁主要分成 3 區 
    • (1)地圖區:顯示地圖
    • (2) ASP.Net button 區:負責從資料庫取得資料,並呈現在地圖
    • (3) JS Button區:主要是 javascript button,是操作google map的功能,如"取得目前位置"、"清除地圖" 這類的功能

  • 以下列出比較重要的程式片段,完整的程式請到網頁最下方再去下載。
  • HTML Head區:需要注意的是 style 那一區,因為 Google Map DIV必須指定大小,所以指定在Head裡面。其他就是引用 jquery 與 google map api v3
<head>
    <title>Google Maps API v3 and ASP.Net AJAX</title>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1" /> 
    <style type="text/css">
      html { height: 100% }
      body { height: 100%; margin: 0px; padding: 0px }
      #map_canvas { height: 100% }
    </style>
 <link rel="stylesheet" href="/css/jquery.mobile-1.2.0.min.css" />
    <script src="/js/jquery-1.8.2.min.js"></script>
    <script src="/js/jquery.mobile-1.2.0.min.js"></script>
    <script src="https://maps.googleapis.com/maps/api/js?v=3.exp&sensor=false"></script>

  • 再下來就是控制google maps div 的區域,首先是global的變數
        //Global variables
        var map;            //地圖
        var marker;         //地圖上的點
        var infowindow;     //點上面跳出的視窗
        var overlays = [];  //目前地圖上所有套疊的圖層
        var mapcenter = new google.maps.LatLng(25.228664, 121.750202);  //預設的地圖中心

  • 地圖初始化的function,等下會在body onload呼叫。裡面的詳細API 請參考Google 文件。在初始化完成後,呼叫 取得目前座標的function: getLocation。
        function initialize() {
            var mapOptions = {
                zoom: 6,
                mapTypeId: google.maps.MapTypeId.ROADMAP,
                center: mapcenter,
                mapTypeControl: true,
                mapTypeControlOptions:
                {
                    style: google.maps.MapTypeControlStyle.DROPDOWN_MENU,
                    poistion: google.maps.ControlPosition.TOP_RIGHT,
                    mapTypeIds: [google.maps.MapTypeId.ROADMAP,
                    google.maps.MapTypeId.TERRAIN,
                    google.maps.MapTypeId.HYBRID,
                    google.maps.MapTypeId.SATELLITE]
                },
                navigationControl: true,
                navigationControlOptions:
                {
                    style: google.maps.NavigationControlStyle.ZOOM_PAN
                },
                scaleControl: true,
                disableDoubleClickZoom: false,
                streetViewControl: true,
                draggableCursor: 'move'
            };

            infowindow = null;
            infowindow = new google.maps.InfoWindow({
                content: "info window content"
            });

            map = new google.maps.Map(document.getElementById('map_canvas'),
            mapOptions);
            
            //取得使用者目前座標
            getLocation();
        }

  • 取得目前位置化的function:使用HTML5的方式取得座標(點)與精確度(圓圈),並繪製到地圖上。
        //
        //取得目前位置
        //
        function getLocation() {
            var x = document.getElementById("Message");
            if (navigator.geolocation) {
                navigator.geolocation.getCurrentPosition(showPosition);
                //如果要持續取得,用watchPosition
//navigator.geolocation.watchPosition(showPosition); } else { x.innerHTML = "Geolocation is not supported by this browser."; } } function showPosition(position) { var x = document.getElementById("Message"); //x.innerHTML = "Latitude: " + position.coords.latitude + " Longitude: " + position.coords.longitude + " Accuracy" + position.coords.accuracy; var newMapCenter = new google.maps.LatLng(position.coords.latitude, position.coords.longitude); map.setCenter(newMapCenter); //以marker方式出現 setMarker(newMapCenter, "My Current Location" + " Accuracy is :" + position.coords.accuracy); //以圓形出現 if (position.coords.accuracy != null) { DrawCircle(newMapCenter, position.coords.accuracy); if (position.coords.accuracy < 1000) { map.setZoom(14); } } else { DrawCircle(newMapCenter, 100); } }
  • 繪圖用的function,繪製圓形、繪製單一點座標與視窗(infowindow)訊息
        //繪製圓形
        function DrawCircle(center,rad) {
            var draw_circle;
            draw_circle = new google.maps.Circle({
                center: center,
                radius: rad,
                strokeColor: "#FF0000",
                strokeOpacity: 0.8,
                strokeWeight: 2,
                fillColor: "#FFFF00",
                fillOpacity: 0.35,
                map: map
            });
            overlays.push(draw_circle);
        }

        //繪製單一點座標
        function setMarker(singleCoord,infoWindowContent) {
            marker = new google.maps.Marker({
                map: map,
                draggable: true,
                animation: google.maps.Animation.DROP,
                position: singleCoord
            });

            google.maps.event.addListener(marker, 'click', function () {
                infowindow.setContent(infoWindowContent);
                infowindow.open(map, this);
            });

            //
            overlays.push(marker);
        }

        //產生彈跳效果
        function toggleBounce() {
            if (marker.getAnimation() != null) {
                marker.setAnimation(null);
            } else {
                marker.setAnimation(google.maps.Animation.BOUNCE);
            }
        }
  • 繪製多個點,提供給asp.net程式呼叫。Javascript Array的格式是['Mount Evans', 59.32522, 18.17002, 4, 'This is Mount Evans.'];,C#要繪製多點的時候,就是去準備出這個多筆陣列,再交給 setMarkers 去繪製。
        function setMarkers(markers) {
            for (var i = 0; i < markers.length; i++) {
                var sites = markers[i];
                var siteLatLng = new google.maps.LatLng(sites[1], sites[2]);
                var marker = new google.maps.Marker({
                    position: siteLatLng,
                    map: map,
                    title: sites[0],
                    zIndex: sites[3],
                    html: sites[4]
                });

                var contentString = "info window content";

                google.maps.event.addListener(marker, "click", function () {
                    infowindow.setContent(this.html);
                    infowindow.open(map, this);

                    if (marker.getAnimation() != null) {
                        marker.setAnimation(null);
                    } else {
                        marker.setAnimation(google.maps.Animation.BOUNCE);
                    }
                });

                //
                overlays.push(marker);
            }
        }
  • 清除全部繪製的圖層
        function clearMarkers() {
            while (overlays[0]) {
                overlays.pop().setMap(null);
            }
        }
  • 另外在.ASPX網頁設計上,只有ASPX的Controls(Button1~4,顯示A~D類的圖層資料),放在UpdatedPanel裡面。Google Map DIV 更要在Form 以外才能正常顯示。控制地圖的HTML Button(Button 6,7)則是單純的HTML button就可以
<div id="Message"></div>
    <div id="map_canvas" style="width:100%; height:80%"></div>
    <form id="form1" runat="server">
        <asp:ScriptManager ID="ScriptManager1" runat="server">
        </asp:ScriptManager>
        <asp:UpdatePanel ID="UpdatePanel1" runat="server">
            <ContentTemplate>
                AJAX Button:<asp:Button ID="Button1" runat="server" OnClick="Button1_Click" Text="All" />
                <asp:Button ID="Button2" runat="server" OnClick="Button2_Click" Text="A" />
                <asp:Button ID="Button3" runat="server" OnClick="Button3_Click" Text="B" />
                <asp:Button ID="Button4" runat="server" OnClick="Button4_Click" Text="C" />
                <asp:Label ID="Label1" runat="server" Visible="False"></asp:Label>
                <br />
                <asp:Label ID="lbErr" runat="server" Visible="False"></asp:Label>
                <br />
            </ContentTemplate>
        </asp:UpdatePanel>
        JS Button:
        <input id="Button5" type="button" value="Find My Location" onclick="getLocation()"/><input id="Button6" type="button" value="Clear Markers" onclick="clearMarkers()" /></form>
  • C#程式端,以Button3為例,這個按鈕的目的是取得B類的座標並繪製於地圖上
    protected void Button3_Click(object sender, EventArgs e)
    {
        Label1.Text = System.DateTime.Now.ToLongTimeString();
        Label1.Visible = true;

        string strJS = "";
        ArrayList alMarkers = GetMarkers("B");

        strJS = @"var sites = [" + String.Join(",", alMarkers.ToArray()) + "];setMarkers(sites);";
        ScriptManager.RegisterStartupScript(this.Page, this.Page.GetType(), "alert", strJS, true);
    }
  • C#程式端,GetMarkers的目的是從資料庫取出對應的
protected ArrayList GetMarkers(string strType)
    {
        ArrayList alMarkers = new ArrayList();
        try
        {
            string conn = ConfigurationManager.ConnectionStrings["mydevConnectionString"].ConnectionString;
            using (SqlConnection connection = new SqlConnection(conn))
            {
                connection.Open();

                SqlCommand command = connection.CreateCommand();
                command.Connection = connection;

                if (strType == "ALL")
                {
                    command.CommandText = @"SELECT * FROM [MyDev].[dbo].[MyMap] with (nolock)";
                }
                else
                {
                    command.CommandText = @"SELECT * FROM [MyDev].[dbo].[MyMap] with (nolock) where Category=@Category";
                    command.Parameters.Add("@Category", SqlDbType.NVarChar, 20).Value = strType;
                }

                using (SqlDataReader dr = command.ExecuteReader())
                {
                    while (dr.Read())
                    {
                        alMarkers.Add("['" + dr["Project"].ToString() + "', " + dr["Latitude"].ToString() + "," + dr["Longtitude"].ToString() + ", 1, '" + dr["Info"].ToString() + "
" + dr["Addr"].ToString() + "']");
                    }
                }
            }
        }
        catch (Exception ex)
        {
            lbErr.Text = ex.ToString();
            lbErr.Visible = true;
        }
        return alMarkers;
    }

  • Table的設計很簡單,主要就是 Latitude(緯度), Longtitude(經度),其他都是輔助描述的欄位
CREATE TABLE [dbo].[MyMap](
 [ID] [bigint] IDENTITY(1,1) NOT NULL,
 [Category] [nvarchar](20) NOT NULL,
 [Code] [nvarchar](20) NULL,
 [Info] [nvarchar](200) NULL,
 [Project] [nvarchar](50) NULL,
 [Addr] [nvarchar](100) NOT NULL,
 [Latitude] [float] NOT NULL,
 [Longtitude] [float] NOT NULL,
 [Coord] [geography] NULL
) ON [PRIMARY]


完成,結果如圖:
一進入網頁時,會直接定位目前座標,My Current Location 是我目前的座標,黃色是72公尺的精準度,其他的marker是點了B,帶入B類的座標,顯示在地圖上。

歡迎討論!

程式下載:
https://dl.dropbox.com/u/3330791/Sharecode/GoogleMapV3_ASPNET.rar

reference:
https://developers.google.com/maps/documentation/javascript/reference

2012年11月19日 星期一

[Toolkit] Get to Get coordinates in Images ? / 如何批次取得照片中的座標?

  • 目的:手邊有一些照片有座標資訊,現在需要取出座標內容進行後續的加值,除了一張一張照片打開看EXIF座標欄位以外,希望可以批次取得。
  • 使用工具:感謝這位日本網友 nissuk (https://github.com/nissuk),提供可用的powershell (https://gist.github.com/1360596),但是在中文環境下,會有問題,所以小弟有修改過,放在 http://dl.dropbox.com/u/3330791/get-exif.ps1 
  • 使用方式:
1 集合照片到同一個目錄,把 ps 直接放到到照片的目錄下

2 以Powershell執行 ps1,如果不只jpg,則請修改ps1的最後一行:
原本:dir "*.jpg" | get-exif | select name, latitude, longitude | export-csv
改為:dir "*.gif" | get-exif | select name, latitude, longitude | export-csv out.csv

2 執行結果:產生CSV檔案,這樣就完成了! 超級方便!


對這些有興趣的朋友,請看這個PowerShell Image module:
http://archive.msdn.microsoft.com/PSImage/