Quantcast
Channel: 黑暗執行緒
Viewing all 2337 articles
Browse latest View live

WCF探勘14-使用By Reference傳遞參數

$
0
0

同事回報一起案例,某WCF服務的[OperationContract]方法宣告為void Blah(ref int i, ref string s),以傳址方式(By Reference)傳遞參數(延伸閱讀:Self Test - Value Type vs Reference Type),程式運作多時,詢問我-WCF可以使用 ref 傳參數嗎?

一隻黑天鵝默默在我面前飛過,看得我瞠目結舌…

令人訝異之處在於,依我的理解WCF的Client/Server身處不同程序(Process),不管輸入參數還是傳回結果,都得序列化、反序列化才能正確傳遞,既然不屬於同一程序,記憶體地址空間不同,何來傳「址」?

爬文後,在MSDN找到一段說明

Out and Ref Parameters

In most cases, you can use in parameters (ByVal in Visual Basic) and out and ref parameters (ByRef in Visual Basic). Because both out and ref parameters indicate that data is returned from an operation, an operation signature such as the following specifies that a request/reply operation is required even though the operation signature returns void.

由此可知,WCF真的支援在參數使用 ref、out!而隨後的說明暗示,使用 out 或 ref 時,即使傳回型別為 void,實際仍會傳回資料。意味WCF在背後做了一些處置,偷偷傳回標為 out、ref 的參數到客戶端,模擬傳值參數行為。

哼!我冷笑一聲,這種「偽」傳值的做法,應該三兩下就破功吧?為什麼WCF要提供容易踩雷的黑心規格?

那就做個實驗踢爆「偽」傳值參數的黑暗面吧!

我設計以下的資料物件,共有三個成員,屬性 PubProp 標註 [DataMember] 可序列化,NonSerProp 則是一般欄位,預期不在序列化範圍,與 PubProp 形成對照。另外, Check() 方法可印出 PubProp 及 NonSerProp 偵查資料內容。

using System.Runtime.Serialization;
 
namespace WcfDto
{
    [DataContract]
publicclass Foo
    {
privatestring _PubProp;
        [DataMember]
publicstring PubProp {
            get
            {
return _PubProp;
            }
            set
            {
                _PubProp = value;
            }
        }
 
publicstring NonSerProp = "Default";
 
publicstring Check()
        {
returnstring.Format("PubProp={0}, NonSerProp={1}",
                PubProp, NonSerProp);
        }
    }
}

建立一個 ByRefCall.svc,豪邁地使用 ref 傳址宣告 Test1(ref int i, ref stirng s) 及 Test2(ref Foo f) 兩個作業方法,被呼叫時修 i、s 及 f 值。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
using WcfDto;
 
namespace WcfWas
{
    [ServiceContract]
publicinterface IByRefCall
    {
        [OperationContract]
void Test1(refint i, refstring s);
 
        [OperationContract]
void Test2(ref WcfDto.Foo f);
    }
 
publicclass ByRefCall : IByRefCall
    {
publicvoid Test1(refint i, refstring s)
        {
            i = 32767;
            s = "Darkthread";
        }
 
publicvoid Test2(ref Foo f)
        {
            f.PubProp = "Server";
            f.NonSerProp = "Server";
        }
    }
}

呼叫端長這樣,分別執行 Test1 及 Test2,並比對變數如何變化:

staticvoid Main(string[] args)
        {
            BRC.ByRefCallClient c = new BRC.ByRefCallClient();
int i = 1;
string s = "Jeffrey";
            Console.WriteLine("Before: i={0}, s={1}", i, s);
            c.Test1(ref i, ref s);
            Console.WriteLine("After: i={0}, s={1}", i, s);
 
            WcfDto.Foo f = new Foo();
            f.PubProp = "Client";
            f.NonSerProp = "Client";
            Console.WriteLine("Before: {0}", f.Check());
            c.Test2(ref f);
            Console.WriteLine("After: {0}", f.Check());
 
            Console.Read();
        }

執行結果如下:

Before: i=1, s=Jeffrey
After: i=32767, s=Darkthread
Before: PubProp=Client, NonSerProp=Client
After: PubProp=Server, NonSerProp=

Test1(ref i, ref s) 沒什麼意外,i 與 s 都被正確更新。但 Test2(ref f) 就精彩了,原本 PubProp 跟 NonSerProp 都是"Client",呼叫 Test2 後,PubProp 正確換成"Server",但 NonSerProp 被改掉,既不是"Client",也不是"Server",而呈現空白。實際應用時,這種未預期結果已可認定為Bug,可能導致系統出錯。

但,事情是怎麼發生的?

為進一步調查,我修改 Foo.PubProp ,在資料被外界修改時輸出 Environment.StackTrace 追查呼叫來源:

        [DataMember]
publicstring PubProp {
            get
            {
return _PubProp;
            }
            set
            {
                Debug.WriteLine(string.Format("Set PubProp=>{0}", value));
                Debug.WriteLine(Environment.StackTrace.ToString());
                _PubProp = value;
            }
        }

追蹤結果如下:(省略部分冗長內容)

Set PubProp=>Client
   於 System.Environment.GetStackTrace(Exception e, Boolean needFileInfo)
   於 System.Environment.get_StackTrace()
   於 WcfDto.Foo.set_PubProp(String value) 於 X:\WorkRoom\WCFLab\WcfDto\Foo.cs: 行 24
   於 WCFClient.Program.Main(String[] args) 於 X:\WorkRoom\WCFLab\WCFClient\Program.cs: 行 89
   … 省略 …
   於 System.Threading.ThreadHelper.ThreadStart()
Set PubProp=>Client
   於 System.Environment.GetStackTrace(Exception e, Boolean needFileInfo)
   於 System.Environment.get_StackTrace()
   於 WcfDto.Foo.set_PubProp(String value) 於 X:\WorkRoom\WCFLab\WcfDto\Foo.cs: 行 24
   於 ReadFooFromXml(XmlReaderDelegator , XmlObjectSerializerReadContext , XmlDictionaryString[] , XmlDictionaryString[] )
   於 System.Runtime.Serialization.ClassDataContract.ReadXmlValue(XmlReaderDelegator xmlReader, XmlObjectSerializerReadContext context)
   於 System.Runtime.Serialization.XmlObjectSerializerReadContext.ReadDataContractValue(DataContract dataContract, XmlReaderDelegator reader)
    … 省略 …
   於 System.ServiceModel.Dispatcher.DataContractSerializerOperationFormatter.DeserializeParameterPart(XmlDictionaryReader reader, PartInfo part, Boolean isRequest)
   於 System.ServiceModel.Dispatcher.DataContractSerializerOperationFormatter.DeserializeParameter(XmlDictionaryReader reader, PartInfo part, Boolean isRequest)
   於 System.ServiceModel.Dispatcher.DataContractSerializerOperationFormatter.DeserializeParameters(XmlDictionaryReader reader, PartInfo[] parts, Object[] parameters, Boolean isRequest)
   … 省略 …
   於 System.ServiceModel.Channels.HttpPipeline.EmptyHttpPipeline.BeginProcessInboundRequest(ReplyChannelAcceptor replyChannelAcceptor, Action dequeuedCallback, AsyncCallback callback, Object state)
   於 System.ServiceModel.Channels.HttpChannelListener`1.HttpContextReceivedAsyncResult`1.ProcessHttpContextAsync()
   於 System.ServiceModel.Channels.HttpChannelListener`1.BeginHttpContextReceived(HttpRequestContext context, Action acceptorCallback, AsyncCallback callback, Object state)
   …省略…
Set PubProp=>Server
   於 System.Environment.GetStackTrace(Exception e, Boolean needFileInfo)
   於 System.Environment.get_StackTrace()
   於 WcfDto.Foo.set_PubProp(String value) 於 X:\WorkRoom\WCFLab\WcfDto\Foo.cs: 行 24
   於 WcfWas.ByRefCall.Test2(Foo& f) 於 X:\WorkRoom\WCFLab\WcfWas\ByRefCall.svc.cs: 行 31
   於 SyncInvokeTest2(Object , Object[] , Object[] )
   於 System.ServiceModel.Dispatcher.SyncMethodInvoker.Invoke(Object instance, Object[] inputs, Object[]& outputs)
   … 省略 …
Set PubProp=>Server
   於 System.Environment.GetStackTrace(Exception e, Boolean needFileInfo)
   於 System.Environment.get_StackTrace()
   於 WcfDto.Foo.set_PubProp(String value) 於 X:\WorkRoom\WCFLab\WcfDto\Foo.cs: 行 24
   於 ReadFooFromXml(XmlReaderDelegator , XmlObjectSerializerReadContext , XmlDictionaryString[] , XmlDictionaryString[] )
   … 省略 …
   於 System.ServiceModel.Channels.ServiceChannelProxy.InvokeService(IMethodCallMessage methodCall, ProxyOperationRuntime operation)
   於 WCFClient.BRC.ByRefCallClient.WCFClient.BRC.IByRefCall.Test2(Test2Request request) 於 X:\WorkRoom\WCFLab\WCFClient\Service References\BRC\Reference.cs: 行 152
   於 WCFClient.BRC.ByRefCallClient.Test2(Foo& f) 於 X:\WorkRoom\WCFLab\WCFClient\Service References\BRC\Reference.cs: 行 158
   於 WCFClient.Program.Main(String[] args) 於 X:\WorkRoom\WCFLab\WCFClient\Program.cs: 行 92
   … 省略 …

由 StackTrace 的程式位置,PubProp 總共被設定四次:

第一次,Main() f.PubProp = "Client"
第二次,有個 ReadFooFromXml() 函式由SOAP傳送內容讀出Foo,轉成輸入參數交給 ByRefCall.Test2
第三次,ByRefCall.Text2() 執行 f.PubProp = "Server"
第四次,Test2() 執行結果傳回客戶端,再用 ReadFooFromXml() 由 SOAP 讀取結果更新回Foo物件

第四次 StackTrace 有兩個程式行數值得注意。Reference.cs 158行呼叫152行,追到原始碼(如下圖),謎底揭曉。
使用 ref 時,WCF Proxy 會在背後宣告一個 inValue,將 Foo 放進 inValue.f,呼叫 Test2() 取得 retVal,再將原本的 inVal.f 換成 retVal.f。

在這種邏輯,retVal.f 有可能是另一顆新建立的 Foo,也有可能是原來的 f 物件,透過 ReadFooFromXml() 將屬性更新為 SOAP傳回內容。試過為 Foo 加入建構式,發現在 Client 端 Foo 只被建立一次,故可排除另建 Foo 的假設,物件 f 應該還是同一顆,藉由 ReadFooFromXml() 更新 PupProp 屬性,至於為什麼 NonSerProp 會被改成空白,得深入 ReadFooFromXml 邏輯一探究竟。至少,我們得到一項結論:

使用 By Reference 傳遞參數時,不在序列化範圍的屬性或欄位有可能出現非預期結果。

基於以上行為,我認為用 ref 傳遞傳遞Value Type(int、string、decimal…)還好,不致出問題。但用 ref 傳送物件參數是件危險的事,序列化範圍以外的屬性、欄位就有錯亂的風險,跟大家想像的傳統 ref 傳址行為不完全相同,再加上 By Referecne 傳遞違反 WCF 傳輸的本質,不如大家就忘掉「WCF 可以用 ref」這件事吧!


縮略字大小寫之惑:LINQHelper還是LinqHelper?

$
0
0

在.NET,舉凡變數、類別、命名空間名稱,若由多個英文字組成(例如:Temp Folder Name),大小寫有兩種規則可遵循:

  1. Pascal命名法
    第一個字母及所有單字的第一個字母都用大寫,如TempFolderName
  2. Camel命名法
    第一個字母取小寫,隨後串聯文字的第一個字母採大寫,如tempFolderName

至於何時該用Pascal何時Camel?我的記法是:區域變數、私有欄位屬性,以及方法參數用Camel,其餘用Pascal。

不過有個情況我一直沒搞清楚,當名稱中有一部分是英文縮寫時,該不該全部大寫?例如我有個系統縮寫為AFA,用於命名空間名稱,該叫AFA.Utility,還是Afa.Utility?理由是看過XML寫成System.Xml,也看過UI被寫成System.Web.UI,兩種都有。照理說,命名規則這玩意兒,在開發團隊形成共識然後大家遵循即可,沒有絕對對錯。要命的是,我在兩者間猶豫不決(看心情來著),在斷斷續續開發一段時間後,才赫然發現怎麼專案裡同時有AFA.Utility跟Afa.Security,又有Afa.DAL… 亂到我想巴自己的頭。

痛定思痛,認真查了MSDN文件,依據微軟的.NET命名指南,整理成以下心得:

  1. 一直以來,我習慣叫多個大寫英文字母組成的名詞為「縮寫」並不夠精確。同樣是數個大寫字母組成,可區分為縮略字(Acronym)及縮寫(Abbreviation)。縮略字由數個單字或片語取第一個字母組成,例如:POC=Proof Of Concept、RFC=Request For Comments;而縮寫是由單字取幾個字母做為代表,例如:ID=Identiication、OK=Okay。(註:縮略字的大小寫規則在.NET 4版MSDN文件有提,.NET 4.5版調整後未納入)
  2. 只有眾所周知且通用的縮略字,才適合直接用在識別名稱上,例如:HTML、XML。不過「眾所周知且通用」是相對性的,以組件的使用者為範圍,系統代號、自訂的協定名稱,只要你有自信使用組件的相關人員一定懂,就大方用下去吧!
  3. 為什麼是System.Xml跟System.IO?其實是有規則的,很簡單:兩個字母就全部大寫,兩個以上只有第一個字母大寫。
  4. 兩個字母的「縮寫」不套用全部大寫規則,如:ID、OK,Pascal時寫Id、Ok,Camel時則為id、ok。
  5. 兩個字母的縮略字原則都寫大寫,但用於字首又遇上Camel,則都改用小寫,例如:void Process(IOFlag ioFlag)
  6. 兩個字母以上的縮略字放在字首並採Camel時,也全部改用小寫,例如:void Process(string htmlString)
  7. 針對複合字(Compound Word),有一些慣例,例如:Callback(不是CallBack)、Email(不是EMail)、UserName(不是Username)、Canceled(不是Cancelled)、Indexes(不是Indice)、LogOn(不用LogIn)、Metadata(不是MetaData)。MSDN文件還列了一些範例,這部分我想開發團隊求一致即可,僅為訂定標準的參考。
  8. 來個例外搗蛋一下,前面說兩個字母以上的縮略字只留第一個字母大寫,但RSACryptoServiceProvider的RSA全部大寫又是怎麼一回事?
    在此奉送豆知識一枚:RSA是三位數學家Ron Rivest、Adi Shamir與Leonard Adleman姓氏的縮寫,寫成Rsa只怕Shamir跟Adleman兩位大師會不開心,那就破例全部大寫吧!

所以,回到我專案的例子:
AFA.Utility應寫成Afa.Utility
Afa.Security是對的
Afa.DAL應改為Afa.Dal

標題的問題,大家應該也有答案了,應該寫LinqHelper。

最後再強調一次:命名規則只求團隊一致,沒有絕對標準,但依循官方建議或業界標準有好處,可縮短新成員的磨合期。

閒聊:剛好昨天就討論提到命名規則,同事說,他前陣子看了微軟開放的原始碼,在裡面看到一堆程式也沒照微軟官方的命名規則來,我說:軟體之所以偉大,並不在於它有依循命名規則… 哈!別誤會我的意思-依循程式規範將讓你的程式碼更偉大,共勉之。

Windows C槽空間不足問題

$
0
0

照片來源:民報

公司或家裡都有 i7 桌機可用,左手筆電只是輔助,所以當年買筆電選了Vaio T13,以中階價位買到 i5+SSD+觸控螢幕+金屬外殼,十分划算。唯一美中不足就是小不啦嘰的 128GB SSD,扣掉還原 Partition 只剰 100G 左右。隨著軟體愈裝愈多,系統碟 C槽可用空間捉襟見肘,三不五時低於10%警戒線。每每看到C槽的刺眼紅條我就會焦慮,忙著刪暫存檔、移除軟體調頭寸。空間大戶非 Visual Studio 莫屬,用觸控螢幕測試Windows Phone 8.1 App,用Cordova寫Android App,酷是很酷,但模擬器動轍數GB起跳,令人驚心;而 Windows 習慣保留安裝程式以備日後修復或解除安裝,每次裝 Windows Update 也會耗用空間,刪掉系統檔案怕有後遺症,只能看著 Windows 資料夾日益肥大。

這麼下去不是辦法,一咬牙買了第二顆 SSD(T13擴充槽規格特殊,還得花半顆SSD的價錢買轉換頭 orz),自此筆電多了 X 槽。移除 VS2013、Office 2013 改裝到新磁碟,不少檔案似乎綁死 C 槽,擠出空間沒想像多。今天在筆電安裝 Visual Studio 2015,安裝程式預估兩顆磁碟總共要用掉 22GB,有種不祥預感,果不其然,C槽又要爆了…

由 WinDirStat 分析結果,抓到幾個大咖,Windows 暴增到 42GB,ProgramData/Package Cache 用掉 7GB!

探了來歷,極有可能是Visual Studio的傑作。依MSDN部落格文章提到,就算安裝在其他磁碟,Visual Studio還是固定會佔用不少系統碟空間:

  • Shared runtimes like the .NET Framework and Visual C/C++ which require installing into the system drive.
  • Packages shared with other products that have their own installation folders like SQL Server and Windows SDK
  • Components shared with other applications like Office or services like IIS
  • Windows Installer and package caches

換句話說,有些檔案註定只能裝在系統碟,由不得我作主 orz

爬文找到一個好解法,將 Package Cache 移到 X 槽,並將 C 槽原目錄指向新位置,不需要 Hacking 改 Registry,就能輕鬆轉移空間:

  1. 將 C:\ProgramData\Package Cache 目錄搬到 X:\DirJunction\ProgramData\Package Cache
  2. 使用 mklink /J 指令建立目錄連接( Directory Junction
    mklink /J "C:\ProgramData\Package Cache" "X:\DirJunction\ProgramData\Package Cache" 

目錄連接對程式甚至OS來說是透明的,存取時跟位於該實體位置的資料夾沒有兩樣,搬移後幾乎無感,很完美的解決方案。

同樣原理可應用在其他歸檔備用性質的目錄搬移,騰出寶貴 C 槽空間,但仍有些限制。由於原目錄必須完全搬移後刪除,該目錄不能有任何檔案在執行中或被鎖定,另一方面,顧及第二磁碟故障時作業系統至少要能開機,搬移對象以「備而不用」性質為佳,安裝檔、舊版元件的封存資料夾,或是不涉及Windows服務/ActiveX元件的高可攜性軟體是較好的選擇,另外應避免有特殊NTFS權限要求的資枓夾,以免搬移後權限改變造成問題。

陸續搬了\Program Files (x86)\Windows Kits、\Program Files (x86)\Android、ProgramData\Package Cache,挪出15GB,但空間仍不充裕。於是我把腦筋動到高達 22GB 的Windows\Installers,但它是個系統隱藏資料夾,搬動可能引發災難。爬文學到,並不是每個 Installers 檔案都有保存價值,有些孤兒快取檔(Orphaned Cache)是可以放心刪除的。不過文章提到Microsoft Installer CleanUp 工具已隨XP退役、WICleanUp 工具年代久遠,我不確定是否可用在Windows 8,考慮後,決定採行打安全牌-手動搬檔。

Installer 目錄下的msi、msp 採隨機檔名,看不出所以然,在增加作者及標題欄位可以提供一些線索:

標題不是給人看的文字,但從關鍵字可看出端倪,看起來 Office 安裝檔是吃光空間的主謀集團,依大小、標題、修改時間判斷,很多還是重複檔案。

猜得到檔案用途,動手搬移心裡就篤多了,我選擇不刪除檔案,而是將檔案搬移到X槽,待日後要移除或修復時再由X槽複製還原。

C槽剩餘空間來到34GB的安全水位,開心!

Fenix 3 資料欄位 Runner Window 上架

$
0
0

某人曾經說過:身為程式魔人居然不會在自己手機上寫Code將是一生的污點…

如今,我又意外解除另一項成就:在自己的 GPS 錶上寫程式~

入手 Fenix3三個月,對它的外型、GPS精準度、穩定性、耗電都無從挑剔,唯一感覺不足的只有跑步App畫面欄位最多只能放四個。我自己跑步時會關心步速(Pace)、步頻、心率、距離、時間,跑LSD時還會想知道現在幾點,跑山路馬拉松時則會關心現在爬多高快到山頂沒,算一算四欄顯然不夠,而我又不喜歡邊跑邊切畫面,Fenix3 的錶面夠大,理論上可以再擠更多資訊。

後來在 Connect IQ Store 找到有人寫出一次塞進七種資料的資料欄位-367RunFields,二話不說馬上下載,果然是我的菜,從此跑長程幾乎就全程用它搞定。

好景不長。某次自動更新後,發現作者註銷舊版另推新版,而邪惡的新版其實只是試用版,使用一段時間後就轉成廣告畫面,提醒要升級完整版需洽詢作者付費。見此情景,身為以寫程式為生,連閒暇都要靠 Coding 放鬆心情的程式魔人,是可忍孰不可忍,立刻查資料,裝工具,學語言,怒寫一套自用。

最後,依我自己的需要,寫成一套八合一資料欄位,納入現在時刻、跑步時間、步頻、平均步速(Pace)、海拔高度、總距離、心率、目前步速(20秒平均值)八種跑步資訊,另外再加上電池電量百分比以及 GPS 訊號狀態(綠色表示訊號正常,紅色表示收不到訊號,無法偵測強弱)。平約步速與高度對我來說是次要資訊,所以共用最中央的顯示位置,每五秒切換一次。

GPS 斷訊時左上角符號跟 GPS 字樣將呈現紅色(本想顯示訊號強弱,但API未提供),電池除了電量百分比外也有長條顯示,電量偏低時會變紅。

猜想可能有其他跑友也會喜歡這種「海陸大拼盤式」的整合資訊錶面,順手把它上架到 Connect IQ Store,有興趣的 Fenix3 跑友可以下載試用,如遇使用問題或出現錯誤可留言回饋給我。

另外,整理 Connect IQ App 開發的相關資源如下:

  1. Connect IQ SDK 入口網站
    http://developer.garmin.com/connect-iq/overview/
  2. 工具安裝指南
    http://developer.garmin.com/connect-iq/programmers-guide/getting-started/
    Eclipse 配合 Connect IQ 套件,編譯後可自動部署到模擬器測試,挺方便的
  3. Monkey C 語言簡介
    http://developer.garmin.com/connect-iq/programmers-guide/monkey-c/
    名字有C,寫起來其實比較像JavaScript,弱型別概念,支援OOP,但實測發現物件化對記憶體的需求遠超過資料欄位程式的16KB記憶體上限,平舖直述全部塞在一個函式裡才是王道。
    但程式碼沒有物件化跟模組化,維護起來會出人命。最後我想出的做法是用C#寫OOP程式實現顯示元素繼承及彈性欄位排版,程式執行結果輸出為 Monkey C 程式碼交給 Eclipse 編譯測試,用 Visual Studio 開發Garmin Connect IQ 程式,我應該是史上第一人吧?哈!
  4. 開發者討論區
    Connect IQ開發比較冷門,網路上能查到的幾乎都集中在Garmin的開發者討論區 ,有問題也可在上面發問。
    https://forums.garmin.com/forumdisplay.php?479-Connect-IQ

再談組件版本導向技巧與Copy Local屬性

$
0
0

組件版本相容是常見的.NET實務問題。用以下的解決方案舉例,假設有個 MyConApp Console Application參照 MyLib Class Library,兩個專案都用NuGet裝了Newtonsot.Json(Json.NET)組件,差別在MyConApp用的是Json.NET 7.0版,MyLib用的則是6.0版。

不意外地,因為Json.NET版本不同,將會出現版本相容錯誤:

FileLoadException was unhandled
無法載入檔案或組件'Newtonsoft.Json, Version=6.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed'

理由是 MyLib 限定使用 Newtonsoft.Json.dll 6.0.0.0 版,MyConApp 雖然有 Newtonsoft.Json.dll,卻是 7.0.0.0版,不能取代 6.0.0.0 版。此時有兩種解法,第一種是將 MyLib 的 Json.NET 也升級到 7.0 重新編譯,第二種做法則是之前提過的繫結導向,修改 MyConApp App.config 加入導向設定(name 及 publicKeyToken 可以由上圖錯誤訊息取得):

這樣就可以在不更動 MyLib Json.NET 版本的狀況下順利執行。

但有一個小訣竅,如果你在 Visual Studio 重新編譯 MyLib 卻不重新編譯 MyConApp,MyConApp bin目錄下的 Newtonsoft.Json.dll 將被覆寫成 6.0.0.0 版,此時執行 MyConApp 會變成找不到 7.0.0.0 版。

這是 Visual Studio 特性使然,正常情況下,基於專案相依性 Visual Studio 會先編譯 MyLib 再編譯 MyConApp,編譯 MyLib 時 MyLib.dll 及其參照的 Newtonsot.Json.dll 將被複製到 MyConApp 的 bin 目錄,等到編譯 MyConApp 時再被覆寫成 7.0.0.0 版 Newtonsoft.Json,得到正確結果。單獨手動編譯 MyLib 時,會打破前述的覆寫順序導致錯誤。要避免上述狀況,可調整 MyLib 的 Newtonsoft.Json 設定(如下圖),取消 Copy Local,這樣編譯 MyLib 時就不會自動複製 Newtonsoft.Json 到目標資料夾,可避免非預期的覆寫結果,而引用 MyLib 的專案必須自行參照 Newtonsoft.Json,開發人員可視狀況運用。

Visual Studio 2015的偵錯例外選項

$
0
0

一般來說,在 try { …  } 範圍發生的例外(Exception)不會觸發Visual Studio偵錯中斷,若想改成一出錯就中止執行,一直以來我都是由Visual Studio DEBUG選單找到Exceptions選項:

開啟Exceptions設定把「Common Language Runtime Exceptions」勾起來:

最近在Visual Studio 2015想做相同設定,卻發現「Exceptions」選項不見了!原來VS2015幫它搬了家,並更名為Exception Settings,快捷鍵則仍維持Ctrl + D, E:

Exception Settings也不再採Modal Dialog強制式對話框,改成常駐視窗,預設跟輸出(Output)、錯誤清單(Error List)放在同一區:

VS2015更動UI的理由,是為了加入新功能。新版的設定介面可使用關鍵字過濾尋找特定例外項目:

Exception清單可加入自訂Exception型別

至於舊版的未處理(User Unhandled)選項,在VS2015被移到右鍵選單的「Continue When Unhandled in User Code」。

延伸閱讀:The New Exception Settings Window in Visual Studio 2015 - Microsoft Application Lifecycle Manage

RunnerWindow/RiderWindow for fenix 3

$
0
0

(註:前方500公尺有中文版,請直行)

Ver 1.5, 2015-10-15 Download:  Runner Window / Rider Window

Yet another complex running information data field.

I want to see all the information of running in a single screen, so I wrote this data field for myself.

Runner Window is composed of 9 fields, including current time, timer time, cadence(stride), average pace, altitude, grade(slope), total distance, heart rate and current pace (average over 30-60 seconds). It also provides battery power percentage and GPS signal quality information.

RiderWindow provides almost the same functions, only average pace and current pace are replaced with average speed and current speed. 

Features

  1. Automatic metric/statute detection

    According to user's setting to display disntance in mile or km, altitude in feet or meter, pace in time per km or time per mile, speed in km per hour or miles per hour.

  2. GPS signal quality and battery power percentage

    Use signal bars to indicate the quality of GPS signal, when the GPS is unusable, GPS characters will be red. 
    A battery bar with different color (>50% green, >15% range, <15% red) and power percentage number.
  3. Support black and white background color

    RunnerWindow/RiderWindow provides both black and white background colors  and you can choose background color. In firmware 6.08b/Connect IQ 1.2, RunnerWindow/RiderWindow will change background according to your data field background color setting in Garmin's built-in menu. 
    In firmware 3.8-4.9 with Connect IQ 1.1x, you need to adjuect the 12 hour/24 hour settings to change the background, 24h for black and 12h for white. 

  4. Smart switching frequence of shared field

    The average pace, altitude and grade share the middle field and switch in a *smart* frequence.
    When running on plain roads (grade <= 1%), the altitude show only 5 seconds each 30 seconds so you can read the average pace more easily.
    When climbing or decending (grade > 1%), the altitude and grade information is important, so the shared field changes every 5 seconds.
    The grade will be displayed in the altitude header and also shows in the shared field as larger font for 1 to 2 seconds according to the grade value. 1 second when grade < 3%, 2 senconds when altitude > 3%.
    Hope this design can cover more scenarios.

FB: https://www.facebook.com/darkthread.net

Ver 1.5 Changlog

    • Refactor code to reduce memory usage or it won't survive under CIQ 1.2.
    • Add grade(slope) field
    • Add smart rolling feature to shared field
    • Black and white background colors
    • Current pace algorithm improved

Ver 1.4 Changelog

    • Program refactored to reduce memory usage to avoid out of memory issue on some firmware version

Ver 1.3 Changelog

    • Bug Fixed: The data field will crash when it is set on the first page

Ver 1.2 Changelog

    • Use separate metric/statute settings for distance, pace and altitude, for example: ft fo altitude and km for distance.

Ver 1.1 Changelog

    • Display current time with format HH:mm and larger font
    • Display GPS signal strength
    • Avoid empty period of current pace (average over 20 seconds) in first 20 seconds
    • Display distance and pace in KM or mile according to user's metric/statute setting
    • Display unit in altitude (ft or m) and distance (mi or km) headers

 

中文版說明

Ver 1.5, 2015-10-15 下載: Runner Window / Rider Window

懶得跑步時還要按鈕切畫面看資訊,身為程式魔人在GPS錶上寫個程式給自己用,也是很合理滴~

RunnerWindow 總共包含九種跑步資訊,包含現在時刻、跑步時間(Time)、步頻(Cadence)、平均步速(Avg Pace)、海拔高度(Altitude)、坡度(Grade/Slop,每100公尺上升或下降幾公尺,以百分比表示)、心率(HR)、目前步速(Pace,採30-60秒平均值),另外再加上電池電量百分比以及 GPS 訊號狀態(包含訊號強弱顯示)。平約步速與高度、爬升率共用最中央的顯示位置,採智慧切換頻率。RiderWindow與RunnerWindow功能相同,只有將目前步速及平均步速改為目前時速及平均時速,方便自行車運動時使用。

功能介紹:

  1. 自動偵測公制英制設定

    讀取手錶設定,依使用偏好的公制英制單位顯示距離、高度、步速(或時速)
  2. GPS訊號品質及電量百分比顯示

    以訊息條呈現 GPS 訊號強度,無訊號時顯示紅色,且提供電量條(>50%綠色,>15%橘色,<5%紅色)及電量百分比顯示。
  3. 可選擇黑色或白色背景

    支援黑色及白色兩種背景並可自由選擇。依手錶韌體版本,切換方法不同。
    Firmware 3.8-4.9 Connect IQ 1.1.3/1.1.4 使用時間12小時/24小時制切換,設為24小時制時進入黑底模式,12小時制時進入白底模式。(因資料欄位無法與外界溝通改變設定,故借用12/24小時制設定切換顏色,反正大多數人都有內建12/24小時換算模組,12/24小時制設定可有可無 :P)
    Firmware 6.08b Connect IQ 1.2則會依手錶選單內的資料欄位底色設定,配合改為黑色或白色。

  4. 共用欄位智慧切換

    高度對平坦路段(爬升或下降小於1%)跑步/自行車的意義不大,故高度與坡度數據每30秒只顯示5秒,其餘時間顯示平均步速時速,方便使用者參考。
    當遇到爬坡或下降時(爬升或下降大於1%),高度及坡度就值得被觀注,故共用欄位恢復5秒切換一次,以求二者兼顧。
    坡度(爬升或下降百分比)標示於高度標題後方,並在高度顯示期間最後1-2秒,改移至共用欄位以大字顯示。


Ver 1.5 改版說明

    • 再用力重構擠出更多記憶體以便在Connect IQ 1.2環境存活
    • 加入坡度(Grade/Slop)欄位
    • 共用欄位加入智慧型切換頻率
    • 加入黑白雙底色支援

Ver 1.4 改版說明

    • 程式重構減少記憶體用量,解決在新版韌體可能因記憶不足當機的情況

Ver 1.3 改版說明

    • 修正當資料欄位被設在第一頁會當掉的問題

Ver 1.2 改版說明

    • 英制/公制依距離、高度、步速之使用者設定可分別設定,例如:高度用公尺、距離採英里

Ver 1.1 改版說明

    • 現在時刻顯示格式改為HH:mm並加大字體
    • 增加 GPS 訊號強弱顯示
    • 移除目前步速前20秒空白期
    • 依使用者之公制/英制設定決定距離及步速顯示單位
    • 在高度及距離標題加入單位顯示,高度:ft或m、距離:mi或km

2015飛龍盃烘爐地馬拉松

$
0
0

第30馬,飛龍盃烘爐地馬拉松,標榜結合烘爐地土地公廟(南山福德宮)特色的地方賽事。對烘爐地的印象停留在大學時代,搞畢業紀念冊要找景點拍照,在同學強烈推薦下來過一回,夜景很美。賽道為山路接河濱,人數不多,離家不遠,符合「小而美」準則,沒想太多就報了名。

賽前兩天出了點意外,上班途中在公司前斑馬線近百路人面前表演仆街美技,所幸僅膝蓋擦傷無大礙,但左膝撞傷瘀青腫痛,走路微跛,剩不到48小時,能否恢復到可出賽狀態是大變數。為加速復原消腫,連暖暖包都派上場熱敷,比賽當日雖還有輕微腫痛,小跑步已不成問題,心想有七小時的奢華關門時間頂著,理論上用時速六公里快走都能達陣,做好隨時棄賽的心理準備,還是出發了。

會場設在福德宮前廣場,抵達會場前的最後一公里多爬升一百多公尺,坡度讓人打冷顫,等下用跑的可精彩了。天色仍暗,高聳的土地公像,倒讓人有說不出的安心感,速速寄好物拉筋,五點半鳴槍出發。

 

一起跑就是一公里多的陡下,受傷左膝隱隱作痛,只好不斷降速緩行慢慢退到隊伍的後段,GPS錶上的Runner Window(發揮吃自己狗食的精神,大好的實測機會當然不能放過)顯示步速7:30,其實已比想像快不少。

下山後穿過一段街道巷弄,左彎右拐進入另一段高潮,華夏科技大學旁的「哭坡」。各位觀眾-坡度+24%!(每100公尺上升24公尺)

前四公里爬完兩段雲霄飛車級賽道,再過穿過一段馬路街道轉入河濱後就是一路平坦。發生奇妙的事情,兩段變態坡道爬完,不知是血路打通還是密集活動加速消腫,膝蓋不適感近乎消失,真是土地公顯靈。但仍不敢跨大步,一改平時習慣的節奏,改採步頻177小碎步,跑來卻出奇順暢,Pace維持6:30-7:00的水準,比原本期望速度快很多,且心率不高,又發現一種未來可嘗試的輕鬆新跑法。

穿過一段「峽谷」,前陣子颱風帶來的淤土尚未清完,先挖出一條通道。峽谷景色是很獨特,但風一吹,迎面就是一陣黃塵,呸呸呸,我吃土了。

沿著永福橋、中正橋、華中橋,一路到大漢橋折返,剛好跟海山馬顛倒。六月跑海山馬看到的壁畫半成品,如今已完成,原來是新北上河圖來著。另外還有不少通俗作品,從老皮阿寶小小兵到熊本熊都有。

一路上重要路口叉路都有交通指揮志工,大會補給站超多超密,幾乎2-3公里就有一站,沒有什麼花俏項目,就是水、運動飲料、餅乾、香蕉、塩、汽水… 等,但供給充分,水站志工親切熱情,不愧是有土地公庇佑的優質賽事。

一路膝蓋的狀況潮入佳境,除了爬坡段,Pace都能維持在6:30-8:00之前,原本準備貼著七小時關門跑完是多慮。近午太陽才露臉,氣溫漸升但不時有風,再重回24%哭坡,身處樹蔭林間吹著涼風,美哉!不愧是有土地公庇佑的優質賽事。

嘩!亮點來了,比基尼美眉啦啦隊耶!跑壇浮沈多年,這是我見過最強大的「精神補給」,不愧是有土地公庇佑的優質賽事。

去程的雲霄飛車賽道,回程得倒帶再走一次,最後2K的山腳下看到有趣的牌子。

抬頭一看,土地公還真的站在遠方的山腰觀賽哩~ XD

最後,5:26:51完賽,成績比想像好很多,土地公保佑。Runner Window也伴我完成第一場全馬,跑完5個半小時,Fenix 3電量消耗40%(開GLONASS、非省電模式),所以充飽電可以連跑十小時不是問題,手錶沒問題,腳會不會斷掉才是大問題 XD

跑回會場近中午,光線明亮,正式細看烘爐地的大景,回想上回到這裡已是二十年前的事,時光飛逝啊~

賽道兩側的廣告牌,讓人有置身國際級賽事的錯覺,酷!

大會備有接駁車接送跑友到山下停車場跟捷運站,考慮周到。接駁小巴班次頗密集,但我跑完的時間還有不少半馬跑友仍在會場,小排了半小時才搭到車,等車時順便欣賞廟裡的忠孝節義浮雕。

 

 

戰利品巡禮:非常有台灣味風格一致的大會紀念衫、毛巾以及環保提袋,還有土地公發財金一枚。黃黑紅配色的飛龍在天戰袍廟會味十足,儼然是神轎轎夫或報馬仔的甲種服裝,很有趣也很有特色。

 

      
    

很棒的一場馬拉松,謝謝大會,謝謝土地公~


EF SaveChanges()未寫入Entity資料異動

$
0
0

Entity Framework等ORM技術讓更新資料表動作變得異常簡單。

在ADO.NET時代,若要更新Product資料表某一筆資料的兩個欄位,得先想好SQL指令建立SqlCommand,接著逐一新増SqlParameter @p1, @p2, @k1, @k2 並傳入各參數值,:

var sql = "UPDATE Product SET Col1 = @p1, Col2 = @p2 WHERE ColPK = @k1 AND ColPK2 = @k2"; 
SqlCommand cmd = cn.CreateCommand(sql); 
cmd.Parameters.Add("@p1", SqlDbType.VarChar).Value = p1值; 
cmd.Parameters.Add("@p2", SqlDbType.VarChar).Value = p2值; 
cmd.Parameters.Add("@k1", SqlDbType.VarChar).Value = k1值; 
cmd.Parameters.Add("@k2", SqlDbType.VarChar).Value = k2值; 

cmd.ExecuteNonQuery();
最後來個SqlCommand.ExecuteNonQuery(),大功告成。

有了EF,我們只需這樣做:

var item = entities.Product.Where(o => o.ColPK == k1值 && o.ColPK2 = k2值);
item.Col1 = p1值;
item.Col2 = p2值;
entities.SaveChanges();
是不是乾淨俐落許多?

今天踩到小圖釘一根。範例以下,程式透過PeriodId、CostCenter及JobType比對在DataGroup中抓出兩筆資料,將其Tag欄位改成現在時間的HHmmss字串,最後呼叫SaveChanges()將結果寫入DB。

staticvoid Test1()
        {
using (var ctx = new XDBEntities())
            {
                var dateGrps = ctx.DataGroup.Where(o => o.PeriodId == "2021A"
&& (o.CostCenter == "100"&& o.JobType == "A" ||
                    o.CostCenter == "101"&& o.JobType == "S")).ToList();
foreach (var dg in dateGrps)
                {
                    Console.WriteLine("DataGroup:{0}-{1}", dg.CostCenter, dg.JobType);
                    dg.Tag = DateTime.Now.ToString("HHmmss");
                }
                ctx.SaveChanges();
            }
        }

測試結果如預期,兩筆資料的Tag欄位正確更新。但以上寫法並不實用,因為實務上查詢條件幾乎都由使用者輸入動態決定,怎麼可能直接寫死Where(o => …)?

針對動態條件,我最常用Database.SqlQuery<TElement>(String, Object[])處理自訂查詢,TElement可以傳入任何型別,只要屬性名稱跟資料庫欄位名稱對得上,EF可自動轉型生出指定物件型別,十分方便。憑著直覺,依著傳入變數產生WHERE條件字串及參數值,我將原本的.Where()查詢改寫成Database.SqlQuery<DataGroup>()搞定:

staticstring[] keys = newstring[] { "100-A", "101-S" };
staticvoid Test2()
        {
using (var ctx = new XDBEntities())
            {
int paramCount = 0;
                List<object> paramValues = new List<object>();
stringwhere = "PeriodId='2021A' AND (" +
string.Join(" OR ",
                    keys.Select(o =>
                    {
                        var p = o.Split('-');
                        var criteria = "CostCenter={" + paramCount++ +
"} AND JobType={" + paramCount++ + "}";
                        paramValues.Add(p[0]);
                        paramValues.Add(p[1]);
return criteria;
                    }).ToArray()) + ")";
                var dataGrps = ctx.Database.SqlQuery<DataGroup>(
"select * from datagroup where " + where, paramValues.ToArray())
                    .ToList();
foreach (var dg in dataGrps)
                {
                    Console.WriteLine("DataGroup:{0}-{1}", dg.CostCenter, dg.JobType);
                    dg.Tag = DateTime.Now.ToString("HHmmss");
                }
                ctx.SaveChanges();
            }
        }

安全宣導:組裝SQL字串時不可直接串接使用者輸入內容,以免埋下致命的SQL Injection漏洞(如果你不知道什麼是SQL Injection,建議儘快雙手打上石膏裝殘,沒搞清楚前先別寫任何資料庫相關程式以免禍國殃民)。可參考上述程式的做法,用{0}{1}在SQL語法中嵌入參數,再以object[]傳入參數內容。{0}{1}標註源自string.Format,但絕對不是字串置換,背後EF會將其轉為DbParamater型別,故能杜絕SQL Injection風險。另外,當DB為SQL Server,{0}{1}也可用@p0、@p1取代,但不同資料庫符號規則有別,在Oracle則要寫成:p0、:p1,基於EF儘可能降低DB依賴的思維,我個人偏好{0}{1}寫法。

Database.SqlQuery<T>正確地取回兩筆資料,但修改Tag呼叫ctx.SaveChanges(),修改結果卻沒寫回資料庫。

經過一番研究,才知道自己錯在哪裡。SqlQuery有兩種:

Database.SqlQuery<TElement> vs DbSet<TEntity>.SqlQuery

二者的最大差異在於前者的TElement可以是任意類別,只要屬性能跟SELECT欄位對應即可,但Context永遠不追蹤傳回物件的改變,即使它是EF所定義的Entity型別(The results of this query are never tracked by the context even if the type of object returned is an entity type.);後者限定只能用於EF定義好的Entity,傳回結果時預設會追蹤其變化(By default, the entities returned are tracked by the context)。

找出原因,解決只在彈指之間,將ctx.Database.SqlQuery<DataGroup>()改成ctx.DataGroup.SqlQuery(),一切搞定!

組裝動態LINQ條件的利器-LINQKit

$
0
0

昨天提到實務上查詢條件多半由使用者動態決定,往往得靠SqlQuery或Dapper配合WHERE條件字串動態組裝搞定。(提醒:SQL語法可以用串接的,使用者輸入內容務必使用參數嵌入)但如此就得放棄LINQ .Where()的強型別優點。介紹一個好物,讓你可以兩者兼顧-LINQKit

老樣子,用NuGet就可安裝,查詢關鍵字請填"LinqKit":

最新版的LINQKit需要EntityFramework 6.1.3以上,但依其運作原理,EF5應該也適用,等遇到再說。

LINQKit在Github有詳細說明,這裡只簡單介紹如何用它取代自組SQL WHERE指令。LINQKit靠PredicateBuilder建立動態條件,起手式有兩種:

var pred = PredicateBuilder.False<MyEntity>();
pred = pred.Or(o => o.Col1 == 1);
pred = pred.Or(o => o.Col2 == "X");
ctx.MyEntity.AsExpandable().Where(pred);
等同於ctx.MyEntity.Where(o => o.Col1 == 1 || o.Clo2 == "X")

var pred = PredicateBuilder.True<MyEntity>();
pred = pred.And(o => o.Col1 == 1);
pred = pred.And(o => o.Col2 == "X");
ctx.MyEntity.AsExpandable().Where(pred);
等同於ctx.MyEntity.Where(o => o.Col1 == 1 && o.Clo2 == "X")

注意,OR比對一開始用False<T>,AND則是True<T>,而EF的DbSet<T>要先.AsExpandable()才能在Where()傳入Predicate,否則會出現 The LINQ expression node type 'Invoke' is not supported in LINQ to Entities. 錯誤。另一種做法是不加.AsExpandable(),而是.Where(pred.Expand()),二者皆可。

如果WHERE條件又有AND又有OR怎麼辦?昨天的案例剛好拿來當範例:WHERE PeriodId='2021A' AND (CostCenter='100' AND JobType='A' OR CostCenter='101' AND JobType='S')。秘訣在於Predicate的And()或Or()的傳入參數也可接受其他Predicate,故前述需求可寫成:
var pred2 = PredicateBuilder.False<MyEntity>();
pred2 = pred2.Or(o => o.CostCenter == "100" && o.JobType == "A");
pred2 = pred2.Or(o => o.CostCenter == "101" && o.JobType == "S");
var pred = PredicateBuilder.True<MyEntity>();
pred = pred.And(o => o.PeriodId == "2021A");
pred = pred.And(pred2);
ctx.DataGroup.AsExpandable().Where(pred);

很簡單吧?最後來看看昨天的案例如何用LINQKit改寫:

staticstring[] keys = newstring[] { "100-A", "101-S" };
staticvoid Test3()
{
using (var ctx = new XDBEntities())
    {
        var pOr = PredicateBuilder.False<DataGroup>();
        keys.ForEach(o =>
        {
            var p = o.Split('-');
            var costCenter = p[0];
            var jobType = p[1];
            pOr = pOr.Or(q => q.CostCenter == costCenter && q.JobType == jobType);
        });
 
        var pWhere = PredicateBuilder.True<DataGroup>();
        pWhere = pWhere.And(o => o.PeriodId == "2021A");
        pWhere = pWhere.And(pOr);
 
        ctx.Database.Log = (m) =>
        {
            Console.WriteLine("**** EF Log ****");
            Console.WriteLine(m);
        };
        var dataGrps = ctx.DataGroup.AsExpandable().Where(pWhere).ToList();
//var dataGrps = ctx.DataGroup.Where(pWhere.Expand()).ToList();
foreach (var dg in dataGrps)
        {
            Console.WriteLine("DataGroup:{0}-{1}", dg.CostCenter, dg.JobType);
        }
    }
}

順便示範如何觀察EF6產生的SQL指令,ctx.Database有個Action<string> Log屬性,覆寫成自訂函式,就能接收到EF6的Log,其中包含SQL指令,測試結果如下:

**** EF Log ****
Opened connection at 2015/10/23 上午 05:33:11 +08:00

**** EF Log ****
SELECT
    [Extent1].[PeriodId] AS [PeriodId],
    [Extent1].[CostCenter] AS [CostCenter],
    [Extent1].[JobType] AS [JobType],
    [Extent1].[Status] AS [Status],
    [Extent1].[Tag] AS [Tag]
    FROM [dbo].[DataGroup] AS [Extent1]
    WHERE ('2021A' = [Extent1].[PeriodId]) AND ((([Extent1].[CostCenter] = @p__l
inq__0) AND ([Extent1].[JobType] = @p__linq__1)) OR (([Extent1].[CostCenter] = @
p__linq__2) AND ([Extent1].[JobType] = @p__linq__3)))
**** EF Log ****


**** EF Log ****
-- p__linq__0: '100' (Type = AnsiString, Size = 8000)

**** EF Log ****
-- p__linq__1: 'A' (Type = AnsiString, Size = 8000)

**** EF Log ****
-- p__linq__2: '101' (Type = AnsiString, Size = 8000)

**** EF Log ****
-- p__linq__3: 'S' (Type = AnsiString, Size = 8000)

**** EF Log ****
-- Executing at 2015/10/23 上午 05:33:11 +08:00

**** EF Log ****
-- Completed in 11 ms with result: SqlDataReader

**** EF Log ****


**** EF Log ****
Closed connection at 2015/10/23 上午 05:33:11 +08:00

DataGroup:100-A
DataGroup:101-S

Predicate產生的WHERE條件符合我們的期望:('2021A' = [Extent1].[PeriodId]) AND ((([Extent1].[CostCenter] = @p__l inq__0) AND ([Extent1].[JobType] = @p__linq__1)) OR (([Extent1].[CostCenter] = @p__linq__2) AND ([Extent1].[JobType] = @p__linq__3)))

與自組SQL指令相比,使用LINQKit Predicate寫法相對簡潔,省去為嵌入參數命名、管理參數值陣列的瑣碎手工,又可享受LINQ強型別優勢,不用擔心「打錯字」(呃,被刺傷了),決定將LINQKit收入常備工具箱,多加利用。

WCF探勘15-DataContract與DataMember

$
0
0

學藝不精,陸續踩了幾次雷,整理DataContract與DataMember對序列化的影響備忘。

WCF預設使用DataContractSerializer執行序列化,而DataContractSerializer可依物件類別標註的[DataContract]及[DataMember]、[IgnoreDataMember]等Attribute決定哪些屬性該序列化。依照MSDN文件,DataContractSerializer的處理原則為:

  1. 類別標示[DataContract]時,只序列化有標示[DataMember]的項目
    註:DataMember不限於public Property,可用於各種存取層級(public、private、internal、protected)的Property或Field。
  2. 若類別未標示[DataContract],DataContractSerializer預設會序列化「所有可讀寫的public Property及Field」,此時可透過[IgnoreDataMember]負向表列不要序列化的Property或Field。

我設計了以下幾個型別來觀察DataContractSerializer行為,有Property有Field,有public有private,再配合[DataContract]、[DataMember]、[IgnoreDataMember]組合出不同變化:

using System.Runtime.Serialization;
 
[DataContract]
publicclass Blah1
{
publicint PropNoAttr { get; set; }
publicint Field;
privateint PrivProp { get; set; }
privateint PrivField;
 
}
 
[DataContract]
publicclass Blah2
{
publicint PropNoAttr { get; set; }
    [DataMember]
publicint PropAttr { get; set; }
publicint Field;
    [DataMember]
privateint PrivProp { get; set; }
    [DataMember]
privateint PrivField;
 
}
 
[DataContract]
publicclass Blah3
{
publicint PropNoAttr { get; set; }
    [IgnoreDataMember]
publicint PropIgnoreAttr { get; set; }
publicint Field;
privateint PrivProp { get; set; }
privateint PrivField;
 
}
 
 
publicclass Blah4
{
publicint PropNoAttr { get; set; }
    [DataMember]
publicint PropAttr { get; set; }
    [IgnoreDataMember]
publicint PropIgnoreAttr { get; set; }
 
publicint Field;
privateint PrivProp { get; set; }
privateint PrivField;
}
 
publicclass Blah5
{
publicint PropNoAttr { get; set; }
publicint ReadonlyProp { get; }
publicint Field;
privateint PrivProp { get; set; }
privateint PrivField;
}

測試程式很簡單,用DataContractSerializer將Blah1到Blah5序列化成XML,Console.WriteLine出來,借用XDocument將XML以縮排格式輸出。

using System;
using System.IO;
using System.Runtime.Serialization;
using System.Text;
using System.Xml.Linq;
 
namespace ConsoleApplication1
{
class Program
    {
staticvoid Main(string[] args)
        {
            Console.WriteLine(Serialize<Blah1>(new Blah1()));
            Console.WriteLine(Serialize<Blah2>(new Blah2()));
            Console.WriteLine(Serialize<Blah3>(new Blah3()));
            Console.WriteLine(Serialize<Blah4>(new Blah4()));
            Console.WriteLine(Serialize<Blah5>(new Blah5()));
            Console.Read();
        }
staticstring Serialize<T>(T graph)
        {
            DataContractSerializer dcs = new DataContractSerializer(typeof(T));
            MemoryStream ms = new MemoryStream();
            dcs.WriteObject(ms, graph);
            XDocument xd = XDocument.Parse(Encoding.UTF8.GetString(ms.ToArray()));
return
typeof(T).ToString() +
"\n=====================================\n" +
                xd.ToString() + "\n";
        }
    }
}

執行結果如下:

Blah1
=====================================
<Blah1xmlns="http://schemas.datacontract.org/2004/07/"
xmlns:i="http://www.w3.org/2001/XMLSchema-instance"/>
 
Blah2
=====================================
<Blah2xmlns="http://schemas.datacontract.org/2004/07/"
xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<PrivField>0</PrivField>
<PrivProp>0</PrivProp>
<PropAttr>0</PropAttr>
</Blah2>
 
Blah3
=====================================
<Blah3xmlns="http://schemas.datacontract.org/2004/07/"
xmlns:i="http://www.w3.org/2001/XMLSchema-instance"/>
 
Blah4
=====================================
<Blah4xmlns="http://schemas.datacontract.org/2004/07/"
xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<Field>0</Field>
<PropAttr>0</PropAttr>
<PropNoAttr>0</PropNoAttr>
</Blah4>
 
Blah5
=====================================
<Blah5xmlns="http://schemas.datacontract.org/2004/07/"
xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<Field>0</Field>
<PropNoAttr>0</PropNoAttr>
</Blah5>
  1. Blah1
    只標了[DataContract]沒標任何[DataMember],XML空空如也,沒有任何Property或Field被序列化
  2. Blah2
    標註[DataContract]後,所有標註[DataMember]的成員都包含在XML中,不論public或private,不管是Property或是Field
  3. Blah3
    標註[DataContract]後DataContractSerializer只認[DataMember]辦事,[IgnoreDataMember]沒有作用,XML是空的
  4. Blah4
    未標註[DataContract],除了[IgnoreDataMember] PropIgnoreAttr外,所有public的Property及Field都被序列化
  5. Blah4
    未加任何Attribute,DataContractSerializer將序列化所有公開Property及Field,但Property限定可讀寫者,刻意放了一個public ReadonlyProp { get; }唯讀屬性,果然被排除了。

實驗驗證完畢!

以JSON傳送大量物件引發ASP.NET MVC反序列化錯誤

$
0
0

某專案使用[FromPartialBody]在ASP.NET MVC Action接收jQuery送來的物件陣列,初測無誤後進行正式測試,發現只要物件陣列的筆數一多,網頁就會爆炸:

System.InvalidOperationException: The JSON request was too large to be deserialized.
   於 System.Web.Mvc.JsonValueProviderFactory.EntryLimitedDictionary.Add(String key, Object value)
   於 System.Web.Mvc.JsonValueProviderFactory.AddToBackingStore(EntryLimitedDictionary backingStore, String prefix, Object value)
   於 System.Web.Mvc.JsonValueProviderFactory.AddToBackingStore(EntryLimitedDictionary backingStore, String prefix, Object value)
   於 System.Web.Mvc.JsonValueProviderFactory.AddToBackingStore(EntryLimitedDictionary backingStore, String prefix, Object value)
   於 System.Web.Mvc.JsonValueProviderFactory.AddToBackingStore(EntryLimitedDictionary backingStore, String prefix, Object value)
   於 System.Web.Mvc.JsonValueProviderFactory.GetValueProvider(ControllerContext controllerContext)
   於 System.Web.Mvc.ValueProviderFactoryCollection.GetValueProvider(ControllerContext controllerContext)
   於 System.Web.Mvc.ControllerBase.get_ValueProvider()
   於 System.Web.Mvc.ControllerActionInvoker.GetParameterValue(ControllerContext controllerContext, ParameterDescriptor parameterDescriptor)
   於 System.Web.Mvc.ControllerActionInvoker.GetParameterValues(ControllerContext controllerContext, ActionDescriptor actionDescriptor)
   於 System.Web.Mvc.Async.AsyncControllerActionInvoker.<>c__DisplayClass21.<BeginInvokeAction>b__19(AsyncCallback asyncCallback, Object asyncState)

爬文很快找到解決方案,web.config裡有個aspnet:MaxJsonDeserializerMembers參數,預設值為1000,超過上限就會出現前述錯誤。留下一個疑點,測試資料筆數不可能超過400筆,MaxJsonDeserializerMembers上限1000的單位為何?

用F12開發者工具檢查Request內容,果然資料只有366筆。

依MSDN文件的MaxJsonDeserializerMembers說明:Specifies the limit of the maximum number of items that can be present in any dictionary deserialized by the JavaScriptSerializer type. 推測每組 PropName: "PropValue" 就算一個Item。換言之,data陣列有366個元素,每個各有CostCenter、JobType、UserId、Weight四個屬性,366x4 + 2(approve及wuid)=1466,突破1000關卡。為證實猜想,刻意將aspnet:MaxJsonDeserializerMembers調為1465以及1466,果真得到1465失敗,1466成功的結果,驗證前面的Member個數定義正確。有了這個基礎,我們就能精準估算MaxJsonDeserializerMembers該設多大。

咦?為什麼不設一個宇宙無敵大的上限就好,要傷這種腦筋?依老骨頭的直覺,預設值偏低必有玄機,恣意加大絕非上策。微軟的這篇KB說明始末:MaxJsonDeserializerMembers是在MS11-100安全更新時刻意調小的,目的在防止有人藉由傳送超大JSON癱瘓ASP.NET網站達到DoS攻擊的目的。也許是說,MaxJsonDeserializerMembers被調得愈大,網站被攻擊癱瘓的風險也愈高,修改設定宜謹慎拿捏。

2015貓空半馬

$
0
0

在後院舉辦的路跑,睡飽散步到會場,免寄物直接出發,豈有不參加的道理?連續第六年參加貓空半馬也是很合理滴。

前年靠20度低溫創下1:57:39山路半馬PB,去年比賽日氣溫高出7度,跑到心臟都快噴出來也殺不進SUB 2,今年老天爺再次幫忙,21-24度陰天到偶雨,是挑戰PB的絕佳天氣。不料,比賽前一晚被教練團(謎:那來的組織?)急召,總教練語重心長再三交待,今年要我執行一項秘密任務,配速必須貼著關門時間(3小時)完賽,總教練拍拍我的肩:「這對有能力SUB 2的你是一項考驗,但我相信你的心理素質可以勝任」。假裝面有難色地接下指令,一轉頭得強忍不要笑出來聲:哈哈哈,還以為要叫我努力拼PB哩,原來是要跑關門半馬,爽啦!

一如往例,7:15才懶洋洋地晃到會場,正好是暖身操時間,不慌不忙擠到出發隊伍後半截免得一起跑被人刷卡刷到破皮,狀況不錯跑關門半馬,倒是新鮮體驗。

抱著玩耍的心情,帶著相機東拍西拍,48分跑完5K攻上杏花林,跑得慢汗少水也喝得少,就破例不去明德宮洗手間報到了。估算3小時完賽總均速不可慢於Pace 8:34,但前5K內含300米爬升,50分內跑完還算OK。

按照計劃,第二個5K繼續跑跑走走,Pace維持7:30-8:30間,如預期地在1h32m左右抵達草湳橋,3小時剛好用掉一半的時間。

一路上擔心後段班會沒水喝,所幸大會準備充分加上氣溫偏涼,礦泉水、運動飲料、小餅乾、七七乳加都沒斷糧(今年沒有香蕉,嗚…),原本還安排了B計劃視狀況可緊急徵召隱藏版私補-貓纜站萊爾富(當然,要拿出悠遊卡才能召喚 XD),最終沒派上用場。倒是在草湳橋折返時還是遇到「後段班歧視」-志工滿面歉意地說:信物發光了!水跟補給抓不準每位跑友吃多少可能斷炊,但信物一人一個,且報名人數為已知數,發生信物不夠發的狀況倒是新鮮事兒,哈!

還有一半的時間折返,回程又包含近4公里的下坡,只要不發生抽筋爆膝的意外,準時3小時完賽在望。

吊車尾風景果真不同,寧靜的賽道,孤零零的里程牌,別有一番意境呀~

大誠高中的志工同學好有活力啊,年輕真好。

2h18m左右回到杏花林,只剩最後5K,看看時間還有42分鐘,吃下定心丸。

最後5K的下坡Pace加快到630到7分速之間,越過最後一段蔣公銅像斷魂坡還有12分鐘關門,綽綽有餘。銅像前看到裁判大車隊已經收隊,比賽接近尾聲。回到體育場,在熟到不能再熟悉的跑道小小衝刺一番,離關門不到10分鐘通過終點,達成教練團交付使命!

今天陪我跑半馬的Data Field是還沒上架的Runner Window彩色版,用顏色呈現不同儲備心率區間,步頻跟Pace也依快慢加上顏色顯示,只是今天奉命跑關門半馬,運動強度資訊派不上用場,照片是最後半圈衝刺一半快閃拍下的,成功讓心率衝上紫色區間,臉色也到達紫色區間 XD 軟體通過測試,綠燈!

咦?兩塊半馬完賽獎牌?

西滴,本屆比賽奉命擔任黑暗女王的破風手、護駕士兵、醫護員(雖然只帶了一罐肌樂充數)、活動補水瓶、人肉配速App、跑者鼔勵員,終究不辱使命,排除萬難,達成任務,黑暗皇家半馬俱樂部成員再添一名!(灑花)

FAQ-如何在 Fenix 3 設定自訂資料欄位

$
0
0

Runner Window上架後,陸續遇到不少朋友詢問安裝使用的方法,次數累積越過值得寫成FAQ的門檻,所以,FAQ來了。:P

Fenix 3支援Connect IQ,使用者可以下載安裝熱心開發者寫好的Watch Face(錶面)、App(應用程式)、Widget(小工具)、Data Field(資料欄位),而Runner Window就是一種資料欄位。要在Fenix 3使用Runner Window前,你必須先使用Garmin Connect App或Garmin Express,透過手機或個人電腦下載,再同步傳到手錶上。以下分別示範如何用Garmin Connect及Garmin Express下載Runner Window。

Garmin Connect App:Android iOS

  1. 進入App後點選Connect IQ商店
  2. 輸入關鍵字按搜尋(運氣好的話,它會直接出現在下方熱門Data Fields清單)
  3. 點選搜尋結果的Data Fields頁籤,便可在清單中找到它
  4. 選取Runner Window,再點選下載鈕,下次與手錶同步,Runner Window便會被安裝到手錶上

如果你習慣用PC或Mac跟手錶同步,則要使用Garmin Express:

  1. 開啟Garmin Express,點選Fenix 3
  2. 開啟「管理應用程式」
  3. 點選「取得更多應用程式」
  4. Garmin Express開啟瀏覽器連上Connect IQ商店Fenix 3專區,請搜尋Runner Window Data Field(運氣好的話可以直接在熱門Data Fields找到它)
  5. 點選下載,瀏覽器會試著連結Garmin Express下載Runner Window,一樣會在下次同步時安裝到手錶上

在Garmin Connect或Garmin Express同步並安裝Data Field後,你必須設定跑步App訓練頁面,將訓練頁面調成單一欄位並從Connect IQ清單選取Runner Window。之後進入跑步App,再切換到指定的訓練頁面,Runner Window就可以陪你一起跑馬、練LSD囉。步驟有點多,難以言傳,請看VCR!

影片

KendoGrid+Angular顯示大量資料的效能問題

$
0
0

接獲使用者反應,某個使用KendoGrid顯示大量資料的網頁,用IE檢視的話速度慢到嚇人。聞此言,馬上打開IE11測試,果真嚇得我差點閃了兩滴… 也太慢了吧!

先交代問題情境,專案使用Angular搭配Kendo UI開發,順理成章使用Kendo UI提供的Angular Directive,寫成<div kendo-grid …>,資料筆數偏多,大約一千多到兩千筆,基於KendoGri的強大彈性及高複雜度,處理起來多耗點時間倒也合理,但這個案例在IE的表現與期望出入頗大,值得調查。

修改KendoGrid的官方範例,我設計了一個實驗:

<divid="example"ng-app="KendoDemos">
<divng-controller="MyCtrl">
<buttonng-click="loadData()">Load Data</button>
<spanng-bind="duration"></span>
<divkendo-grid="grid"options="mainGridOptions">
</div>
 
</div>
</div>
<script>
    angular.module("KendoDemos", [ "kendo.directives" ])
        .controller("MyCtrl", function($scope){
var data = [];
for (var i = 0; i < 2000; i++) {
              data.push({
                FirstName: "FN" + i,
                LastName: "LN" + i,
                Country: "CN" + i,
                City: "CT" + i,
                Title: "T" + i
              });
            }
            $scope.loadData = function () {
                st = new Date();
                $scope.grid.dataSource.data(data);
            };
var st;
            $scope.mainGridOptions = {
                dataSource: [],
                height: 600,
                sortable: true,
                pageable: false,
                columns: [
                    { field: "FirstName", title: "First Name", width: "120px" },
                    { field: "LastName", title: "Last Name", width: "120px" },
                    { field: "Country", width: "120px" },
                    { field: "City", width: "120px" },
                    { field: "Title" }
                ],
                dataBound: function () {
if (st) {
                        setTimeout(function () {
                            $scope.duration = new Date() - st + "ms";
                            $scope.$digest();
                        }, 1);
                    }
                }
            };
        })
</script>

網頁使用Directive建立KendoGrid,按鈕後將2000筆資料放入KendoGrid中,重點按鈕到資料顯示完成所需的時間(dataBound事件將在整個Grid的DOM生成後觸發),實測IE11耗時3秒。透過F12開發者工具的分析工具,抓出速度慢的源頭在Angular的compile、publicLinkFn等方法,共執行1979次,耗費2秒以上。

此一發現讓人合理推測如果不用Angular Directive,改以純jQuery方式建立KendoGrid,效率應會提升。所以我又寫了對照組:

<divid="example">
<div>
<button>Load Data</button>
<spanid="duration"></span>
<divid="grid">
</div>
 
</div>
</div>
<script>
var data = [];
for (var i = 0; i < 2000; i++) {
            data.push({
                FirstName: "FN" + i,
                LastName: "LN" + i,
                Country: "CN" + i,
                City: "CT" + i,
                Title: "T" + i
            });
        }
var st, ed;
        $("#grid").kendoGrid({
            dataSource: [],
            height: 600,
            sortable: true,
            pageable: false,
            columns: [
                { field: "FirstName", title: "First Name", width: "120px" },
                { field: "LastName", title: "Last Name", width: "120px" },
                { field: "Country", width: "120px" },
                { field: "City", width: "120px" },
                { field: "Title" }
            ],
            dataBound: function () {
if (st) $("#duration").text(new Date() - st + "ms");
            }
        });
        $("button").click(function () {
            st = new Date();
            $("#grid").data("kendoGrid").dataSource.data(data);
        });
</script>

幾乎相同的程式邏輯,差別在改用jQuery建立KendoGrid及處理按鈕事件,實測速度可縮短至1.2秒,證明速度慢確實與啟用Angular有關。使用分析工具觀察,同樣的欄位設定,不使用Angular時,KendoGrid在_renderContent()經由指定innerHTML產生畫面元素,省去Angular模式的comple、建立scope、產生dataItem物件環節,邏輯相對單純,應是耗費時間變少的主因。

不過,這種模式不能在欄位Template使用<span ng-bind="dataItem.propName">等Angular寫法,無法叫用現成的Directive、Filter等,不利邏輯集中與程式碼共用。有利有弊,實務應用時就必須做出取捨,而二者的效能差距數字是判斷的關鍵。

寫了兩個測試網頁放在JSBin:KendoGrid + Angular版 vs KendoGrid + 純jQuery,用IE11、Edge、Chrome及Firefox做了不專業的測試。操作方法是重新載入網頁,按下Load Data鈕,反覆數次取最低值,測試結果為:

  • IE11:2.3秒 vs 0.8秒
  • Edge:2.2秒 vs 0.8秒
  • Chrome:1.1秒 vs 0.3秒
  • Firefox:1.1秒 vs 0.3秒

由於沒有精準地控制環境、變因,以上數據並不具權威性,但足以確定啟用Angular時速度慢了約三倍,在資料量小的情境差別有限,若遇到上千筆資料時,就有可能讓使用者皺眉。而這個問題在IE上又格外嚴重,在面對類似情境時要特別留意,甚至放棄Angular Template回歸純jQuery模式改善效能。

留下兩點值得探討:

  1. 原本預期Edge的效能應逼近Chrom,但實測卻跟IE11近乎相同,有一種可能是KendoGrid的程式有針對不同瀏覽器優化,Edge未受惠,或是我的測試環境有問題,歡迎使用Windows 10的朋友幫忙複測看看。
  2. 直覺KendoGrid使用Angular架構產生資料列所遇到的效能議題,在巨量ng-repeat或網頁內含大量採用Template生成DOM的Directive時也將面臨相同挑戰,這點留待日後驗證。

Angular顯示大量物件效能測試

$
0
0

前幾天調查KendoGrid+Angular效能變差三倍的,推測問題根源不在KendoGrid,而在於Angular建立2000個具有獨立$scope的DOM元素,本身就是重度耗用資源工作。換句話說,就算不用KendoGrid,改以ng-repeat實作產生2000個頁面元素,對效能一樣是嚴峻考驗。光憑想像永遠不知真相,實地測上一回自然會有方向!

<!DOCTYPEhtml>
<html>
<head>
<metacharset="utf-8">
<title>KendoGrid + NG</title>
<scriptsrc="https://kendo.cdn.telerik.com/2015.3.930/js/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.5/angular.js"></script>
<style>
        .list > span {
            display: block; float: left; text-align: center;
            border: 1px solid gray; 
            margin: 2px; padding: 2px; width: 110px;
        }
</style>
</head>
<body>
<div id="example" ng-app="KendoDemos">
<div ng-controller="MyCtrl">
<button ng-click="loadData()">Load Data(NG)</button>
<button id="btnLoadDataHtml">Load Data(jQuery html)</button>
<button id="btnLoadDataDOM">Load Data(jQuery DOM)</button>
<div class="list">
<span ng-repeat="item in items">
                    {{item.FirstName}} {{item.LastName}}
                    {{$parent.calcDura($last)}}
</span>
</div>
 
</div>
</div>
<script>
var data = [];
for (var i = 0; i < 2000; i++) {
            data.push({
                FirstName: "FN" + i,
                LastName: "LN" + i,
                Country: "CN" + i,
                City: "CT" + i,
                Title: "T" + i
            });
        }
        angular.module("KendoDemos", [])
            .controller("MyCtrl", function ($scope) {
var st;
                $scope.calcDura = function (last) {
if (last && st) {
var dura = (new Date() - st) + "ms";
                        st = null;
                        setTimeout(function () {
                            alert(dura);
                        }, 1);
                    }
return"";
                };
                $scope.loadData = function () {
                    st = new Date();
                    $scope.items = data;
                };
            });
        $("#btnLoadDataHtml").click(function () {
var st = new Date();
var h = $.map(data, function (item, i) {
return"<span>" + item.FirstName + " " + item.LastName + "</span>";
            }).join("\n");
            $(".list").html(h);
            alert((new Date() - st) + "ms");
        });
        $("#btnLoadDataDOM").click(function () {
var st = new Date();
var $list = $(".list");
            $.each(data, function (i, item) {
                $("<span>" + item.FirstName + " " + item.LastName + "</span>").appendTo($list);
            });
            alert((new Date() - st) + "ms");
        });
 
</script>
</body>
</html>

設計了以上的實驗,分別使用ngRepeat、jQuery組HTML字串以及jQuery逐一加入DOM三種做法,測試產生2000個<span>所需的時間。耗時毫秒數以alert顯示,ngRepeat要抓DOM生成時機有點挑戰,我想到的解法是檢查$last變數,在<span>多埋一個{{$parent.calcDura($last)}}偵測最後一筆。擔心額外機制可能干擾效數字,經Profiler觀察,加入前後執行時間差異極小,可忽略其影響。

依理而論,組合HTML字串再一次反應到DOM是最有效率的做法,至於逐筆產生<span>加入DOM跟ngRepeat誰比較快,則有待實驗證明。

將程式放上JSBin,測試方法為每次重新載入網頁,按鈕,取得耗時數據,反覆數次取最低值。

 ng-repeathtml()appendTo()
IE1164826786
Edge73635788
Chrome26513155
Firefox31825172

Chrome及IE效能分析工具顯示,ng-repeat的瓶頸出現在ngRepeatAction / controllerBoundTransclude / publicLinkFn

IE11的效能分析報告又更精確些,publicLinkFn跑了1963次,boundTransclude 37次,佔用絕大部分時間。

結論:針對動態產生大量DOM元素的應用情境,若後續不需繫結連動,組裝HTML再一次產生DOM仍是王道,其效能表現讓ngRepeat及逐筆新増DOM望塵莫及,甚至可快上數十倍。在Chrome及Firefox使用ng-repeat比逐筆加DOM更慢,在IE及Edge,ng-repeat則比逐筆加DOM快。

最後補充Hina大人的寶貴經驗:面對20MB JSON、8000筆資料使用Agular產生DOM元素,下場只能用一個慘字形容,嘗試改用React也難有起色,依據一些NG、React高手的看法-或許該考慮自己用Canvas畫!

customErrors與httpErrors

$
0
0
被抽考IIS網站的自訂HTTP 404錯誤網頁設定,學到新東西也釐清一些觀念,筆記備忘。

以Windows 2008 R2 IIS 7.5為例,網站管理介面有兩處可以自訂錯誤頁面,上方的ASP.NET區的.NET Error Pages與下方IIS區的Error Pages:

兩個設定介面有點不同,試著各自加上HTTP 404設定,但導向不同網頁,.NET Error Pages設定指向/NotFound/SystemWeb404.html:

Error Pages指向/NotFound/SystemWebServer404.html

設定結果會反應在web.config,.NET Error Pages設定被寫入system.web/customErrors,Error Pages則是寫到system.webServer/httpErrors

<?xmlversion="1.0"encoding="UTF-8"?>
<configuration>
<system.web>
<customErrorsmode="On">
<errorstatusCode="404"redirect="/NotFound/SystemWeb404.html"/>
</customErrors>
</system.web>
<system.webServer>
<urlCompressiondoDynamicCompression="true"/>
<httpErrors>
<removestatusCode="404"subStatusCode="-1"/>
<errorstatusCode="404"prefixLanguageFilePath=""
path="/NotFound/SystemWebServer404.html"responseMode="ExecuteURL"/>
</httpErrors>
</system.webServer>
</configuration>

這兩個設定有什麼不同呢?簡單來說,存取靜態檔案(如.js、.html、.css、.jpg…)發生錯誤會依照httpErrors設定辦事;由.NET處理程序接手的URL(例如:.aspx、.ashx、.svc、MVC註冊路由),出錯時則看customErrors裡的設定。

以下是簡單示範,輸入不存在的blah.gif看到的是SystemWebServer404.html、輸入不存在的blah.aspx則是SystemWeb.404.html,故得證。

補充一點:httpErrors有個errorMode屬性,預設為DetailedLocalOnly,相當於customErrors mode="RemoteOnly",故在本機測試將看不到自訂錯誤頁,要改成Cutom才看得到。這是IIS 7起加入的行為,還停在IIS 6的腦袋沒意識到有差異,花了點時間才搞定,特別加記一筆。

The type 'Global' is defined in an assembly that is not referenced errors in VS2015

$
0
0

[Abastract]

When you open web form aspx source in VS2015, you may get two compilation errors for each aspx:

  • The type 'Global' is defined in an assembly that is not referenced. You must add a reference to assembly 'App_Code.****, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null'.
  • Cannot convert type 'System.Web.HttpApplication' to 'ASP.global_asax'

I found that the code-behind Global.asx.cs is the cause and moving Global.asax.cs logic to Global.asax as inline code can solve the problem. After reporting to Microsoft Connect and getting feedback from support team, I found it is more like a bug of Productivity Power Tools 2015. Reported again.

【問題說明】

Visuals Studio 2015開啟舊Web Form專案,發現奇怪現象,原本專案可以正確編譯,但開啟幾個Web Form ASPX編輯後,網頁功能正常,但每個開啟的ASPX會出現兩個Global.asax相關編譯錯誤:

  • The type 'Global' is defined in an assembly that is not referenced. You must add a reference to assembly 'App_Code.****, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null'.
  • Cannot convert type 'System.Web.HttpApplication' to 'ASP.global_asax'

歸納並爬文後發現,這有點像VS2015的Bug-只要在VS2015開啟編輯WebForm/ASPX,就可能發生以上問題。再做了一些對照實驗,理出一些結論,會不會發生問題跟Global.asax的寫法有關。我弄了一個小專案重現問題:

VS2015開啟Web Site,原本可以正常編譯。

 

開啟Default.aspx,VS2015顯示Default.aspx有CS0012跟CS0030編譯錯誤,但專案編譯跟執行都正常。

問題關鍵在於Global.asax的寫法,舊專案的Global.asax使用Code Behind,程式邏輯寫在App_Code\Global.asax.cs。而新Web Site專案範本預設是直接寫在Global.asax內。將Global.asax.cs的內容搬到Global.asax裡,問題消失!

調整Global.asax改成Inline寫法可以避免這個問題,但如果Global.asax需要使用動用繼承、實做介面等進階技巧時,Code Behind是唯一選擇(延伸閱讀),故這不算完整解決方案。

將問題回報到Microsoft Connect,很快得到微軟Support回覆,建議我改用安全模式測試,對照後才發現問題出在Productivity Power Tools 2015,停用問題便會消失。身為一名盡責的使用者,立刻再將狀況回報到Productivity Power Tools官網,剩下來就靜待修正囉~

RunnerWindow 1.6-運動強度色彩顯示及參數設定

$
0
0

RunnerWindow更新到1.6版囉!在此介紹本次改版增加的功能。

大家如果有試過預覽版(先前叫彩色版,現已改名為預覽版,未來實驗性的新功能在這個版本先上,想當白老鼠嚐鮮的朋友可試用,待測試穩定後,新功能再加入主版本),應該知道CAD(步頻)、HR(心率)跟PACE(步速)已變成彩色,一來加大跟周圍欄位區隔提高可讀性,二則透過顏色提供運動強度資訊(CAD<170 紅 CAD>180 藍,HR依HRR%儲備心率百分比呈現灰藍綠橘紅,PACE則是慢於總平均值5%紅色,快過5%藍色,詳細說明)。

不過,資料欄位的程式可用空間很少,為了加入變色邏輯,只能忍痛拿掉電池圖示及GPS訊號條才不致Out of Memory(沒錯,就這麼吃緊)。原以為將這兩項雞肋資訊簡化不會有人在意,沒想到陸續有網友在評語欄呼喊「把電池跟GPS圖示還給我!」 orz (記憶體好少,臣妾做不到呀~)

但大家應該也注意到,現在這個版本的電池跟GPS又恢復成完整版。不是說辦不到,又是花生什麼素?並不是我突然開竅變聰明,想出更省記憶體的絕妙程式寫法,而是Garmin在11/6釋出Connect SDK 1.2.1,很神奇地還給開發者近3K的空間,寫過Datafield的人才知道3K有多珍貴,就像錢快燒完的新創公司莫名拿到3億天使基金,哇哈哈,又有好多錢可以燒好多空間可以加程式碼,於是電池及GPS圖示就回來了。

而SDK 1.2還解決了另一個久懸難解的問題-資料欄位沒有設定介面,無從調整客製化。(逼得我想出「用12/24小時調背景顏色」的怪招) 從SDK 1.2起,使用者可用Garmin Express及Garmin Connect Mobile修改資料欄位的設定(但手錶軟體必須更新到新版本),終於能提供簡易的參數讓使用者輸入或調整。

目前Fenix 3軟體版本已更新到4.9(11/13更新,最近Fenix 3的版本幾個月內一口氣從3.9->4.7->4.9,狂追到只落後英文版一版,在趕進度來著,但趕得好哇),雖然手錶的Connect IQ SDK還沒到1.2,但實測已可使用Garmin Express執行參數調整,Gamin Connect Mobile也會出現選單,但點下去會出錯,得再等App或手錶軟體更新,故現階段只能用Garmin Express調整設定。操作方式為連上裝置,在應用程式清單找到Runner Window(記得更新到1.6版),標題旁邊應能看到三個點的圖示:

點下去即可開啟Runner Window的參數選項畫面:

目前可調整的選項包含:

  1. Background Color
    在Fenix 3軟體5.00版上Runner Window會自動依資料欄位背景調整黑底或白底,5.00以前的版本則使用12/24小時設定決定白底或黑底。陸續有網友反應底色切換不正確,索性加入此選項開放強制指定背景色。
  2. Show ALT Field
    若不跑山路,高度與坡度欄位就沒什麼參考價值,此選項可取消高度顯示,讓中央欄位永遠顯示AVG PACE。
  3. Show HRR%
    部分跑友習慣參考心率百分比抓運動強度,這個選項可切換HR欄位改以儲備心率百分比數字取代心率。
  4. Max HR
    原先最大心率只能用年齡估算,現在起開放自行輸入,可望大幅提高HRR%數字的代表性,另外,個人資料的安靜心率值也要記得設定。(註:輸入220時表示採用年齡推算)
  5. Low CAD/High CAD
    CAD(步頻)低於Low CAD顯示紅色,高於High CAD時顯示藍色,落於二者之間則為綠色,步頻目標區間現在也可自訂囉~

可彈性支援Transaction的EF資料異動方法

$
0
0

考慮以下DataHelper靜態型別,有AddWorkItem及AddDataGroup兩個方法透過Entity Framework新増資料:

publicclass DataHelper
    {
//使用統一的靜態函式建立DbContext物件,避免自行建構
publicstatic BBDPEntities CreateDbContext()
        {
//正式應用時,設定檔之連線字串應加密
//在此進行讀取設定並解密以建構DbContext,細節待日後介紹
returnnew BBDPEntities();
        }
publicstaticvoid AddWorkItem(Guid wuid, string subject)
        {
using (var ctx = CreateDbContext())
            {
                var wi = new WorkItem()
                {
                    Wuid = wuid,
                    Subject = subject
                };
                ctx.WorkItem.Add(wi);
                ctx.SaveChanges();
            }
        }
publicstaticvoid AddDataGroup(string costCenter, string jobType)
        {
using (var ctx = CreateDbContext())
            {
                var dg = new DataGroup()
                {
                    CostCenter = costCenter,
                    JobType = jobType
                };
                ctx.DataGroup.Add(dg);
                ctx.SaveChanges();
            }
        }
    }

在兩個方法裡各自產生DbContext物件(實務上多使用統一的靜態方法,不要自行用new建構,以集中連線字串解密以及其他初始化邏輯),建立Entities物件,使用Add()加入,呼叫SaveChanges()存入。如果我們想將AddWorkItem()及AddDataGroup()兩個新増動作包成交易(Transaction)要怎麼做?

跟LINQ to SQL一樣,要將EF的多個資料庫操作包成交易也有三種選擇:

    1. Implicit Transaction:
      使用同一個DbContext進行新增/更新/刪除等動作,SaveChanges()時EF會自動將資料庫異動包成交易,這是最單純最有效率的做法。
    2. Explicit Local Transaction:
      使用DbContext.Database.BeginTransaction()取得DbContextTransaction物件,自行呼叫Commit()、Rollback()控制交易成立或回滾。這個做法還有一個好處-透過SqlQuery()及ExecuteSqlCommand()直接執行的指令也能納入交易範圍。
    3. Explicit Distributable Transaction:
      使用TransactionScope將資料庫動作包起來,範圍內的資料庫操作都會參與交易。Transaction的彈性最大,甚至能支援異種資料庫的分散式交易,但要注意包在TransactionScope中的資料庫動作會啟用LTM或OleTx,成本較為昂貴(LTM較輕巧,但有些限制,如果涉及一台以上DB或對遠端SQL開了兩條不同連線,就一定要用貴森森的OleTx),必要時要將不必參與Transaction的部分隔離開以增進效能。(延伸閱讀: .NET分散式交易程式開發FAQ )

TransactionScope做法最無腦最好寫,將資料異動方法當成黑盒子用TransactionScope包起來就搞定。

publicstaticvoid Test()
        {
using (var tx = new TransactionScope())
            {
                DataHelper.AddWorkItem(Guid.NewGuid(), "SUBJECT");
                DataHelper.AddDataGroup("12345", "A");
            }
        }

TransactoinScope固然方便,但不管走LTM或OleTx,效能都不如在同一條資料庫連線進行的Implicit Transaction。實務上我偏好的做法是由Test()建立DbContext,讓AddWorkItem與AddDataGroup共用一個DbContext,AddWorkItem及AddDataGroup新増資料後不執行SaveChanges(),待全部動作完成再由Test()呼叫SaveChanges()。

如此,AddWorkItem跟AddDataGroup需增加一個參數接收外部傳入的DbContext。為了方便使用,我常用一個小技巧讓AddWorkItem跟AddDataGroup變聰明,遇到外部傳入DbContext就共用,外部沒有傳入時自己生一個。如以下範例:

publicstaticvoid Test()
        {
using (var ctx = DataHelper.CreateDbContext())
            {
                DataHelper.AddWorkItem(Guid.NewGuid(), "SUBJECT", ctx);
                DataHelper.AddDataGroup("12345", "A", ctx);
            }
        }
//...略...
 
publicclass DataHelper
    {
//使用統一的靜態函式建立DbContext物件,避免自行建構
publicstatic BBDPEntities CreateDbContext()
        {
//正式應用時,設定檔之連線字串應加密
//在此進行讀取設定並解密以建構DbContext,細節日後介紹
returnnew BBDPEntities();
        }
publicstaticvoid AddWorkItem(Guid wuid, string subject, 
            BBDPEntities exCtx = null)
        {
            var ctx = exCtx ?? CreateDbContext())
            ctx.Database.BeginTransaction();
            var wi = new WorkItem()
            {
                Wuid = wuid,
                Subject = subject
            };
            ctx.WorkItem.Add(wi);
if (exCtx == null)
            {
                ctx.SaveChanges();
                ctx.Dispose(); 
            }
        }
publicstaticvoid AddDataGroup(string costCenter, string jobType, 
            BBDPEntities exCtx = null)
        {
            var ctx = exCtx ?? CreateDbContext())
            var dg = new DataGroup()
            {
                CostCenter = costCenter,
                JobType = jobType
            };
            ctx.DataGroup.Add(dg);
if (exCtx == null) 
            {
                ctx.SaveChanges();
                ctx.Dispose();
            }
        }
    }

以上是我處理EF交易常用的小技巧,分享大家參考。

Viewing all 2337 articles
Browse latest View live