C# 또는 Visual Basic Windows 런타임 구성 요소를 만들고 JavaScript에서 호출하는 연습

이 연습에서는 Visual Basic 또는 C#과 함께 .NET을 사용하여 Windows 런타임 구성 요소로 패키지된 고유한 Windows 런타임 형식을 만드는 방법 및 JavaScript를 사용하여 JavaScript UWP(유니버설 Windows 플랫폼) 앱에서 구성 요소를 호출하는 방법을 보여 줍니다.

Visual Studio를 사용하면 간편하게 사용자 지정 Windows 런타임 형식을 작성하고 C# 또는 Visual Basic으로 작성된 WRC(Windows 런타임 구성 요소) 프로젝트 내에 배포한 다음, JavaScript 애플리케이션 프로젝트에서 해당 WRC를 참조하고 해당 애플리케이션에서 사용자 지정 형식을 사용할 수 있습니다.

내부적으로 Windows 런타임 형식은 UWP 애플리케이션에 허용되는 모든 .NET 기능을 사용할 수 있습니다.

참고 항목

자세한 내용은 C# 및 Visual Basic이 포함된 Windows 런타임 구성 요소UWP 앱용 .NET 개요를 참조하세요.

외부적으로 형식의 멤버는 매개 변수 및 반환 값에 대한 형식만 공개할 수 있습니다. 솔루션을 빌드할 때 Visual Studio는 .NET WRC 프로젝트를 빌드한 다음, Windows 메타데이터(.winmd) 파일을 만드는 빌드 단계를 실행합니다. 이 Windows 런타임 구성 요소는 Visual Studio가 앱 안에 포함시켜놓았습니다.

참고 항목

.NET은 기본 데이터 형식이나 컬렉션 형식처럼 널리 사용되는 일부 .NET 형식을 해당 Windows 런타임 형식에 자동으로 매핑합니다. 이러한 .NET 형식은 Windows 런타임 구성 요소의 공용 인터페이스에 사용할 수 있으며, 구성 요소 사용자에게 해당 Windows 런타임 형식으로 표시됩니다. C# 및 Visual Basic이 포함된 Windows 런타임 구성 요소를 참조하세요.

필수 조건:

참고 항목

Visual Studio 2019에서는 JavaScript를 사용하는 UWP(유니버설 Windows 플랫폼) 프로젝트가 지원되지 않습니다. 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 Basic 또는 Visual C#을 선택하고 Windows, 유니버설을 차례대로 선택합니다. Windows 런타임 구성 요소(유니버설 Windows) 템플릿을 선택하고 프로젝트 이름으로 SampleComponent를 입력합니다.

  3. 클래스 이름을 Example으로 변경합니다. 기본적으로 클래스는 봉인된 퍼블릭으로 표시됩니다 (Visual Basic에서는 Public NotInheritable). 구성 요소에서 공개하는 모든 Windows 런타임 클래스를 봉인해야 합니다.

  4. 클래스에 두 개의 단순 멤버, 정적 메서드(Visual Basic의 공유 메서드) 및 인스턴스 속성을 추가합니다.

    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 런타임을 자연스럽게 사용할 수 있습니다. 네임스페이스 및 클래스 이름은 파스칼식으로 되어있습니다. 모두 소문자인 이벤트 이름을 제외하고, 멤버 이름은 카멜식으로 되어있습니다. JavaScript에서 Windows 런타임 사용하기를 참고하세요. 카멜식 케이스 규칙은 혼동을 초래할 수 있습니다. 일련의 초기 대문자는 일반적으로 소문자로 표시하지만, 대문자 세 개 뒤에 소문자가 나오는 경우 첫 두 문자만 소문자로 나타냅니다. 예를 들어 IDStringKind라는 멤버는 idStringKind로 나타냅니다. Visual Studio에서 Windows 런타임 구성 요소 프로젝트를 빌드한 다음 JavaScript 프로젝트에서 IntelliSense를 사용하여 올바른 대/소문자를 확인할 수 있습니다.

유사한 방식으로, .NET은 관리 코드에서 Windows 런타임을 자연스럽게 사용할 수 있도록 지원합니다. 이에 대해서는 이 문서의 후속 단원과 C# 및 Visual Basic이 포함된 Windows 런타임 구성 요소UWP 앱 및 Windows 런타임에 대한 .NET 지원 문서에 설명되어 있습니다.

단순 사용자 인터페이스 만들기

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;
}

이제 default.js에서 app.onactivated의 processAll 호출에 then 절을 추가하여 이벤트 수신기 등록 코드를 추가합니다. 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는 먼저 클래스 라이브러리를 컴파일한 후 Winmdexp.exe (Windows Runtime Metadata Export Tool)를 실행하는 MSBuild 작업을 실행하여 런타임 구성 요소를 만듭니다. 구성 요소는 관리 코드와 코드를 설명하는 Windows 메타데이터를 모두 포함하는 .winmd 파일에 포함됩니다. WinMdExp.exe는 Windows 런타임 구성 요소에서 유효하지 않은 코드를 작성할 때 빌드 오류 메시지를 생성하고, 해당 오류 메시지는 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 런타임 객체는 두 개 사이에서 앞뒤로 전달될 수 있으며 어느 쪽에서든 이벤트를 처리할 수 있습니다. 그러나 JavaScript 및 .NET은 Windows 런타임을 다르게 지원하므로 두 환경에서 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

이벤트 처리기는 이벤트를 보낸 사람(이 경우 PropertySet 개체)이 Windows 런타임 인터페이스 IObservableMap<K, V>의 인스턴스화인 IObservableMap<string, object> 인터페이스(Visual Basic의 경우 IObservableMap(Of String, Object))로 캐스팅된다는 점을 제외하고 익숙한 .NET Framework 이벤트 패턴을 따릅니다. (필요한 경우 발신자를 해당 타입으로 캐스팅할 수 있습니다.) 또한 이벤트 인수는 객체가 아닌 인터페이스로 표시됩니다.

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'를 내보내는 것은 현재 지원되지 않습니다라는 오류 메시지가 뜹니다. 봉인됨"으로 표시하세요.

런타임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 런타임 타입을 사용하는 방식의 또 다른 차이점을 강조 표시합니다. 이 코드를 직접 입력하면, JavaScript 코드에 사용한 삽입 메서드가 IntelliSense에서는 표시되지 않습니다. 대신 .NET에서 컬렉션에 일반적으로 표시되는 Add 메서드를 표시합니다. 이는 Windows 런타임 및 .NET에서 자주 사용하는 일부 컬렉션 인터페이스가 이름은 다르지만 기능이 유사하기 때문입니다. 관리 코드에서 해당 인터페이스를 사용하면 .NET Framework와 동등한 인터페이스로 표시됩니다. 이 내용에 대해서는 C# 및 Visual Basic이 포함된 Windows 런타임 구성 요소에서 설명합니다. JavaScript에서 동일한 인터페이스를 사용하는 경우, 멤버 이름의 시작 부분에 있는 대문자가 소문자로 바뀌는 것이 Windows 런타임에서의 유일한 변경 내용입니다.

마지막으로 예외 처리를 사용하여 AddMore 메서드를 호출하려면, default.js runtime2 함수를 추가합니다.

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 키를 선택하여 앱을 실행합니다. 런타임 1을 선택한 다음 런타임 2를 선택합니다. JavaScript 이벤트 처리기는 컬렉션에 첫 번째 변경 사항을 보고합니다. 그러나 두 번째 변경에는 중복 키가 있습니다. .NET Framework 사전의 사용자는 Add 메서드로 예외가 발생할 것으로 예상하며, 실제로 발생합니다. JavaScript는 .NET 예외를 처리합니다.

참고 JavaScript 코드에서는 예외의 메시지를 표시할 수 없습니다. 메시지 텍스트는 스택 추적으로 대체됩니다. 자세한 내용은 C# 및 Visual Basic에서 Windows 런타임 구성 요소 만들기에서 “예외 발생”을 참조하세요.

반면 JavaScript가 중복 키를 사용하여 insert 메서드를 호출했을 때 항목의 값이 변경되었습니다. 이러한 동작의 차이는 C# 및 Visual Basic이 포함된 Windows 런타임 구성 요소에 설명된 대로 JavaScript 및 .NET에서 Windows 런타임을 지원하는 방법이 서로 다르기 때문입니다.

구성 요소에서 관리 타입 반환하기

앞에서 설명한 것처럼 JavaScript 코드와 C# 또는 Visual Basic 코드 간에 네이티브 Windows 런타임 타입을 자유롭게 전달할 수 있습니다. 대부분의 경우, 타입 이름과 멤버 이름은 두 경우 모두 동일합니다(JavaScript에서 멤버 이름이 소문자로 시작하는 경우는 제외). 그러나 이전 섹션에서 PropertySet 클래스는 관리 코드에 다른 멤버가 있는 것처럼 보였습니다. (예를 들어 JavaScript에서 insert 메서드를 호출했고 .NET 코드에서 Add 메서드를 호출했습니다.) 이 섹션에서는 이러한 차이점이 JavaScript에 전달된 .NET Framework 타입에 미치는 영향을 살펴봅니다.

구성 요소에서 만들거나 JavaScript에서 구성 요소에 전달한 Windows 런타임 타입을 반환하는 것 외에도, 관리 코드로 만든 관리 타입을 해당 Windows 런타임 타입인 것처럼 JavaScript에 반환할 수 있습니다. 런타임 클래스의 첫 번째 간단한 예제에서도, 멤버의 매개 변수 및 반환 타입은 .NET Framework 타입인 Visual Basic 또는 C# 기본 타입이었습니다. 컬렉션에 대해 이를 보여 주려면, 다음 코드를 Example 클래스에 추가하여 정수로 인덱싱된 문자열의 제네릭 사전을 반환하는 메서드를 만듭니다.

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가 사용하는 인터페이스는 목록에 가장 먼저 표시됩니다. 예를 들어 Dictionaryint,<string을> JavaScript 코드로 반환하는 경우 반환 타입으로 지정한 인터페이스와 관계없이 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>";
}

이벤트 등록 코드를 다른 이벤트 등록 코드와 동일한 블록에 추가합니다.

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

이 JavaScript 코드에 대해 몇 가지 흥미로운 사항을 관찰할 수 있습니다. 우선, 사전 내용을 HTML에 표시하는 showMap 함수가 포함되어 있습니다. showMap에 대한 코드에서 반복 패턴을 확인합니다. .NET에서 일반 IDictionary 인터페이스에는 First 메서드가 없으며, Size 메서드가 아니라 Count 속성에 의해 크기가 반환됩니다. JavaScript에서, IDictionary<int, string은> Windows 런타임 타입 IMap int, string으<로 나타납니다>. (IMap K,V 인터페이스<를> 참조하세요.)

앞의 예제와 같이, returns2 함수에서 JavaScript는 Insert 메서드(JavaScript에 삽입)를 호출하여 사전에 항목을 추가합니다.

F5 키를 선택하여 앱을 실행합니다. 사전의 초기 내용을 만들고 표시하려면 반환 1 버튼을 선택합니다. 사전에 두 개의 항목을 더 추가하려면 반환 2 버튼을 선택합니다. Dictionary<TKey, TValue에서 예상한 대로, 삽입 순서대로 항목이 표시됩니다>. 정렬하려는 경우 GetMapOfNames에서 SortedDictionary<int, string을> 반환할 수 있습니다. (이전 예제에서 사용된 PropertySet 클래스에는 Dictionary TKey, TValue<와 다른 내부 조직이 있습니다>.)

물론 JavaScript는 강력한 타입의 언어가 아니므로 강력한 타입의 제네릭 컬렉션을 사용하여 놀라운 결과를 도출할 수도 있습니다. Returns 2 버튼을 다시 선택합니다. JavaScript에서 "7"을 숫자 7로 강제 변환하고 ct에 저장된 숫자 7을 문자열로 강제 변환합니다. 그리고 문자열 "40"을 0으로 강제 변환합니다. 이는 시작에 불과합니다. Returns 2 버튼을 몇 번 더 선택합니다. 관리 코드에서, 값이 올바른 타입으로 캐스팅된 경우에도 Add 메서드는 중복 키 예외를 생성합니다. 반면, Insert 메서드는 기존 키와 연관된 값을 업데이트하고 새 키가 사전에 추가되었는지 여부를 나타내는 부울 값을 반환합니다. 이 때문에 키 7과 연관된 값이 계속 변경됩니다.

또 다른 예기치 않은 동작: 할당되지 않은 JavaScript 변수를 문자열 인수로 전달하는 경우, "undefined" 문자열을 가져옵니다. 즉, .NET Framework 컬렉션 타입을 JavaScript 코드에 전달할 때는 주의해야 합니다.

참고 연결할 텍스트가 많은 경우, showMap 함수에 표시된 것처럼 코드를 .NET Framework 메서드로 이동하고 StringBuilder 클래스를 사용하여 보다 효율적으로 연결할 수 있습니다.

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는> JavaScript에서 Windows 런타임 타입 IVector<T로 표시>되는 IList<T를> 구현합니다.

이벤트 선언하기

표준 .NET Framework 이벤트 패턴 또는 Windows 런타임 사용되는 다른 패턴을 사용하여 이벤트를 선언할 수 있습니다. .NET Framework는 System.EventHandler<TEventArgs> 대리자와 Windows 런타임 EventHandler<T> 대리자 간의 동등성을 지원하므로, EventHandler<TEventArgs를 사용하는 것은> 표준 .NET Framework pattern을 구현하는 좋은 방법입니다. 작동 방식을 확인하려면, 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로부터 이벤트 인수 클래스가 상속됩니다. EventArgs는 Windows 런타임 형식이 아니기 때문에 .NET과 같이 System.EventArgs에서 상속되지 않습니다.

이벤트에 대한 사용자 지정 이벤트 접근자(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);
}

이벤트 등록 코드를 다른 이벤트 등록 코드와 동일한 블록에 추가합니다.

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

비동기 오퍼레이션 노출하기

.NET Framework에는 Task 및 제네릭 Task<TResult> 클래스를 기반으로 하는 비동기 처리 및 병렬 처리를 위한 다양한 도구 세트가 있습니다. Windows 런타임 구성 요소에서 태스크 기반 비동기 처리를 노출하려면 Windows 런타임 인터페이스 IAsyncAction, IAsyncActionWithProgress<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 토큰과 IProgressT 인터페이스<를> 대리자에게 모두 전달합니다.

      참고 Promise 객체가 취소에 대응하는 함수를 제공하지 않는 경우에도, AsyncInfo.Run은 여전히 취소 가능한 토큰을 전달하며 취소가 계속 발생할 수 있습니다. Promise 객체가 진행률 업데이트를 처리하는 함수를 제공하지 않는 경우에도, AsyncInfo.Run은 여전히 IProgress<T>를 구현하는 객체를 제공하지만 해당 보고서는 무시됩니다.

    • 대리자는 Task.Run<TResult>(Func<TResult>, CancellationToken) 메서드를 통해 토큰과 진행률 인터페이스를 사용하는 시작된 작업을 만듭니다. 시작된 작업의 대리자는 원하는 결과를 계산하는 람다 함수에 의해 제공됩니다. 잠시 후에 자세히 설명하겠습니다.

    • AsyncInfo.Run 메서드는 IAsyncOperationWithProgressTResult, TProgress 인터페이스<를> 구현하는 개체를 만들고, Windows 런타임 취소 메커니즘을 토큰 소스와 연결하고, Promise 객체의 진행률 보고 함수를 IProgress<T> 인터페이스와 연결합니다.

    • IAsyncOperationWithProgress<TResult, TProgress> 인터페이스가 JavaScript로 반환됩니다.

  • 시작된 태스크에서 나타내는 람다 함수는 인수를 사용하지 않습니다. 람다 함수이므로 토큰 및 IProgress 인터페이스에 액세스할 수 있습니다. 후보 번호를 평가할 때마다 람다 함수는 다음과 같습니다.

    • 다음 진행률 지점에 도달했는지 여부를 확인합니다. 있는 경우, 람다 함수는 IProgress<T>.Report 메서드를 호출하며, 백분율은 Promise 객체가 진행률을 보고하기 위해 지정한 함수로 전달됩니다.
    • 오퍼레이션이 취소된 경우, 취소 토큰을 사용하여 예외 던지기를 합니다. 만약 IAsyncInfo.Cancel 메서드(IAsyncOperationWithProgress<TResult, TProgress> 인터페이스가 상속)가 호출된 경우 AsyncInfo.Run 메서드가 설정한 연결은 취소 토큰에 알림을 받도록 합니다.
  • 람다 함수가 소수 목록을 반환하면, 결과를 처리하기 위해 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 객체를 만듭니다. 객체의 다음 메서드는 반환된 결과를 처리하고 오류(취소 포함)에 대응하고 진행률 보고서를 처리하는 세 가지 함수를 사용합니다. 이 예제에서는 반환된 결과가 출력 영역에 출력됩니다. 취소 또는 완료는 오퍼레이션을 시작하고 취소하는 버튼을 다시 설정합니다. 진행률 보고는 진행률 컨트롤을 업데이트합니다.

asyncCancel 함수는 WinJS.Promise 객체의 취소 메서드를 호출합니다.

F5 키를 선택하여 앱을 실행합니다. 비동기 오퍼레이션을 시작하려면 비동기 버튼을 선택합니다. 컴퓨터 속도에 따라 다음에 어떤 일이 일어날 지가 달라집니다. 진행률 표시줄이 깜빡일 시간도 없이 완료될 경우 GetPrimesInRangeAsync에 전달되는 시작 번호의 크기를 십진법으로 한 자리 이상으로 늘립니다. 테스트할 숫자 수를 늘리거나 줄여 작업 기간을 미세 조정할 수 있지만 시작 번호 중간에 0을 추가하면 영향이 더 커집니다. 오퍼레이션을 취소하려면 비동기 취소 버튼을 선택합니다.