Parte 4: Aggiungere attività utente e notifiche di Windows

Questa è la quarta parte di un'esercitazione che illustra come modernizzare un'app desktop WPF di esempio denominata Contoso Expenses. Per una panoramica dell'esercitazione, dei prerequisiti e delle istruzioni per il download dell'app di esempio, vedere Esercitazione: Modernizzare un'app WPF. Questo articolo presuppone che tu abbia già completato la parte 3.

Nelle parti precedenti di questa esercitazione hai aggiunto controlli XAML UWP all'app usando XAML Islands. Come conseguenza di questa operazione, si ha anche abilitata l'app per chiamare qualsiasi API WinRT. Ciò consente all'app di usare molte altre funzionalità offerte da Windows, non solo i controlli XAML UWP.

Nello scenario fittizio di questa esercitazione il team di sviluppo di Contoso ha deciso di aggiungere due nuove funzionalità all'app: attività e notifiche. Questa parte dell'esercitazione illustra come implementare tali funzionalità.

Aggiungere un'attività utente

Nota

La funzionalità Sequenza temporale non è più disponibile a partire da Windows 11

In Windows 10 le app possono tenere traccia delle attività eseguite dall'utente, ad esempio l'apertura di un file o la visualizzazione di una pagina specifica. Queste attività vengono quindi rese disponibili tramite Sequenza temporale, una funzionalità introdotta in Windows 10 versione 1803, che consente all'utente di tornare rapidamente indietro nel tempo e riprendere un'attività avviata in precedenza.

Windows Timeline image

È possibile tenere traccia delle attività utente tramite Microsoft Graph. Tuttavia, quando crei un'app per Windows 10, non è necessario interagire direttamente con gli endpoint REST forniti da Microsoft Graph. Puoi invece usare un pratico set di API WinRT. Queste API WinRT verranno usate nell'app Contoso Expenses per tenere traccia di tutte le volte in cui l'utente apre una spesa all'interno dell'app e sarà possibile usare schede adattive per consentire agli utenti di creare l'attività.

Introduzione alle schede adattive

Questa sezione fornisce una breve panoramica delle schede adattive. Se non hai bisogno di queste informazioni, puoi ignorare la sezione e passare direttamente alle istruzioni per aggiungere una scheda adattiva.

Le schede adattive consentono agli sviluppatori di scambiare contenuto delle schede in modo comune e coerente. Una scheda adattiva è descritta da un payload JSON che ne definisce il contenuto, che può includere testo, immagini, azioni e altro ancora.

Una scheda adattiva definisce solo il contenuto e non l'aspetto visivo di quest'ultimo. La piattaforma in cui viene ricevuta la scheda adattiva può eseguire il rendering del contenuto usando gli stili più appropriati. Le schede adattive vengono progettate tramite un renderer, che è in grado di prendere il payload JSON e convertirlo nell'interfaccia utente nativa. Ad esempio, l'interfaccia utente potrebbe essere XAML per un'app WPF o UWP, AXML per un'app Android o HTML per un sito Web o una chat bot.

Di seguito è riportato un esempio di un semplice payload di una scheda adattiva.

{
    "type": "AdaptiveCard",
    "body": [
        {
            "type": "Container",
            "items": [
                {
                    "type": "TextBlock",
                    "size": "Medium",
                    "weight": "Bolder",
                    "text": "Publish Adaptive Card schema"
                },
                {
                    "type": "ColumnSet",
                    "columns": [
                        {
                            "type": "Column",
                            "items": [
                                {
                                    "type": "Image",
                                    "style": "Person",
                                    "url": "https://pbs.twimg.com/profile_images/3647943215/d7f12830b3c17a5a9e4afcc370e3a37e_400x400.jpeg",
                                    "size": "Small"
                                }
                            ],
                            "width": "auto"
                        },
                        {
                            "type": "Column",
                            "items": [
                                {
                                    "type": "TextBlock",
                                    "weight": "Bolder",
                                    "text": "Matt Hidinger",
                                    "wrap": true
                                },
                                {
                                    "type": "TextBlock",
                                    "spacing": "None",
                                    "text": "Created {{DATE(2017-02-14T06:08:39Z,SHORT)}}",
                                    "isSubtle": true,
                                    "wrap": true
                                }
                            ],
                            "width": "stretch"
                        }
                    ]
                }
            ]
        }
    ],
    "actions": [
        {
            "type": "Action.ShowCard",
            "title": "Set due date",
            "card": {
                "type": "AdaptiveCard",
                "style": "emphasis",
                "body": [
                    {
                        "type": "Input.Date",
                        "id": "dueDate"
                    },
                    {
                        "type": "Input.Text",
                        "id": "comment",
                        "placeholder": "Add a comment",
                        "isMultiline": true
                    }
                ],
                "actions": [
                    {
                        "type": "Action.OpenUrl",
                        "title": "OK",
                        "url": "http://adaptivecards.io"
                    }
                ],
                "$schema": "http://adaptivecards.io/schemas/adaptive-card.json"
            }
        },
        {
            "type": "Action.OpenUrl",
            "title": "View",
            "url": "http://adaptivecards.io"
        }
    ],
    "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
    "version": "1.0"
}

L'immagine seguente illustra come viene eseguito il rendering di questo codice JSON in modi diversi da un canale Teams, da Cortana e da una notifica di Windows.

Adaptive Card rendering image

Le schede adattive svolgono un ruolo importante in Sequenza temporale perché rappresentano il modo in cui Windows esegue il rendering delle attività. Ogni anteprima visualizzata all'interno di Sequenza temporale è effettivamente una scheda adattiva. Di conseguenza, quando creerai un'attività utente all'interno della tua app, ti verrà richiesto di fornire una scheda adattiva per eseguirne il rendering.

Nota

Un ottimo modo per fare brainstorming per progettare una scheda adattiva consiste nell'usare la finestra di progettazione online. Avrai infatti la possibilità di progettare la scheda con blocchi predefiniti (immagini, testi, colonne e così via) e di ottenere il codice JSON corrispondente. Dopo esserti fatto un'idea di come sarà la struttura finale, puoi usare una libreria denominata Schede adattive per creare più facilmente la scheda adattiva usando le classi C# anziché il solo codice JSON, che potrebbe essere difficile da sottoporre a debug e da compilare.

Aggiungere una scheda adattiva

  1. Fai clic con il pulsante destro del mouse sul progetto ContosoExpenses.Core in Esplora soluzioni e scegli Gestisci pacchetti NuGet.

  2. Nella finestra Gestione pacchetti NuGet fai clic su Sfoglia. Cerca il pacchetto Newtonsoft.Json e installa la versione disponibile più recente. Si tratta di una libreria di manipolazione JSON diffusa che si usa per modificare più facilmente le stringhe JSON richieste dalle schede adattive.

    NewtonSoft.Json NuGet package

    Nota

    Se non installi il pacchetto Newtonsoft.Json separatamente, la libreria Schede adattive farà riferimento a una versione precedente del pacchetto Newtonsoft.Json che non supporta .NET Core 3.0.

  3. Nella finestra Gestione pacchetti NuGet fai clic su Sfoglia. Cerca il pacchetto AdaptiveCards e installa la versione disponibile più recente.

    Adaptive Cards NuGet package

  4. In Esplora soluzioni, fare clic con il pulsante destro del mouse sul progetto ContosoExpenses.Core e scegliere Aggiungi > Classe. Assegna alla classe il nome TimelineService.cs e fai clic su OK.

  5. Nella parte superiore del file TimelineService.cs aggiungi le istruzioni seguenti.

    using AdaptiveCards;
    using ContosoExpenses.Data.Models;
    
  6. Modifica lo spazio dei nomi dichiarato nel file da ContosoExpenses.Core a ContosoExpenses.

  7. Aggiungere il seguente metodo alla classe TimelineService.

     private string BuildAdaptiveCard(Expense expense)
     {
         AdaptiveCard card = new AdaptiveCard("1.0");
    
         AdaptiveTextBlock title = new AdaptiveTextBlock
         {
             Text = expense.Description,
             Size = AdaptiveTextSize.Medium,
             Wrap = true
         };
    
         AdaptiveColumnSet columnSet = new AdaptiveColumnSet();
         AdaptiveColumn photoColumn = new AdaptiveColumn
         {
             Width = "auto"
         };
    
         AdaptiveImage image = new AdaptiveImage
         {
             Url = new Uri("https://appmodernizationworkshop.blob.core.windows.net/contosoexpenses/Contoso192x192.png"),
             Size = AdaptiveImageSize.Small,
             Style = AdaptiveImageStyle.Default
         };
         photoColumn.Items.Add(image);
    
         AdaptiveTextBlock amount = new AdaptiveTextBlock
         {
             Text = expense.Cost.ToString(),
             Weight = AdaptiveTextWeight.Bolder,
             Wrap = true
         };
    
         AdaptiveTextBlock date = new AdaptiveTextBlock
         {
             Text = expense.Date.Date.ToShortDateString(),
             IsSubtle = true,
             Spacing = AdaptiveSpacing.None,
             Wrap = true
         };
    
         AdaptiveColumn expenseColumn = new AdaptiveColumn
         {
             Width = "stretch"
         };
         expenseColumn.Items.Add(amount);
         expenseColumn.Items.Add(date);
    
         columnSet.Columns.Add(photoColumn);
         columnSet.Columns.Add(expenseColumn);
    
         card.Body.Add(title);
         card.Body.Add(columnSet);
    
         string json = card.ToJson();
         return json;
     }
    

Informazioni sul codice

Questo metodo riceve un oggetto Expense con tutte le informazioni sulla spesa di cui eseguire il rendering e crea un nuovo oggetto AdaptiveCard. Il metodo aggiunge alla scheda quanto segue:

  • Un titolo, che usa la descrizione della spesa.
  • Un'immagine, ovvero il logo Contoso.
  • L'importo della spesa.
  • La data della spesa.

Gli ultimi 3 elementi sono suddivisi in due colonne diverse, in modo che il logo Contoso e i dettagli relativi alla spesa possano essere posizionati l'uno accanto agli altri. Dopo la creazione dell'oggetto, il metodo restituisce la stringa JSON corrispondente grazie al metodo ToJson.

Definire l'attività utente

Ora che hai definito la scheda adattiva, puoi creare un'attività utente basata su di essa.

  1. Aggiungi le istruzioni seguenti nella parte superiore del file TimelineService.cs:

    using Windows.ApplicationModel.UserActivities;
    using System.Threading.Tasks;
    using Windows.UI.Shell;
    

    Nota

    Sono spazi dei nomi UWP. Vengono risolti perché il pacchetto NuGet Microsoft.Toolkit.Wpf.UI.Controls installato nel passaggio 2 include un riferimento al pacchetto Microsoft.Windows.SDK.Contracts, che consente al progetto ContosoExpenses.Core di fare riferimento alle API WinRT anche se è un progetto .NET Core 3.

  2. Aggiungi alla classe TimelineService le dichiarazioni di campo seguenti.

    private UserActivityChannel _userActivityChannel;
    private UserActivity _userActivity;
    private UserActivitySession _userActivitySession;
    
  3. Aggiungere il seguente metodo alla classe TimelineService.

    public async Task AddToTimeline(Expense expense)
    {
        _userActivityChannel = UserActivityChannel.GetDefault();
        _userActivity = await _userActivityChannel.GetOrCreateUserActivityAsync($"Expense-{expense.ExpenseId}");
    
        _userActivity.ActivationUri = new Uri($"contosoexpenses://expense/{expense.ExpenseId}");
        _userActivity.VisualElements.DisplayText = "Contoso Expenses";
    
        string json = BuildAdaptiveCard(expense);
    
        _userActivity.VisualElements.Content = AdaptiveCardBuilder.CreateAdaptiveCardFromJson(json);
    
        await _userActivity.SaveAsync();
        _userActivitySession?.Dispose();
        _userActivitySession = _userActivity.CreateSession();
    }
    
  4. Salva le modifiche in TimelineService.cs.

Informazioni sul codice

Il metodo AddToTimeline ottiene prima di tutto un oggetto UserActivityChannel necessario per archiviare le attività utente. Viene quindi creata una nuova attività utente con il metodo GetOrCreateUserActivityAsync, che richiede un identificatore univoco. In questo modo, se esiste già un'attività, l'app può aggiornarla; in caso contrario, ne creerà una nuova. L'identificatore da passare dipende dal tipo di applicazione che stai creando:

  • Se vuoi aggiornare sempre la stessa attività in modo che in Sequenza temporale venga visualizzata solo quella più recente, puoi usare un identificatore fisso, ad esempio Expenses.
  • Se vuoi tenere traccia di ciascuna attività come un'attività diversa in modo che in Sequenza temporale vengano visualizzate tutte le attività, puoi usare un identificatore dinamico.

In questo scenario l'app terrà traccia di ogni spesa aperta come un'attività utente diversa, quindi il codice crea ogni identificatore usando la parola chiave Expense- seguita dall'ID spesa univoco.

Dopo che il metodo crea un oggetto UserActivity, lo popola con le informazioni seguenti:

  • Un ActivationUri che viene richiamato quando l'utente fa clic sull'attività in Sequenza temporale. Il codice usa un protocollo personalizzato denominato contosoexpenses che verrà gestito dall'app in un secondo momento.
  • L'oggetto VisualElements, che contiene un set di proprietà che definiscono l'aspetto visivo dell'attività. Questo codice imposta DisplayText, ovvero il titolo visualizzato sopra la voce immessa in Sequenza temporale, e Content.

Questo è il punto in cui entra in gioco la scheda adattiva definita in precedenza. L'app passa la scheda adattiva progettata precedentemente come contenuto al metodo. Tuttavia, per rappresentare una scheda, Windows 10 usa un oggetto diverso da quello usato dal pacchetto NuGet AdaptiveCards. Viene pertanto creata di nuovo la scheda usando il metodo CreateAdaptiveCardFromJson esposto dalla classe AdaptiveCardBuilder. Dopo che il metodo crea l'attività utente, la salva e crea una nuova sessione.

Quando un utente fa clic su un'attività in Sequenza temporale, viene attivato il protocollo contosoexpenses:// e l'URL include le informazioni necessarie all'app per recuperare la spesa selezionata. Come operazione facoltativa, puoi implementare l'attivazione del protocollo in modo che l'applicazione reagisca correttamente quando l'utente usa Sequenza temporale.

Integrare l'applicazione con Sequenza temporale

Ora che hai creato una classe che interagisce con Sequenza temporale, puoi iniziare a usarla per migliorare l'esperienza dell'applicazione. Il momento migliore per usare il metodo AddToTimeline esposto dalla classe TimelineService è quando l'utente apre la pagina dei dettagli di una spesa.

  1. Nel progetto ContosoExpenses.Core espandi la cartella ViewModels e apri il file ExpenseDetailViewModel.cs. Questo è il modello visualizzazione (ViewModel) che supporta la finestra dei dettagli della spesa.

  2. Individua il costruttore pubblico della classe ExpenseDetailViewModel e aggiungi il codice seguente alla fine del costruttore. Ogni volta che viene aperta la finestra della spesa, il metodo chiama il metodo AddToTimeline e passa la spesa corrente. La classe TimelineService usa queste informazioni per creare un'attività utente con le informazioni relative alla spesa.

    TimelineService timeline = new TimelineService();
    timeline.AddToTimeline(expense);
    

    Al termine, il costruttore avrà un aspetto simile al seguente.

    public ExpensesDetailViewModel(IDatabaseService databaseService, IStorageService storageService)
    {
        var expense = databaseService.GetExpense(storageService.SelectedExpense);
    
        ExpenseType = expense.Type;
        Description = expense.Description;
        Location = expense.Address;
        Amount = expense.Cost;
    
        TimelineService timeline = new TimelineService();
        timeline.AddToTimeline(expense);
    }
    
  3. Premi F5 per compilare ed eseguire l'app nel debugger. Seleziona un dipendente dall'elenco e quindi scegli una spesa. Nella pagina dei dettagli nota la descrizione della spesa, la data e l'importo.

  4. Premi Start + TAB per aprire Sequenza temporale.

  5. Scorri verso il basso l'elenco delle applicazioni attualmente aperte fino a visualizzare la sezione intitolata Oggi. Questa sezione visualizza alcune delle attività utente più recenti. Fai clic sul collegamento Visualizza tutte le attività accanto all'intestazione Oggi.

  6. Verifica che venga visualizzata una nuova scheda con le informazioni relative alla spesa appena selezionata nell'applicazione.

    Contoso Expenses Timeline

  7. Se ora apri altre spese, vedrai che verranno aggiunte nuove schede come attività utente. Ricordati che il codice usa un identificatore diverso per ciascuna attività e che quindi crea una scheda per ogni spesa che apri nell'app.

  8. Chiudere l'app.

Aggiungere una notifica

La seconda funzionalità che il team di sviluppo di Contoso intende aggiungere è una notifica da visualizzare all'utente ogni volta che una nuova spesa viene salvata nel database. A tale scopo, puoi sfruttare il sistema di notifiche incorporato in Windows 10, che viene esposto agli sviluppatori tramite le API WinRT. Questo sistema di notifica offre molti vantaggi:

  • Le notifiche sono coerenti con il resto del sistema operativo.
  • Sono interattive.
  • Vengono archiviate nel Centro operativo, in modo che possano essere esaminate in un secondo momento.

Per aggiungere una notifica all'app:

  1. In Esplora soluzioni, fare clic con il pulsante destro del mouse sul progetto ContosoExpenses.Core e scegliere Aggiungi > Classe. Assegna alla classe il nome NotificationService.cs e fai clic su OK.

  2. Nella parte superiore del file NotificationService.cs aggiungi le istruzioni seguenti.

    using Windows.Data.Xml.Dom;
    using Windows.UI.Notifications;
    
  3. Modifica lo spazio dei nomi dichiarato nel file da ContosoExpenses.Core a ContosoExpenses.

  4. Aggiungere il seguente metodo alla classe NotificationService.

    public void ShowNotification(string description, double amount)
    {
        string xml = $@"<toast>
                          <visual>
                            <binding template='ToastGeneric'>
                              <text>Expense added</text>
                              <text>Description: {description} - Amount: {amount} </text>
                            </binding>
                          </visual>
                        </toast>";
    
        XmlDocument doc = new XmlDocument();
        doc.LoadXml(xml);
    
        ToastNotification toast = new ToastNotification(doc);
        ToastNotificationManager.CreateToastNotifier().Show(toast);
    }
    

    Le notifiche di tipo avviso popup sono rappresentate da un payload XML, che può includere testo, immagini, azioni e altro ancora. Puoi trovare tutti gli elementi supportati qui. Questo codice usa uno schema molto semplice con due righe di testo: il titolo e il corpo. Dopo che il codice definisce il payload XML e lo carica in un oggetto XmlDocument, esegue il wrapping del codice XML in un oggetto ToastNotification e lo mostra usando la classe ToastNotificationManager.

  5. Nel progetto ContosoExpenses.Core espandi la cartella ViewModels e apri il file AddNewExpenseViewModel.cs.

  6. Individua il metodo SaveExpenseCommand, che viene attivato quando l'utente preme il pulsante per salvare una nuova spesa. Aggiungi il codice seguente a questo metodo, subito dopo la chiamata al metodo SaveExpense.

    NotificationService notificationService = new NotificationService();
    notificationService.ShowNotification(expense.Description, expense.Cost);
    

    Al termine, il metodo SaveExpenseCommand dovrebbe essere simile al seguente.

    private RelayCommand _saveExpenseCommand;
    public RelayCommand SaveExpenseCommand
    {
        get
        {
            if (_saveExpenseCommand == null)
            {
                _saveExpenseCommand = new RelayCommand(() =>
                {
                    Expense expense = new Expense
                    {
                        Address = Address,
                        City = City,
                        Cost = Cost,
                        Date = Date,
                        Description = Description,
                        EmployeeId = storageService.SelectedEmployeeId,
                        Type = ExpenseType
                    };
    
                    databaseService.SaveExpense(expense);
    
                    NotificationService notificationService = new NotificationService();
                    notificationService.ShowNotification(expense.Description, expense.Cost);
    
                    Messenger.Default.Send<UpdateExpensesListMessage>(new UpdateExpensesListMessage());
                    Messenger.Default.Send<CloseWindowMessage>(new CloseWindowMessage());
                }, () => IsFormFilled
                );
            }
    
            return _saveExpenseCommand;
        }
    }
    
  7. Premi F5 per compilare ed eseguire l'app nel debugger. Scegli un dipendente dall'elenco e quindi fai clic sul pulsante Aggiungi nuova spesa. Completa tutti i campi del modulo e fai clic su Salva.

  8. Riceverai l'eccezione seguente.

    Toast notification error

Questa eccezione è causata dal fatto che l'app Contoso Expenses non ha ancora l'identità del pacchetto. Alcune API WinRT, inclusa l'API delle notifiche, richiedono l'identità del pacchetto prima di poter essere usate in un'app. Le app UWP ricevono l'identità del pacchetto per impostazione predefinita perché possono essere distribuite solo tramite pacchetti MSIX. Anche altri tipi di app per Windows, tra cui le app WPF, possono essere distribuiti tramite pacchetti MSIX per ottenere l'identità del pacchetto. La parte successiva di questa esercitazione illustrerà come eseguire questa operazione.

Passaggi successivi

A questo punto dell'esercitazione, hai aggiunto correttamente all'app un'attività utente che si integra con Sequenza temporale di Windows e hai aggiunto all'app una notifica che viene attivata quando gli utenti creano una nuova spesa. La notifica però ancora non funziona perché l'app, per poter usare l'API delle notifiche, richiede l'identità del pacchetto. Per informazioni su come creare un pacchetto MSIX in modo che l'app possa ottenere l'identità del pacchetto e beneficiare di altri vantaggi dal punto di vista della distribuzione, vedere Parte 5:Creare e distribuire con MSIX.