Implementación de un proveedor de widgets en un Aplicación de Windows de C#

En este artículo se explica cómo crear un proveedor de widgets sencillo que implementa la interfaz IWidgetProvider. El host del widget invoca los métodos de esta interfaz para solicitar los datos que definen un widget o para permitir que el proveedor del widget responda a una acción de usuario en un widget. Los proveedores de widgets pueden admitir un único widget o varios widgets. En este ejemplo, definiremos dos widgets diferentes. Un widget es un widget meteorológico ficticio que ilustra algunas de las opciones de formato proporcionadas por el marco de Tarjetas adaptables. El segundo widget mostrará las acciones del usuario y la característica de estado del widget personalizado manteniendo un contador que se incrementa cada vez que el usuario hace clic en un botón que se muestra en el widget.

Captura de pantalla de un widget meteorológico sencillo. El widget muestra algunos gráficos y datos relacionados con el tiempo, así como texto de diagnóstico que ilustra que se muestra la plantilla para el widget de tamaño medio.

Captura de pantalla de un widget simple de recuento. El widget muestra una cadena que contiene el valor numérico que se va a incrementar y un botón denominado Incrementar, así como un texto de diagnóstico que ilustra que se está mostrando la plantilla para el widget de tamaño pequeño.

Este código de ejemplo de este artículo se adapta a partir del ejemplo de widgets de SDK de Aplicaciones para Windows. Para implementar un proveedor de widgets con C++/WinRT, consulte Implementación de un proveedor de widgets en una aplicación win32 (C++/WinRT).

Requisitos previos

  • Un dispositivo Windows 11 con el panel widgets disponible para las pruebas.
  • El dispositivo debe tener habilitado el modo de desarrollador. Para obtener más información, consulte Configuración para desarrolladores.
  • Visual Studio 2022 o posterior con la carga de trabajo de desarrollo de aplicaciones WinUI.

Creación de una aplicación de consola de C#

En Visual Studio, cree un nuevo proyecto. En el cuadro de diálogo Crear un nuevo proyecto, establezca el filtro de lenguaje en "C#" y el filtro de plataforma en Windows y, a continuación, seleccione la plantilla de proyecto Aplicación de consola. Ponga al nuevo proyecto el nombre "ExampleWidgetProvider". Cuando se le solicite, establezca la versión de .NET a 8.0.

Cuando se carga el proyecto, en Explorador de soluciones haga clic con el botón derecho en el nombre del proyecto y seleccione Properties. En la página General, desplácese hacia abajo hasta Target OS y seleccione "Windows". En Versión del sistema operativo de destino, seleccione la versión 10.0.19041.0 o posterior.

Para actualizar el proyecto para admitir .NET 8.0, en Explorador de soluciones haga clic con el botón derecho en el nombre del proyecto y seleccione Editar archivo del proyecto. Dentro de PropertyGroup, agregue el siguiente elemento RuntimeIdentifiers.

<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>

Tenga en cuenta que en este tutorial se usa una aplicación de consola que muestra la ventana de consola cuando se activa el widget para habilitar la depuración sencilla. Cuando esté listo para publicar la aplicación de proveedor de widgets, puede convertir la aplicación de consola en una aplicación de Windows siguiendo los pasos descritos en Convertir la aplicación de consola a una aplicación de Windows.

Agregar referencias al SDK de Aplicaciones para Windows

En este tutorial se usa Microsoft. WindowsAppSDK 2.5.1. En Explorador de soluciones, haga clic con el botón derecho en Dependencies y seleccione Administrar paquetes NuGet... . En el administrador de paquetes NuGet, seleccione la pestaña Browse y busque "Microsoft. WindowsAppSDK". Seleccione la versión 2.5.1 o una versión estable compatible posterior de los canales de lanzamiento de SDK de Aplicaciones para Windows y, a continuación, haga clic en Instalar. Use la misma versión de paquete en los proyectos de la aplicación y de empaquetado. Esta es la versión que usa el tutorial, no la versión mínima que introdujo las API de widget.

Adición de una clase WidgetProvider para controlar las operaciones del widget

En Visual Studio, haga clic con el botón derecho en el proyecto ExampleWidgetProvider en Explorador de soluciones y seleccione Add->Class. En el cuadro de diálogo Agregar clase, asigne a la clase el nombre WidgetProvider y, después, haga clic en Agregar. En el archivo WidgetProvider.cs generado, actualice la definición de clase para indicar que implementa la interfaz IWidgetProvider.

// WidgetProvider.cs
internal class WidgetProvider : IWidgetProvider

Preparación para realizar un seguimiento de los widgets habilitados

Los proveedores de widgets pueden admitir un único widget o varios widgets. Cada vez que el host del widget inicia una operación con el proveedor de widgets, pasa un identificador para identificar el widget asociado a la operación. Cada widget también tiene un nombre asociado y un valor de estado que se puede usar para almacenar datos personalizados. En este ejemplo, declararemos una estructura auxiliar sencilla para almacenar el identificador, el nombre y los datos de cada widget anclado. Los widgets también pueden estar en un estado activo, que se describe en la sección Activar y desactivar a continuación, y realizaremos un seguimiento de este estado para cada widget con un valor booleano. Agregue la siguiente definición al archivo WidgetProvider.cs, dentro del espacio de nombres ExampleWidgetProvider, pero fuera de la definición de clase WidgetProvider.

// WidgetProvider.cs

public class CompactWidgetInfo
{
    public string? widgetId { get; set; }
    public string? widgetName { get; set; }
    public int customState = 0;
    public bool isActive = false;

}

Dentro de la definición de clase WidgetProvider en WidgetProvider.cs, agregue un miembro para el mapa que mantendrá la lista de widgets habilitados, con el identificador de widget como clave para cada entrada.

// WidgetProvider.cs

// Class member of WidgetProvider
public static Dictionary<string, CompactWidgetInfo> RunningWidgets = new Dictionary<string, CompactWidgetInfo>(); 

Declarar cadenas de texto JSON de plantilla de widget

En este ejemplo se declararán algunas cadenas estáticas para definir las plantillas JSON para cada widget. Por comodidad, estas plantillas se almacenan en las variables miembro de la clase WidgetProvider. Si necesita un almacenamiento general para las plantillas, se pueden incluir como parte del paquete de aplicación: Acceso a los archivos de paquete. Para obtener información sobre cómo crear el documento JSON de plantilla de widget, consulte Crear una plantilla de widget con el diseñador de Tarjetas adaptables.

Las aplicaciones que implementan Windows widgets pueden personalizar el encabezado que se muestra para su widget en hosts que admiten la personalización de encabezados. Para obtener más información, consulte Personalizar el área de encabezado del widget.

Nota

Un widget web puede mostrar HTML servido desde una dirección URL. El proveedor sigue suministrando una carga JSON de tarjeta adaptable para especificar esa dirección URL y sigue implementando IWidgetProvider. Las API de recursos y mensajería se introdujeron en SDK de Aplicaciones para Windows 1.6.3. Para conocer los requisitos de configuración y host, consulte Proveedores de widgets web.

// WidgetProvider.cs

// Class members of WidgetProvider
        const string weatherWidgetTemplate = """
{
    "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
    "type": "AdaptiveCard",
    "version": "1.0",
    "speak": "<s>The forecast for Seattle January 20 is mostly clear with a High of 51 degrees and Low of 40 degrees</s>",
    "backgroundImage": "https://raw.githubusercontent.com/microsoft/AdaptiveCards/59402c8d43f7a69a68e0b4fccc08f8a76fc98ccd/source/nodejs/adaptivecards-site/themes/adaptivecards/source/content/Mostly%20Cloudy-Background.jpg",
    "body": [
        {
            "type": "TextBlock",
            "text": "Redmond, WA",
            "size": "large",
            "isSubtle": true,
            "wrap": true
        },
        {
            "type": "TextBlock",
            "text": "Mon, Nov 4, 2019 6:21 PM",
            "spacing": "none",
            "wrap": true
        },
        {
            "type": "ColumnSet",
            "columns": [
                {
                    "type": "Column",
                    "width": "auto",
                    "items": [
                        {
                            "type": "Image",
                            "url": "https://raw.githubusercontent.com/microsoft/AdaptiveCards/59402c8d43f7a69a68e0b4fccc08f8a76fc98ccd/source/nodejs/adaptivecards-site/themes/adaptivecards/source/content/Mostly%20Cloudy-Square.png",
                            "size": "small",
                            "altText": "Mostly cloudy weather"
                        }
                    ]
                },
                {
                    "type": "Column",
                    "width": "auto",
                    "items": [
                        {
                            "type": "TextBlock",
                            "text": "46",
                            "size": "extraLarge",
                            "spacing": "none",
                            "wrap": true
                        }
                    ]
                },
                {
                    "type": "Column",
                    "width": "stretch",
                    "items": [
                        {
                            "type": "TextBlock",
                            "text": "°F",
                            "weight": "bolder",
                            "spacing": "small",
                            "wrap": true
                        }
                    ]
                },
                {
                    "type": "Column",
                    "width": "stretch",
                    "items": [
                        {
                            "type": "TextBlock",
                            "text": "Hi 50",
                            "horizontalAlignment": "left",
                            "wrap": true
                        },
                        {
                            "type": "TextBlock",
                            "text": "Lo 41",
                            "horizontalAlignment": "left",
                            "spacing": "none",
                            "wrap": true
                        }
                    ]
                }
            ]
        }
    ]
}
""";

    const string countWidgetTemplate = """
{                                                                     
    "type": "AdaptiveCard",                                         
    "body": [                                                         
        {                                                               
            "type": "TextBlock",                                    
            "text": "You have clicked the button ${count} times"    
        },
        {
                "text":"Rendering Only if Small",
                "type":"TextBlock",
                "$when":"${$host.widgetSize==\"small\"}"
        },
        {
                "text":"Rendering Only if Medium",
                "type":"TextBlock",
                "$when":"${$host.widgetSize==\"medium\"}"
        },
        {
            "text":"Rendering Only if Large",
            "type":"TextBlock",
            "$when":"${$host.widgetSize==\"large\"}"
        }                                                                    
    ],                                                                  
    "actions": [                                                      
        {                                                               
            "type": "Action.Execute",                               
            "title": "Increment",                                   
            "verb": "inc"                                           
        }                                                               
    ],                                                                  
    "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
    "version": "1.5"                                                
}
""";

Implementación de los métodos IWidgetProvider

En las secciones siguientes, implementaremos los métodos de la interfaz IWidgetProvider. El método auxiliar UpdateWidget al que se llama en varias de estas implementaciones de método se mostrará más adelante en este artículo.

Nota

Solo se garantiza que los objetos pasados a los métodos de la interfaz de devolución de llamada IWidgetProvider sean válidos mientras dure la ejecución de la llamada. No debe almacenar referencias a estos objetos porque su comportamiento fuera del contexto de la devolución de llamada no está definido.

CreateWidget

El host del widget llama a CreateWidget cuando el usuario ha anclado uno de los widgets de la aplicación en el host del widget. En primer lugar, este método obtiene el identificador y el nombre del widget asociado y agrega una nueva instancia de nuestra estructura auxiliar, CompactWidgetInfo, a la colección de widgets habilitados. A continuación, enviamos la plantilla inicial y los datos del widget, que se encapsulan en el método auxiliar UpdateWidget.

// WidgetProvider.cs

public void CreateWidget(WidgetContext widgetContext)
{
    var widgetId = widgetContext.Id; // To save RPC calls
    var widgetName = widgetContext.DefinitionId;
    CompactWidgetInfo runningWidgetInfo = new CompactWidgetInfo() { widgetId = widgetId, widgetName = widgetName };
    RunningWidgets[widgetId] = runningWidgetInfo;


    // Update the widget
    UpdateWidget(runningWidgetInfo);
}

Eliminar Widget

El host del widget llama a DeleteWidget cuando el usuario ha desanclado uno de los widgets de la aplicación del host del widget. Cuando esto ocurra, quitaremos el widget asociado de nuestra lista de widgets habilitados para que no enviemos más actualizaciones para ese widget.

// WidgetProvider.cs

public void DeleteWidget(string widgetId, string customState)
{
    RunningWidgets.Remove(widgetId);

    if(RunningWidgets.Count == 0)
    {
        emptyWidgetListEvent.Set();
    }
}

En este ejemplo, además de quitar el widget con lo especificado de la lista de widgets habilitados, también comprobamos si la lista está vacía y, si es así, establecemos un evento que se usará más adelante para permitir que la aplicación se cierre cuando no haya widgets habilitados. Dentro de la definición de clase, agregue la declaración de ManualResetEvent y una función accesora pública.

// WidgetProvider.cs
static ManualResetEvent emptyWidgetListEvent = new ManualResetEvent(false);

public static ManualResetEvent GetEmptyWidgetListEvent()
{
    return emptyWidgetListEvent;
}

OnActionInvoked

El host del widget llama a OnActionInvoked cuando el usuario interactúa con una acción que definió en la plantilla de widget. Para el widget de contador usado en este ejemplo, se declaró una acción con un valor de verbo de "inc" en la plantilla JSON del widget. El código del proveedor de widgets usará este valor de verbo para determinar qué acción se realizará en respuesta a la interacción del usuario.

...
    "actions": [                                                      
        {                                                               
            "type": "Action.Execute",                               
            "title": "Increment",                                   
            "verb": "inc"                                           
        }                                                               
    ], 
...

En el método OnActionInvoked, obtenga el valor de verbo comprobando la propiedad Verb del widgetActionInvokedArgs pasado al método. Si el verbo es "inc", sabemos que vamos a incrementar el recuento en el estado personalizado del widget. En WidgetActionInvokedArgs, obtenga el objeto WidgetContext y, a continuación, su propiedad Id para identificar la instancia del widget que se está actualizando. Busque la entrada en nuestro mapa de widgets habilitados con el identificador especificado y, a continuación, actualice el valor de estado personalizado que se usa para almacenar el número de incrementos. Por último, actualice el contenido del widget con el nuevo valor con la función auxiliar UpdateWidget.

// WidgetProvider.cs

public void OnActionInvoked(WidgetActionInvokedArgs actionInvokedArgs)
{
    var verb = actionInvokedArgs.Verb;
        if (verb == "inc")
        {
            var widgetId = actionInvokedArgs.WidgetContext.Id;
            // If you need to use some data that was passed in after
            // Action was invoked, you can get it from the args:
            var data = actionInvokedArgs.Data;
            if (RunningWidgets.ContainsKey(widgetId))
            {
                var localWidgetInfo = RunningWidgets[widgetId];
                // Increment the count
                localWidgetInfo.customState++;
                UpdateWidget(localWidgetInfo);
            }
        }
}

Para obtener información sobre la sintaxis Action.Execute para Tarjetas adaptables, vea Action.Execute. Para obtener instrucciones sobre cómo diseñar la interacción para widgets, consulte Guía sobre el diseño de la interacción de widgets.

OnWidgetContextChanged

En la versión actual, solo se llama a OnWidgetContextChanged cuando el usuario cambia el tamaño de un widget anclado. Puede optar por devolver una plantilla o datos JSON diferentes al host del widget en función del tamaño solicitado. También puede diseñar el JSON de plantilla para admitir todos los tamaños disponibles mediante la representación condicional basada en el valor de host.widgetSize. Si no necesita enviar una nueva plantilla o datos para tener en cuenta el cambio de tamaño, puede utilizar el OnWidgetContextChanged con fines de telemetría.

// WidgetProvider.cs

public void OnWidgetContextChanged(WidgetContextChangedArgs contextChangedArgs)
{
    var widgetContext = contextChangedArgs.WidgetContext;
    var widgetId = widgetContext.Id;
    var widgetSize = widgetContext.Size;
    if (RunningWidgets.ContainsKey(widgetId))
    {
        var localWidgetInfo = RunningWidgets[widgetId];
        UpdateWidget(localWidgetInfo);
    }
}
    

Activar y desactivar

Se llama al método Activar para notificar al proveedor del widget que el host del widget está interesado actualmente en recibir contenido actualizado del proveedor. Por ejemplo, podría significar que el usuario está viendo activamente el host del widget. Se llama al método Desactivar para notificar al proveedor de widgets que el host del widget ya no solicita actualizaciones de contenido. Estos dos métodos definen una ventana en la que el host del widget está más interesado en mostrar el contenido más actualizado. Los proveedores de widgets pueden enviar actualizaciones al widget en cualquier momento, como en respuesta a una notificación de inserción, pero como con cualquier tarea en segundo plano, es importante equilibrar la provisión de contenido actualizado con problemas de recursos como la duración de la batería.

Activar y Desactivar se llaman por separado para cada widget. En este ejemplo se realiza un seguimiento del estado activo de cada widget en la estructura auxiliar CompactWidgetInfo. En el método Activar, llamamos al método auxiliar UpdateWidget para actualizar el widget. Tenga en cuenta que el período de tiempo entre Activar y Desactivar puede ser pequeño, por lo que se recomienda intentar que la ruta de acceso del código de actualización del widget sea lo más rápida posible.

// WidgetProvider.cs

public void Activate(WidgetContext widgetContext)
{
    var widgetId = widgetContext.Id;

    if (RunningWidgets.ContainsKey(widgetId))
    {
        var localWidgetInfo = RunningWidgets[widgetId];
        localWidgetInfo.isActive = true;

        UpdateWidget(localWidgetInfo);
    }
}
public void Deactivate(string widgetId)
{
    if (RunningWidgets.ContainsKey(widgetId))
    {
        var localWidgetInfo = RunningWidgets[widgetId];
        localWidgetInfo.isActive = false;
    }
}

Actualización de un widget

Defina el método auxiliar UpdateWidget para actualizar un widget habilitado. En este ejemplo, comprobamos el nombre del widget en la estructura auxiliar CompactWidgetInfo que se pasa al método y, a continuación, establecemos la plantilla y el JSON de datos adecuados en función del widget que se esté actualizando. Se inicializa WidgetUpdateRequestOptions con la plantilla, los datos y el estado personalizado del widget que se está actualizando. Llame a WidgetManager::GetDefault para obtener una instancia de la clase WidgetManager y, a continuación, llame a UpdateWidget para enviar los datos del widget actualizado al host del widget.

// WidgetProvider.cs

void UpdateWidget(CompactWidgetInfo localWidgetInfo)
{
    WidgetUpdateRequestOptions updateOptions = new WidgetUpdateRequestOptions(localWidgetInfo.widgetId);

    string? templateJson = null;
    if (localWidgetInfo.widgetName == "Weather_Widget")
    {
        templateJson = weatherWidgetTemplate.ToString();
    }
    else if (localWidgetInfo.widgetName == "Counting_Widget")
    {
        templateJson = countWidgetTemplate.ToString();
    }

    string? dataJson = null;
    if (localWidgetInfo.widgetName == "Weather_Widget")
    {
        dataJson = "{}";
    }
    else if (localWidgetInfo.widgetName == "Counting_Widget")
    {
        dataJson = "{ \"count\": " + localWidgetInfo.customState.ToString() + " }";
    }

    updateOptions.Template = templateJson;
    updateOptions.Data = dataJson;
    updateOptions.IsPlaceholderContent = localWidgetInfo.widgetName == "Weather_Widget";
    // You can store some custom state in the widget service that you will be able to query at any time.
    updateOptions.CustomState= localWidgetInfo.customState.ToString();
    WidgetManager.GetDefault().UpdateWidget(updateOptions);
}

Establezca WidgetUpdateRequestOptions.IsPlaceholderContent en true para el contenido que aún no está personalizado ni es accionable. El widget meteorológico ficticio muestra una ubicación fija, por lo que este ejemplo lo marca como contenido de marcador de posición. El host puede priorizar widgets con contenido personalizado sobre este widget meteorológico de ejemplo. Establezca la propiedad en false cuando proporcione contenido personalizado. Esta propiedad se agregó en SDK de Aplicaciones para Windows 1.6.6.

Inicialización de la lista de widgets habilitados en el inicio

Cuando el proveedor de widgets se inicializa por primera vez, es una buena idea preguntar a WidgetManager si hay algún widget en ejecución que nuestro proveedor esté atendiendo actualmente. Ayudará a recuperar la aplicación al estado anterior en caso del reinicio del equipo o bloqueo del proveedor. Llame a WidgetManager::GetDefault para obtener la instancia predeterminada del administrador de widgets para la aplicación. A continuación, llame a GetWidgetInfos, que devuelve una matriz de objetos WidgetInfo. Copie los identificadores, los nombres y el estado personalizado de los widgets en la estructura auxiliar CompactWidgetInfo y guárdelo en la variable de miembro RunningWidgets. Pegue el código siguiente en el constructor de la clase WidgetProvider.

// WidgetProvider.cs

public WidgetProvider()
{
    var runningWidgets = WidgetManager.GetDefault().GetWidgetInfos();

    foreach (var widgetInfo in runningWidgets)
    {
        var widgetContext = widgetInfo.WidgetContext;
        var widgetId = widgetContext.Id;
        var widgetName = widgetContext.DefinitionId;
        var customState = widgetInfo.CustomState;
        if (!RunningWidgets.ContainsKey(widgetId))
        {
            CompactWidgetInfo runningWidgetInfo = new CompactWidgetInfo() { widgetId = widgetId, widgetName = widgetName };
            if (!string.IsNullOrEmpty(customState))
            {
                if (int.TryParse(customState, out int count))
                {
                    runningWidgetInfo.customState = count;
                }
                else
                {
                    Console.Error.WriteLine($"Invalid saved count for widget {widgetId}; resetting to 0.");
                }
            }
            RunningWidgets[widgetId] = runningWidgetInfo;
        }
    }
}

Implementación de un generador de clases que creará una instancia de WidgetProvider a petición

Para que el host del widget se comunique con nuestro proveedor de widgets, debemos llamar a CoRegisterClassObject. Esta función requiere que creemos una implementación de IClassFactory que creará un objeto de clase para nuestra clase WidgetProvider. Implementaremos nuestro generador de clases en una clase auxiliar independiente.

En Visual Studio, haga clic con el botón derecho en el proyecto ExampleWidgetProvider en Explorador de soluciones y seleccione Add->Class. En el cuadro de diálogo Agregar clase, asigne a la clase el nombre "FactoryHelper" y, después, haga clic en Agregar.

Reemplace todo el contenido del archivo FactoryHelper.cs por el siguiente código. Este código define la interfaz IClassFactory e implementa dos métodos: CreateInstance y LockServer. Este código es el típico reemplazable para implementar un generador de clases y no es específico de la funcionalidad de un proveedor de widgets, excepto que indicamos que el objeto de clase que se está creando implementa la interfaz IWidgetProvider.

// FactoryHelper.cs

using Microsoft.Windows.Widgets.Providers;
using System.Runtime.InteropServices;
using WinRT;

namespace COM
{
    static class Guids
    {
        public const string IClassFactory = "00000001-0000-0000-C000-000000000046";
        public const string IUnknown = "00000000-0000-0000-C000-000000000046";
    }

    /// 
    /// IClassFactory declaration
    /// 
    [ComImport, ComVisible(false), InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid(COM.Guids.IClassFactory)]
    internal interface IClassFactory
    {
        [PreserveSig]
        int CreateInstance(IntPtr pUnkOuter, ref Guid riid, out IntPtr ppvObject);
        [PreserveSig]
        int LockServer(bool fLock);
    }

    [ComVisible(true)]
    class WidgetProviderFactory<T> : IClassFactory
    where T : IWidgetProvider, new()
    {
        public int CreateInstance(IntPtr pUnkOuter, ref Guid riid, out IntPtr ppvObject)
        {
            ppvObject = IntPtr.Zero;

            if (pUnkOuter != IntPtr.Zero)
            {
                return CLASS_E_NOAGGREGATION;
            }

            IntPtr provider = IntPtr.Zero;
            try
            {
                provider = MarshalInspectable<IWidgetProvider>.FromManaged(new T());
                return Marshal.QueryInterface(provider, in riid, out ppvObject);
            }
            catch (Exception exception)
            {
                Console.Error.WriteLine(exception);
                return Marshal.GetHRForException(exception);
            }
            finally
            {
                if (provider != IntPtr.Zero)
                {
                    Marshal.Release(provider);
                }
            }
        }

        int IClassFactory.LockServer(bool fLock)
        {
            return 0;
        }

        private const int CLASS_E_NOAGGREGATION = -2147221232;

    }
}

Creación de un GUID que representa el CLSID para el proveedor de widgets

A continuación, debe crear un GUID que represente el CLSID que se usará para identificar el proveedor de widgets para la activación COM. También se usará el mismo valor al empaquetar la aplicación. Para generar un GUID en Visual Studio, vaya a Tools->Crear GUID. Seleccione la opción de formato del Registro y haga clic en Copiar. A continuación, péguela en un archivo de texto para poder copiarla más tarde.

Registro del objeto de clase de proveedor de widgets con OLE

En el archivo Program.cs de nuestro ejecutable, llamaremos a CoRegisterClassObject para registrar nuestro proveedor de widgets con OLE, de forma que el host de widgets pueda interactuar con él. Reemplace el contenido de Program.cs por el código siguiente. Este código importa la función CoRegisterClassObject y la llama, pasando la interfaz WidgetProviderFactory definida en un paso anterior. Asegúrese de actualizar la declaración de variable CLSID_Factory para usar el GUID que generó en el paso anterior.

// Program.cs

using System.Runtime.InteropServices;
using ComTypes = System.Runtime.InteropServices.ComTypes;
using Microsoft.Windows.Widgets;
using ExampleWidgetProvider;
using COM;
using System;

[DllImport("kernel32.dll")]
static extern IntPtr GetConsoleWindow();

[DllImport("ole32.dll")]

static extern int CoRegisterClassObject(
            [MarshalAs(UnmanagedType.LPStruct)] Guid rclsid,
            [MarshalAs(UnmanagedType.IUnknown)] object pUnk,
            uint dwClsContext,
            uint flags,
            out uint lpdwRegister);

[DllImport("ole32.dll")] static extern int CoRevokeClassObject(uint dwRegister);

WinRT.ComWrappersSupport.InitializeComWrappers();
Guid CLSID_Factory = Guid.Parse("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX");
var factory = new WidgetProviderFactory<WidgetProvider>();
Marshal.ThrowExceptionForHR(
    CoRegisterClassObject(CLSID_Factory, factory, 0x4, 0x1, out uint cookie));

try
{
    if (GetConsoleWindow() != IntPtr.Zero)
    {
        Console.WriteLine("Registered successfully. Press ENTER to exit.");
        Console.ReadLine();
    }
    else
    {
        WidgetProvider.GetEmptyWidgetListEvent().WaitOne();
    }
}
finally
{
    int revokeResult = CoRevokeClassObject(cookie);
    if (revokeResult < 0)
    {
        System.Diagnostics.Trace.TraceError($"CoRevokeClassObject failed: 0x{revokeResult:X8}");
    }
    GC.KeepAlive(factory);
}

Tenga en cuenta que este ejemplo de código importa la función GetConsoleWindow para determinar si la aplicación se ejecuta como una aplicación de consola, el comportamiento predeterminado de este tutorial. Si la función devuelve un puntero válido, se escribe información de depuración en la consola. De lo contrario, la aplicación se ejecuta como una aplicación de Windows. En ese caso, esperamos el evento que establecemos en el método DeleteWidget cuando la lista de widgets habilitados está vacía y, a continuación, salimos de la aplicación. Para obtener información sobre cómo convertir la aplicación de consola de ejemplo en una aplicación de Windows, consulte Convert your console app to a Windows app.

Nota

Esperar a que se elimine el último widget es una directiva de duración simplificada para este ejemplo, no un requisito de IWidgetProvider. El host puede activar y liberar un proveedor mientras los widgets permanezcan anclados. En un proveedor de producción, administre la duración del servidor COM de forma independiente, restaure el estado del widget guardado en el inicio y sincronice el acceso al estado compartido si las devoluciones de llamada se ejecutan simultáneamente. Vea los comentarios de IWidgetProvider.

Empaqueta la aplicación del proveedor de widgets

Este tutorial registra el proveedor de widgets de escritorio mediante un manifiesto de paquete MSIX y usa la implementación dependiente del marco del tiempo de ejecución de SDK de Aplicaciones para Windows. Los pasos siguientes empaquetan la aplicación y registran su proveedor. Si elige la implementación independiente en su lugar, siga Implementar aplicaciones independientes, incluida la configuración de ambos proyectos cuando use un proyecto de empaquetado de aplicaciones de Windows.

Creación de un proyecto de empaquetado MSIX

En Explorador de soluciones, haga clic con el botón derecho en la solución y seleccione Agregar->Nuevo Project... . En el cuadro de diálogo Agregar un nuevo project, seleccione la plantilla "Windows Application Packaging Project" y haga clic en Next. Establezca el nombre del proyecto en "ExampleWidgetProviderPackage" y haga clic en Crear. Cuando se le solicite, establezca el destino en la versión 1809 o posterior y haga clic en Aceptar. A continuación, haga clic con el botón derecho en el proyecto ExampleWidgetProviderPackage y seleccione Agregar->referencia de proyecto. Seleccione el proyecto ExampleWidgetProvider y haga clic en Aceptar.

Agregar la referencia de paquete de SDK de Aplicaciones para Windows al proyecto de empaquetado

Debe agregar una referencia al paquete Nuget SDK de Aplicaciones para Windows al proyecto de empaquetado MSIX. En Explorador de soluciones, haga doble clic en el proyecto ExampleWidgetProviderPackage para abrir el archivo ExampleWidgetProviderPackage.wapproj. Agregue el siguiente xml dentro del elemento Project.

<!--ExampleWidgetProviderPackage.wapproj-->
<ItemGroup>
    <PackageReference Include="Microsoft.WindowsAppSDK" Version="2.5.1">
        <IncludeAssets>build</IncludeAssets>
    </PackageReference>  
</ItemGroup>

Nota

El fragmento de código usa la versión 2.5.1, no la versión mínima que introdujo las API de widget. Si seleccionó una versión estable compatible posterior en el paso anterior, actualice el valor de Versión para que coincida.

Actualización del manifiesto del paquete

En Explorador de soluciones haga clic con el botón derecho en el archivo Package.appxmanifest y seleccione View Code para abrir el archivo xml de manifiesto. A continuación, debe agregar algunas declaraciones de namespace para las extensiones del paquete de la aplicación que usaremos. Agregue las siguientes definiciones de espacio de nombres al elemento de nivel superior Package.

<!-- Package.appmanifest -->
<Package
  ...
  xmlns:uap3="http://schemas.microsoft.com/appx/manifest/uap/windows10/3"
  xmlns:com="http://schemas.microsoft.com/appx/manifest/com/windows10"

Dentro del elemento Application, cree un nuevo elemento vacío denominado Extensions. Asegúrese de que aparece después de la etiqueta de cierre de uap:VisualElements.

<!-- Package.appxmanifest -->
<Application>
...
    <Extensions>

    </Extensions>
</Application>

La primera extensión que necesitamos agregar es ComServer. Registra el punto de entrada del ejecutable en el sistema operativo. Esta extensión es el equivalente en aplicaciones empaquetadas a registrar un servidor COM estableciendo una clave del Registro, y no es específica de los proveedores de widgets. Agregue el siguiente elemento com:Extension como elemento secundario del elemento Extensions. Cambie el GUID en el atributo Id del elemento com:Class por el GUID que generó en un paso anterior.

<!-- Package.appxmanifest -->
<Extensions>
    <com:Extension Category="windows.comServer">
        <com:ComServer>
            <com:ExeServer Executable="ExampleWidgetProvider\ExampleWidgetProvider.exe" DisplayName="ExampleWidgetProvider">
                <com:Class Id="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" DisplayName="ExampleWidgetProvider" />
            </com:ExeServer>
        </com:ComServer>
    </com:Extension>
</Extensions>

A continuación, agregue la extensión que registra la aplicación como proveedor de widgets. Pegue el elemento uap3:Extension en el siguiente fragmento de código, como elemento secundario del elemento Extensions. Asegúrese de reemplazar el atributo ClassId del elemento COM por el GUID que usó en los pasos anteriores.

<!-- Package.appxmanifest -->
<Extensions>
    ...
    <uap3:Extension Category="windows.appExtension">
        <uap3:AppExtension Name="com.microsoft.windows.widgets" DisplayName="WidgetTestApp" Id="ContosoWidgetApp" PublicFolder="Public">
            <uap3:Properties>
                <WidgetProvider>
                    <ProviderIcons>
                        <Icon Path="Images\StoreLogo.png" />
                    </ProviderIcons>
                    <Activation>
                        <!-- Apps exports COM interface which implements IWidgetProvider -->
                        <CreateInstance ClassId="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" />
                    </Activation>

                    <TrustedPackageFamilyNames>
                        <TrustedPackageFamilyName>Microsoft.MicrosoftEdge.Stable_8wekyb3d8bbwe</TrustedPackageFamilyName>
                    </TrustedPackageFamilyNames>

                    <Definitions>
                        <Definition Id="Weather_Widget"
                            DisplayName="Weather Widget"
                            Description="Weather Widget Description"
                            AllowMultiple="true">
                            <Capabilities>
                                <Capability>
                                    <Size Name="small" />
                                </Capability>
                                <Capability>
                                    <Size Name="medium" />
                                </Capability>
                                <Capability>
                                    <Size Name="large" />
                                </Capability>
                            </Capabilities>
                            <ThemeResources>
                                <Icons>
                                    <Icon Path="ProviderAssets\Weather_Icon.png" />
                                </Icons>
                                <Screenshots>
                                    <Screenshot Path="ProviderAssets\Weather_Screenshot.png" DisplayAltText="For accessibility" />
                                </Screenshots>
                                <!-- DarkMode and LightMode are optional -->
                                <DarkMode />
                                <LightMode />
                            </ThemeResources>
                        </Definition>
                        <Definition Id="Counting_Widget"
                                DisplayName="Microsoft Counting Widget"
                                Description="Counting Widget Description">
                            <Capabilities>
                                <Capability>
                                    <Size Name="small" />
                                </Capability>
                            </Capabilities>
                            <ThemeResources>
                                <Icons>
                                    <Icon Path="ProviderAssets\Counting_Icon.png" />
                                </Icons>
                                <Screenshots>
                                    <Screenshot Path="ProviderAssets\Counting_Screenshot.png" DisplayAltText="For accessibility" />
                                </Screenshots>
                                <!-- DarkMode and LightMode are optional -->
                                <DarkMode>

                                </DarkMode>
                                <LightMode />
                            </ThemeResources>
                        </Definition>
                    </Definitions>
                </WidgetProvider>
            </uap3:Properties>
        </uap3:AppExtension>
    </uap3:Extension>
</Extensions>

Para obtener descripciones detalladas e información de formato para todos estos elementos, vea Formato XML del manifiesto de paquete del proveedor de widgets.

Adición de iconos y otras imágenes al proyecto de empaquetado

En Explorador de soluciones, haga clic con el botón derecho en el ExampleWidgetProviderPackage y seleccione Agregar->Nueva carpeta. Asigne a esta carpeta el nombre ProviderAssets, ya que es lo que se utilizó en Package.appxmanifest del paso anterior. Aquí es donde almacenaremos nuestros iconos y capturas de pantalla para nuestros widgets. Una vez que agregue los iconos y capturas de pantalla deseados, asegúrese de que los nombres de imagen coinciden con lo que viene después de Path=ProviderAssets\ en Package.appxmanifest o los widgets no se mostrarán en el host de widgets.

Para obtener información sobre los requisitos de diseño de las imágenes de las capturas de pantalla y las convenciones de nomenclatura para las capturas de pantalla localizadas, consulte Integración con el selector de widgets.

Prueba del proveedor de widgets

Seleccione la arquitectura que coincida con su máquina de desarrollo en el menú desplegable Plataformas de solución, por ejemplo, "x64". En Explorador de soluciones, haga clic con el botón derecho en la solución y seleccione Compilar solución y haga clic con el botón derecho en ExampleWidgetProviderPackage y seleccione Implementar. En Windows 11, abra Panel de widgets con Windows+W, abra el selector de widgets y encuentre Widget del tiempo y Widget de conteo de Microsoft. Agréguelos al tablero y pruebe su funcionalidad. Los controles de selector y la ubicación pueden variar con las actualizaciones de Windows y el panel de widgets.

El Diseñador de Tarjetas adaptables no proporciona valores $host de Windows, no invoca las devoluciones de llamada del proveedor ni aplica los límites representados de Panel de widgets. Realice estas comprobaciones en Panel de widgets:

  1. Confirme que el widget aparece en el selector con el nombre esperado, el icono, la captura de pantalla y la descripción.
  2. Ancle el widget y compruebe sus datos y plantilla iniciales.
  3. Cambie el tamaño a todos los tamaños declarados en el manifiesto del paquete. Compruebe si hay contenido recortado, oculto o desbordado en cada tamaño.
  4. Pruebe cada operación Action.Execute cuando $host.isUserContextAuthenticated sea true. Cuando sea falso, oculte las acciones no admitidas y proporcione una alternativa adecuada; solo Action.OpenUrl se admite en ese contexto.
  5. Detenga y reinicie el proceso del proveedor y, a continuación, confirme que cada widget anclado se restaura desde su estado personalizado.
  6. Desanclar el widget y confirmar que se llama a DeleteWidget .

Para un proyecto de C# orientado a CLI que usa MSIX de un solo proyecto en lugar del proyecto de empaquetado independiente en este tutorial, coloque Package.appxmanifest en el proyecto de proveedor, establezca EnableMsixTooling en true y use Aplicación de Windows Development CLI para compilar y registrar un paquete de desarrollo:

Important

La CLI de desarrollo de Aplicación de Windows se encuentra actualmente en public preview. Las características y comandos pueden cambiar antes de la versión final. Compruebe la salida de winapp run --help de la versión instalada antes de usar este comando.

winapp run ExampleWidgetProvider.csproj --configuration Release --arch x64 --no-launch

El comando registra el paquete sin iniciar la aplicación directamente. Abra el Panel de widgets para activar el servidor COM a través del host del widget.

Depuración del proveedor de widgets

Después de anclar los widgets, la plataforma de widgets iniciará la aplicación del proveedor de widgets para recibir y enviar información pertinente sobre el widget. Para depurar el widget en ejecución, puede adjuntar un depurador a la aplicación de proveedor de widgets en ejecución o configurar Visual Studio para iniciar automáticamente la depuración del proceso del proveedor de widgets una vez iniciado.

Para asociarse al proceso en ejecución:

  1. En Visual Studio haga clic en Debug -> Asociar al proceso.
  2. Filtre los procesos y busque la aplicación del proveedor de widgets deseada.
  3. Asocie el depurador.

Para asociar automáticamente el depurador al proceso cuando se inicia por primera vez:

  1. En Visual Studio haga clic en Debug -> Otros destinos de depuración -> Depurar paquete de aplicación instalado.
  2. Filtre los paquetes y busque el paquete de proveedor de widgets deseado.
  3. Selecciónelo y active la casilla que indica No iniciar, pero depurar mi código cuando se inicie.
  4. Haga clic en Adjuntar.

Conversión de la aplicación de consola en una aplicación de Windows

Para convertir la aplicación de consola creada en este tutorial en una aplicación de Windows, haga clic con el botón derecho en el proyecto ExampleWidgetProvider en Explorador de soluciones y seleccione Properties. En Application->General, cambie el tipo Output de "Aplicación de consola" a "aplicación de Windows".

Una captura de pantalla que muestra las propiedades del proyecto del proveedor de widgets de C# con el tipo de salida configurado como Windows Application.

Para una edición de archivo de proyecto, establezca OutputType en WinExe:

<OutputType>WinExe</OutputType>

Haga esto antes de validar la experiencia de usuario final o publicar. Un proveedor de consola puede abrir una ventana de terminal cada vez que el host del widget activa su servidor COM.

Publicación del widget

Después de desarrollar y probar el widget, puede publicar la aplicación en el Microsoft Store para que los usuarios instalen los widgets en sus dispositivos. Para obtener instrucciones paso a paso para publicar una aplicación, consulta Publish your app in the Microsoft Store.

Colección de widgets en Store

Una vez que tu aplicación se haya publicado en Microsoft Store, puedes solicitar que se incluya en la Colección de Widgets de la Tienda, que ayuda a los usuarios a descubrir aplicaciones que son compatibles con Windows Widgets. Para enviar la solicitud, consulte Envío de la información del widget para agregarlo a la colección de Store.

Captura de pantalla de la Microsoft Store que muestra la colección de widgets que permite a los usuarios descubrir aplicaciones que incorporan Windows Widgets.

Implementación de la personalización de widgets

A partir de SDK de Aplicaciones para Windows 1.4, los widgets pueden admitir la personalización del usuario. Cuando se implementa esta característica, se agrega una opción Personalizar widget al menú de puntos suspensivos situado encima de la opción Desanclar widget.

Captura de pantalla que muestra un widget con el cuadro de diálogo de personalización mostrado.

En los pasos siguientes, se resume el proceso de personalización del widget.

  1. En condiciones normales de funcionamiento, el proveedor de widgets responde a las solicitudes del host de widgets con la plantilla y las cargas de datos necesarias para la experiencia habitual del widget.
  2. El usuario hace clic en el botón Personalizar widget en el menú de elipsis.
  3. El widget genera el evento OnCustomizationRequested en el proveedor de widgets para indicar que el usuario ha solicitado la experiencia de personalización del widget.
  4. El proveedor de widgets establece una marca interna para indicar que el widget está en modo de personalización. Mientras está en modo de personalización, el proveedor de widgets envía las plantillas JSON para la interfaz de usuario de personalización del widget en lugar de la interfaz de usuario normal del widget.
  5. Mientras está en modo de personalización, el proveedor de widgets recibe eventos OnActionInvoked a medida que el usuario interactúe con la interfaz de usuario de personalización y ajuste su configuración interna y comportamiento en función de las acciones del usuario.
  6. Cuando la acción asociada al evento de OnActionInvoked es la acción "personalización de salida" definida por la aplicación, el proveedor de widgets restablece su marca interna para indicar que ya no está en modo de personalización y reanuda el envío de las plantillas JSON de datos y visuales para la experiencia regular del widget, lo que refleja los cambios solicitados durante la personalización. Es posible que el usuario cierre la experiencia de personalización sin hacer clic en la acción de personalización de salida definida por la aplicación. En este caso, se activará el IWidgetProviderAnalytics.OnAnalyticsInfoReported y el WidgetAnalyticsInfoReportedArgs.AnalyticsJson tendrá un interactionKind de "exitCustomization".
  7. El proveedor de widgets conserva las opciones de personalización en el disco o en la nube para que los cambios se conserven entre las invocaciones del proveedor de widgets.

Nota

Hay un error conocido con el panel de widgets de Windows, para los widgets creados mediante la SDK de Aplicaciones para Windows, lo que hace que el menú de puntos suspensivos deje de responder después de que se muestre la tarjeta de personalización.

En escenarios típicos de personalización de widgets, el usuario elegirá qué datos se mostrarán en el widget o ajustará la presentación visual del widget. Para una mayor simplicidad, en el ejemplo de esta sección se agregará el comportamiento de personalización que permite al usuario restablecer el contador del widget de recuento implementado en los pasos anteriores.

Nota

La personalización del widget requiere SDK de Aplicaciones para Windows 1.4 o posterior. En este tutorial se usa Microsoft.WindowsAppSDK versión 2.5.1.

Actualice el manifiesto del paquete para declarar el soporte de personalización

Para que el host del widget sepa que el widget admite la personalización, agregue el atributo IsCustomizable al elemento Definición para el widget y establézcalo en verdadero.

...
<Definition Id="Counting_Widget"
    DisplayName="Microsoft Counting Widget"
    Description="CONFIG counting widget description"
    IsCustomizable="true">
...

Seguimiento cuando un widget está en modo de personalización

En el ejemplo de este artículo se usa la estructura de aplicación auxiliar CompactWidgetInfo para realizar un seguimiento del estado actual de nuestros widgets activos. Agregue el campo inCustomization, que se usará para realizar el seguimiento cuando el host del widget espere que enviemos nuestra plantilla JSON de personalización en lugar de la plantilla de widget normal.

// WidgetProvider.cs
public class CompactWidgetInfo
{
    public string widgetId { get; set; }
    public string widgetName { get; set; }
    public int customState = 0;
    public bool isActive = false;
    public bool inCustomization = false;
}

Implementación de IWidgetProvider2

La característica de personalización del widget se expone a través de la interfaz IWidgetProvider2. Actualice la definición de clase WidgetProvider para implementar esta interfaz.

// WidgetProvider.cs
internal class WidgetProvider : IWidgetProvider, IWidgetProvider2

Agregue una implementación para la OnCustomizationRequested devolución de llamada de la interfaz deIWidgetProvider2. Este método usa el mismo patrón que los otros callbacks que hemos usado. Obtenemos el identificador para que el widget se personalice desde WidgetContext, busque la estructura auxiliar CompactWidgetInfo asociada a ese widget y establezca el campo inCustomization en verdadero.

// WidgetProvider.cs
public void OnCustomizationRequested(WidgetCustomizationRequestedArgs customizationInvokedArgs)
{
    var widgetId = customizationInvokedArgs.WidgetContext.Id;
    if (RunningWidgets.ContainsKey(widgetId))
    {
        var localWidgetInfo = RunningWidgets[widgetId];
        localWidgetInfo.inCustomization = true;
        UpdateWidget(localWidgetInfo);
    }
}

Por último, declare una variable de cadena que defina la plantilla JSON para la interfaz de usuario de personalización del widget. En este ejemplo, tenemos un botón "Restablecer contador" y un botón "Salir de la personalización", que indicarán a nuestro proveedor que vuelva al comportamiento normal del widget. Sitúe esta definición junto a las demás definiciones de plantilla.

// WidgetProvider.cs
const string countWidgetCustomizationTemplate = @"
{
    ""type"": ""AdaptiveCard"",
    ""actions"" : [
        {
            ""type"": ""Action.Execute"",
            ""title"" : ""Reset counter"",
            ""verb"": ""reset""
            },
            {
            ""type"": ""Action.Execute"",
            ""title"": ""Exit customization"",
            ""verb"": ""exitCustomization""
            }
    ],
    ""$schema"": ""http://adaptivecards.io/schemas/adaptive-card.json"",
    ""version"": ""1.5""
}";

Enviar plantilla de personalización en UpdateWidget

A continuación, actualizaremos nuestro método auxiliar UpdateWidget, que enviará nuestras plantillas JSON visuales y datos al host del widget. Cuando se actualiza el widget de recuento, se envía la plantilla de widget normal o la de personalización, en función del valor del campo inCustomization. Para una mayor brevedad, el código no relevante para la personalización se omite en este fragmento de código.

// WidgetProvider.cs
void UpdateWidget(CompactWidgetInfo localWidgetInfo)
{
    ...
    else if (localWidgetInfo.widgetName == "Counting_Widget")
    {
        if (!localWidgetInfo.inCustomization)
        {
            templateJson = countWidgetTemplate.ToString();
        }
        else
        {
            templateJson = countWidgetCustomizationTemplate.ToString();
        }
    
    }
    ...
    updateOptions.Template = templateJson;
    updateOptions.Data = dataJson;
    // You can store some custom state in the widget service that you will be able to query at any time.
    updateOptions.CustomState = localWidgetInfo.customState.ToString();
    WidgetManager.GetDefault().UpdateWidget(updateOptions);
}

Responder a acciones de personalización

Cuando los usuarios interactúan con entradas en nuestra plantilla de personalización, se llama al mismo controlador OnActionInvoked que cuando el usuario interactúa con la experiencia de widget normal. Para admitir la personalización, se buscan los verbos "reset" y "exitCustomization" en nuestra plantilla JSON de personalización. La acción "Restablecer contador" restablece customState a 0 y sale de la personalización. La acción "Salir de la personalización" sale sin restablecer el contador. Ambos establecen inCustomization en false para que UpdateWidget envíe la plantilla de widget normal.

// WidgetProvider.cs
public void OnActionInvoked(WidgetActionInvokedArgs actionInvokedArgs)
{
    var verb = actionInvokedArgs.Verb;
    if (verb == "inc")
    {
        var widgetId = actionInvokedArgs.WidgetContext.Id;
        // If you need to use some data that was passed in after
        // Action was invoked, you can get it from the args:
        var data = actionInvokedArgs.Data;
        if (RunningWidgets.ContainsKey(widgetId))
        {
            var localWidgetInfo = RunningWidgets[widgetId];
            // Increment the count
            localWidgetInfo.customState++;
            UpdateWidget(localWidgetInfo);
        }
    } 
    else if (verb == "reset") 
    {
        var widgetId = actionInvokedArgs.WidgetContext.Id;
        var data = actionInvokedArgs.Data;
        if (RunningWidgets.ContainsKey(widgetId))
        {
            var localWidgetInfo = RunningWidgets[widgetId];
            // Reset the count
            localWidgetInfo.customState = 0;
            localWidgetInfo.inCustomization = false;
            UpdateWidget(localWidgetInfo);
        }
    }
    else if (verb == "exitCustomization")
    {
        var widgetId = actionInvokedArgs.WidgetContext.Id;
        var data = actionInvokedArgs.Data;
        if (RunningWidgets.ContainsKey(widgetId))
        {
            var localWidgetInfo = RunningWidgets[widgetId];
            // Stop sending the customization template
            localWidgetInfo.inCustomization = false;
            UpdateWidget(localWidgetInfo);
        }
    }
}

Ahora, al implementar el widget, debería ver el botón Personalizar widget en el menú de puntos suspensivos. Al hacer clic en el botón Personalizar, se mostrará la plantilla de personalización.

Captura de pantalla que muestra la interfaz de usuario de personalización de widgets.

Haga clic en el botón Restablecer contador para restablecer el contador a 0. Haga clic en el botón Salir de la personalización para volver al comportamiento normal del widget.