Tutorial para crear un componente de Windows Runtime de C# o Visual Basic y llamarlo desde JavaScript

En este tutorial se muestra cómo puede usar .NET con Visual Basic o C# para crear sus propios tipos de Windows Runtime, empaquetados en un componente de Windows Runtime y cómo llamar a ese componente desde una aplicación de Plataforma universal de Windows javaScript (UWP).

Visual Studio facilita la creación e implementación de sus propios tipos de Windows Runtime personalizados dentro de un proyecto de componente de Windows Runtime (WRC) escrito con C# o Visual Basic y, a continuación, hacer referencia a ese WRC desde un proyecto de aplicación javaScript y consumir esos tipos personalizados de esa aplicación.

Internamente, los tipos de Windows Runtime pueden usar cualquier funcionalidad de .NET permitida en una aplicación para UWP.

Externamente, los miembros de los tipos pueden exponer solo los tipos de Windows Runtime para sus parámetros y valores devueltos. Cuando se compila una solución, Visual Studio compila el proyecto de WRC de .NET y, después, ejecuta un paso de compilación que crea un archivo de metadatos de Windows (.winmd). Este es el componente de Windows Runtime que Visual Studio incluye en tu aplicación.

Nota

.NET asigna automáticamente algunos de los tipos de .NET más usados (como tipos de datos primitivos y tipos de colección) a sus equivalentes de Windows Runtime. Estos tipos de .NET pueden usarse en la interfaz pública de un componente de Windows Runtime y se mostrarán a los usuarios del componente como los tipos correspondientes de Windows Runtime. Consulte Componentes de Windows Runtime con C# y Visual Basic.

Requisitos previos:

Nota

los proyectos de Plataforma universal de Windows (UWP) que usan JavaScript no se admiten en Visual Studio 2019. Consulte JavaScript y TypeScript en Visual Studio 2019. Para seguir este tema, se recomienda usar Visual Studio 2017. Consulte JavaScript en Visual Studio 2017.

Creación de una clase simple de Windows Runtime

En esta sección se crea una aplicación para UWP de JavaScript y se agrega a la solución un proyecto de componente de visual Basic o C# Windows Runtime. Muestra cómo definir un tipo de Windows Runtime, crear una instancia del tipo a partir de JavaScript y llamar a miembros estáticos e de instancia. La presentación visual de la aplicación de ejemplo es deliberadamente baja para mantener el foco en el componente.

  1. En Visual Studio, crea un nuevo proyecto de JavaScript: en la barra de menús, selecciona Archivo, Nuevo, Proyecto. En la sección Plantillas instaladas del cuadro de diálogo Nuevo proyecto, selecciona JavaScripty, a continuación, elige Windows y después Universal. (Si Windows no está disponible, asegúrate de que estás usando Windows 8 o versiones posteriores). Elige la plantilla Aplicación vacía y escribe SampleApp como nombre del proyecto.

  2. Crea el proyecto de componente: en el Explorador de soluciones, abre el menú contextual para la solución SampleApp y selecciona Agregary, a continuación, elige Nuevo proyecto para agregar un nuevo proyecto de C# o Visual Basic a la solución. En la sección Plantillas instaladas del cuadro de diálogo Agregar nuevo proyecto, selecciona Visual Basic o Visual C#y, a continuación, elige Windows y después Universal. Selecciona la plantilla Componente de Windows Runtime y escribe SampleComponent en el nombre del proyecto.

  3. Cambia el nombre de la clase por Example. Ten en cuenta que, de manera predeterminada, la clase se marca como public sealed (Public NotInheritable en Visual Basic). Todas las clases de Windows Runtime que expongas desde tu componente deben estar selladas.

  4. Agrega dos miembros simples a la clase, un método static (método Shared en Visual Basic) y una propiedad de instancia:

    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. Opcional: para habilitar IntelliSense en los miembros recién agregados, en el Explorador de soluciones, abra el menú contextual del proyecto SampleComponent y, a continuación, elija Compilación.

  6. En el Explorador de soluciones, en el proyecto de JavaScript, abre el menú contextual de Referencias y, a continuación, elige Agregar referencia para abrir el Administrador de referencias. Elige Proyectos y, a continuación, Solución. Seleccione la casilla de verificación del proyecto SampleComponent y elija Aceptar para agregar una referencia.

Llamar al componente desde JavaScript

Para usar el tipo de Windows Runtime desde JavaScript, agrega el código siguiente a la función anónima del archivo default.js (en la carpeta js del proyecto) que se proporciona en la plantilla de Visual Studio. Debería hacerse después del controlador de eventos app.oncheckpoint y antes de la llamada a 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;
}

Ten en cuenta que la primera letra del nombre de cada miembro pasa de mayúsculas a minúsculas. Esta transformación es parte de la compatibilidad que proporciona JavaScript para habilitar el uso natural de Windows Runtime. Los espacios de nombres y los nombres de clases utilizan la convención de mayúsculas y minúsculas de Pascal. Los nombres de miembros utilizan la convención de mayúsculas y minúsculas Camel, excepto para los nombres de eventos, que van todos en minúsculas. Consulta Uso de Windows Runtime en JavaScript. Las reglas de la convención de mayúsculas y minúsculas Camel pueden resultar confusas. Una serie de letras mayúsculas iniciales aparece normalmente en minúsculas, pero si hay tres letras en mayúsculas seguidas de una letra en minúscula, solo las dos primeras letras aparecen en minúsculas: por ejemplo, un miembro denominado IDStringKind aparece como idStringKind. En Visual Studio, puedes compilar el proyecto de componente de Windows Runtime y después utilizar IntelliSense en tu proyecto de JavaScript para ver las mayúsculas y minúsculas correctamente.

De forma similar, .NET proporciona compatibilidad para habilitar el uso natural de la Windows Runtime en código administrado. Esto se describe en las secciones posteriores de este artículo y en los artículos Windows Runtime componentes con C# y Visual Basic y .NET compatibles con aplicaciones para UWP y la Windows Runtime.

Crear una interfaz de usuario sencilla

En el proyecto de JavaScript, abre el archivo default.html y actualiza el cuerpo tal como se muestra en el código siguiente. Este código incluye el conjunto completo de controles de la aplicación de ejemplo y especifica los nombres de función de los eventos de clic.

Nota Cuando se ejecuta por primera vez la aplicación, solo se admiten el botón Basics1 y 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>

En el proyecto de JavaScript, en la carpeta css, abre default.css. Modifica la sección del cuerpo, tal como se muestra, y agrega estilos para controlar el diseño de los botones y la colocación del texto de salida.

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

Ahora, agrega el código de registro del agente de escucha de eventos agregando una cláusula "then" a la llamada processAll en app.onactivated del archivo default.js. Reemplaza la línea de código existente que llama a setPromise y cámbialo por el código siguiente:

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

Esta es una mejor manera de agregar eventos a los controles HTML que agregando un controlador de eventos clic directamente en formato HTML. Consulta Crear una aplicación "Hola mundo" (JS).

Compilación y ejecución de la aplicación

Antes de compilar, cambie la plataforma de destino de todos los proyectos a Arm, x64 o x86, según corresponda para el equipo.

Para compilar y ejecutar la solución, presiona la tecla F5. (Si recibes un mensaje de error en tiempo de ejecución que indica que SampleComponent no está definido, falta la referencia al proyecto de la biblioteca de clases).

Visual Studio compila primero la biblioteca de clases y, a continuación, ejecuta una tarea de MSBuild que ejecuta Winmdexp.exe (herramienta de exportación de metadatos de Windows Runtime) para crear el componente de Windows Runtime. El componente se incluye en un archivo .winmd que contiene el código administrado y los metadatos de Windows que describen el código. WinMdExp.exe genera mensajes de error de compilación cuando escribes código que no es válido en un componente de Windows Runtime, y los mensajes de error se muestran en el IDE de Visual Studio. Visual Studio agrega el componente al paquete de la aplicación (archivo .appx) para la aplicación para UWP y genera el manifiesto adecuado.

Selecciona el botón Basics 1 para asignar el valor de devolución desde el método estático GetAnswer en el área de salida, crear una instancia de la clase Example y mostrar el valor de la propiedad SampleProperty en el área de salida. El resultado se muestra aquí:

"The answer is 42."
0

Selecciona el botón Basics 2 para aumentar el valor de la propiedad SampleProperty y mostrar el nuevo valor en el área de salida. Pueden utilizarse tipos primitivos, como cadenas y números, como tipos de parámetros y tipos devueltos y se pueden pasar entre código administrado y JavaScript. Como los números en JavaScript se almacenan en formato de punto flotante de precisión doble, se convierten en tipos numéricos de .NET Framework.

Nota De forma predeterminada, solo puede establecer puntos de interrupción en el código JavaScript. Para depurar el código de Visual Basic o C#, vea Creación de componentes de Windows Runtime en C# y Visual Basic.

Para detener la depuración y cerrar la aplicación, pasa de la aplicación a Visual Studio y presiona Mayús + F5.

Uso de Windows Runtime desde JavaScript y código administrado

Windows Runtime se puede llamar desde JavaScript o código administrado. Los objetos de Windows Runtime se pueden pasar del uno al otro, y los eventos se pueden controlar desde cualquier lado. Sin embargo, las formas en que se usan Windows Runtime tipos en los dos entornos difieren en algunos detalles, ya que JavaScript y .NET admiten la Windows Runtime de forma diferente. En el ejemplo siguiente se muestran estas diferencias mediante la clase Windows.Foundation.Collections.PropertySet. En este ejemplo, puedes crear una instancia de la colección PropertySet en código administrado y registrar un controlador de eventos para controlar los cambios en la colección. A continuación, puedes agregar el código de JavaScript que obtiene la colección, registra su propio controlador de eventos y utiliza la colección. Por último, puedes agregar un método que realiza cambios en la colección desde el código administrado y muestra cómo JavaScript controla una excepción administrada.

Importante En este ejemplo, el evento se desencadena en el subproceso de la interfaz de usuario. Si desencadenas el evento desde un subproceso en segundo plano, por ejemplo en una llamada asincrónica, deberás realizar algún proceso adicional para que JavaScript controle el evento. Para obtener más información, vea Generar eventos en componentes de Windows Runtime.

En el proyecto SampleComponent, agrega una nueva clase public sealed (clase Public NotInheritable en Visual Basic) denominada PropertySetStats. La clase encapsula una colección PropertySet y controla su evento MapChanged. El controlador de eventos realiza un seguimiento del número de cambios de cada tipo que se producen, y el método DisplayStats genera un informe con formato HTML. Ten en cuenta la instrucción adicional using (instrucción Imports en Visual Basic); asegúrate de agregarla a las instrucciones using existentes en lugar de sobrescribirlas.

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

El controlador de eventos sigue el conocido patrón de eventos de .NET Framework, excepto que el remitente del evento (en este caso, el objeto PropertySet) se convierte en la cadena IObservableMap<, la interfaz de objeto> (IObservableMap(Of String, Object) en Visual Basic), que es una instancia de la interfaz Windows Runtime IObservableMap<K, V>. (Puedes convertir el remitente a su tipo si es necesario). Además, los argumentos del evento se presentan como una interfaz en lugar de como un objeto.

En el archivo default.js, agrega la función Runtime1 tal como se muestra. Este código crea un objeto PropertySetStats, obtiene su colección PropertySet y agrega su propio controlador de eventos, la función onMapChanged, para controlar el evento MapChanged. Después de realizar cambios en la colección, runtime1 llama al método DisplayStats para mostrar un resumen de los tipos de cambio.

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

La forma de controlar los eventos de Windows Runtime en JavaScript es muy diferente de la forma de controlarlos en código de .NET Framework. El controlador de eventos de JavaScript solo utiliza un argumento. Cuando ves este objeto en el depurador de Visual Studio, la primera propiedad es el remitente. Los miembros de la interfaz del argumento de evento también aparecen directamente en este objeto.

Para ejecutar la aplicación, presiona la tecla F5. Si no se sella la clase, recibirás el mensaje de error "Exporting unsealed type 'SampleComponent.Example' is not currently supported. Márcalo como sellado."

Selecciona el botón Runtime 1. El controlador de eventos muestra los cambios a medida que se agregan o cambian los elementos y al final se llama al método DisplayStats para generar un resumen de recuentos. Para detener la depuración y cerrar la aplicación, vuelve a Visual Studio y presiona Mayús + F5.

Para agregar dos elementos más a la colección PropertySet desde el código administrado, agrega el código siguiente a la clase 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

Este código resalta otra diferencia en la manera de utilizar los tipos de Windows Runtime en los dos entornos. Si escribes este código tú mismo, te darás cuenta de que IntelliSense no muestra el método "insert" que has utilizado en el código de JavaScript. En su lugar, muestra el método Add que se suele ver en colecciones en .NET. Esto se debe a que algunas interfaces de colección usadas normalmente tienen nombres diferentes, pero una funcionalidad similar en el Windows Runtime y .NET. Cuando utilizas estas interfaces en código administrado, aparecen como sus equivalentes de .NET Framework. Esto se describe en Windows Runtime componentes con C# y Visual Basic. Cuando utilizas las mismas interfaces en JavaScript, el único cambio respecto a Windows Runtime es que las letras mayúsculas al principio de los nombres de los miembros se convierten en minúsculas.

Por último, para llamar al método AddMore con control de excepciones, agrega la función runtime2 a default.js.

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

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

Agrega el código de registro del controlador de eventos tal como lo has hecho anteriormente.

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

Para ejecutar la aplicación, presiona la tecla F5. Elige Runtime 1 y después Runtime 2. El controlador de eventos de JavaScript notifica el primer cambio en la colección. El segundo cambio, sin embargo, tiene una clave duplicada. Los usuarios de los diccionarios de .NET Framework esperan que el método Add produzca una excepción, y esto es lo que sucede. JavaScript controla la excepción de .NET.

Nota No se puede mostrar el mensaje de la excepción desde código JavaScript. El texto del mensaje se reemplaza por un seguimiento de la pila. Para obtener más información, vea "Iniciar excepciones" en Creación de componentes de Windows Runtime en C# y Visual Basic.

Por el contrario, cuando JavaScript llama al método "insert" con una clave duplicada, se cambia el valor del elemento. Esta diferencia de comportamiento se debe a las distintas formas en que JavaScript y .NET admiten el Windows Runtime, como se explica en Windows Runtime componentes con C# y Visual Basic.

Devolver tipos administrados desde el componente

Como se ha explicado anteriormente, puedes pasar tipos nativos de Windows Runtime libremente entre el código de JavaScript y el código de C# o Visual Basic. La mayoría de las veces, los nombres de tipos y los nombres de miembros serán los mismos en ambos casos (salvo que los nombres de miembros empiecen con letras en minúscula en JavaScript). Sin embargo, en la sección anterior, parecía que la clase PropertySet tiene diferentes miembros en código administrado. (Por ejemplo, en JavaScript llamó al método insert y, en el código .NET, llamó al método Add). En esta sección se explora la forma en que esas diferencias afectan a los tipos de .NET Framework pasados a JavaScript.

Además de devolver los tipos de Windows Runtime que creaste en tu componente o que pasaste a tu componente desde JavaScript, puedes devolver un tipo administrado, creado en código administrado, a JavaScript como si fuera el tipo de Windows Runtime correspondiente. Incluso en el primer y sencillo ejemplo de una clase en tiempo de ejecución, los parámetros y los tipos devueltos de los miembros eran tipos primitivos de Visual Basic o C#, que son tipos de .NET Framework. Para demostrarlo en las colecciones, agrega el código siguiente a la clase Example para crear un método que devuelva un diccionario genérico de cadenas indexado por enteros:

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

Tenga en cuenta que el diccionario debe devolverse como una interfaz implementada por Dictionary<TKey, TValue> y que se asigna a una interfaz Windows Runtime. En este caso, la interfaz es IDictionary<int, string> [IDictionary(Of Integer, String) en Visual Basic]. Cuando se pasa el tipo de Windows Runtime IMap<int, string> a código administrado, aparece como IDictionary<int, string>, y sucede justo lo contrario cuando el tipo administrado se pasa a JavaScript.

Importante Cuando un tipo administrado implementa varias interfaces, JavaScript usa la interfaz que aparece primero en la lista. Por ejemplo, si devuelves Dictionary<int, string> al código JavaScript, aparece como IDictionary<int, string> independientemente de qué interfaz especifiques como tipo devuelto. Esto significa que si la primera interfaz no incluye a un miembro que aparece en las últimas interfaces, ese miembro no es visible para JavaScript.

 

Para probar el nuevo método y utilizar el diccionario, agrega las funciones returns1 y returns2 a 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>";
}

A continuación, agrega el código de registro de eventos al mismo bloque que el otro código de registro de eventos:

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

Se deben tener en cuenta algunos aspectos interesantes sobre este código de JavaScript. En primer lugar, incluye una función showMap para mostrar el contenido del diccionario en HTML. En el código de showMap, observa el patrón de iteración. En .NET, no hay ningún método First en la interfaz IDictionary genérica y un método Size devuelve el tamaño en lugar de con un método Size. En JavaScript, IDictionary<int, string> parece que es el tipo de Windows Runtime IMap<int, string>. (Consulta la interfaz IMap<K,V>).

En la función returns2, al igual que en ejemplos anteriores, JavaScript llama al método Insert ("insert" en JavaScript) para agregar elementos al diccionario.

Para ejecutar la aplicación, presiona la tecla F5. Para crear y mostrar el contenido inicial del diccionario, selecciona el botón Returns 1. Para agregar dos entradas más al diccionario, selecciona el botón Returns 2. Ten en cuenta que las entradas se muestran en orden de inserción, como cabría esperar de Dictionary<TKey, TValue>. Si deseas ordenarlas, puedes devolver un SortedDictionary<int, string> desde GetMapOfNames. (La clase PropertySet que se usa en ejemplos anteriores presenta una organización interna distinta de Dictionary<TKey, TValue>).

Por supuesto, JavaScript no es un lenguaje fuertemente tipado, por lo que el uso de colecciones genéricas fuertemente tipadas puede provocar resultados incoherentes. Vuelve a seleccionar el botón Returns 2. JavaScript convierte el número "7" en un 7 numérico, y el 7 numérico que se almacena en ct, en una cadena. Y convierte la cadena "cuarenta" en cero. Pero eso es solo el principio. Selecciona el botón Returns 2 unas cuantas veces más. En código administrado, el método Add generaría excepciones de clave duplicada, incluso aunque los valores se convirtieran a los tipos correctos. En cambio, el método Insert actualiza el valor asociado a una clave existente y devuelve un valor booleano que indica si se ha agregado una nueva clave al diccionario. Es por eso que el valor asociado a la clave de 7 va cambiando.

Otro comportamiento inesperado: si pasas una variable sin asignar de JavaScript como un argumento de cadena, lo que obtienes es la cadena "undefined". En resumen, ten cuidado cuando pases tipos de la colección de .NET Framework a código JavaScript.

Nota Si tiene grandes cantidades de texto para concatenar, puede hacerlo de forma más eficaz moviendo el código a un método de .NET Framework y usando la clase StringBuilder, como se muestra en la función showMap.

Aunque no puedas exponer tus propios tipos genéricos desde un componente de Windows Runtime, puedes devolver colecciones genéricas de .NET Framework para las clases de Windows Runtime usando código como el siguiente:

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> implementa IList<T>, que aparece como el tipo de Windows Runtime IVector<T> en JavaScript.

Declarar eventos

Puedes declarar eventos con el patrón de eventos estándar de .NET Framework u otros patrones usados por Windows Runtime. .NET Framework admite la equivalencia entre el delegado System.EventHandler<TEventArgs> y el delegado EventHandler<T> de Windows Runtime, por lo que usar EventHandler<TEventArgs> es una buena forma de implementar el patrón estándar de .NET Framework. Para ver cómo funciona esto, agrega el siguiente par de clases al proyecto 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

Cuando expongas un evento de Windows Runtime, la clase de argumento de evento se hereda de System.Object. No hereda de System.EventArgs, como lo haría en .NET, porque EventArgs no es un tipo de Windows Runtime.

Si declaras los descriptores de acceso de eventos personalizados para el evento (palabra clave Custom en Visual Basic), debes usar el patrón de eventos de Windows Runtime. Consulte Eventos personalizados y descriptores de acceso de eventos en Windows Runtime componentes.

Para controlar el evento Test, agrega la función events1 a default.js. La función events1 crea una función de controlador de eventos para el evento Test e invoca inmediatamente el método OnTest para generar el evento. Si colocas un punto de interrupción en el cuerpo del controlador de eventos, podrás ver que el objeto pasado al parámetro único incluye el objeto de origen y los dos miembros de 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);
}

A continuación, agrega el código de registro de eventos al mismo bloque que el otro código de registro de eventos:

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

Exponer operaciones asincrónicas

.NET Framework cuenta con un amplio conjunto de herramientas para el procesamiento asincrónico y el procesamiento en paralelo, basado en la clase Task y la clase Task<TResult> genérica. Para exponer el procesamiento asincrónico basado en tareas en un componente de Windows Runtime, usa las interfaces de Windows Runtime IAsyncAction, IAsyncActionWithProgress<TProgress>, IAsyncOperation<TResult> e IAsyncOperationWithProgress<TResult, TProgress>. (En Windows Runtime, las operaciones devuelven resultados, pero no las acciones).

En esta sección se muestra una operación asincrónica cancelable que notifica el progreso y devuelve resultados. El método GetPrimesInRangeAsync usa la clase AsyncInfo para generar una tarea y conectar sus características de cancelación y notificación del progreso con un objeto WinJS.Promise. Comienza agregando el método GetPrimesInRangeAsync a la clase de ejemplo:

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 es un buscador de números primos muy sencillo gracias a su diseño. Aquí lo primordial es la implementación de una operación asincrónica, por lo que la simplicidad es importante, y una implementación lenta supone una ventaja cuando se quiere mostrar la cancelación. GetPrimesInRangeAsync busca números primos mediante fuerza bruta: divide un candidato por todos los enteros inferiores o iguales a su raíz cuadrada, en lugar de utilizar solamente los números primos. Ejecución paso a paso de este código:

  • Antes de iniciar una operación asincrónica, realiza actividades de mantenimiento, como validar parámetros e iniciar excepciones para las entradas no válidas.

  • La clave de esta implementación es el método AsyncInfo.Run<TResult, TProgress>(Func<CancellationToken, IProgress TProgress<>, Task<TResult>>) y el delegado que es el único parámetro del método. El delegado debe aceptar un token de cancelación y una interfaz para informar del progreso y debe devolver una tarea iniciada que emplee esos parámetros. Cuando JavaScript llama al método GetPrimesInRangeAsync, se producen los pasos siguientes (no necesariamente en el orden indicado aquí):

    • El objeto WinJS.Promise proporciona funciones para procesar los resultados devueltos, reaccionar a la cancelación y controlar informes de progreso.

    • El método AsyncInfo.Run crea un origen de cancelación y un objeto que implementa la interfaz IProgress<T>. Para el delegado, pasa un token CancellationToken desde el origen de la cancelación y la interfaz IProgress<T>.

      Nota Si el objeto Promise no proporciona una función para reaccionar a la cancelación, AsyncInfo.Run sigue pasando un token cancelable y la cancelación todavía puede producirse. Si el objeto Promise no proporciona una función para controlar las actualizaciones del progreso, AsyncInfo.Run sigue proporcionando un objeto que implementa IProgress<T>, pero se omiten sus informes.

    • El delegado usa el método TResult<(Func<TResult>>, CancellationToken) para crear una tarea iniciada que use el token y la interfaz de progreso. Una función lambda proporciona el delegado de la tarea iniciada, que calcula el resultado deseado. Se va a ofrecer más información sobre esto en un momento.

    • El método AsyncInfo.Run crea un objeto que implementa la interfaz IAsyncOperationWithProgress<TResult, TProgress>, conecta el mecanismo de cancelación de Windows Runtime con el origen del token y conecta la función de información del progreso del objeto Promise con la interfaz IProgress<T>.

    • La interfaz IAsyncOperationWithProgress<TResult, TProgress> se devuelve a JavaScript.

  • La función lambda que se representa mediante la tarea iniciada no acepta ningún argumento. Dado que es una función lambda, tiene acceso al token y a la interfaz IProgress. Cada vez que se evalúa un número candidato, la función lambda:

    • Comprueba si se ha llegado al siguiente punto de porcentaje del progreso. Si lo ha hecho, la función lambda llama al método IProgress<T>.Report y el porcentaje se pasa a la función que el objeto Promise especificó para informar del progreso.
    • Utiliza el token de cancelación para producir una excepción si se ha cancelado la operación. Si se ha llamado al método IAsyncInfo.Cancel (que hereda la interfaz IAsyncOperationWithProgress<TResult, TProgress>), la conexión que configura el método AsyncInfo.Run garantiza que se notificará el token de cancelación.
  • Cuando la función lambda devuelve la lista de números primos, la lista se pasa a la función que especificó el objeto WinJS.Promise para procesar los resultados.

Para crear la promesa de JavaScript y configurar el mecanismo de cancelación, agrega las funciones asyncRun y asyncCancel a 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();
}

No olvides que el código de registro de eventos es el mismo que antes.

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

Al llamar al método asincrónico GetPrimesInRangeAsync, la función asyncRun crea un objeto WinJS.Promise. A continuación, el método "then" del objeto utiliza tres funciones que procesan los resultados devueltos, reaccionan en caso de errores (incluida la cancelación) y controlan los informes de progreso. En este ejemplo, se imprimen los resultados devueltos en el área de salida. En caso de cancelación o finalización, se restablecen los botones que inician y cancelan la operación. La información del progreso actualiza el control del progreso.

La función asyncCancel simplemente llama al método cancel del objeto WinJS.Promise.

Para ejecutar la aplicación, presiona la tecla F5. Para iniciar la operación asincrónica, selecciona el botón Async. Lo que suceda después dependerá de la velocidad de tu equipo. Si la barra de progreso llega al final antes de que tengas tiempo de reaccionar, multiplica el tamaño del número inicial que se pasa a GetPrimesInRangeAsync una o varias veces por diez. Para ajustar la duración de la operación, puedes probar de aumentar o reducir la cantidad de números, pero es más eficaz agregar ceros en medio del número inicial. Para cancelar la operación, selecciona el botón Cancel Async.