Friday, June 15, 2012

從 COM 到 Windows Runtime Component

Windows Runtime Component 是 COM 的一種延伸我想這是錯的, 但如果說 WinRT Component 是 COM 的一種演化, 我想這是正確的. 由於 COM 被賦予不少期待所以它很複雜, 從 binary-level interface (在 WinRT 稱 abstract binary interface), system-wide activation, automatic memory management , reflection and Type Library (在 WinRT 為 Windows Runtime metadata), threading model (在 WinRT 稱 concurrency model) 到 out-of-process object 都是 COM 想一併解決的問題. 我猜 Windows Runtime Component 的目標與 COM 相近, 並且想簡化 COM 的複雜度.

Abstract Binary Interface
由於 WinRT Component 可以使用 Pure C++ 搭配 Windows Runtime C++ Template Library (WRL) 來實作, 所以 binary-level interface 跟 COM 是相同的, 換句話說就是跟 Visual C++ 的 VTable 完全同. 不過 ABI 限制了可以傳遞的 type, 所有可以傳遞的 type 必須是 Windows Runtime types. 概念上來說如果不是一個 POD type, 那就必須是一個實作 IInspectable interface 的 COM, IInspectable 是用來做為 language projecting 用的, 簡單的說 IInspectable 可以傳回該 object 所有支援的 interfaces (IInspectable::GetIids) 還有 object 本身的 class fully-qualified name (IInspectable::GetRuntimeClassName). 有了這兩個資訊就足夠查詢 Windows Runtime metadata (*.winmd). WRL::Details:RuntimeClass 會去實作 IInspectable 這個介面.


Object Activation
COM Activation (COM DLL)有幾個步驟 :

  1. COM DLL 可以經由呼叫 DllRegisterServer 來註冊 COM 到系統中
  2. 根據 CLSID 從 HKEY_CLASSES_ROOT\CLSID 找出 object 對應的 binary path.
  3. 載入 binary 並且呼叫 DllGetClassObject function 來取得 Class object.
  4. 呼叫 Class object 的 IClassFactory::CreateInstance 來 create object.
WinRT DLL Component  則是:
  1. 註冊 WinRT DLL Component 的方式跟 COM 很不相同, 我猜這跟 GAC 應該會很像.
    [Update 2011/11/09]
    目前了解是放在 Activation Store (registry)
    HKEY_CURRENT_USER\Software\Classes\ActivatableClasses
    HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\WindowsRuntime\ActivatableClassId
  2. 根據 activatable class ID 找到對應的 binary path
  3. 載入 binary 並且呼叫 DllGetActivationFactory function 來取得 factory.
  4. 呼叫 factory 的 IActivationFactory::ActivateInstance 來 create object.
目前我看到的 WinRT Component 都必須被 package 到 Metro style app 之中也就是 local side-by-side, 不知道微軟允不允許 WinRT Component 也能做為 cross application 的 share component.

Automatic Memory Management 
WinRT Component 採用的 automatic memory management 是與傳統 COM 一樣的 reference counting 而非 .NET Framework 的 mark and sweep garbage collector, 我想這是可想而知的, 因為 C++ 並沒有 runtime 的幫忙來 GC 所以必須靠每個 object 自己來管理. 由於 WinRT Component 被 JavaScript, C#, VB.NET 中被使用所以 reference cycles 這個重要的課題必須被解決, 所以微軟為 WinRT Component 引入了 weak reference 的能力, 基本上使用 WRL 實作的 component 由於繼承了 WRL::Details:RuntimeClass 所以都會實作 IWeakReferenceSource. 當然這必須是設計 component 的 developer 有意識的去使用 weak reference 才能解決 reference cycles 問題. 有興趣實作的人可以去看 WRL::Detial::WeakReference 這個 class 就能夠了解這是如何辦到的. 以下描述一個 reference cycles 如何使用 weak reference 來打斷這個 cycle, 假設有 A, B 兩個 objects:


Reflection, Projection 
IDL 是用來描述 COM interface 跟 class 的描述語言, 使用 C++ (非 C++/CX )實作 WinRT Component 還是必須使用 IDL 來描述 object 的 metadata, MIDL compiler 會根據 IDL 來產生 Type Library (TLB) 與 Windows Runtime metadata (winmd). 有趣的是你可以直接用 .NET Reflector 開啟 winmd 檔案. 以下是微軟 Metro style app 的 sample DLL server authoring sample


如果是使用 C++/CX 則它會自動產生 winmd 而不用去撰寫 IDL. Language Projection 完全仰賴 IInspectable 與 Windows Runtime metadata

Threading model
根據 WRL 的 source code 上來看 reference counter 已經確保是 thread-safety, 這有別於 ATL 的設計, 由於 WinRT component 沒有 registry 或是其他 metadata 來描述 COM 的 threading model, 所以我猜測 WinRT  已經不使用 apartment 的概念來管理 WinRT component, 換句話說, 可能所有的 thread 都是 MTA 所有的 component 都是 Free, 但是 , 令我訝異的是 RoInitialize 還是必須傳入, RO_INIT_SINGLETHREADED 或是 RO_INIT_MULTITHREADED, 來描述 thread 的 concurrency model. 目前我還不知道原因.

更讓我疑惑的是 old-style COM 該怎麼辦呢? 微軟文件上是說 old-style COM 在 Metro style app 下還是能夠有限度的被使用 (PS. 可以被使用的 COM CLSID 被記錄在HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\WindowsRuntime\AllowedCOMCLSIDs), 在使用 COM 之前還是必須呼叫 CoInitializeEx 且傳遞 COINIT_APARTMENTTHREADED 或 COINIT_MULTITHREADED 描述 thread 的 threading model, 根據我測試的結果 C++/CX and XAML 的 UI thread (不是 main thread 喔) 是 STA 並且內建了 message loop. 但如果有兩個 STA 要存取對方所擁有的 COM 時, Workder thread 所 host 的 COM 可能就無法正常運作, 因為我不知道怎麼在一個 Worker thread 實作一個 message loop 來推動 STA 下 Apartment 或是 Both 的 COM object, 因為 GetMessage/PeekMessage API 在 WinRT 已經不能用了.

[Update 2011/11/09]
看了這篇 Windows Runtime internals: understanding "Hello World" article 我才恍然大悟, Threading model 是存在的, 只不過 WinRT 隱藏了這些細節, WinRT 有一個預設的 Win32 exe 也就是 WWAHost.exe ,它被用來 host Metro style application. WWAHost.exe 也就是 application 的 Server, 以我所了解, host 可以是別的 exe, 這些資訊會被記錄在HKEY_CURRENT_USER\Software\Classes\ActivatableClasses\Server\XXX\ExePath. 根據我對這篇 article 的了解, WWAHost.exe 的第一個 thread 會是 MTA, 這個 thread 被用來做為 activation 之用. 所以 WWAHost!Host::Run 會去執行twinapi!Windows::ApplicationModel::Core::CoreApplicationFactory::RunWithBackgroundFactory 並且等待新的 activation request. 當新的 request 進來時, 它會 fork 一個新的 STA thread 並且執行 request 的需求. 這個需求可能任何一種 Contract ex: Launch, Search, BackgroundTasks ....

[Update 2011/11/25]
Introduction to background tasks - Guidelines for developers 對於 threading model 有特別的解釋
For non-JavaScript apps, the background tasks are hosted in an in-proc DLL which is loaded in a multi-threaded apartment (MTA) within the app. For JavaScript apps, background tasks are launched in a new single-threaded apartment (STA) within the WWA host process. The actual background task class can be STA or MTA. Because background tasks can run when an app is in a Suspended or Terminated state, they need to be decoupled from the foreground app. Loading the background task DLL in a separate apartment enforces separation of the background task from the app while allowing the background task to be controlled independently of the app.


Out-of-Process WinRT Component
依照 WRL 的 source code 的設計來看, WinRT Component 是允許 Out-of-Process 的形式, 但是我卻找不到實際的 sample, 如同上述的問題 GetMessage/PeekMessage API 已經不存在了, 這樣如何實作一個 Out-of-Process COM 呢?

[Update 2011/11/10]
我猜想 WinRT 可能提供類似 DLL Surrogates 的方式來實作 DLL Server, 也就是說我們不用寫一個 exe 來 host COM 而是使用 WinRT 提供既有的 host exe. 另外, WinRT 提供 Broker. 

還有許多問題需要釐清.... 如果我知道答案在分享出來 :D

Thursday, June 14, 2012

在ASP.NET MVC 4中使用Kendo UI Grid


之前寫過用ASP.NET WebForm作為AJAX式資料源的Kendo UI Grid範例,最近計劃在一個小專案試用ASP.NET MVC 4 RC,面對的第一個需求又是"以清單呈現查詢結果",就來看看如何用ASP.NET MVC 4 RC滿足Kendo UI Grid的需求吧!
記得前一次用ashx寫資料端,花了不少功夫處理分頁及排序,而且還沒實做Filter過濾功能。但在ASP.NET MVC上,要整合Kendo UI Grid,則有很酷的方便選擇 -- KendoGridBinder!!
以下是我實做Kendo UI Grid + ASP.NET MVC 4的過程:
  1. 建立一個ASP.NET MVC 4專案 
  2. 使用NuGet安裝KendoUIWeb及KendoGridBinder
  3. 借用上回的SimMemberInfo Model類別 ,放在Model目錄下: 
    排版顯示純文字
    using System;
    using System.Collections.Generic;
    using System.Drawing;
    using System.Linq;
    using System.Reflection;
    using System.Web;
     
    namespace KendoGridMvc.Models
    {
        //模擬資料物件
        public class SimMemberInfo
        {
            public string UserNo; //會員編號
            public string UserName; //會員名稱
            public DateTime RegDate; //註冊日期
            public int Points; //累積點數
     
            //模疑資料來源
            public static List<SimMemberInfo> SimuDataStore = null;
     
            static SimMemberInfo()
            {
                Random rnd = new Random();
                //借用具名顏色名稱來產生隨機資料
                string[] colorNames = typeof(Color)
                    .GetProperties(BindingFlags.Static | BindingFlags.Public)
                    .Select(o => o.Name).ToArray();
                SimuDataStore =
                    colorNames
                    .Select(cn => new SimMemberInfo()
                    {
                        UserNo = string.Format("C{0:00000}", rnd.Next(99999)),
                        UserName = cn,
                        RegDate = DateTime.Today.AddDays(-rnd.Next(1000)),
                        Points = rnd.Next(9999)
                    }).ToList();
            }
        }
    }
  4. 要引用Kendo UI,需要載入必要的JS及CSS,此時昨天介紹過的ASP.NET MVC打包壓縮功能馬上派上用場! 編輯App_Start/BundleConfig.cs,加入以下程式: 

    排版顯示純文字
                bundles.Add(new ScriptBundle("~/bundles/kendoUI").Include(
                    "~/Scripts/kendo/2012.1.322/kendo.web.min.js"
                    ));
    //經實測,SytleBundle virtualPath參數使用"2012.1.322"會有問題,故向上搬移一層
    //將/Content/kendo/2012.1.322的內容搬至Content/kendo下
                bundles.Add(new StyleBundle("~/Content/kendo/css").Include(
                    "~/Content/kendo/kendo.common.min.css",
                    "~/Content/kendo/kendo.blueopal.min.css"
                    ));
    PS: 此處有一個眉角:由於CSS檔路徑會被當成引用圖檔的基準,原本Kendo UI的.css及圖檔被放在~/Content/kendo/2012.1.322/下,理論上StyleBundle應設成"~/Content/kendo/2012.1.322/css”,才能引導瀏覽器到該目錄下取用圖檔。不幸地,我發現StyleBundle的virtualPath參數出現2012.1.322時,會導致Styles.Render("~/Content/kendo/2012.1.322/css”)時傳回HTTP 404錯誤~ 為克服問題,我決定將2012.1.322目錄的內容向上搬一層,直接放在~/Content/keno目錄下,並將virtualPath設成"~/Content/kendo/css",這樣就能避開問題。
  5. 為了省去每個View都要加掛Kendo UI JS及CSS的麻煩,我索性將它們加在~/Views/Shared/_Layout.cshtml中: 
    排版顯示純文字
    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width" />
        <title>@ViewBag.Title</title>
        @Styles.Render("~/Content/themes/base/css", "~/Content/css",
                       "~/Content/kendo/css")
        @Scripts.Render("~/bundles/modernizr")
        @Scripts.Render("~/bundles/jquery", "~/bundles/kendoUI")
        @RenderSection("scripts", required: false)
    </head>
    <body>
        @RenderBody()
    </body>
    </html>
  6. 網頁Index.cshtml的Client端做法,則跟上回WebForm AJAX範例幾乎完全相同:
    排版顯示純文字
    @section Scripts
    {
        <style>
            body { font-size: 9pt; }
            #dvGrid { width: 500px; }
            span.hi-lite { color: red; }
            #dvGrid th.k-header { text-align: center; }
        </style>
        <script>
            $(function () {
                //建立資料來源物件
                var dataSrc = new kendo.data.DataSource({
                    transport: {
                        read: {
                            //以下其實就是$.ajax的參數
                            type: "POST",
                            url: "/Home/Grid",
                            dataType: "json",
                            data: {
                                //額外傳至後方的參數
                                keywd: function () {
                                    return $("#tKeyword").val();
                                }
                            }
                        }
                    },
                    schema: {
                        //取出資料陣列
                        data: function (d) { return d.data; },
                        //取出資料總筆數(計算頁數用)
                        total: function (d) { return d.total; }
                    },
                    pageSize: 10,
                    serverPaging: true,
                    serverSorting: true
                });
                //JSON日期轉換
                var dateRegExp = /^\/Date\((.*?)\)\/$/;
                window.toDate = function (value) {
                    var date = dateRegExp.exec(value);
                    return new Date(parseInt(date[1]));
                }
                $("#dvGrid").kendoGrid({
                    dataSource: dataSrc,
                    columns: [
                        { field: "UserNo", title: "會員編號" },
                        { field: "UserName", title: "會員名稱",
                            template: '#= "<span class=\\"u-name\\">" + UserName + "</span>" #'
                        },
                        { field: "RegDate", title: "加入日期",
                            template: '#= kendo.toString(toDate(RegDate), "yyyy/MM/dd")#'
                        },
                        { field: "Points", title: "累積點數" },
                    ],
                    sortable: true,
                    pageable: true,
                    dataBound: function () {
                        //AJAX資料Bind完成後觸發
                        var kw = $("#tKeyword").val();
                        //若有設關鍵字,做Highlight處理
                        if (kw.length > 0) {
                            var re = new RegExp(kw, "g");
                            $(".u-name").each(function () {
                                var $td = $(this);
                                $td.html($td.text()
                               .replace(re, "<span class='hi-lite'>$&</span>"));
                            });
                        }
                    }
                });
                //按下查詢鈕
                $("#bQuery").click(function () {
                    //要求資料來源重新讀取(並指定切至第一頁)
                    dataSrc.read({ page: 1, skip: 0 });
                    //Grid重新顯示資料
                    $("#dvGrid").data("kendoGrid").refresh();
                });
            });
        </script>
    }
    <div style="padding: 10px;">
        關鍵字:
        <input id="tKeyword" /><input type="button" value="查詢" id="bQuery" />
    </div>
    <div id="dvGrid">
    </div>
  7. 最後來到重頭戲,負責以AJAX方式傳回資料的HomeController.cs的Grid() Action:
    排版顯示純文字
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Web;
    using System.Web.Mvc;
    using KendoGridBinder;
    using KendoGridMvc.Models;
     
    namespace KendoGridMvc.Controllers
    {
        public class HomeController : Controller
        {
            //
            // GET: /Home/
     
            public ActionResult Index()
            {
                return View();
            }
     
            public JsonResult Grid(KendoGridRequest request, string keywd)
            {
                var result = SimMemberInfo.SimuDataStore.Where(o =>
                string.IsNullOrEmpty(keywd) || o.UserName.Contains(keywd));
                return Json(new KendoGrid<SimMemberInfo>(request, result));
            }
        }
    }
    什麼? 沒看錯吧? 這樣就好? 
    是的,感謝Ryan Whitmire及Jose Ball的佛心與巧思,只要return Json(new KendoGrid<T>(KendoGridRequest, IEnumerable<T>)),餘下的換頁、排序,甚至欄位過濾功能,就都交給KendoGridBinder全權處理囉!
實際測試,換頁、排序功能一切正常,有了Kendo UI助陣,ASP.NET MVC之路順利多了~

No Threads for you ! (in metro style apps)


As would say this guy, since you’ve most probably been using threads the wrong way (as Microsoft seems to think), you won’t be able to use the Thread class anymore in Metro Style applications. The class is simply not available anymore, and neither are Timer or ThreadPool.
That may come a shock to you, but this actually makes a lot of sense. But don’t worry, the concept of parallel execution is still there, but it takes the form of Tasks.

Why using Threads is not good for you

Threads are very powerful but there are a lot of terrible gotchas that come with it :
  • Unhandled exceptions in threads handlers, either raised from a Timer, a Thread or ThreadPool thread, lead to the termination of the process
  • Using Abort is quite bad for the process, and should be avoided
  • People tend to use Thread.Sleep to arbitrarily wait for some constant time that will most probably be incorrect, and that will waste CPU resources to manage a thread that does not do anything while it waits,
  • People tend to come up with complex designs to chain operations on threads, which most of the time fail miserably.
There are some more, but these a main scenarios where using Threads fall short.
I’ve been advocating to stay away from Threads, at least not directly, for all these reasons (and more, but that’s out of scope here).

Using Task, exclusively

Since Microsoft went back to rethink some patterns that were introduced in the original BCL and CLR, they probably thought it was time to time to remove the Thread class in favor of the Task class, which does a far better job, and handles all the cases I listed above :
All these operations blend very nicely into the new async feature, for which it is very easy to wait on a Task.

ThreadPool and Timer moved to WinRT

The Thread Pool is still there actually, and so is the timer in the form of the class TheadPoolTimer, but they’ve both moved to the WinRT side.
ThreadPool is async awaitable, and supports priority, much like Task. As of now, I do not see a very good compelling reason to use it, since Task has a far greater feature set.
ThreadPoolTimer can still be interesting, though exceptions thrown in this context seem to be handled silently by WinRT, and I’ve yet to find where that goes. But I’d recommend not using it, in favor of theReactive Extensions' Observable.Timer which far more useful than this simple timer.

Thread left-overs

There’s actually one place where the “Threads” still surface in the BCL.
If we take this C# code :
?
1
2
3
4
public IEnumerable IteratorSample()
{
    yield return 1;
}
One feature of the compiler generated iterators is that they are explicitly not thread safe, and must be used on the thread they were generated on. The iterator is capturing the original thread, and is checking that subsequent calls stay on that thread.
So if we look at the what is internally expanded to an internal iterator class, using the C# compiler on .NET 4.0 and earlier :
?
1
2
3
4
5
6
[DebuggerHidden]
public d__0(int <>1__state)
{
   this.<>1__state = <>1__state;
   this.<>l__initialThreadId = Thread.CurrentThread.ManagedThreadId;
}
Whereas, using the .NET 4.5 C# compiler, this will be generated :
?
1
2
3
4
5
6
[DebuggerHidden]
public d__0(int <>1__state)
{
   this.<>1__state = <>1__state;
   this.<>l__initialThreadId = Environment.CurrentManagedThreadId;
}
The .NET 4.5 generated code is making use of the new Environment.CurrentManagedThreadId property, because that specific iterator feature needs to have access to the actual thread ID, even though the Thread class does not exist anymore.
Interesting, isn’t it ?
This has a very unfortunate effect, though. C# compiled by a compiler below .NET 4.5 is not binary compatible with the Metro Style apps BCL, and will not run without being recompiled. But that’s not a big deal, because most the .NET surface APIs have either changed (to be async only), moved (like the Reflection API) or simply removed (like the System.IO namespace that went into WinRT), so you would have to adapt your code anyway.
I’m guessing that the C# team had to fight for this feature to maintain compatibility and have the same behavior as in previous versions of C#. And I’m glad this property stayed, because I’ve been using it to log the ThreadID in my logging framework.

Happy WinRT'ing !