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

【茶包射手日記】Word VSTO程式問題兩則

$
0
0

先前寫過Word套表服務,透過C#程式呼叫Word進行文字置換並轉存PDF,包裝成Windows Service執行並透過ASP.NET Web API接受需求。

程式在開發環境與測試環境運作良好,部署到正式環境Windows Server 2003卻遇上麻煩。

首先,套表程式的Windows Service先前在開發機Windows 2008 R2跟測試台Windows 2003的執行身份都是設成Local System,運行無誤,但正式台Windows 2003上Word存檔時卻一直冒出:

System.Runtime.InteropServices.COMException (0x8001010A): The message filter indicated that the application is busy. (Exception from HRESULT: 0x8001010A (RPC_E_SERVERCALL_RETRYLATER))

直到我將執行身分改換成網域帳號上述錯誤才消失(但為何Local System不行? 原因成謎),接著卻冒出另一個錯誤:

Exception from HRESULT: 0xC0000005 Stack Trace : at Microsoft.Office.Interop.Word. Find.get_Replacement() ...

很幸運地找到一篇相同情境的MSDN論壇文章,追到一篇KB指出問題是Excel 95的COM GUID與Word Interface打架(詳情可以看這篇),KB提供了兩種解決方式:

  1. 程式改採Late Binding
  2. 使用regtlib.exe重新註解Word元件壓過Excel 95的設定

非常不爽為此改寫程式,一來工程浩大,二來為了Excel **95**改程式感覺太窩囊,故決定動用regtlib撥亂反正!! 有趣的是,找不到regtlib.exe,上網查才發現它是Visual Studio 6.0時代的產物(2007年出廠... orz),Windows 7之後改由.NET 2.0 Framework的regtlibv12.exe接任,找到Word元件(MSWORD.OLB)所在路徑,執行以下指令註冊:

C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727>regtlibv12.exe "C:\Program Files\Microsoft Office\Office12\MSWORD.OLB"
Registration of C:\Program Files\Microsoft Office\Office12\MSWORD.OLB successful
.

問題排除,搞定收工~

PS: 覆寫Excel 5.0介面會有後遺症嗎? 依KB的說法:
Several globally unique identifiers (GUIDs) that are used by Word for its interface identifiers were used by Excel 5.0 for an older object model that is now obsolete.
衝突的GUID用於Excel 5.0 Disinterface,已被宣告為過時避免使用,理論上風險不高,如真有問題,那就是命囉~ XD


Autofac筆記2-淺談Singleton

$
0
0

開始前先聲明(坦白從寬~),我對Design Pattern的研究十分淺薄,寫起IoC、Singleton的題材有種越級打怪的心虛感,我知道本部落格有不少讀者深諳此道,如筆記有疑或有誤之處,懇請十方大德不吝指正。

Singleton是挺常見的設計模式,旨在確保該型別於Process中只會產生單一Instance(執行個體)。在.NET實現Singleton慣用的做法是將建構式設成private,另外宣告一個static屬性命名為Instance,在第一次get時建立物件,之後每次要取用該類別時不再重新建構,而是直接取用Instance屬性,如下例:

using System;
using System.Threading;
 
publicclass TheOne
{
private Guid UniqueKey = Guid.NewGuid();
 
privatestatic TheOne instance = null;
publicstatic TheOne Instance
    {
        get
        {
if (instance == null)
            {
                instance = new TheOne();
            }
return instance;
        }
    }
 
/// <summary>
/// 建構式
/// </summary>
private TheOne()
    {
        Thread.Sleep(2000);
        Console.WriteLine("Constructor Executed");
    }
 
publicvoid ShowUniqueKey()
    {
        Console.WriteLine("Unique Key={0}", UniqueKey);
    }
 
}

應用時透過TheOne.Instance取得唯一的執行個體:

staticvoid Test1()
        {
for (int i = 0; i < 3; i++)
            {
                TheOne theOne = TheOne.Instance;
                theOne.ShowUniqueKey();
            }
        }

執行後可驗證TheOne只被建構了一次,三次使用的都是同一Instance。

Constructor Executed
Unique Key=571842aa-5037-43db-9341-6e82f0ebe6d0
Unique Key=571842aa-5037-43db-9341-6e82f0ebe6d0
Unique Key=571842aa-5037-43db-9341-6e82f0ebe6d0

不過,老鳥們都知道上述寫法未考慮Thread-Safe,在多執行緒下肯定破功。以下我們就來踢爆這個"黑心Singleton"(誤):

staticvoid Test2()
        {
for (int i = 0; i < 3; i++)
            {
                ThreadPool.QueueUserWorkItem((o) =>
                {
                    TheOne theOne = TheOne.Instance;
                    theOne.ShowUniqueKey();
                });
            }
        }

當改用ThreadPool以三個執行緒同時存取TheOne。很好! 建構式跑了三次,生出三個TheOne…

Constructor Executed
Constructor Executed
Unique Key=a3f52f2f-6097-4a90-8a26-5cb91b12c484
Constructor Executed
Unique Key=a06da108-c38b-43c9-aa01-04f5e261c121
Unique Key=37063ff3-990c-44a0-ac0a-798ea0bcf1ae

微軟有一篇很棒的文章詳細討論了.NET Singleton實作,如果要做到Thread-Safe,TheOne最好加上雙重檢查鎖定(Double-Check Locking)機制並改寫如下:

privatestatic TheOne instance = null;
privatestaticobject syncRoot = new Object();
publicstatic TheOne Instance
    {
        get
        {
if (instance == null)
            {
lock (syncRoot)
                {
if (instance == null)
                        instance = new TheOne();
                }
            }
return instance;
        }
    }

如此就能確保多執行緒下也只產生唯一的Instance。

Autofac提供了另一種實現Singleton的選擇,做法是在ContainerBuilder註冊型別時呼叫SngleInstance(),任何類別,不需特殊設計都能實現Singletone。我們另外宣告一個TheNewOne類別,功能與TheOne類似,但直接提供public的建構式,省去static Instace屬性,Singleton需求交給Autofac處理:

using System;
using System.Threading;
 
publicclass TheNewOne
{
private Guid UniqueKey = Guid.NewGuid();
 
public TheNewOne()
    {
        Thread.Sleep(2000);
        Console.WriteLine("Constructor Executed");
    }
 
publicvoid ShowUniqueKey()
    {
        Console.WriteLine("Unique Key={0}", UniqueKey);
    }
}

測試時先宣告ContainerBuilder,註冊TheNewOne型別並宣告SingleInstane(),後續使用時只需透過ResolveType<TheNewOne>()取得TheNewOne,就是Singleton了。

staticvoid Test3()
        {
            ContainerBuilder builder = new ContainerBuilder();
//註冊時加註SingleInstance(),Autofac便會以Singleton方式提供物件
            builder.RegisterType<TheNewOne>().SingleInstance();
            IContainer container = builder.Build();
 
for (int i = 0; i < 3; i++)
            {
                ThreadPool.QueueUserWorkItem((o) =>
                {
                    TheNewOne theOne = container.Resolve<TheNewOne>();
                    theOne.ShowUniqueKey();
                });
            }
        }

測試結果,類別不用特別加入Singleton邏輯就實現了多執行緒下的Singleton。

Constructor Executed
Unique Key=972def1e-8788-45aa-bd15-2aef15870514
Unique Key=972def1e-8788-45aa-bd15-2aef15870514
Unique Key=972def1e-8788-45aa-bd15-2aef15870514

【結論】透過IoC(Autofac)實現Singleton,相形之下比自己DIY簡便許多,但有個缺點: 由於類別建構式公開,無法禁止開發人員繞過Autofac自行另建Instance。但在實務上,若架構啟用Autofac或任何IoC,多會另外宣告Interface,開發人員依賴的是Interface而非Class本身,對底層究竟使用何Class理應處於"無知"狀態,越過Interface直接存取類別可視為"違法亂紀"的罪行,視為人員管理問題而非架構缺陷。依此前題,建構式公開就不算嚴重缺失,可安心服用。

Autofac筆記3-關於Lifetime Scope

$
0
0

在使用IoC設計模式時,有一個有點難懂卻不能迴避的問題 -- 如何妥善管理物件生命週期,避免記憶體洩漏(Memory Leak)?

要了解此議題,先大推一篇關於Autofac物件生命週期的經典文章,其中有頗詳細的闡述,這篇筆記只簡短摘要我實際應用的心得,關於完整說明推薦大家參考原文。

問題從何而來?

基本上,純.NET世界的資源(Managed Resource,例如: 儲存.NET物件所用的記憶體)有GC(Garbage Collection)機制把關,它能精準掌握物件是否仍在有效範圍,當物件已不可能再被使用,便會在必要時(例如: 剩餘記憶體不足)回收這些已消滅物件所耗用的記憶體。但程式要運行,多少會涉及一些非.NET所掌管的資源(Unmanged Resource,例如: 網路連線、磁碟機上的檔案... 等等),當.NET物件使用到這些Unmanaged Resource,建議的做法是實作IDispose介面,在Dispose()方式中確實釋放所有動用到的資源,以免.NET物件消失後佔著茅坑不拉屎,阻礙其他Process使用。

一般來說,若物件有實作IDispose,我們可寫成using (var boo = new SomeDisposableClass()) { … },確保範圍結束後一定呼叫Dispose()釋放資源。但using只適用單一Method內部,若物件建立好要交給其他程式應用,便不能任意Dispose(),以免別人要用時已成廢物;但是,若不確實在物件使用完畢後呼叫Dispose(),又會造成資源不當佔用。這是個難題,且沒有什麼神奇解法,設計實務多半靠"建立物件者必須負責善後"原則作為解決方案。

應用Autofac時,物件都是透過Container.Resolve<SomeType>()方式取得,換言之,物件是由Autofac建立,那麼Dispose()也會由Autofac呼叫嗎? 做個實驗吧! 寫個實作IDisposable的類別:

using System;
 
class ResourceMonster : IDisposable
{
publicstring Name = "Anonymous";
publicvoid Test()
    {
        Console.WriteLine("{0}: Hi there.", Name);
    }
publicvoid Dispose()
    {
        Console.WriteLine("{0}: Disposed.", Name);
    }
}

測試程式如下:

using Autofac;
using System;
 
namespace Lifetime
{
class Program
    {
staticvoid Main(string[] args)
        {
            ContainerBuilder builder = new ContainerBuilder();
            builder.RegisterType<ResourceMonster>();
            IContainer container = builder.Build();
 
            var monster = container.Resolve<ResourceMonster>();
            monster.Test();
            Console.WriteLine("Before IContainer Dispose");
            container.Dispose();
            Console.WriteLine("After IContainer Dispose");
 
            Console.ReadLine();
        }
    }
}

Anonymous: Hi there.
Before IContainer Dispose
Anonymous: Disposed.
After IContainer Dispose

執行結果如上,IContainer有實作IDisposable,當我們呼叫container.Dispose(),container會盡責地善後 -- 呼叫它所建立monster物件的Dispose()方法。

測試程式只用於示範,因此在Main()單一方法內註冊型別、建立IContainer,用完就馬上把IContainer.Dispose(),但這不符合應用實。IContainer建立要註冊所有列管型別,需要耗費資源、時間,不可能每次使用前才建立,用完就丟,下次要用再重建。因此IContainer整個Process多半只會建一份,通常安排在程式(或網站)啟動事件中建立好,並以static屬性方式供整個Process共用。在某個Method呼叫Resolve<T>建立的物件,不可能依賴IContainer.Dispose()善後(因為其他人還要繼續用它建立物件),為此,Autofac提供了ILifetimeScope提供較短的生命週期應用。

運作原理是先透過IContainer.BeginLifetimeScope()建立ILifetimeScope取代IContainer,它具有跟IConatiner幾乎一致的介面,如此我們便可改用ILifetimeScope.Resolve<T>建立物件,而ILifetimeScope算是為特定目所建的獨立容器,在使用完畢後可任意Dispose()而不會影響IContainer。另外,ILifetimeScope還可以透過.BeginLifetimeScope()再建立子ILifetimeScope形成巢狀結構,上層容器Dispose()時會一併呼叫下層容器進行Dispose(),可依不同需求彈性應用。

以下是簡單的ILifetimeScope範例,先透過container.BeginLifetimeScope()建立ILifetimeScope,再改用它來Resolve<ResourceMonster>()取得物件,透過using自動終結scope(using結束時背後會呼叫scope.Dispose()),由執行結果可觀察到兩個ResourceMonster物件也自動被Dispose():

using Autofac;
using System;
 
namespace Lifetime
{
class Program
    {
static IContainer container = null;
staticvoid AutofacConfig()
        {
            ContainerBuilder builder = new ContainerBuilder();
            builder.RegisterType<ResourceMonster>();
            container = builder.Build();
        }
staticvoid Test()
        {
using (var scope = container.BeginLifetimeScope())
            {
                var monster1 = scope.Resolve<ResourceMonster>();
                monster1.Name = "No1";
                monster1.Test();
                var monster2 = scope.Resolve<ResourceMonster>();
                monster2.Name = "No2";
                monster2.Test();
            }
        }
 
staticvoid Main(string[] args)
        {
            AutofacConfig();
            Test();
            Console.ReadLine();
        }
    }
}

執行結果:

No1: Hi there.
No2: Hi there.
No2: Disposed.
No1: Disposed.

另外,先前介紹過Autofac Singleton,ILifetimeScope也可當作Instance共用的單位,例如: 指定只建一個Instance供全ILifetimeScope共用,做法是在RegisterType<T>時加上InstancePerLifetimeScope(),例如:

static IContainer container = null;
staticvoid AutofacConfig()
        {
            ContainerBuilder builder = new ContainerBuilder();
            builder.RegisterType<ResourceMonster>();
            builder.RegisterType<TheNewOne>().InstancePerLifetimeScope();
            container = builder.Build();
        }
 
staticvoid Test2()
        {
            var scope1 = container.BeginLifetimeScope();
            var scope2 = container.BeginLifetimeScope();
            var one1 = scope1.Resolve<TheNewOne>();
            Console.WriteLine("1->");
            one1.ShowUniqueKey();
            var one2A = scope2.Resolve<TheNewOne>();
            var one2B = scope2.Resolve<TheNewOne>();
            Console.WriteLine("2A->");
            one2A.ShowUniqueKey();
            Console.WriteLine("2B->");
            one2A.ShowUniqueKey();
        }
 
staticvoid Main(string[] args)
        {
            AutofacConfig();
            Test2();
            Console.ReadLine();
        }

執行結果如下,如預期兩個ILifetimeScope所取得的TheNewOne Unique Key不同,而第二個ILifetimeScope取得的兩個TheNewOne Unique Key相同,證明為同一個Instance。

Constructor Executed
1->
Unique Key=de3fdf54-a67e-4896-98b4-d7ab1314dbe8
Constructor Executed
2A->
Unique Key=7dc4ac10-aafa-4630-8a64-b517cde8fc82
2B->
Unique Key=7dc4ac10-aafa-4630-8a64-b517cde8fc82

最後提一下Owned Instance,指Autofac在Resolve<T>時先產生新的ILifetimeScope,再用它建立物件並傳回ILifetimeScope。寫法為var ownedService = conatiner.Resolve<Owned<SomeType>>(),而ownedService.Value即為所建立的SomeType Instance,而ownedService.Dispose()則可用來結束該ILifetimeScope。

【結論】

在大多數應用情境下,建議改用ILifetimeScope取代IContainer物件Resolve<T>(),並在作業結束後執行ILifetimeScope.Dispose()以落實資源回收,減少記憶體洩漏的風險。

【延伸閱讀】

Autofac筆記4-建構參數與建構式選擇

$
0
0

在先前的範例(12),透過Resolve<T>()建立的物件都只有單一建構式且不需建構參數,如果有多個建構式或建構時需要建構參數時,Autofac會如何處理?

當類別有多個建構式時,Autofac會依"能符合最多個容器提供參數的建構式優先"做為選擇依據。其英文原文為"Autofac automatically chooses the constructor with the most parameters that are able to be obtained from the container",坦白說並不容易理解,經過一些實驗我也才理出頭緒。

首先,容器提供建構式參數的來源有幾種:

  1. 註冊時直接寫死,例如: builder.Register(o => new Boo(1, 2, 3));,這種情境下,建構參數固定,要使用哪一個建構式也固定。
  2. Resolve<T>時一併傳入參數物件,Autofac提供的參數物件有NamedParameter(指定參數名稱)、TypedParameter(指定參數型別)、PositionalParameter(指定參數順序)等幾種選擇。
  3. 透過builder.RegisterType<Boo>().WithParameters(new Parameters[] { new NamedParameter("i", 1) }),提供部分或全部建構參數。
  4. 若參數物件屬自定型別,而該型別經RegiterType<T>註冊,則建構時Autofac會自動以Resolve<T>()取得物件當成參數之一。

換言之,除了第一種建構參數及建構式固定不需選擇,Autofac在建構物件時會由後面三種來源取得參數構成參數清單,以該清單比對所有建構式,過濾後保留建構參數全部在清單者,再由其中選擇一使用。當有多個建構式吻合,則以建構參數最多者為準先(這就是所謂"符合最多個容器所能提供參數"),若比對結果最多符合參數的建構式有兩個以上,則Autofac無法做決定就會拋出錯誤。

還是佷抽象對吧? 用個複雜實例來釐清說明。假設有一個多建構式的類別如下:

using System;
 
publicclass ArgW { }
publicclass ArgX { }
publicclass ArgY { }
publicclass ArgZ { }
 
publicclass MultiConstructor
{
public MultiConstructor(ArgW w)
    {
        Console.WriteLine("Constructor ArgW");
    }
public MultiConstructor(int i, int j, ArgY y)
    {
        Console.WriteLine("Constructor int, int, ArgY");
    }
public MultiConstructor(ArgY y, ArgZ z)
    {
        Console.WriteLine("Constructor ArgY, ArgZ");
    }
public MultiConstructor(ArgX x) {
        Console.WriteLine("Constructor ArgX");
    }
public MultiConstructor(ArgW w, ArgX x)
    {
        Console.WriteLine("Constructor ArgW, ArgX");
    }
public MultiConstructor(ArgX x, ArgZ z)
    {
        Console.WriteLine("Constructor ArgX, ArgZ");
    }
}

這個類別共有六個建構式,為了TypedParameter型別比對,宣告ArgW, ArgX, ArgY, ArgZ四個自訂型別作為建構式參數型別。ContainerBuilder除了註冊MultiConstructor,也註冊ArgW,使其可透過Autofac自動取得。測試共有六組,傳入參數分別為:

  1. 依順序傳入1, 2, ArgY
  2. 提供ArgY, ArgZ兩種型別的參數值
  3. 不提供任何參數
  4. 指定x = ArgX
  5. 指定x = ArgX, z = ArgZ
  6. 指定i = 1, y = ArgY
staticvoid Main(string[] args)
        {
            ContainerBuilder builder = new ContainerBuilder();
            builder.RegisterType<MultiConstructor>();
            builder.RegisterType<ArgW>();
            IContainer container = builder.Build();
 
            var obj1 = container.Resolve<MultiConstructor>(
new PositionalParameter(0, 1),
new PositionalParameter(1, 2),
new PositionalParameter(2, new ArgY())
                );
            var obj2 = container.Resolve<MultiConstructor>(
new TypedParameter(typeof(ArgY), new ArgY()),
new TypedParameter(typeof(ArgZ), new ArgZ())
                );
            var obj3 = container.Resolve<MultiConstructor>();
            var obj4 = container.Resolve<MultiConstructor>(
new NamedParameter("x", new ArgX())
                );
try
            {
                var obj5 = container.Resolve<MultiConstructor>(
new NamedParameter("x", new ArgX()),
new NamedParameter("z", new ArgX()));
 
            }
catch (Exception ex)
            {
                Console.WriteLine("Error:" + ex.Message);
            }
            var obj6 = container.Resolve<MultiConstructor>(
new NamedParameter("i", 1),
new NamedParameter("y", new ArgX()));
            Console.ReadLine();
        }
    }

猜看看結果為何? 有能力直接回答的同學請到台前領糖果後由教室前門離開去操場玩,不需要繼續往下看浪費時間。其他同學可參考我的題解:

由於RegisterType<ArgW>,不管Resolve<MultiConstructor>傳入參數為何,ArgW都可列入參數清單,各測試的比對結果如下:

  1. 依順序傳入1, 2, ArgY
    符合建構式: (ArgW w)、(int i, int j, ArgY y)
    前者只有1個參數,後者有3個參數,3 > 1,後者勝出 => Constructor int, int, ArgY
  2. 提供ArgY, ArgZ兩種型別的參數值
    符合的建構式: (ArgW w)、(ArgY y, ArgZ z)
    2 > 1,後者勝出 => Constructor ArgY, ArgZ
  3. 不提供任何參數
    唯一符合者: (ArgW w)
    同額參選,直接當選 => Constructor ArgW
  4. 指定x = ArgX
    符合建構式: (ArgW w)、(ArgX x)、(ArgW w, ArgX x)
    兩個參數勝出 => Constructor ArgW, ArgX
  5. 指定x = ArgX, z = ArgZ
    符合者: (ArgW w)、(ArgX x)、(ArgW w, ArgX x)、(ArgX x, ArgZ z)
    2 > 1,但參數為2的有兩個,無法決定,丟出例外
    Error:Cannot choose between multiple constructors with equal length 2 on type 'M
    ultiConstructor'. Select the constructor explicitly, with the UsingConstructor()
    configuration method, when the component is registered.
  6. 指定i = 1, y = ArgY
    少了j,(int i, int j , ArgY y)不算符合,故只剩(ArgW w)同額參選 => Constructor ArgW

由以上測試,再歸納一次Autofac挑選建構式的原理: 建構式所有參數都必須在參數清單中才能入選(參數可能來自Resolve<T>時傳入、RegisterType<T>().WithPrameters()指定,或是參數型別已RegisterType<T>自動取得),若有多個建構式入選,以參數最多者優先,若參數最多的建構式有多個就丟出例外。

如果不喜歡"參數最多者優先"的邏輯,Autofac也允許自訂邏輯,方法是寫個類別實作IConstructorSelector決定由多個符合建構式挑選的邏輯,並在註冊時透過UsingConstructor()指定之。如果連"判定建構式是否符合目前的參數清單"也想自訂,則可透用FindConstructorsWith實現。不過基於KISS(Keep It Simple, Stupid)法則,實務上應該很少人會把架構搞到這麼複雜吧~

Autofac筆記5-屬性注入

$
0
0

前面談過傳入建構參數,但並非所有物件參數都可由建構式傳入,有些要透過屬性指定(例如: new MyObject() { SomeProperty = SomeValue };),而這也是IoC/DI的工作職掌之一,專業術語叫Property Injection(屬性注入)。

解說前先介紹幾個測試用類別: Worker類別有個屬性Logger,接受實作ILogger介面的記錄元件;我們簡單寫個Logger類別實作ILogger,將訊息輸出到Console敷衍兩下湊數。

using System;
publicclass Worker
{
public ILogger Logger { get; set; }
publicvoid DoSomething(string command)
    {
        Console.WriteLine("JOB:" + command);
        Logger.Log(command);
    }
}
publicinterface ILogger
{
void Log(string msg);
}
publicclass Logger : ILogger
{
publicvoid Log(string msg)
    {
        Console.WriteLine("LOG:" + msg);
    }
}

Autofac指定屬性的方法有三種。第一種的做法是透過Register()自訂物件建立細節,Register() Lambda所傳入的c即為Autofac容器(IContainer或ILifetimeScope),可透過c.ResolveType<ILogger>取得已註冊的ILogger實作。

privatestaticvoid test1()
{
    ContainerBuilder builder = new ContainerBuilder();
    builder.RegisterType<Logger>().As<ILogger>();
//方法自訂建構程序,傳回物件。建立物件時一併指定Property
    builder.Register(c => 
new Worker() { 
            Logger = c.Resolve<ILogger>() 
        });
    IContainer container = builder.Build();
 
    var worker = container.Resolve<Worker>();
    worker.DoSomething("Wash the dog");
}

第二種做法是利用Autofac物件建立事件OnActivated,於物件建立完成後指定。OnActivated事件傳入參數的Instance屬性為剛建好的物件,而Context屬性則為Autofac容器。(PS: 除了OnActivated,還有OnActivating事件可以置換Instance、注入屬性或進行其他初始化;OnRelease事件則可取代物件原有的Dispose()邏輯,提供良好的自訂彈性,細節可參考文件)

privatestaticvoid test2()
{
    ContainerBuilder builder = new ContainerBuilder();
    builder.RegisterType<Logger>().As<ILogger>();
//利用OnActivated事件,物件建立後指定Property
//OnActivated事件會傳入IActivatedEventArgs,
//其中的Instance為剛建好的物件、Context為IContainer或ILifetimeScope容器
    builder.RegisterType<Worker>().OnActivated(
        e => e.Instance.Logger = e.Context.Resolve<ILogger>());
    IContainer container = builder.Build();
 
    var worker = container.Resolve<Worker>();
    worker.DoSomething("Wash the dog");
}

第三種方法我覺得最酷!

RegisterType()時直接加上PropertyAutowired(),則Autofac建立物件時將一併掃瞄物件所有屬性,只要該屬性型別已被註冊,就自動產生(或取得)Instance傳入,即便事後增加Property也無需更動註冊程序,算是貫徹了IoC/DI的精神,深得我心。

privatestaticvoid test3()
{
    ContainerBuilder builder = new ContainerBuilder();
    builder.RegisterType<Logger>().As<ILogger>();
//透過PropertyAutowired()交由Autofac自動解析
    builder.RegisterType<Worker>().PropertiesAutowired();
    IContainer container = builder.Build();
 
    var worker = container.Resolve<Worker>();
    worker.DoSomething("Wash the dog");
}

2013台灣米倉田中馬拉松~

$
0
0

去年首辦佳評如潮的田中馬,一如預期報名上演秒殺,幸運搶到門票,抱著朝聖心情參加我的第12馬。

投宿南投,5點出發前往田中兒童公園。印象中來彰化的次數屈指可數,唯一的模糊記憶是小時候到八卦山看大佛吧? 田中鎮,自然也是首次造訪,說起來跑馬拉松強迫不愛出門的阿宅四處遊歷增廣見聞,豐富我的貪乏人生呀!

一到會場,六七頂辦桌圓頂天篷一字排開,選手休息區的氣勢就讓人讚嘆,這一屆全馬、超半馬加11K健跑人數近萬人,清晨天色仍暗,會場人聲鼎沸,這對田中小鎮也是很難得的景況吧! (聽說前一天踩街遊行十分精彩,因行程頗緊沒參加有些可惜,代表明年得再來一次嗎? 呵。)

有趣的是,海軍陸戰隊是本次賽事的一大亮點,從開場的魔鬼筋肉蛙人表演、狙擊裝備展示、起跑點的陸海空軍娃娃(PS: 海軍娃娃因"漏氣"黯然退場)到海陸鼓號樂隊演奏大力水手助陣,回程時在原地列隊加油,陽剛又青春,Man到我都想再當一次兵了~

起跑區依實力分段,依實力為自己訂下期許,2:30-3:00?? 這是神的速度吧? 我還是認份地排在我該站的地方,四千人的陣仗真浩大,4:30-5:00區離起點好遠,起跑拱門小到快看不見,花了近四分才通過起跑線。不過,當步兵走一公里就多四分鐘,對我這種走路走得理直氣壯的後段班,沒人在計較出發時間的啦!

田中真是台灣米倉,賽道穿過好多稻田,長長人龍在稻田間穿梭,充滿田野之趣。可惜天陰無風,景色帶點朦朧,若是晴天配上涼風,吹起一波波稻浪,肯定更美。

在米倉奔駞,應該是在稻海間倘佯才對。天堂路500公尺? 好漢坡1050公尺? 要在一公里內爬升到海沷210公尺? 不不不,這其中一定有什麼誤會。於是,賽前壓根沒看高度圖,以為一路平坦到底的大叔,莫名被逼著爬上比貓空還陡的山路... 幸好跟我同一梯的跑友們都很團結,全員一起緩步爬坡,無人衝刺,誰都不想傷害別人的心靈。XD

田中馬的補給頗為豐盛,除了水、運動飲料、香蕉、西瓜、小番茄等標準配備,我還喝到好喝的鳳梨汁,而幾乎每個水站都有成堆的田中名產蜜麻花,而最最值得一提的隱藏版補給品,非維力酢醬麵莫屬! 起跑前鄭鎮長介紹田中鎮時提到田中是維力跟泰山食品的故鄉,沒想到酢醬麵真的在水站登場了。

跑馬拉松吃維力炸醬麵也太銷魂了吧!! 雖然吃麵很花時間,但我知道要是錯過這味,我一定不會原諒自己,於是做了一件很奢侈的事 -- 在分秒必爭的比賽中,我站在路旁花一分多鐘吃麵(水站阿姨送上前又補了點熱湯,好燙 XD),有了如此獨特體驗,這場沒有白來,落馬也無憾了~ (喂)

最後幾公里,太陽開始露臉,氣溫也急速上升(氣象說有到30度),一路猛灌水也猛飆汗,衣褲鞋襪全濕。跑到力竭之際,看見天堂來的冰啤酒,時機完全到位,一飲而盡,頓時神清氣爽... 了兩分鐘。體能耐力是騙不了人的,最後5K總是能讓所有訓練不足、實力不夠的濫竽(對,就是我)原形畢露。看了看錶,這回很有機會保住Sub 5,還是逼自己靠蹣跚步伐維持速度。

傳說中威力破表的田中熱情,在回程最後一段由陸戰軍樂隊揭開序幕,最後2公里回到市區,有數不盡的啦啦隊、擊不完的掌,大小朋友鼔鑼打鼔夾道歡迎,還有很多青春洋溢的高中美眉... 讓人陷入無比煎熬,腳已累到舉步維艱,但在浩大加油聲中當步兵卻又人神共憤天地不容~ 硬著頭皮苦撐,又痛苦又興奮地奔完最後一哩,這滋味令人難忘。

成績4:57:15,如願保住Sub 5,代價是腳上多領兩塊獎牌,有兩根腳趾又要黑指甲了 orz
完賽的伴手禮很豐盛: 環保袋、毛巾、一大包餅乾、兩雙襪子、一條褲襪、補給口糧、礦泉水、便條紙,最神奇的是還有一小罐蜈蚣油 :D 幾乎都是當地名產,田中馬無疑是透過馬拉松行銷在地文化促進觀光的經典。

賽後,全家一起去田尾公路花園小逛,地方頗大,有四人協力車可租,腳踩之外也有電動版,公主與王子堅持腳踩才是王道! 但猜猜最後誰是主要動力來源? orz 跑完42K馬上換騎腳踏車,也算是完成(偽)鐵人兩項, 哈。

【插曲】
賽後在會場走動,忽然有東西飛到腳前。靠! 這不是我的GPS錶嗎? 撿起一看,發現陪我流汗操練南征北討的戰友,在風吹日曬雨淋之下,橡膠錶帶老化斷裂。幸好跑完才壞,要是發生在途中,說不定會飛入大圳農田懸崖,就算沒有,心情肯定也會大受影響,說起來它盡忠職守,苦撐到最後才不支倒地,算是很講義氣呀!!

在VS2013中使用SQLCE資料庫

$
0
0

發現Visual Studio 2013已悄悄移掉對SQL Server Compact Edition的內建支援,爬文得知大家較推的替代方案為SQL Server Compact Toolbox。下載安裝SQLCE Toolbox,發現新增SQLCE連線功態被停用並出現Runtime 3.5 not found警示:

心想自己要用4.0,應該用不到3.5,便直接下載SQLCE 4.0 SP1安裝,發現我的Win8上已經有了,看起來就算只用4.0也得裝3.5。乖乖下載安裝SQLCE 3.5 SP2,有個提示:

網站下載的安裝檔會解壓縮出SSCERuntime_x86-ENU.msi及SSCERuntime_x64-ENU.msi兩個檔案,在64位元作業系統中,建議32跟64兩個版本都要裝。安裝SQLCE 3.5SP2後,就能在VS2013裡管理SQLCE資料庫囉。

接著想在專案中為SQLCE資料表建立EntityFramework Model,發現失去Visual Studio內建支援後,做法也與先前不同。要先透過NuGet安裝EntityFramework.SqlServerCompact:

在VS2013為SQLCE建立EF Model,要靠SQL Server Compact Toolbox完成,在介面找到資料庫項目按滑鼠右鍵,選取"Add Entity Data Model(EDMX) to current Project"建立Model:

終於,EDMX建立完成~

比起先前Visual Studio 2010/2012內建整合的操作方式,VS2013的操作步驟複雜不少。從移去SQLCE內建支援來看,猜想微軟傾向開發者改用LocalDB取代SQLCE。就功能面而言,LocalDB能完整支援Stored Procedure、View、Trigger、地理資料型別、分散式交易,在系統規模擴大後能無縫接軌移轉到正式SQL Server,比起SQLCE威力強大許多,不如SQLCE之處只在於檔案體積較大(約160M、SQLCE則只需不到20M)、耗用資源較多(SQLCE為In-Process,LocalDB則需另起Process)、安裝時需管理者權限(某些Web Hosting環境不允許額外安裝軟體),大家可視專案需求抉擇。(MSDN Blog有一篇LocalDB、SQL Express、SQLCE的詳細比較,值得一看)

五百萬人次紀念~

$
0
0

黑暗執行緒部落格邁向新里程碑,累積點閱次數衝破500萬大關囉~

前陣子發現數字將破500萬,暗自擬好祕密計劃(:P,稍候公佈),這幾天特別留意計數器,推測會在11/19達陣。臨下班數字來到4,999,950,又不想錯過歷史性一刻,二話不說,切換到行動模式,開著3G搭捷運一路監看訪問記錄。捷運抵達動物園的同時,部落格也解除了500萬次點閱的成就~

由StatCounter記錄,由我踩到的兩百萬零五次往前推,第200萬次點閱落在Win7 Firefox使用者11/19 19:45:56開啟的在LINQ中實踐多條件LEFT JOIN! 賀~

咳... 回到剛才提到的祕密計劃(請不要過度期待,其實沒什麼),為答謝讀者們長期以來對本站的支持,決定舉辦個慶祝突破500萬次點閱摸彩,獎品是我珍藏多年的雞肋實用科技小品 -- 微軟MVP紀念手電筒一只,可手搖發電,有三段燈光顯示,還有警報器功能,萬一在深山迷路或暗夜遇襲,就全靠它保住一線生機了。(並不能好嗎?)

奬品不怎樣,純粹好玩罷了,不嫌棄的朋友請到黑暗執行緒專頁找到摸彩PO文,在下方留言就可以參加抽奬,時間到2013/11/22 23:59:59為止,本週末開奬。

既然是程式魔人辦的活動,當然要用電腦程式抽獎,預先公告抽獎演算法:

  1. 使用以下jQuery自留言區取出候選名單: (程式會排除重複參加)
    var candidants = { }, i = 1; 
    $(".uiCommentContainer .UFICommentActorName").each(function() { 
    var name = $(this).text(); 
    if (!candidants[name]) candidants[name] = i++; 
    });
    var h = []; 
    for (var p in candidants) { 
      h.push(candidants[p] + "." + p); 
    }; 
    console.log(h.join("\n"));
  2. 使用以下C#用LINQ加亂數決定中奬者:
    class Program
        {
    staticvoid Main(string[] args)
            {
    string raw = @"1.Jeffrey
    2.Darkthread
    3.球證
    4.旁證
    5.主辦
    6.協辦
    7.全都是我的人";
                List<string> candidates = new List<string>(raw.Split('\n'));
                Random rnd = new Random(12345);
                Console.Write(candidates.OrderBy(o => rnd.Next()).First());
                Console.Read();
            }
        }
    12345決定奬落誰家,真實的亂數種子暫且保密,但可透露其MD5 -- Convert.ToBase64String(md5.ComputeHash(Encoding.UTF8.GetBytes("?"))) == "T+L0bo18K9JpYcY+5RRqhQ==iTDNUICkvcJuim7yODfKfw==" (么壽,寫到這裡才想到,該不會有人暴力破解亂數種子吧? 獎品不是跑車,拜託不要這麼認真啦! orz)
    【2013-11-21更新】果不其然,種子被破解了! orz (本站的程式魔人讀者很多,如果是我也會手癢吧! XD) 為不減損抽奬樂趣,我已另選亂數種子,這回計算MD5前有加入不公開的SALT,請魔人們就不用費心破解了。

PS1: 本次活動純粹趣味為主,如因抽奬辦法或作業疏失產生不公平,恕由大會無視民意霸道自行裁決,獎品很糟的,請大家不要計較,真的。
PS2: 由於獎品將採郵寄方式,中獎者如住在海外、外星球或其他銀河系,恕只能代寄到指定的台灣住址。(如下期樂透本人高中頭彩則不在此限,即使南極也免運直送,請大家用念力讓我中奬)

【成長歷程】


【茶包射手日記】偵測EWS服務URL

$
0
0

有支排程透過Microsoft Exchange Web Service Managed API 2.0存取Exchange Web Service收發信及讀取公用資料夾,執行時需指定Exchage.asmx URL方能運作。程式每日執行,多年無事,卻在手頭專案烽火連天的某個早上爆開,彈出錯誤訊息:

Microsoft.Exchange.WebServices.Data.ServiceResponseException: Client Access Server 版本不符合所存取之資源的信箱伺服器版本。請使用自動探索與所要存取之資源的位址,判定用以存取特定資源的正確 URL。

Microsoft.Exchange.WebServices.Data.ServiceResponseException: The Client Access server version doesn't match the Mailbox server version of the resource that was being accessed. To determine the correct URL to use to access the resource, use Autodiscover with the address of the resourc

推測是公司近期升級Exchange的副作用,一時問不到正確新址又急著解決問題,爬文自力救濟。stockoverflow討論提到Autodiscover做法,下載Microsoft Exchange Server 2010 SP2 Web Services SDK September 2011,編譯C:\Program Files (x86)\Microsoft\Exchange Server 2010 SP2\Web Services SDK September 2011\Samples\Autodiscover\AutodiscoverSample專案,不知是否公司的AD網域環境特殊還是有我疏漏的環節,半點資訊都沒查到,失敗收場。

所幸在另一篇文章找到好方法:

1.在Outlook圖示上按Ctrl+滑鼠右鍵叫出選單

2.輸入電子郵件地址及密碼,按下測試:

在可用性服務URL欄位可查到Exchange.asm的網址,改用新址,EWS程式總算復活。

將VSS版控的Visual Studio方案切換成TFS

$
0
0

手上有個Visual Studio方案(.sln)原本使用VSS(Visual Source Safe)進行版控,用Visual Studio 2013開啟移除舊版控設定,想切換Source Control Plug-in想改成TFS,冒出以下訊息:

The active solution or project is controlled by a different souce control plug-in than the one you have selected. If you change the sourcce control plug-in, the active solutoin or project will be closed.

大意是目前的.sln或.csproj中使用了不同的版控套件(即VSS),如果硬要切換,目前的方案及專案會被關閉。廢話,既然想換成TFS,當然要答Yes,但下場就是方案被關閉。重新開啟方案,版控又自動跳回VSS,切到TFS,方案被關閉無從繼續設定,重新開啟方案,版控又自動跳回VSS,切到TFS...  很好,這是鬼打牆吧?

直覺推測,專案或方案裡一定有某處保留了VSS版控設定,Visual Studio在開啟時才會被導引自動切回VSS套件。試著刪除.suo及所有.vsscc檔案但無效,VS仍一口咬定這個專案需搭配VSS服用,一切換到TFS便咬舌自盡。不切換到TFS,即便使用File/Source Control/Change Source Control,所能指定的版控來源還是限定為VSS。

最後,我直接用notepad++開啟.sln檔案,找到以下可疑設定。即使IIS Web Site已從方案移除後,設定仍在,而其中SccProvider指向MSSCII:Microsoft Visual SourceSafe,涉嫌重大。

GlobalSection(SourceCodeControl) = preSolution
    SccNumberOfProjects = 2
    SccWebProject0 = true
    SccProjectUniqueName0 = httq://localhost/WebPart
    SccProjectName0 = \u0022$/Portal/DataExtractor/WebPartWeb\u0022,\u0020UVKLAAAA
    SccLocalPath0 = X:\\TFS\\SPS2007\\WebPart
    SccProvider0 = MSSCCI:Microsoft\u0020Visual\u0020SourceSafe
    SccProjectEnlistmentChoice0 = 2
    SccWebProject1 = true
    SccProjectUniqueName1 = httq://localhost/SSO
    SccProjectName1 = \u0022$/Portal/SSO\u0022,\u0020XDEMAAAA
    SccLocalPath1 = X:\\TFS\\SPS2007\\SSO
    SccProvider1 = MSSCCI:Microsoft\u0020Visual\u0020SourceSafe
    SccProjectEnlistmentChoice1 = 2
EndGlobalSection

手動將以上片段自.sln移除,Visual Studio終於不再強迫我使用VSS,專案得以順利簽入TFS。

感覺不像是正規的操作步驟,但這招的確解決了專案一直被鎖定在VSS的問題。另外,直接修改.sln檔有造成方案毁損的風險,提醒在動手前最好先備份檔案。

[2013-11-29更新]感謝網友鮑承佑補充,有時csproj中的<SccProjectName>, <SccLocalPath>, <SccAuxPath>, <SccProvider>也需一併移除。

Windows 2012 ASP.NET安裝經驗一則

$
0
0

專案動用了SignalR 2.0,在我的Windows 2008R2開發機配合Chrome實測卻怎麼都無法開啟WebScoket傳輸,後來才發現SignalR支援WebSocket的必要條件:

  1. 伺服器端: .NET 4.5 Framework + Windows 8 或 Windows 2012
  2. 瀏覽器端: IE10+或其他瀏覽器

為上線預做準備,決定灌台Windows 2012R2 VM演練兼實測。Windows 2012R2介面修改不小,融入許多Windows 8風格,所幸Roles、Feature等觀念仍與Win2008一致,只差得花點時間熟悉新操作介面。

新增了IIS,也勾選了ASP.NET 4.5,心想這樣ASP.NET就安裝完成了。

在IIS管理工具使用Add Application掛上ASP.NET程式,但IIS管理員看起來怪怪的,右方只有IIS區,少了ASP.NET區。

瀏覽ASP.NET網頁,得到500.19錯誤:

HTTP Error 500.19 - Internal Server Error
The requested page cannot be accessed because the related configuration data for the page is invalid.
Error Code   0x80070021
Config Error   This configuration section cannot be used at this path. This happens when the section is locked at a parent level. Locking is either by default (overrideModeDefault="Deny"), or set explicitly by a location tag with overrideMode="Deny" or the legacy allowOverride="false". 

依據過去的知識(參考: 91的文章),常是aspnet_regiis未正確註冊所致,試著註冊卻有新發現:

C:\Windows\Microsoft.NET\Framework\v4.0.30319>aspnet_regiis -i
Microsoft (R) ASP.NET RegIIS version 4.0.30319.33440
Administration utility to install and uninstall ASP.NET on the local machine.
Copyright (C) Microsoft Corporation.  All rights reserved.
Start installing ASP.NET (4.0.30319.33440).
This option is not supported on this version of the operating system.  Administr
ators should instead install/uninstall ASP.NET 4.5 with IIS8 using the "Turn Win
dows Features On/Off" dialog,  the Server Manager management tool, or the dism.e
xe command line tool.  For more details please see http://go.microsoft.com/fwlink/?LinkID=216771.
Finished installing ASP.NET (4.0.30319.33440).

ASP.NET 4.5跟IIS8已不用aspnet_regiis這招了,要透過Feature管理新增才行,但是如第一張圖例所示,我明明已經裝過ASP.NET 4.5呀?

摸索了一陣子才搞懂,我在新增Role時少選了Application Server項目:

Application Role有個Web Server (IIS) Support,記得也要安裝。

加入Web Server (IIS) Support後,Web Server區會多出Application Developer項目,下面有ASP、ASP.NET 3.5、CGI... 等子項可以選擇。如果想啟用SignalR WebSocket傳輸,記得要勾選WebSocket Protocol。

幾經波折,WebSocket + SignalR終於合體完成!

【延伸閱讀】附上官方版的IIS8 ASP.NET安裝步驟

開發筆記-OWIN

$
0
0

這半年來,在開發ASP.NET Web API及SignalR的過程常看到一枚生冷術語--OWIN,不知其所以然好一陣子,今天花點功夫粗略理解一番,特筆記備忘。

OWIN(Open Web Interface for .NET)是一套開放網站介面標準,重新定義了.NET Web Application與Web Server的溝通介面。就系統架構觀點,介面被抽取出來獨立意味著"抽換"的可能性,用白話解釋,意思是ASP.NET程式可以不依賴System.Web.dll,不侷限於IIS,放在Console程式裡跑Self-Hosting模式,甚至透過Kayak網站伺服器在*nix系統運作,都不再是神話。

由下到上,OWIN架構區分為四層:

  • Host
    最底層,負責載入、啟動及關閉OWIN元件
  • Server
    負責連上TCP Port,建構OWIN Pipeline環境以處理Request
  • Middleware
    所有在OWIN Pipeline中Request處理模組的統稱,小到很簡單的資料壓縮模組,大至ASP.NET Web API都算。
  • Application
    依專案需求所開發的應用程式碼

擺脫System.Web.dll的另一項好處: 網站基底架構由龐大的官方Framework轉化成多個中小型模組,可以個別機動抽換更新,不需要苦苦等待官方改版。

現今ASP.NET相關技術中,由ASP.NET Web API跨出第一步,完全排除對System.Web.dll的依賴。.NET社群仿效Ruby Rack的精神,為.NET網站定義了OWIN,而Katana專案則是微軟依OWIN規格所實作的元件,包含Host、Server、身分認證元件... 等,ASP.NET Web API與SignalR則是目前應用OWIN標準的經典範例。

光說不練令人心虛,總得來個簡單演練才踏實,以下是個範例,利用Microsoft.Owin.Hosting.WebApp + Nancy(一套支援OWIN的迷你Web Server模組),在Console Application模擬簡易Web Server。

開啟一個Console Application專案,利用NuGet安裝以下套件:

Microsoft.Owin.Hosting

Microsoft.Owin.Host.HttpListener

Nacy.Owin

簡單寫幾行程式:

  1. 定義MyStartup類別,在其中設定Server要掛載的模組,UseNancy是Nancy提供的Extension Method,一行就搞定Nancy設定。
  2. 定義HomeModule,繼承NancyModule,在建構式中宣告Get["/"]拋回HTML、Post["/money"]傳回Server時間,我們為這個小Web加入 GET / 及 POST /money 兩個功能。
  3. WebApp.Start<MyStarup>("http://+:1234")聆聽本機的1234 Port開始執行網站功能,透過Console.ReadLine()待使用者按Enter後結束服務。(記得要netsh開權限,這點之前提過)
  4. 打完收工。
using Microsoft.Owin.Hosting;
using Nancy;
using Owin;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
namespace OWINLab
{
class Program
    {
staticvoid Main(string[] args)
        {
//記得開放Binding權限
//netsh http add urlacl url=http://+:port_number/ user=machine\username
using (WebApp.Start<MyStartup>("http://+:1234"))
            {
                Console.WriteLine("OWIN self-hosting, press enter to exit");
                Console.ReadLine();
            }
        }
    }
 
publicclass HomeModule : NancyModule
    {
public HomeModule()
        {
            Get["/"] = x =>
            {
return@"
<html><body>
<input type='button' value='Show Me the Money'></input>
<script src='http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.9.1.js'></script>
<script>
$(':button').click(function() {
    $.post('/money',{},function(m) {
        alert(m);
    });
});
</script>
</body></htmol>";
            };
            Post["/money"] = x =>
            {
return"MONEY @ " + DateTime.Now.ToString("HH:mm:ss");
            };
        }
    }
 
publicclass MyStartup
    {
publicvoid Configuration(IAppBuilder app)
        {
            app.UseNancy();
        }
    }
}

就這樣誕生一個超簡單的小Web,很感人吧!

【延伸閱讀】

在專案新增OWIN Startup類別

$
0
0

新增了一個MVC專案要測試SignalR,透過NuGet安裝Microsoft.AspNet.SignalR.Sample,依照readme.txt指示,需在OWIN Startup中加入:
Microsoft.AspNet.SignalR.StockTicker.Startup.ConfigureSignalR(app);

這動作上回做個一次,在ASP.NET專案下有個Startup.cs,把程式擺進去就好,但這回不知為何專案根目錄卻不見Startup.cs蹤影?

爬文得知,專案範本中有Startup的項目可用,手動補上就成了。

餘下的謎是: 為何上回專案中本來就有Startup.cs,但這回卻要自己手動加入?

原來是我為求單純,在新增專案時我選擇不啟用任何身分認證模組:

上回提過身分認證元件是微軟Katana專案(OWIN實作)包含項目之一,故專案模版有啟用身分認證才會自動加入Startup.cs,以便掛上認證模組。Case Closed~

【打破砂鍋系列】SignalR傳輸方式剖析

$
0
0

能依瀏覽器支援能力自動尋找最適合的通訊方式,是SignalR最迷人之處。SignalR 2.0共支援Forever Frame、Long Polling、Server Sent Event、WebSocket四種通訊方式,在Introduction to SignalR的Transports and fallbacks一節有詳細說明,但對茶包射手來說,沒有追究到每一個動作所對應的封包,就不算徹底解開謎團,午夜夢迴之際總要平添幾許遺憾... (謎之聲: 有那麼嚴重嗎?)

於是,為賦新詞強說愁打破砂鍋追根究底的CSI等級鑑識展開了。

第一步是先建立應用SignalR傳輸的測試網頁,寫了一個簡單的MarathonHub,宣告Runner型別當成資料物件,Server端提供OneShotTest()方法,執行時則呼叫Client端的addRunner(),將Runner資料送至前端。

using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Hubs;
using Newtonsoft.Json;
using Owin;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
 
namespace Darkthread.SignalR.Test
{
publicstaticclass Startup
    {
publicstaticvoid ConfigureSignalR(IAppBuilder app)
        {
            app.MapSignalR();
        }
    }
 
    [HubName("marathron")]
publicclass MarathronHub : Hub
    {
//模擬資料物件
publicclass Runner {
publicstring Id { get; set; }
publicstring Name { get; set; }
public DateTime Birthday { get; set; }
publicint PersonalBest { get; set; }
            [JsonIgnore]
public AutoResetEvent RoundTripSync = new AutoResetEvent(false);
        }
 
static Dictionary<string, Runner> dataStore = null;
 
public MarathronHub()
        {
//產生10000筆模擬資料
if (dataStore == null)
            {
                dataStore = new Dictionary<string,Runner>();
                Random rnd = new Random();
int COUNT = 100;
for (var i = 0; i < COUNT; i++)
                {
string id = Guid.NewGuid().ToString();
                    dataStore.Add(id, new Runner()
                    {
                        Id = id,
                        Name = i == COUNT - 1 ? "Last" : "Runner" + i,
                        Birthday = DateTime.Today
                        .AddDays(-6000 - rnd.Next(6000)).ToUniversalTime(),
                        PersonalBest = 7600 + rnd.Next(7600)
                    });
                }
            }
 
        }
 
publicvoid OneShotTest()
        {
            Clients.Caller.addRunner(dataStore.Values.First());
        }
    }
}

網頁部分也很平常,addRunner()在接收到Runner資料後放入陣列,並在網頁顯示其JSON內容,Test鈕則負責觸發Server端的OneShotTest()。特別之處是有一段程式碼由URL的?trans=xxx取得foreverFrame、longPolling、serverSentEvents或webSockets四個字串,透過$.connection.hub.start({ transport: ... })強制指定傳輸方式,防止SignalR自動決定傳輸方式,以便觀察不同傳輸模式的行為。

<!DOCTYPEhtml>
<htmlxmlns="http://www.w3.org/1999/xhtml">
<head>
<title></title>
<scriptsrc="../Scripts/jquery-1.10.2.js"></script>
<script src="../Scripts/jquery.signalR-2.0.0.js"></script>
<script src="../signalr/hubs"></script>
<script>
        $(function () {
var marathron = $.connection.marathron;
            runners = [];
            marathron.client.addRunner = function (runner) {
                $("#dvDisp").text(JSON.stringify(runner));
                runners.push(runner);
            };
 
var re = /[?]trans=(.+)/;
Choose image...
var m = re.exec(location.href);
var transType = m && m.length > 1 ? m[1] : null;
            $("#spnTransType").text(transType);
// Start the connection
            $.connection.hub.start(transType ? { transport: transType } : undefined)
            .done(function () {
                $(":button").click(function () {
                    runners = [];
                    marathron.server.oneShotTest();
                });
            });
        });
</script>
</head>
<body>
<h3>Transport Type [ <spanid="spnTransType"></span> ]</h3>
<div>
<inputtype="button"value="Test"/>
</div>
<divid="dvDisp"style="width: 500px; font-size: 9pt;">
</div>
</body>
</html>

為讓觀察結果單純化,測試過程只有兩個步驟: 載入網頁、按下測試鈕,看到JSON結果就結束。觀察工具主要採用Fiddler,至於Fiddler無法涵蓋之處(如: Server Sent Event及WebSocket的傳輸內容)再動用小型核武—Microsoft Network Monitor

【Forever Frame】

好了,第一個測試是Forever Frame,這是IE獨有選項:

如上圖所示,SignalR偷偷在網頁嵌入一個IFrame,連向SignalR提供的內容,巧妙之處在於其中一段一段的<script>並非一次載入,而是分次送至前端後馬上執行,藉此實現持續傳送內容並控制前端動作的效果。而透過Fiddler可觀察到按鈕呼叫Server端OneShotTest()時,網頁會送出一個/signalr/send Request(如下圖所示),POST Form內容註明Hub為marathron、Method為OneShotTest,無傳入參數,SignalR收到後便會觸發MarathronHub的OneShotTest()方法。

【Long Polling】

接著來看Long Polling:

Long Polling的特色在於網頁會以XHR送出一個/signalr/poll Request(上圖第8個Request,藍底),但Server端先不送回結果讓Client痴等,直到有資料要傳至Client;按下測試鈕時Client送出第9個Request(/signalr/send),第8個Request立即得到Server端回應並結束(即上圖最下方的JSON,包含了參數A為Runner物件,H指定Hub為"marathron",M註明Method為addRunner),接著Client會馬上再送出一個/signalr/poll Request(上圖第10個Request)。以上觀察實證了Long Polling的運作原理,送出一個Request,Server端先Hold住,直到有東西要送至Client端時才傳回結果並結束,一旦Request結束,Client端要馬上另起一個相同Request維持連繫。

【Server Sent Event】

Server Sent Event也是HTML5新增的機制,透過特殊的Header供瀏覽器識別,讓Request持續保持開啟狀態,Server端便可持續透過這個Request不斷送資料到前端。IE不支援Server Sent Event,我們改用Chrome來測試。

Server Sent Event的表現跟Long Polling有點像,如上圖所示,第7個Request(/signalr/connect?transport=serverSentEvents)的Result為"-"、Body為-1,代表Request一直沒有結束。但差異在於Long Polling的Request在Server送回結果後會結束再另建新Request;但在上圖我們連發8、9兩個/signalr/send,Server Sent Event的7號Request仍持續開啟。由於Fiddler無法觀察未結束的Request內容,無法觀察Server送回資料,此時招喚Microsoft Network Monitor上場~

在上圖的第37 Frame,Client端(192.168.1.105)送出GET /SignalR/signalr/connect?transport=serverSentEventS;Frame 38 Server回應OK,Server Sent Event管道建立完成,之後的Frame可看到Server持續透過此管道送封包給Client。

在Frame 61,我們找到Server將Runner JSON內容送至Client端的證據。

【WebSocket】

最後輪到本檔壓軸 -- WebSocket上場。

WebSocket最大的特色在於支援雙向傳輸,因此你不會看到先前一再出現的(/signalr/send) Request。要要怎麼檢視傳輸內容? 又得靠Network Monitor囉!

在上圖中,Frame 140送出GET /signalr/connect?transport=webSockets,Frame 141 Server給了一個特別的回應,Status = HTTP 101 Switching protocols,之後這條連線就換變成雙向的WebSocket傳輸。

Frame 144為按下測試鈕時Client送到Server端的封包(內容似經編碼,無法直接解讀)。Frame 144 Client送出資料後,Server有了一連串回應(幾乎同時,Time of Frame均為1.7886144),於Frame 149,抓到了,Server透過WebSocket傳回Runner JSON的證據。

【結論】

實驗完畢,驗證SignalR能透過四種不同傳輸管道完成相同動作,並且也觀察到Forever Frame、Long Polling、Server Sent Event及WebSocket的運作細節,對SignalR有更進一步的認識後,開發應用時心裡就更踏實囉~

SignalR傳輸效能評測-單向傳輸

$
0
0

上篇文章剖析了SignalR的四種傳輸方式: Forever Frame、Long Polling、Server Sent Event及WebSocket,延伸出另一個議題,這四種傳輸方式效率如何? 理論上WebSocket Overhead最少且支援雙向傳送,很有HTML5傳輸霸主之相,但我期待眼見為憑。

我設計了以下的實驗,先測Server端傳至Client端的效率,沿用前文的MarathronHub,以亂數模擬1000筆Runner資料,經由BatchTest()方法一骨腦把1000筆資料拋至Client端: (註: 程式碼改寫自傳輸剖析一文,這裡只提增修的部分,請對照參考)

publicvoid BatchTest()
        {
            var caller = Clients.Caller;
foreach (var runner in dataStore.Values)
            {
                caller.addRunner(runner);
            }
        }

聲明在先,實務上我們很少會產生如此兇猛的資料傳輸量,理由有二: 1) 送至前端的資料多半會反應在UI上,短時間產生巨量資料,遠超過使用者視覺所能接收消化的上限,意義不大。 2) 若使用者從Internet連上網站,網路頻寬有其限制,若是透過行動裝置瀏覽,更要考量CPU/Memory負載,能達成巨量資料傳輸的可行性存疑。進一步,設計實務上更建議對即時傳輸加上節流控制,限定單位時間可傳送資料量上限,以維持良好的效能及效果。雖然高速傳送大量資料的情境很少真實上演,這類測試結果還是可視為"壓力測試",具有一定參考價值。

前端程式如下。由?trans=…參數指定傳輸方式,按下測試鈕會觸發Server端BatchTest(),傳回1000筆Runner資料。addRunner()除將資料放入陣列,還一併顯示耗時,遇到最後一筆(runner.Name == "Last"),便將結果產生一筆<li>加至網頁下方列表,方便連續測試記錄之用。

<!DOCTYPEhtml>
<htmlxmlns="http://www.w3.org/1999/xhtml">
<head>
<title></title>
<scriptsrc="../Scripts/jquery-1.10.2.js"></script>
<script src="../Scripts/jquery.signalR-2.0.0.js"></script>
<script src="../signalr/hubs"></script>
<script>
        $(function () {
var marathron = $.connection.marathron;
            runners = [];
var $counter = $("#spnCount"), startTime;
            marathron.client.addRunner = function (runner) {
                runners.push(runner);
                $counter.text(runners.length + "@" + (new Date() - startTime) + "ms");
if (runner.Name == "Last")
                    $("#ulDisplay").append("<li>" + $counter.text() + "</li>");
            };
 
var re = /[?]trans=(.+)/;
var m = re.exec(location.href);
var transType = m && m.length > 1 ? m[1] : null;
            $("#spnTransType").text(transType);
// Start the connection
            $.connection.hub.start(transType ? { transport: transType } : undefined)
            .done(function () {
                $(":button").click(function () {
                    startTime = new Date();
                    runners = [];
                    marathron.server.batchTest();
                });
            });
        });
</script>
<style>
        fieldset { margin: 6px; }
</style>
</head>
<body>
<h3>Transport Type [ <spanid="spnTransType"></span> ]</h3>
<div>
<inputtype="button"value="Test"/>
        Runner Count = <spanid="spnCount"></span>
</div>
<fieldset>
<ulid="ulDisplay"></ul>
</fieldset>
</body>
</html>

網頁執行效果如下圖:

實測結果如下: (資料定量,時間愈短愈快)

  • IE10 Forever Frame
    545ms, 490ms, 450ms, 517ms, 533ms
  • IE10 Long Polling
    664ms, 693ms, 628ms, 659ms, 618ms
  • IE10 WebSocket
    533ms, 469ms, 564ms, 565ms, 546ms
  • Chrome Long Polling
    343ms, 312ms, 249ms, 280ms, 312ms
  • Chorme Sever Sent Events
    234ms, 249ms, 250ms, 250ms, 218ms
  • Chrome WebSocket
    234ms, 234ms, 218ms, 234ms, 234ms

【結論】

以Server對Client的單向傳輸來說,不管是IE或Chrome,Long Polling都是最慢的,因為每次得到結果後需結束原有Request重發一個新的。排除了Long Polling,IE10的Forever Frame與WebSocket,Chrome的Server Sent Event與WebSocket速度差不多,看不出明顯差距。而你知道我知道獨眼龍都知道的事實是–Chrome比IE10快。

在單純Server傳送給Client的情境,WebSocket並未展現明顯優勢,下回我們再測試雙向傳輸,見識WebSocket的威力。


SignalR傳輸效能評測-雙向傳輸

$
0
0

上回測過SignalR四種傳輸方式的Server到Client段效能表現,確認Long Polling因不斷重發Request效率稍差,其餘兩種方式效能則相去不遠,WebSocket並無格外突出。先前剖析中,我們知道WebSocket最大特色在於"支援雙向傳輸",這回我們來個Server到Client、Client到Server傳輸各半的模擬情境。(WebSocket都做球給你了,好好表現囉~)

規劃以下測試情境: 每次Server送一個Runner到Client後(經由addRunner),Client必須用接收到的Runner資料呼叫Server端AckRunner()方式作為簽收,Server端在收到簽收回饋才算完成一次Round-Trip後,再送出下一筆Runner資料(經由AutoResetEvent實現收到回饋才送下一筆的同步邏輯),這個情境同時考驗接收與傳送兩個方向的效率。

Server端程式新增RoundTripTest()及AckRunner()兩個方法,在RoundTripTest()會在送出Runner後將其RoundTripSync(型別為AutoResetEvent) Reset(),接著WaitOne()等待它被Set();當Client端收到Runner後,呼叫該Server端的AckRunner()方法,其中會執行AutoResetEvent.Set(),使前述WaitOne()被放行,繼續執行下一筆。AckRunner()其實傳入Runner.Id即可,但我想讓Client到Server也傳送相同資料量,故選擇傳入整個Runner物件。

publicvoid RoundTripTest()
        {
            var caller = Clients.Caller;
            Task.Factory.StartNew(() =>
            {
foreach (var runner in dataStore.Values)
                {
                    runner.RoundTripSync.Reset();
                    caller.addRunner(runner);
runner.RoundTripSync.WaitOne();
                }
            });
        }
 
publicvoid AckRunner(Runner runner)
        {
dataStore[runner.Id].RoundTripSync.Set();
        }

HTML端修改得不多,只在addRunner()中加入一行marathron.server.ackRunner(runner);

            marathron.client.addRunner = function (runner) {
                runners.push(runner);
                $counter.text(runners.length + "@" + (new Date() - startTime) + "ms");
marathron.server.ackRunner(runner);
if (runner.Name == "Last")
                    $("#ulDisplay").append("<li>" + $counter.text() + "</li>");
            };

實測結果:

  • IE10 Forever Frame
    3756ms, 3670ms, 3719ms
  • IE10 Long Polling
    8764ms, 8280ms, 8510ms
  • IE10 WebSocket
    2539ms, 1850ms, 2808ms
  • Chrome Long Polling
    3651ms, 3853ms, 3572ms
  • Chrome Server Sent Event
    3245ms, 2995ms, 3167ms
  • Chrome WebSocket
    1575ms, 2215ms, 1123ms

【結論】

一如預期,支援雙向傳送的WebSocket免除1000次呼叫AckRunner()的(/signalr/send) Request,獲得壓倒性勝利! 快了近3倍。而Long Polling原本就不斷地在結束並重開Request,多了額外的1000次/signalr/send Request後,效能慘不忍睹。如此可推論,當Client呼叫Server端的次數愈頻繁,WebSocket就愈佔優勢,而Long Polling輸得愈慘,雖然用什麼傳輸方式取決於瀏覽器與伺服器的支援度,這個結果還是可做為不同情境效能表現的評估參考。

【補充】

SignalR決定傳輸方式的邏輯如下: (參考)

  1. IE6/7/8? 直接保送Long Polling (這又給了我們一個不該用老IE的好理由)
  2. 若啟動連線時指定了JSONP參數 –> Long Polling
  3. 如果SignlaR連線對象為跨網域,且滿足以下情境,將採WebSocket,否則用Long Polling
    * Client支援CORS
    * Client支援WebSocket
    * Server支援WebSocket
  4. 若未指定JSONP參數且Server/Client都支援WebSocket –> WebSocket
  5. 若Client或Server端不支援WebSocket,但支援Server Sent Event –> Server Sent Event
  6. 如果前述Server Sent Event不可用,改用Forever Frame
  7. 如果前述Forever Frame也失敗,改用Long Polling

【茶包射手日記】Redmine卡卡奇案

$
0
0

Redmine是一套架構在Ruby on Rails的專案平台,開發團隊最近在公司架了個Windows版,用它追蹤及管理Bug,取代先前使用的BugTracker.NET

不知從何時起,我手邊兩台機器連上Redmine回應奇慢,明明是在Intranet 100M LAN,開啟網頁時畫面都會頓一下,等上幾秒鐘才會顯示。最令人氣結的是,只有我的兩台機器有此症狀,其他人用起來速度飛快,完全感受不到任何延遲,只有我活似從火星衛星連線,讓人氣到想"冰斗"。

用瀏覽器側錄網路傳輸看到一個現象: 從我機器發出的所有Request會先被卡住4秒,之後才像閘門被打開,幾個Request結果一起被傳回來。

同樣的現象在IE跟Chrome都存在,但只發生在這台Redmine(httq://redmine_host:3000/*),改連同一主機IIS的80 Port不會有問題,在其他台IIS開個HTTP Port 3000 Web Site,連起來也沒有任何延遲。

最後,我整理出一個極精簡的問題重現法: 在LINQPad4執行一小段程式,用WebClient從Remine主機取回一個匿名存取圖檔,實測要耗時9秒鐘。

從沒遇過這種玄疑狀況,毫無頭緒,甚至懷疑該不會機器中木馬被攔截特定傳輸被才會如此,一想到不禁冒了冷汗。(資安偏執狂很會疑神疑鬼呀~)

最後,決定動用對付網路茶包的小型核武 – Microsoft Network Monitor。

錄下LINQPad測試過程的來往封包,很快發現一些怪異記錄,除了LINQPad的傳輸,另外冒出一段Redmine主機與主機間的System層次網路活動:

檢視System的網路活動,研判是連續六次NetBIOS查詢:

對照WebClient的相關封包,可以看出有趣關聯:

  1. 3.7750秒時,WebClient發出GET /images/loading.gif Request
  2. 從3.7885起,分別在3.7885, 5.2875, 6.7872, 8.2887, 9.7877, 11.2875六個時點,Redmine主機陸續丟過來六次NetBIOS查詢(推測是要查詢我的機器名稱),但我的主機未給回應。
  3. 為了降低被攻擊風險,我習慣關閉對外的NetBIOS相關Port,這是主機沒回應NetBIOS查詢的原因。
  4. 在六次NetBIOS查詢失敗後,在12.7959秒Redmine主機終於送回loading.gif圖檔內容,足足慢了九秒。

由此推測,Redmine主機似乎會反查機器名稱,但因為我關閉NetBIOS Port導致查詢失敗,歷經六次嘗試後才放棄,甘心傳回結果,這可以解釋每次連線時會先停頓幾秒(試著反查機器名稱),之後才多個Request一起傳回的現象。而關閉NetBIOS Port是我這種資安偏執狂才會做的機車之舉,無怪乎同一個網站大家用得好好的,唯獨我一直卡到陰。

為了驗證假設,試著在Redmine主機的system32/drivers/etc/hosts加入我的機器IP與名稱,果不其然,卡住狀況瞬間消失。掌握住問題大方向,沒多就爬文找到根本原因,原來我們的Redmine採用Webrick做為網站伺服器,而Webrick有一個廣為人知的問題 -- 預設會反查Client機器名稱,有時導致回應緩慢,但config.rb中有個DoNotReverseLookup選項可以關閉設定。找到C:\Ruby\v193\lib\ruby\1.9.1\webrick\config.rb後,將其中的 :DoNotReverseLookup => nil 改為 :DoNotReverseLookup => true,另一台未設hosts記錄的機器也立刻不再卡卡,茶包正式消滅,Microsoft Network Monitor萬歲!

【茶包射手日記】讓IE執行速度差10倍的關鍵

$
0
0

故事是這樣的,工作專案有個大量使用JavaScript的重量級網頁,稍做修改後在工作機的IE10 @ Windows 2008 R2測試耗時居然超過10秒,比起Chrome慢上N倍,本以為這又是你知道我知道獨眼龍也知道的"IE特色",後來才發現事情沒想像單純。

同事用IE9 @ Win7執行相同網頁,速度較Chrome慢,但至少比我的電腦快了一倍以上,我才意識到"有茶包!!"。測了IE10 @ Win8筆電,IE9 @ Win7 VM,確認只有這台Windows 2008R2 IE10慢得像烏龜。

開啟IE Dev Tool的Profiler功能測錄Script執行過程,追到一段Kendo UI執行4萬多筆資料DataBinding的邏輯,呼叫了15萬次以上jQuery.isPlainObject函數及jQuery.type,耗時1.5秒,是速度如龜的根源:

對照筆電上的IE10,同樣的執行次數卻只要210ms,而且筆電CPU是i5,不如工作機是i7,速度卻快上N倍,很不合理。

抽絲剝繭,組出了一個可重現問題的迷你測試:

<!DOCTYPEhtml>
<html>
<head>
<metacharset=utf-8/>
<title>JS Bin</title>
</head>
<body>
<scriptsrc="http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script>
var ary = [];
for (var i = 0; i < 42207;  i++) 
    ary.push({ a:"00000",b:"TEXT",c:1,t:"0000 TEXT" });
var st = new Date();
for (var i = 0; i < ary.length; i++) {
var b = jQuery.isPlainObject(ary[i]);
}
alert(new Date() - st);
</script>
</body>
</html>

這個測試在問題IE執行要耗時400ms以上,而一般的IE大概都在50ms以下,在Facebook專頁PO文請大家幫測,得到很多回饋(特此感謝!),只有兩位跟我一樣遇到慢郎中,大部分的人都是50ms以內,由此推測應是環境問題使然。

整個晚上茶包在腦中打轉,半夢半醒之間腦海還繼續精簡測試Script(我的大腦是一個IDE嗎?),自己都覺好笑。一早就睡不著爬起來,繼續將Script精簡到完全不用jQuery,純粹只呼叫一個永遠return true的函數5萬次(程式如下),猜猜怎麼了? 問題IE要耗時100ms以上,正常IE只要---0ms!!! 這個差異可大了。

<!DOCTYPEhtml>
<html>
<head>
<metacharset=utf-8/>
<title>JS Bin</title>
</head>
<body>
<script>
function testType( obj ) {
returntrue;
}  
var obj = {a:"A"};
var st = new Date();
for (var i = 0; i < 50000; i++) {
  testType(1);
}
alert(new Date() - st);
</script>
</body>
</html>

試過關閉所有Add-On(外掛),速度不見改善。但我做了個很關鍵的對照測試,用另一個帳號登入問題IE所在的Windows 2008 R2,開IE測試速度是正常的,由此可斷定IE本質沒問題,問題出在沾到髒東西或不正確的設定。

於是我打閞IE的進階設定選項全力進攻,試著開關不同設定,終於找出關鍵:

關鍵居然在Disable script debugging(停用指令碼偵錯)設定,停用指令碼偵錯,問題IE的執行速度立刻變成0ms;而在正常IE啟用指令碼偵錯,速度就立刻上升到近100ms。至此,困擾我近24小時的茶包懸案終告偵破!!

知道這個設定N年了,第一次發現它對執行效能影響這麼大!! 當然,連續執行同一個JavaScript函數十萬次的情境並不常見,所以在實務上它對效能的干擾並不會像我的案例被如此放大。但有一點不會錯: 如果你希望你的IE快一點,平時請保持"停用指令碼偵錯"狀態(注意: 勾選起來是停用),需要偵錯時再調整。

使用jQuery.post傳送字串陣列參數到ASP.NET MVC

$
0
0

本範例展示如何透過jQuery.post傳送string[]參數給ASP.NET MVC。

情境模擬訊息發送操作,提供網頁介面供使用者挑選接收者(採用複選式下拉選單)、輸入發送內容後按鈕傳送訊息給指定對象。

ASP.NET MVC Controller如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
 
namespace Mvc.Controllers
{
publicclass HomeController : Controller
    {
//
// GET: /Home/
public ActionResult Index()
        {
return View();
        }
 
public ActionResult SendMessage(string[] users, string message)
        {
//假裝發通知給指定的使用者, 此處省略
return Content(string.Format("已傳送訊息給{0}",
                users == null ? "null" : string.Join(",", users)));
        }
    }
}

至於前端,輸入欄位借重Knockout幾行搞定,按鈕動作則透過jQuery.post()以AJAX方式呼叫Controller的SendMessage方法,其中的users參數在C#端宣告型別為string[],Client端靠Knockout的下拉選單複選繫結selectedOptions直接取得使用者挑選的字串陣列,依直覺寫成{ users: vm.selUsers() }將字串陣列傳至Server端。

 
@{
    Layout = null;
}
 
<!DOCTYPEhtml>
 
<html>
<head>
<metaname="viewport"content="width=device-width"/>
<title>Test</title>
<scriptsrc="~/Scripts/jquery-2.0.3.js"></script>
<script src="~/Scripts/knockout-3.0.0.debug.js"></script>
</head>
<body>
<div>
<input type="text" data-bind="value: msg" />
<input type="button" data-bind="click: sendMsg" value="Send Message" />
<br /> 
<select data-bind="options: users, selectedOptions: selUsers" multiple></select>
</div>
<script>
function myViewModel() {
var self = this;
            self.msg = ko.observable("Test");
            self.users = ko.observableArray(
                ["Jeffrey", "Darkthread", "Admin", "Guest"]);
            self.selUsers = ko.observableArray();
            self.sendMsg = function () {
                $.post("@Url.Content("~/home/sendmessage")",
                    { users: vm.selUsers(), msg: vm.msg() },
function (res) {
                        alert(res);
                    });
            };
        }
var vm = new myViewModel();
        ko.applyBindings(vm)
</script>
</body>
</html>

事與願違,ASP.NET MVC端的輸入參數string[]沒接到值,users為null:

用IE Dev Tool檢視傳送內容,可以發現jQuery.post()自動將字串陣列序列化為users[]=...&users[]=...(上圖黃底部分,%5B%5D是"["與"]"的UrlEncode編碼),但ASP.NET MVC卻認不得,所幸,我們離接通只差一小步。

關於陣列參數的序列化形式,傳統採用a=..&a=..格式,後來如PHP、Ruby on Rails等Framework改採a[]=..&a[]=..格式,jQuery從1.4版起便將陣列序列化預設格式改為a[]=..,但仍提供traditional參數可指定採用舊格式。而ASP.NET MVC仍採a=..&a=..格式,所以只需將傳遞參數交給$.param()轉換並指定traditional參數為true: $.param({ users: vm.selUsers(), msg: vm.msg() }, true), 問題就解決囉!

PS: 如對陣列化序列格式有興趣深入了解,可以參考jQuery.param()文件

Json.NET技巧-反序列化還原為不同型別的集合

$
0
0

情境如下,我們定義一個抽象型別Notification保存排程發送通知的資料(包含JobType、ScheduleTime及Message),依發送管道分為電子郵件通知及簡訊通知,故實作成EmailNotification及SMSNotification兩個類別,並各自增加Email及PhoneNo屬性。

using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using System;
 
namespace CustCreate
{
publicenum Channels
    {
        Email, SMS
    }
//通知作業
publicabstractclass Notification
    {
        [JsonConverter(typeof(StringEnumConverter))]
//通知管道
public Channels JobType { get; protected set; }
//排程時間
public DateTime ScheduledTime { get; set; }
//訊息內容
publicstring Message { get; set; }
 
protected Notification(DateTime time, string msg)
        {
            ScheduledTime = time;
            Message = msg;
        }
 
protected Notification() { }
    }
//電子郵件通知
publicclass EmailNotification : Notification
    {
publicstring Email { get; set; }
public EmailNotification(DateTime time, string email, string msg)
            : base(time, msg)
        {
            JobType = Channels.Email;
            Email = email;
        }
    }
//簡訊通知
publicclass SMSNotification : Notification
    {
//電話號碼
publicstring PhoneNo { get; set; }
 
public SMSNotification()
        {
            JobType = Channels.SMS;
        }
 
public SMSNotification(DateTime time, string phoneNo, string msg)
            : base(time, msg)
        {
            JobType = Channels.SMS;
            PhoneNo = phoneNo;
        }
 
    }
}

依循上述資料結構,我們可輕易產生一個List<Notification>,其中包含EmailNotification及SMSNotification兩種不同型別的物件,用JsonConvert.SerializeObject()簡單轉成JSON:

    var jobs = new List<Notification>();
    jobs.Add(new EmailNotification(
        DateTime.UtcNow, "blah@bubu.blah.boo", "Test 1"));
    jobs.Add(new SMSNotification(
        DateTime.UtcNow, "0912345678", "Test 2"));
    Console.WriteLine(
        JsonConvert.SerializeObject(jobs, Formatting.Indented));
    Console.Read();

產生JSON字串如下:

[
  {"Email": "blah@bubu.blah.boo","JobType": "Email","ScheduledTime": "2013-12-13T13:43:25.6881876Z","Message": "Test 1"
  },
  {"PhoneNo": "0912345678","JobType": "SMS","ScheduledTime": "2013-12-13T13:43:25.6891803Z","Message": "Test 2"
  }
]

到目前為止非常輕鬆愉快吧? 然後呢? 以前學過的JsonConvert.DeserializeObject<T>都只能轉回單一型別,要怎麼把它反序列化還原回內含EmailNotification及SMSNotification不同型別物件的List<Notification>?

雖然不算常見情境,但這可難不倒偉大的Json.NET!

以下是程式範例,關鍵在於我們得繼承CustomCreationConverter<T>自訂一個轉換Notification物件的轉換器作為DesializeObject時的第二個參數。而在轉換器中可透過覆寫ReadJson()方法,依輸入JSON內容,傳回轉換後的物件。如此,我們就能依JobType動態建立不同型別的物件,再從JSON內容取得屬性值,達成反序列化還原出不同型別物件的目標。

另外,稍早定義物件時有預留伏筆,EmailNotification只提供有參數的建構式,而SMSNotification則支援無參數建構式(可沿用內建的屬性對應機制),以便示範兩種不同做法,細節部分請直接看程式碼及註解。

using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
 
namespace CustCreate
{
class Program
    {
staticstring json = @"[
  {
""Email"": ""blah@bubu.blah.boo"",
""JobType"": ""Email"",
""ScheduledTime"": ""2013-12-13T13:43:25.6881876Z"",
""Message"": ""Test 1""
  },
  {
""PhoneNo"": ""0912345678"",
""JobType"": ""SMS"",
""ScheduledTime"": ""2013-12-13T13:43:25.6891803Z"",
""Message"": ""Test 2""
  }
]";
//自訂轉換器,繼承CustomCreationConverter<T>
class NotificationConverter : CustomCreationConverter<Notification>
        {
//由於ReadJson會依JSON內容建立不同物件,用不到Create()
publicoverride Notification Create(Type objectType)
            {
thrownew NotImplementedException();
            }
//自訂解析JSON傳回物件的邏輯
publicoverrideobject ReadJson(JsonReader reader, Type objectType, 
object existingValue, JsonSerializer serializer)
            {
                JObject jo = JObject.Load(reader);
//先取得JobType,由其決定建立物件
string jobType = jo["JobType"].ToString();
if (jobType == "Email")
                {
//做法1: 由JObject取出建構式所需參數建構物件
                    var target = new EmailNotification(
                            jo["ScheduledTime"].Value<DateTime>(),
                            jo["Email"].ToString(),
                            jo["Message"].ToString()
                        );
return target;
                }
elseif (jobType == "SMS")
                {
//做法2: 若物件支援無參數建構式,則可直接透過
//       serializer.Populate()自動對應屬性
                    var target = new SMSNotification();
                    serializer.Populate(jo.CreateReader(), target);
return target;
                }
else
thrownew ApplicationException("Unsupported type: " + jobType);
            }
        }
staticvoid Main(string[] args)
        {
//JsonConvert.DeserializeObject時傳入自訂Converter
            var list = JsonConvert.DeserializeObject<List<Notification>>(
                json, new NotificationConverter());
            var item = list[0];
            Console.WriteLine("Type:{0} Email={1}", 
                item.JobType, (item as EmailNotification).Email);
            item = list[1];
            Console.WriteLine("Type:{0} PhoneNo={1}", 
                item.JobType, (item as SMSNotification).PhoneNo);
            Console.Read();
        }
    }
}

就醬,我們就從JSON完整重現原本的List<Notification>囉~

Type:Email Email=blah@bubu.blah.boo
Type:SMS PhoneNo=0912345678
Viewing all 2311 articles
Browse latest View live