建立 C# 或 Visual Basic Windows 執行階段元件,並從 JavaScript 呼叫該元件的逐步解說

本逐步解說示範如何使用 .NET 搭配 Visual Basic 或 C#,建立您自己的 Windows 執行階段類型 (封裝於 Windows 執行階段元件中),以及如何從 JavaScript 通用 Windows 平台 (UWP) 應用程式呼叫元件。

Visual Studio 讓您可以輕鬆地在使用 C# 或Visual Basic 撰寫的 Windows 執行階段元件 (WRC) 專案中,製作和部署自己的自訂 Windows 執行階段類型,然後從 JavaScript 應用程式專案參考該 WRC,並從該應用程式使用這些自訂類型。

您的 Windows 執行階段類型可以在內部使用 UWP 應用程式允許的任何 .NET 功能。

您的類型成員在外部只可以針對其參數和傳回值公開 Windows 執行階段類型。 在您建置方案時,Visual Studio 會建置 .NET WRC 專案,然後執行可建立 Windows 中繼資料 (.winmd) 檔案的建置步驟。 這是您的 Windows 執行階段元件,Visual Studio 將其包含在您的應用程式中。

注意

.NET 會將一些常用的 .NET 類型 (如基本資料類型和集合類型),自動對應到其 Windows 執行階段對等用法。 這些 .NET 類型可用於 Windows 執行階段元件的公用介面,並且會對元件的使用者顯示為對應的 Windows 執行階段類型。 請參閱使用 C# 和 Visual Basic 建立 Windows 執行階段元件

先決條件:

注意

Visual Studio 2019 不支援使用 JavaScript 的通用 Windows 平台 (UWP) 專案。 請參閱 Visual Studio 2019 中的 JavaScript 和 TypeScript 若要跟著本主題操作,建議您使用 Visual Studio 2017。 請參閱 Visual Studio 2017 中的 JavaScript

建立一個簡單的 Windows 執行階段類別

本節會建立 JavaScript UWP 應用程式,並在解決方案中新增 Visual Basic 或 C# Windows 執行階段元件專案。 它示範如何定義 Windows 執行階段類型、從 JavaScript 建立該類型的執行個體,以及呼叫靜態成員和執行個體成員。 範例應用程式的視覺效果顯示會刻意低調呈現,以便將焦點保持在元件上。

  1. 在 Visual Studio 中,建立新的 JavaScript 專案:在功能表列上,依序選擇檔案、新增、專案。 在新專案對話方塊的已安裝範本區段中,選擇 JavaScript,然後選擇 Windows,接著選擇一般。 (如果 Windows 無法使用,請確認您使用的是 Windows 8 或更新版本。) 選擇空白應用程式範本,然後在專案名稱輸入 SampleApp。

  2. 建立元件專案:在方案總管中,開啟 SampleApp 解決方案的捷徑功能表,選擇新增,然後選擇新專案以將新的 C# 或 Visual Basic 專案新增至解決方案。 在新增專案對話方塊的已安裝範本區段中,選擇 Visual BasicVisual C#,然後選擇 Windows,接著選擇通用。 選擇 Windows 執行階段元件範本,然後在專案名稱輸入 SampleComponent

  3. 將類別的名稱變更為 Example。 請注意,預設情況下,該類別會標記為公用密封 (在 Visual Basic 中為 Public NotInheritable)。 從您的元件公開的所有 Windows 執行階段類別都必須密封。

  4. 在類別中加入兩個簡單成員:一個靜態方法 (在 Visual Basic 中為 Shared 方法),和一個執行個體屬性:

    namespace SampleComponent
    {
        public sealed class Example
        {
            public static string GetAnswer()
            {
                return "The answer is 42.";
            }
    
            public int SampleProperty { get; set; }
        }
    }
    
    Public NotInheritable Class Example
        Public Shared Function GetAnswer() As String
            Return "The answer is 42."
        End Function
    
        Public Property SampleProperty As Integer
    End Class
    
  5. 選用:若要為新增的成員啟用 IntelliSense,請在方案總管中開啟 SampleComponent 專案的捷徑功能表,然後選擇建置

  6. 在「方案總管」的 JavaScript 專案中,開啟參考的捷徑功能表,然後選擇新增參考以開啟參考管理員。 選擇 [專案],然後選擇 [解決方案]。 選取 SampleComponent 專案的核取方塊,然後選擇確定以新增參考。

從 JavaScript 呼叫元件

若要從 JavaScript 使用 Windows 執行階段類型,請在 Visual Studio 範本提供的 default.js 檔案 (位於專案的 js 資料夾中) 的匿名函式中新增以下程式碼。 它應該位於 app.oncheckpoint 事件處理常式之後,和對 app.start 的呼叫之前。

var ex;

function basics1() {
   document.getElementById('output').innerHTML =
        SampleComponent.Example.getAnswer();

    ex = new SampleComponent.Example();

   document.getElementById('output').innerHTML += "<br/>" +
       ex.sampleProperty;

}

function basics2() {
    ex.sampleProperty += 1;
    document.getElementById('output').innerHTML += "<br/>" +
        ex.sampleProperty;
}

請注意,每個成員名稱的第一個字母都從大寫變更為小寫。 此轉換是屬於 JavaScript 提供的支援,以實現在受控程式碼中自然使用 Windows 執行階段。 命名空間和類別名稱採用 Pascal 大小寫。 除事件名稱均為小寫外,成員名稱均以駝峰式命名。 請參閱在 JavaScript 中使用 Windows 執行階段。 駱駝大小寫的規則可能會造成混淆。 一連串的起始大寫通常顯示為小寫,但如果三個大寫字母後面跟著一個小寫字母,則只有前兩個字母顯示為小寫:例如,名為 IDStringKind 的成員顯示為 idStringKind。 在 Visual Studio 中,您可以建置 Windows 執行階段元件專案,然後在 JavaScript 專案中使用 IntelliSense 來查看正確的大小寫。

以類似的方式,.NET 提供支援以在受控程式碼中自然使用 Windows 執行階段。 這將在本文的後續各節,以及文章使用 C# 和 Visual Basic 的 Windows 執行階段元件.NET 對 UWP 應用程式和 Windows 執行階段的支援 中進行討論。

建立一個簡單的使用者介面

在您的 JavaScript 專案中,開啟 default.html 檔案並更新主體,如以下程式碼所示。 此程式碼包括範例應用程式的完整控制項集,並指定點擊事件的函式名稱。

注意:第一次執行應用程式時,只會支援 Basics1 和 Basics2 按鈕。

<body>
            <div id="buttons">
            <button id="button1" >Basics 1</button>
            <button id="button2" >Basics 2</button>

            <button id="runtimeButton1">Runtime 1</button>
            <button id="runtimeButton2">Runtime 2</button>

            <button id="returnsButton1">Returns 1</button>
            <button id="returnsButton2">Returns 2</button>

            <button id="events1Button">Events 1</button>

            <button id="btnAsync">Async</button>
            <button id="btnCancel" disabled="disabled">Cancel Async</button>
            <progress id="primeProg" value="25" max="100" style="color: yellow;"></progress>
        </div>
        <div id="output">
        </div>
</body>

在 JavaScript 專案的 css 資料夾中,開啟 default.css。 按照展示內容修改主體區段,並新增樣式來控制按鈕的版面配置和輸出文字的位置。

body
{
    -ms-grid-columns: 1fr;
    -ms-grid-rows: 1fr 14fr;
    display: -ms-grid;
}

#buttons {
    -ms-grid-rows: 1fr;
    -ms-grid-columns: auto;
    -ms-grid-row-align: start;
}
#output {
    -ms-grid-row: 2;
    -ms-grid-column: 1;
}

現在,透過將 then 子句新增至 default.js 中 app.onactivated 的 processAll 呼叫,來新增事件接聽程式註冊代碼。 取代現有呼叫 setPromise 的程式碼行,並將其變更為以下程式碼:

args.setPromise(WinJS.UI.processAll().then(function () {
    var button1 = document.getElementById("button1");
    button1.addEventListener("click", basics1, false);
    var button2 = document.getElementById("button2");
    button2.addEventListener("click", basics2, false);
}));

與直接在 HTML 中新增點擊事件處理常式相比,用此方法向 HTML 控制項新增事件更好。 請參閱建立 "Hello, World" 應用程式 (JS)

建置並執行應用程式

在建置之前,請根據您的電腦的情況,將所有專案的目標平台變更為 Arm、x64 或 x86。

若要建置並執行解決方案,請選擇 F5 鍵。 (如果您收到一則執行階段錯誤訊息,指出 SampleComponent 未定義,則表示缺少對類別庫專案的參考。)

Visual Studio 會先編譯類別庫,然後執行 MSBuild 工作,該工作會執行 Winmdexp.exe (Windows 執行階段中繼資料匯出工具) 來建立 Windows 執行階段元件。 該元件包含在 .winmd 檔案中,其中包含受控程式碼和描述程式碼的 Windows 中繼資料。 當您撰寫在 Windows 執行階段元件中無效的程式碼時,WinMdExp.exe 會產生產生錯誤訊息,並且錯誤訊息會顯示在 Visual Studio IDE 中。 Visual Studio 會將元件新增至 UWP 應用程式的應用程式套件 (.appx 檔案) 中,並產生適當的清單。

選擇 [Basics 1] 按鈕,將靜態 GetAnswer 方法的傳回值指派給輸出區域,建立 Example 類別的執行個體,並在輸出區域中顯示其 SampleProperty 屬性的值。 輸出會如下所示:

"The answer is 42."
0

選擇 [Basics 2] 按鈕以增加 SampleProperty 屬性的值,並在輸出區域中顯示新值。 字串和數字等基本類型可以用作參數類型和傳回類型,並且可以在受控程式碼和 JavaScript 之間傳遞。 由於 JavaScript 中的數字以雙精確度浮點格式儲存,因此它們會轉換為 .NET Framework 數字類型。

注意:預設情況下,您只能在 JavaScript 程式碼中設定斷點。 若要偵錯 Visual Basic 或 C# 程式碼,請參閱在 C# 和 Visual Basic 中建立 Windows 執行階段元件。

若要停止偵錯並關閉應用程式,請從應用程式切換到 Visual Studio,然後選擇 Shift+F5。

透過 JavaScript 和受控程式碼使用 Windows 執行階段

您可以從 JavaScript 或受控程式碼呼叫 Windows 執行階段。 Windows 執行階段物件可以在兩者之間來回傳遞,並且可以從任一端處理事件。 但是,在兩個環境中使用 Windows 執行階段類型的方式在某些細節上有所不同,因為 JavaScript 和 .NET 對 Windows 執行階段的支援不同。 以下範例使用 Windows.Foundation.Collections.PropertySet 類別示範這些差異。 在此範例中,您會在受控程式碼中建立 PropertySet 集合的執行個體,並註冊事件處理常式以追蹤集合中的變更。 然後新增 JavaScript 程式碼來取得該集合、註冊自己的事件處理常式並使用該集合。 最後要新增一個方法,以從受控程式碼變更集合,並顯示 JavaScript 處理受控例外狀況。

重要:在此範例中,會在 UI 執行緒上觸發事件。 如果您從背景執行緒觸發事件 (例如在非同步呼叫中),則需要執行一些額外的工作才能讓 JavaScript 處理該事件。 如需詳細資訊,請參閱 在 Windows 執行階段元件中引發事件

在 SampleComponent 專案中,新增一個名為 PropertySetStats 的新公用密封類別 (在 Visual Basic 中為 Public NotInheritable 類別)。 此類別會包裝 PropertySet 集合,並處理其 MapChanged 事件。 事件處理常式會追蹤發生的每種變更的數量,並且 DisplayStats 方法會產生 HTML 格式的報告。 請注意附加的 using 陳述式 (在 Visual Basic 中為 Imports 陳述式); 請小心地將其加入現有的 using 陳述式中,而不是覆寫它們。

using Windows.Foundation.Collections;

namespace SampleComponent
{
    public sealed class PropertySetStats
    {
        private PropertySet _ps;
        public PropertySetStats()
        {
            _ps = new PropertySet();
            _ps.MapChanged += this.MapChangedHandler;
        }

        public PropertySet PropertySet { get { return _ps; } }

        int[] counts = { 0, 0, 0, 0 };
        private void MapChangedHandler(IObservableMap<string, object> sender,
            IMapChangedEventArgs<string> args)
        {
            counts[(int)args.CollectionChange] += 1;
        }

        public string DisplayStats()
        {
            StringBuilder report = new StringBuilder("<br/>Number of changes:<ul>");
            for (int i = 0; i < counts.Length; i++)
            {
                report.Append("<li>" + (CollectionChange)i + ": " + counts[i] + "</li>");
            }
            return report.ToString() + "</ul>";
        }
    }
}
Imports System.Text

Public NotInheritable Class PropertySetStats
    Private _ps As PropertySet
    Public Sub New()
        _ps = New PropertySet()
        AddHandler _ps.MapChanged, AddressOf Me.MapChangedHandler
    End Sub

    Public ReadOnly Property PropertySet As PropertySet
        Get
            Return _ps
        End Get
    End Property

    Dim counts() As Integer = {0, 0, 0, 0}
    Private Sub MapChangedHandler(ByVal sender As IObservableMap(Of String, Object),
        ByVal args As IMapChangedEventArgs(Of String))

        counts(CInt(args.CollectionChange)) += 1
    End Sub

    Public Function DisplayStats() As String
        Dim report As New StringBuilder("<br/>Number of changes:<ul>")
        For i As Integer = 0 To counts.Length - 1
            report.Append("<li>" & CType(i, CollectionChange).ToString() &
                          ": " & counts(i) & "</li>")
        Next
        Return report.ToString() & "</ul>"
    End Function
End Class

事件處理常式會遵循常見的 .NET Framework 事件模式,不同之處在於事件的傳送者 (在本例中為 PropertySet 物件) 強制轉換為 IObservableMap<string, object> 介面 (在Visual Basic 中為IObservableMap(Of String, Object)),其為 Windows 執行階段介面 IObservableMap<K, V> 的具現化。 (如果需要,您可以將傳送者轉換為其類型。) 此外,事件參數會以介面而不是物件的形式呈現。

在 default.js 檔案中,新增 Runtime1 函式,如下所示。 此程式碼會建立一個 PropertySetStats 物件,取得其 PropertySet 集合,並加入自己的事件處理常式 onMapChanged 函式來處理 MapChanged 事件。 對集合進行變更後,runtime1 會呼叫 DisplayStats 方法來顯示變更類型的摘要。

var propertysetstats;

function runtime1() {
    document.getElementById('output').innerHTML = "";

    propertysetstats = new SampleComponent.PropertySetStats();
    var propertyset = propertysetstats.propertySet;

    propertyset.addEventListener("mapchanged", onMapChanged);

    propertyset.insert("FirstProperty", "First property value");
    propertyset.insert("SuperfluousProperty", "Unnecessary property value");
    propertyset.insert("AnotherProperty", "A property value");

    propertyset.insert("SuperfluousProperty", "Altered property value")
    propertyset.remove("SuperfluousProperty");

    document.getElementById('output').innerHTML +=
        propertysetstats.displayStats();
}

function onMapChanged(change) {
    var result
    switch (change.collectionChange) {
        case Windows.Foundation.Collections.CollectionChange.reset:
            result = "All properties cleared";
            break;
        case Windows.Foundation.Collections.CollectionChange.itemInserted:
            result = "Inserted " + change.key + ": '" +
                change.target.lookup(change.key) + "'";
            break;
        case Windows.Foundation.Collections.CollectionChange.itemRemoved:
            result = "Removed " + change.key;
            break;
        case Windows.Foundation.Collections.CollectionChange.itemChanged:
            result = "Changed " + change.key + " to '" +
                change.target.lookup(change.key) + "'";
            break;
        default:
            break;
     }

     document.getElementById('output').innerHTML +=
         "<br/>" + result;
}

在 JavaScript 中處理 Windows 執行階段事件的方式,與在 .NET Framework 程式碼中處理它們的方式差異極大。 JavaScript 事件處理常式僅採用一個引數。 當您在 Visual Studio 偵錯工具中查看此物件時,第一個屬性是傳送者。 事件參數介面的成員也直接出現在該物件上。

若要執行該應用程式,請選擇 F5 鍵。 如果類別未密封,您會收到錯誤訊息「目前不支援匯出未密封類型『SampleComponent.Example』。 請將其標記為密封。」

選擇 Runtime 1 按鈕。 事件處理常式會顯示元素新增或變更時的變更內容,最後呼叫 DisplayStats 方法來產生計數摘要。 若要停止偵錯並關閉應用程式,請切換回 Visual Studio 並選擇 Shift+F5。

若要從受控程式碼將另外兩個專案新增至 PropertySet 集合,請將以下程式碼新增至 PropertySetStats 類別:

public void AddMore()
{
    _ps.Add("NewProperty", "New property value");
    _ps.Add("AnotherProperty", "A property value");
}
Public Sub AddMore()
    _ps.Add("NewProperty", "New property value")
    _ps.Add("AnotherProperty", "A property value")
End Sub

此程式碼會醒目顯示在兩種環境中使用 Windows 執行階段類型的方式的另一個差異。 如果您自己鍵入此程式碼,您會注意到 IntelliSense 不會顯示您在 JavaScript 程式碼中使用的插入方法。 相反地,它會顯示 .NET 集合中常見的 Add 方法。 這是因為一些常用的集合介面在 Windows 執行階段和 .NET 中具有不同的名稱,但其功能相似。 當您在受控程式碼中使用這些介面時,它們將顯示為其 .NET Framework 對等介面。 這會在使用 C# 和 Visual Basic 的 Windows 執行階段元件中進行討論。 當您在 JavaScript 中使用相同的介面時,Windows 執行階段唯一的變更是成員名稱開頭的大寫字母變成小寫。

最後,若要呼叫具有例外狀況處理的 AddMore 方法,請將 runtime2 函式新增至 default.js。

function runtime2() {
   try {
      propertysetstats.addMore();
    }
   catch(ex) {
       document.getElementById('output').innerHTML +=
          "<br/><b>" + ex + "<br/>";
   }

   document.getElementById('output').innerHTML +=
       propertysetstats.displayStats();
}

按照與之前相同的方式新增事件處理常式註冊代碼。

var runtimeButton1 = document.getElementById("runtimeButton1");
runtimeButton1.addEventListener("click", runtime1, false);
var runtimeButton2 = document.getElementById("runtimeButton2");
runtimeButton2.addEventListener("click", runtime2, false);

若要執行該應用程式,請選擇 F5 鍵。 選擇 Runtime 1,然後選擇 Runtime 2。 JavaScript 事件處理常式會報告集合的第一個變更。 不過,第二個變更具有重複的索引鍵。 .NET Framework 字典的使用者預期 Add 方法會擲回例外狀況,而事實正是如此。 JavaScript 會處理 .NET 例外狀況。

注意:您無法從 JavaScript 程式碼顯示例外狀況訊息。 訊息文字會由堆疊追蹤取代。 如需詳細資訊,請參閱使用 C# 和 Visual Basic 建立 Windows 執行階段元件中的「擲回例外狀況」。

相較之下,當 JavaScript 使用重複索引鍵呼叫 insert 方法時,項目的值會變更。 這種行為差異是由於 JavaScript 和 .NET 支援 Windows 執行階段的方式不同造成的,如使用 C# 和 Visual Basic 的 Windows 執行階段元件中所述。

從元件傳回受控類型

如前所述,您可以在 JavaScript 程式碼和 C# 或 Visual Basic 程式碼之間自由地來回傳遞原生 Windows 執行階段類型。 大多數時候,類型名稱和成員名稱在兩種情況下都是相同的 (除了 JavaScript 中成員名稱以小寫字母開頭)。 然而,在上一節中,PropertySet 類別在受控程式碼中似乎有不同的成員。 (例如,您在 JavaScript 中呼叫了 insert 方法,而在 .NET 程式碼中呼叫了 Add 方法。) 本節探討這些差異如何影響傳遞給 JavaScript 的 .NET Framework 類型。

除了傳回在元件中建立的或從 JavaScript 傳遞到元件的 Windows 執行階段類型之外,您還可以將在受控程式碼中建立的受控類型傳回 JavaScript,就像它是對應的 Windows 執行階段類型一樣。 即使在執行階段類別的第一個簡單範例中,成員的參數和傳回類型也是 Visual Basic 或 C# 基本類型,它們是 .NET Framework 類型。 若要在集合中示範這一點,請將以下程式碼新增到範例類別中,以建立傳回通用字串字典 (依整數索引) 的方法:

public static IDictionary<int, string> GetMapOfNames()
{
    Dictionary<int, string> retval = new Dictionary<int, string>();
    retval.Add(1, "one");
    retval.Add(2, "two");
    retval.Add(3, "three");
    retval.Add(42, "forty-two");
    retval.Add(100, "one hundred");
    return retval;
}
Public Shared Function GetMapOfNames() As IDictionary(Of Integer, String)
    Dim retval As New Dictionary(Of Integer, String)
    retval.Add(1, "one")
    retval.Add(2, "two")
    retval.Add(3, "three")
    retval.Add(42, "forty-two")
    retval.Add(100, "one hundred")
    Return retval
End Function

請注意,字典必須以由 Dictionary<TKey, TValue> 實作的介面傳回,並對應到 Windows 執行階段介面。 在本例中,介面為 IDictionary<int, string> (在 Visual Basic 中為 IDictionary(Of Integer, String))。 當 Windows 執行階段類型 IMap<int, string> 傳遞給受控程式碼時,它顯示為 IDictionary<int, string>,而當受控類型傳遞給 JavaScript 時,反之亦然。

重要:當受控類型實作多個介面時,JavaScript 會使用清單中第一個出現的介面。 例如,如果您向 JavaScript 程式碼傳回 Dictionary<int, string>,則無論您指定哪個介面作為傳回類型,它都會顯示為 IDictionary<int, string>。 這代表如果第一個介面不包含出現在後續介面上的成員,則該成員就不會顯示在 JavaScript 中。

 

若要測試新方法並使用字典,請將 returns1 和 returns2 函式新增至default.js:

var names;

function returns1() {
    names = SampleComponent.Example.getMapOfNames();
    document.getElementById('output').innerHTML = showMap(names);
}

var ct = 7;

function returns2() {
    if (!names.hasKey(17)) {
        names.insert(43, "forty-three");
        names.insert(17, "seventeen");
    }
    else {
        var err = names.insert("7", ct++);
        names.insert("forty", "forty");
    }
    document.getElementById('output').innerHTML = showMap(names);
}

function showMap(map) {
    var item = map.first();
    var retval = "<ul>";

    for (var i = 0, len = map.size; i < len; i++) {
        retval += "<li>" + item.current.key + ": " + item.current.value + "</li>";
        item.moveNext();
    }
    return retval + "</ul>";
}

將事件註冊代碼新增至與其他事件註冊代碼相同的 then 區塊:

var returnsButton1 = document.getElementById("returnsButton1");
returnsButton1.addEventListener("click", returns1, false);
var returnsButton2 = document.getElementById("returnsButton2");
returnsButton2.addEventListener("click", returns2, false);

關於這段 JavaScript 程式碼,有些值得觀察的部分。 首先,它包含一個 showMap 函式,用於以 HTML 形式顯示字典的內容。 在 showMap 的程式碼中,請注意迭代模式。 在 .NET 中,泛型 IDictionary 介面上沒有 First 方法,且大小由 Count 屬性而非 Size 方法傳回。 對於 JavaScript,IDictionary<int, string> 似乎是 Windows 執行階段類型 IMap<int, string>。 (請參閱 IMap<K,V> 介面。)

在 returns2 函式中,如前面的範例所示,JavaScript 呼叫 Insert 方法 (在 JavaScript 中為 insert) 以將專案新增到字典中。

若要執行該應用程式,請選擇 F5 鍵。 若要建立並顯示字典的初始內容,請選擇 Returns 1 按鈕。 若要將兩個專案新增至字典,請選擇 Returns 2 按鈕。 請注意,項目會依插入順序顯示,正如您對 Dictionary<TKey, TValue> 所預期的那樣 如果您想進行排序,可以從 GetMapOfNames 傳回一個 SortedDictionary<int, string>。 (先前範例中使用的 Property Set 類別具有與 Dictionary<TKey, TValue> 不同的內部組織。)

當然,JavaScript 不是強型別語言,因此使用強型別泛型集合可能會導致意料之外的結果。 再次選擇 Returns 2 按鈕。 JavaScript 會強制將「7」轉換為數字 7,並將儲存在 ct 中的數字 7 強制轉換為字串。 它會將字串「forty」強制變為零。 但這只是開始。 多選擇幾次 Returns 2 按鈕。 在受控程式碼中,即使值已轉換為正確的類型,Add 方法也會產生重複的鍵例外狀況。 相反地,Insert 方法會更新與現有索引鍵關聯的值,並傳回布林值,該值指示是否將新索引鍵新增到字典中。 這就是為什麼與索引鍵 7 關聯的值會不斷變化的原因。

另一個意外行為:如果將未指派的 JavaScript 變數作為字串參數傳遞,則會得到字串「undefined」。 簡而言之,在將 .NET Framework 集合類型傳遞給 JavaScript 程式碼要特別注意。

注意:如果需要連接大量文字,則可以透過將程式碼移至 .NET Framework 方法,並使用 StringBuilder 類別來更有效地完成操作,如 showMap 函式所示。

儘管無法從 Windows 執行階段元件公開自己的泛型類型,但可以使用以下程式碼傳回 Windows 執行階段類別的 .NET Framework 泛型集合:

public static object GetListOfThis(object obj)
{
    Type target = obj.GetType();
    return Activator.CreateInstance(typeof(List<>).MakeGenericType(target));
}
Public Shared Function GetListOfThis(obj As Object) As Object
    Dim target As Type = obj.GetType()
    Return Activator.CreateInstance(GetType(List(Of )).MakeGenericType(target))
End Function

List<T> 實作了 IList<T>,其在 JavaScript 中顯示為 Windows 執行階段類型 IVector<T>。

宣告事件

您可以使用標準 .NET Framework 事件模式,或 Windows 執行階段所使用的其他模式來宣告事件。 .NET Framework 支援 System.EventHandler<TEventArgs> 委派和 Windows 執行階段 EventHandler<T> 委派之間的對等性,因此使用 EventHandler<TEventArgs> 是實現標準 .NET Framework 模式的好方法。 若要了解其運作情形,請將以下類別組新增至 SampleComponent 專案中:

namespace SampleComponent
{
    public sealed class Eventful
    {
        public event EventHandler<TestEventArgs> Test;
        public void OnTest(string msg, long number)
        {
            EventHandler<TestEventArgs> temp = Test;
            if (temp != null)
            {
                temp(this, new TestEventArgs()
                {
                    Value1 = msg,
                    Value2 = number
                });
            }
        }
    }

    public sealed class TestEventArgs
    {
        public string Value1 { get; set; }
        public long Value2 { get; set; }
    }
}
Public NotInheritable Class Eventful
    Public Event Test As EventHandler(Of TestEventArgs)
    Public Sub OnTest(ByVal msg As String, ByVal number As Long)
        RaiseEvent Test(Me, New TestEventArgs() With {
                            .Value1 = msg,
                            .Value2 = number
                            })
    End Sub
End Class

Public NotInheritable Class TestEventArgs
    Public Property Value1 As String
    Public Property Value2 As Long
End Class

當您在 Windows 執行階段中公開事件時,事件參數類別會從 System.Object 繼承。 它不像在 .NET 中那樣從 System.EventArgs 繼承,因為 EventArgs 不是 Windows 執行階段類型。

如果為事件宣告自訂事件存取子 (在 Visual Basic 中為 Custom 關鍵字),則必須使用 Windows 執行階段事件模式。 請參閱 Windows 執行階段元件中的自訂事件和事件存取子

若要處理 Test 事件,請將 events1 函式新增至 default.js。 events1 函式為 Test 事件建立事件處理函式,並立即呼叫 OnTest 方法來引發事件。 如果在事件處理常式的主體中放置斷點,您可以看到傳遞給單一參數的物件包括來源物件和 TestEventArgs 的兩個成員。

var ev;

function events1() {
   ev = new SampleComponent.Eventful();
   ev.addEventListener("test", function (e) {
       document.getElementById('output').innerHTML = e.value1;
       document.getElementById('output').innerHTML += "<br/>" + e.value2;
   });
   ev.onTest("Number of feet in a mile:", 5280);
}

將事件註冊代碼新增至與其他事件註冊代碼相同的 then 區塊:

var events1Button = document.getElementById("events1Button");
events1Button.addEventListener("click", events1, false);

公開非同步作業

.NET Framework 擁有一組豐富的工具,用於根據 Task 和通用 Task<TResult> 類別進行非同步處理和平行處理。 若要在 Windows 執行階段元件中公開基於工作的非同步處理,請使用 Windows 執行階段介面 IAsyncActionIAsyncActionWithProgress<TProgress>IAsyncOperation<TResult>IAsyncOperationWithProgress<TResult, TProgress>。 (在 Windows 執行階段中,作業會傳回結果,但動作不會傳回結果。)

本節示範了一個可取消的非同步作業,該作業會報告進度並傳回結果。 GetPrimesInRangeAsync 方法使用 AsyncInfo 類別產生工作,並將其取消和進度報告功能連接到 WinJS.Promise 物件。 首先,將 GetPrimesInRangeAsync 方法新增至範例類別:

using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Foundation;

public static IAsyncOperationWithProgress<IList<long>, double>
GetPrimesInRangeAsync(long start, long count)
{
    if (start < 2 || count < 1) throw new ArgumentException();

    return AsyncInfo.Run<IList<long>, double>((token, progress) =>

        Task.Run<IList<long>>(() =>
        {
            List<long> primes = new List<long>();
            double onePercent = count / 100;
            long ctProgress = 0;
            double nextProgress = onePercent;

            for (long candidate = start; candidate < start + count; candidate++)
            {
                ctProgress += 1;
                if (ctProgress >= nextProgress)
                {
                    progress.Report(ctProgress / onePercent);
                    nextProgress += onePercent;
                }
                bool isPrime = true;
                for (long i = 2, limit = (long)Math.Sqrt(candidate); i <= limit; i++)
                {
                    if (candidate % i == 0)
                    {
                        isPrime = false;
                        break;
                    }
                }
                if (isPrime) primes.Add(candidate);

                token.ThrowIfCancellationRequested();
            }
            progress.Report(100.0);
            return primes;
        }, token)
    );
}
Imports System.Runtime.InteropServices.WindowsRuntime

Public Shared Function GetPrimesInRangeAsync(ByVal start As Long, ByVal count As Long)
As IAsyncOperationWithProgress(Of IList(Of Long), Double)

    If (start < 2 Or count < 1) Then Throw New ArgumentException()

    Return AsyncInfo.Run(Of IList(Of Long), Double)( _
        Function(token, prog)
            Return Task.Run(Of IList(Of Long))( _
                Function()
                    Dim primes As New List(Of Long)
                    Dim onePercent As Long = count / 100
                    Dim ctProgress As Long = 0
                    Dim nextProgress As Long = onePercent

                    For candidate As Long = start To start + count - 1
                        ctProgress += 1

                        If ctProgress >= nextProgress Then
                            prog.Report(ctProgress / onePercent)
                            nextProgress += onePercent
                        End If

                        Dim isPrime As Boolean = True
                        For i As Long = 2 To CLng(Math.Sqrt(candidate))
                            If (candidate Mod i) = 0 Then
                                isPrime = False
                                Exit For
                            End If
                        Next

                        If isPrime Then primes.Add(candidate)

                        token.ThrowIfCancellationRequested()
                    Next
                    prog.Report(100.0)
                    Return primes
                End Function, token)
        End Function)
End Function

GetPrimesInRangeAsync 是一個非常簡單的質數尋找器,這是設計好的。 這裡的重點是實現非同步作業,因此重點是要簡單,當我們示範取消時,緩慢的實作會是優勢。 GetPrimesInRangeAsync 透過暴力破解找出質數:它會將候選數除以所有小於或等於其平方根的整數,而不是只使用質數。 逐步執行此程式碼:

  • 在開始非同步作業之前,執行內務處理活動,例如驗證參數和針對無效輸入擲回例外狀況。

  • 此實作的重點在於 AsyncInfo.Run<TResult, TProgress>(Func<CancellationToken, IProgress<TProgress>, Task<TResult>>) 方法,以及作為該方法唯一參數的委派。 委派必須接受取消權杖和用於報告進度的介面,並且必須傳回使用這些參數的已啟動工作。 當 JavaScript 呼叫 GetPrimesInRangeAsync 方法時,會發生以下步驟 (不一定依照此處提供的順序):

    • WinJS.Promise 物件會提供處理傳回結果、對取消做出反應以及處理進度報告的函式。

    • AsyncInfo.Run 方法會建立取消來源和實作 IProgress<T> 介面的物件。 對於委派,它傳遞來自取消來源的 CancellationToken 權杖和 IProgress<T> 介面。

      注意:如果 Promise 物件不提供對取消做出反應的函式,AsyncInfo.Run 仍會傳遞可取消標記,且取消仍可能發生。 如果 Promise 物件不提供處理進度更新的函式,AsyncInfo.Run 仍提供實作 IProgress<T> 的物件,但其報告將被忽略。

    • 委派會使用 Task.Run<TResult>(Func<TResult>, CancellationToken) 方法建立使用權杖和進度介面的已啟動工作。 已啟動工作的委派由計算所需結果的 lambda 函式提供。 稍後再詳細說明。

    • AsyncInfo.Run 方法會建立實作 IAsyncOperationWithProgress<TResult, TProgress> 介面的物件,將 Windows 執行階段取消機制與權杖來源連接起來,並將 Promise 物件的進度報告函式與 IProgress<T> 介面連接起來。

    • IAsyncOperationWithProgress<TResult, TProgress> 介面會傳回給 JavaScript。

  • 由已啟動工作表示的 lambda 函式不會採用任何引數。 因為它是 lambda 函式,所以它可以存取權杖和 IProgress 介面。 每次計算候選數時,lambda 函式會:

    • 檢查進度是否已達到下一個百分點。 如果已達到,則 lambda 函式會呼叫 IProgress<T>.Report 方法,並將百分比傳遞給 Promise 物件指定用於報告進度的函式。
    • 如果作業已取消,則使用取消標記擲回例外狀況。 如果呼叫了 IAsyncInfo.Cancel 方法 (IAsyncOperationWithProgress<TResult, TProgress> 介面繼承的方法),則 AsyncInfo.Run 方法建立的連線將確保通知取消權杖。
  • 當 lambda 函式傳回質數清單時,該清單將傳遞給 WinJS.Promise 物件指定用於處理結果的函式。

若要建立 JavaScript Promise 並設定取消機制,請將 asyncRun 和 asyncCancel 函式新增至 default.js。

var resultAsync;
function asyncRun() {
    document.getElementById('output').innerHTML = "Retrieving prime numbers.";
    btnAsync.disabled = "disabled";
    btnCancel.disabled = "";

    resultAsync = SampleComponent.Example.getPrimesInRangeAsync(10000000000001, 2500).then(
        function (primes) {
            for (i = 0; i < primes.length; i++)
                document.getElementById('output').innerHTML += " " + primes[i];

            btnCancel.disabled = "disabled";
            btnAsync.disabled = "";
        },
        function () {
            document.getElementById('output').innerHTML += " -- getPrimesInRangeAsync was canceled. -- ";

            btnCancel.disabled = "disabled";
            btnAsync.disabled = "";
        },
        function (prog) {
            document.getElementById('primeProg').value = prog;
        }
    );
}

function asyncCancel() {    
    resultAsync.cancel();
}

像之前一樣記下事件註冊碼。

var btnAsync = document.getElementById("btnAsync");
btnAsync.addEventListener("click", asyncRun, false);
var btnCancel = document.getElementById("btnCancel");
btnCancel.addEventListener("click", asyncCancel, false);

透過呼叫非同步 GetPrimesInRangeAsync 方法,asyncRun 函式建立 WinJS.Promise 物件。 該物件的 then 方法採用三個函式來處理傳回的結果、對錯誤做出反應 (包括取消) 以及處理進度報告。 在本例中,傳回的結果會列印在輸出區域中。 取消或完成會重設啟動和取消作業的按鈕。 進度報告更新進度控制。

asyncCancel 函式只是呼叫 WinJS.Promise 物件的 cancel 方法。

若要執行該應用程式,請選擇 F5 鍵。 若要啟動非同步作業,請選擇非同步按鈕。 接下來的情況取決於您電腦的速度。 如果進度條在瞬間就完成,請將傳遞給 GetPrimesInRangeAsync 的起始數字的大小增加一個或多十倍。 您可以透過增加或減少要測試的數字計數來微調作業的持續時間,但在起始數字中間增加零會產生更大的影響。 若要取消作業,請選擇 取消非同步 按鈕。