Tutoriel : Créer une visionneuse de photos simple qui cible plusieurs plateformes

Une fois que vous aurez créé une application winUI 3 de visionneuse de photos simple de débutant, vous vous demanderez peut-être comment toucher davantage d’utilisateurs sans avoir à réécrire votre application. Ce tutoriel utilise Uno Platform pour élargir la portée de votre application C# WinUI 3 existante, ce qui permet une réutilisation de la logique métier et de la couche d’interface utilisateur sur les plateformes mobiles, web et bureau natives. En apportant un minimum de modifications à l’application de visionneuse de photos simple, nous pouvons porter une copie parfaite de l’application sur ces plateformes.

Screenshot of UnoSimplePhoto app targeting web and WinUI desktop.

Prerequisites

Finaliser votre environnement

  1. Ouvrez une invite de ligne de commande, Terminal Windows si vous l’avez installé, ou sinon Invite de commandes ou Windows PowerShell à partir du menu Démarrer.

  2. Installez ou mettez à jour l’outil uno-check :

    • Utilisez la commande suivante :

      dotnet tool install -g uno.check
      
    • Pour mettre à jour l’outil, si vous avez déjà installé une version antérieure :

      dotnet tool update -g uno.check
      
  3. Exécutez l’outil avec la commande suivante :

    uno-check
    
  4. Suivez les instructions indiquées par l’outil. Sachant qu’il doit modifier votre système, il se peut que des autorisations élevées vous soient demandées.

Installer les modèles de solution Uno Platform

Lancez Visual Studio, puis cliquez sur Continue without code. Cliquez sur Extensions ->Manage Extensions dans la barre de menus.

Screenshot of the Visual Studio Menu bar item that reads manage extensions.

Dans le Gestionnaire d’extensions, développez le nœud En ligne, recherchez Uno, installez l’extension Uno Platform ou téléchargez-la et installez-la sur Visual Studio Marketplace, puis redémarrez Visual Studio.

Screenshot of the manage Extensions window in Visual Studio with Uno Platform extension as a search result.

Créer une application

Maintenant que nous sommes prêts à créer une application multiplateforme, l’approche que nous allons adopter consiste à créer une application Uno Platform. Nous allons copier le code du projet WinUI 3 SimplePhotos du tutoriel précédent dans notre projet multiplateforme. Si cela est possible, c’est parce que Uno Platform vous permet de réutiliser le codebase existant. Pour les fonctionnalités qui dépendent des API de système d’exploitation fournies par chaque plateforme, vous pouvez facilement les faire fonctionner au fil du temps. Cette approche est particulièrement utile si vous souhaitez porter une application existante vers d’autres plateformes.

Assez vite, vous pourrez profiter des avantages de cette approche, car vous pourrez cibler davantage de plateformes avec une saveur XAML que vous connaissez et le codebase que vous possédez déjà.

Ouvrez Visual Studio et créez un projet via File>New>Project :

Screenshot of the create a new project dialog.

Recherchez Uno et sélectionnez le modèle de projet Uno Platform App :

Screenshot of the create a new project dialog with Uno Platform app as the selected project type.

Créez une solution C# en utilisant le type Uno Platform App dans la page de démarrage de Visual Studio. Pour éviter tout conflit avec le code du tutoriel précédent, nous allons donner à cette solution un autre nom, « UnoSimplePhotos ». Spécifiez le nom du projet, le nom de la solution et le répertoire. Dans cet exemple, notre projet multiplateforme UnoSimplePhotos appartient à une solution UnoSimplePhotos, qui résidera dans C:\Projects :

Screenshot of specifying project details for the new Uno Platform project.

Vous allez maintenant choisir un modèle de base pour rendre votre application de galerie Simple Photo multiplateforme.

Le modèle Uno Platform App propose deux options prédéfinies qui vous permettent de commencer rapidement avec une solution Blank (Vide) ou la configuration Default (Par défaut) qui comprend des références aux bibliothèques Uno.Material et Uno.Toolkit. La configuration Default comprend également Uno.Extensions qui est utile pour l’injection de dépendances, la configuration, la navigation et la journalisation. Par ailleurs, elle utilise MVUX à la place de MVVM, ce qui en fait un excellent point de départ pour la création rapide d’applications réelles.

Screenshot of the Uno solution template for project startup type.

Pour simplifier les choses, sélectionnez l’option prédéfinie Blank (Vide). Cliquez ensuite sur le bouton Create (Créer). Attendez que les projets soient créés et que leurs dépendances soient restaurées.

Une bannière en haut de l'éditeur peut demander de recharger les projets, cliquez sur Recharger les projets :

Screenshot of the Visual Studio banner offering to reload your projects to complete changes.

La structure de fichiers par défaut présentée ci-dessous doit alors s’afficher dans votre Explorateur de solutions :

Screenshot of the default file structure in Solution Explorer.

Ajouter des ressources d’images au projet

Votre application va avoir besoin d’images à afficher. Vous pouvez utiliser celles du tutoriel précédent.

Dans le projet UnoSimplePhotos, créez un dossier nommé Assets et copiez les fichiers image JPG dans un sous-dossier Samples. La structure de dossiers Assets doit maintenant se présenter comme ceci :

Screenshot of the Solution Explorer pane in Visual Studio with the new files and folders added.

Pour savoir comment créer le dossier Assets et y ajouter des images, consultez la documentation Uno Platform traitant des ressources et de l’affichage d’images.

Préparation de votre application

Maintenant que vous avez généré le point de départ fonctionnel de votre application WinUI multiplateforme, vous pouvez y copier le code du projet bureau.

Copier la vue

Comme Uno Platform vous permet d’utiliser la saveur XAML que vous connaissez déjà, vous pouvez copier le code que vous avez créé dans le tutoriel précédent.

Retourner dans la projet SimplePhotos du tutoriel précédent. Dans l’Explorateur de solutions, recherchez le fichier nommé MainWindow.xaml et ouvrez-le. Remarquez que le contenu de la vue est défini dans un élément Window et non dans Page. En effet, le projet bureau est une application WinUI 3, qui peut utiliser des éléments Window pour définir le contenu de la vue :

<Window x:Class="SimplePhotos.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="using:SimplePhotos"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d">

    <Grid>
        <Grid.Resources>
            <DataTemplate x:Key="ImageGridView_ItemTemplate" 
                          x:DataType="local:ImageFileInfo">
                <Grid Height="300"
                      Width="300"
                      Margin="8">
                    <Grid.RowDefinitions>
                        <RowDefinition />
                        <RowDefinition Height="Auto" />
                    </Grid.RowDefinitions>

                    <Image x:Name="ItemImage"
                           Source="Assets/StoreLogo.png"
                           Stretch="Uniform" />

                    <StackPanel Orientation="Vertical"
                                Grid.Row="1">
                        <TextBlock Text="{x:Bind ImageTitle}"
                                   HorizontalAlignment="Center"
                                   Style="{StaticResource SubtitleTextBlockStyle}" />
                        <StackPanel Orientation="Horizontal"
                                    HorizontalAlignment="Center">
                            <TextBlock Text="{x:Bind ImageFileType}"
                                       HorizontalAlignment="Center"
                                       Style="{StaticResource CaptionTextBlockStyle}" />
                            <TextBlock Text="{x:Bind ImageDimensions}"
                                       HorizontalAlignment="Center"
                                       Style="{StaticResource CaptionTextBlockStyle}"
                                       Margin="8,0,0,0" />
                        </StackPanel>

                        <RatingControl Value="{x:Bind ImageRating}" 
                                       IsReadOnly="True"/>
                    </StackPanel>
                </Grid>
            </DataTemplate>

            <Style x:Key="ImageGridView_ItemContainerStyle"
                   TargetType="GridViewItem">
                <Setter Property="Background" 
                        Value="Gray"/>
                <Setter Property="Margin" 
                        Value="8"/>
            </Style>

            <ItemsPanelTemplate x:Key="ImageGridView_ItemsPanelTemplate">
                    <ItemsWrapGrid Orientation="Horizontal"
                                   HorizontalAlignment="Center"/>
                </ItemsPanelTemplate>
        </Grid.Resources>

        <GridView x:Name="ImageGridView"
                  ItemsSource="{x:Bind Images}"
                  ItemTemplate="{StaticResource ImageGridView_ItemTemplate}"
                  ItemContainerStyle="{StaticResource ImageGridView_ItemContainerStyle}"
                  ItemsPanel="{StaticResource ImageGridView_ItemsPanelTemplate}"
                  ContainerContentChanging="ImageGridView_ContainerContentChanging" />
    </Grid>
</Window>

Avec Uno Platform, l’implémentation multiplateforme des contrôles présents dans l’élément Window, notamment GridView, Image et RatingControl, est l’assurance que la vue elle-même fonctionnera sur toutes les plateformes prises en charge sans trop d’efforts. Copiez le contenu de cet élément Window et collez-le dans l’élément Page du fichier MainPage.xaml dans le projet Uno Platform UnoSimplePhotos. Le fichier XAML de la vue MainPage doit se présenter comme suit :

<Page x:Class="UnoSimplePhotos.MainPage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:local="using:UnoSimplePhotos"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      mc:Ignorable="d">

    <Grid>
        <Grid.Resources>
            <DataTemplate x:Key="ImageGridView_ItemTemplate"
                          x:DataType="local:ImageFileInfo">
                <Grid Height="300"
                      Width="300"
                      Margin="8">
                    <Grid.RowDefinitions>
                        <RowDefinition />
                        <RowDefinition Height="Auto" />
                    </Grid.RowDefinitions>

                    <Image x:Name="ItemImage"
                           Source="Assets/StoreLogo.png"
                           Stretch="Uniform" />

                    <StackPanel Orientation="Vertical"
                                Grid.Row="1">
                        <TextBlock Text="{x:Bind ImageTitle}"
                                   HorizontalAlignment="Center"
                                   Style="{StaticResource SubtitleTextBlockStyle}" />
                        <StackPanel Orientation="Horizontal"
                                    HorizontalAlignment="Center">
                            <TextBlock Text="{x:Bind ImageFileType}"
                                       HorizontalAlignment="Center"
                                       Style="{StaticResource CaptionTextBlockStyle}" />
                            <TextBlock Text="{x:Bind ImageDimensions}"
                                       HorizontalAlignment="Center"
                                       Style="{StaticResource CaptionTextBlockStyle}"
                                       Margin="8,0,0,0" />
                        </StackPanel>

                        <RatingControl Value="{x:Bind ImageRating}" 
                                       IsReadOnly="True"/>
                    </StackPanel>
                </Grid>
            </DataTemplate>

            <Style x:Key="ImageGridView_ItemContainerStyle"
                   TargetType="GridViewItem">
                <Setter Property="Background" 
                        Value="Gray"/>
                <Setter Property="Margin" 
                        Value="8"/>
            </Style>

            <ItemsPanelTemplate x:Key="ImageGridView_ItemsPanelTemplate">
                <ItemsWrapGrid Orientation="Horizontal"
                               HorizontalAlignment="Center"/>
            </ItemsPanelTemplate>
        </Grid.Resources>

        <GridView x:Name="ImageGridView"
                  ItemsSource="{x:Bind Images}"
                  ItemTemplate="{StaticResource ImageGridView_ItemTemplate}"
                  ItemContainerStyle="{StaticResource ImageGridView_ItemContainerStyle}"
                  ItemsPanel="{StaticResource ImageGridView_ItemsPanelTemplate}"
                  ContainerContentChanging="ImageGridView_ContainerContentChanging">
        </GridView>
    </Grid>
</Page>

Vous vous souvenez peut-être que la solution bureau comportait également un fichier MainWindow.xaml.cs qui contenait du code-behind qui correspond à la vue. Dans le projet Uno Platform, le code-behind de la vue MainPage dans laquelle nous avons effectué la copie est contenu dans le fichier MainPage.xaml.cs.

Pour rendre ce code-behind multiplateforme, nous devons d’abord déplacer les éléments suivants dans le fichier MainPage.xaml.cs :

  • La propriété Images : fournit à GridView une collection observable de fichiers image

  • Le contenu du constructeur : appelle GetItemsAsync() pour remplir la collection Images d’éléments représentant des fichiers image

  • Supprime la modification manuelle de la propriété ItemsSource du contrôle ImageGridView

  • La méthode ImageGridView_ContainerContentChanging : utilisée dans le cadre d’une stratégie visant à charger progressivement les éléments GridView à mesure qu’ils défilent dans la vue

  • La méthode ShowImage : charge les fichiers image dans GridView

  • La méthode GetItemsAsync : obtient les fichiers de ressources image à partir du dossier Samples

  • La méthode LoadImageInfoAsync : construit un objet ImageFileInfo à partir d’un StorageFile créé

Après avoir tout déplacé, MainPage.xaml.cs doit maintenant se présenter comme suit :

using Microsoft.UI.Xaml.Controls;
using System.Collections.ObjectModel;
using Windows.Storage;
using Windows.Storage.Search;

namespace UnoSimplePhotos;

public sealed partial class MainPage : Page
{
    public ObservableCollection<ImageFileInfo> Images { get; } 
    = new ObservableCollection<ImageFileInfo>();

    public MainPage()
    {
        this.InitializeComponent();
        GetItemsAsync();
    }

    private void ImageGridView_ContainerContentChanging(ListViewBase sender,
        ContainerContentChangingEventArgs args)
    {
        if (args.InRecycleQueue)
        {
            var templateRoot = args.ItemContainer.ContentTemplateRoot as Grid;
            var image = templateRoot.FindName("ItemImage") as Image;
            image.Source = null;
        }

        if (args.Phase == 0)
        {
            args.RegisterUpdateCallback(ShowImage);
            args.Handled = true;
        }
    }

    private async void ShowImage(ListViewBase sender, ContainerContentChangingEventArgs args)
    {
        if (args.Phase == 1)
        {
            // It's phase 1, so show this item's image.
            var templateRoot = args.ItemContainer.ContentTemplateRoot as Grid;
            var image = templateRoot.FindName("ItemImage") as Image;
            var item = args.Item as ImageFileInfo;
            image.Source = await item.GetImageThumbnailAsync();
        }
    }

    private async Task GetItemsAsync()
    {
        StorageFolder appInstalledFolder = Package.Current.InstalledLocation;
        StorageFolder picturesFolder = await appInstalledFolder.GetFolderAsync("Assets\\Samples");

        var result = picturesFolder.CreateFileQueryWithOptions(new QueryOptions());

        IReadOnlyList<StorageFile> imageFiles = await result.GetFilesAsync();
        foreach (StorageFile file in imageFiles)
        {
            Images.Add(await LoadImageInfoAsync(file));
        }
    }

    public async static Task<ImageFileInfo> LoadImageInfoAsync(StorageFile file)
    {
        var properties = await file.Properties.GetImagePropertiesAsync();
        ImageFileInfo info = new(properties,
                                    file, file.DisplayName, file.DisplayType);

        return info;
    }
}

Note

Les fichiers de votre projet d’application Uno doivent être utilisés UnoSimplePhotos comme espace de noms.

Jusqu’à présent, les fichiers de la vue principale sur laquelle nous travaillons contiennent toutes les fonctionnalités de la solution bureau. Après avoir copié sur le fichier modèle ImageFileInfo.cs, nous allons apprendre à modifier les blocs de code orientés bureau pour une compatibilité multiplateforme.

Copiez ImageFileInfo à partir du projet bureau et collez-le dans le fichier ImageFileInfo.cs. Effectuez les modifications suivantes :

  • Renommez l’espace de noms UnoSimplePhotos au lieu de SimplePhotos :

    // Found towards the top of the file
    namespace UnoSimplePhotos;
    
  • Changez le type de paramètre de la méthode OnPropertyChanged en Nullable :

    // string -> string?
    protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
    ...
    
  • Rendez PropertyChangedEventHandler nullable :

    // PropertyChangedEventHandler -> PropertyChangedEventHandler?
    public event PropertyChangedEventHandler? PropertyChanged;
    

Au final, le fichier doit se présenter comme suit :

using Microsoft.UI.Xaml.Media.Imaging;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Windows.Storage;
using Windows.Storage.FileProperties;
using Windows.Storage.Streams;
using ThumbnailMode = Windows.Storage.FileProperties.ThumbnailMode;

namespace UnoSimplePhotos;

public class ImageFileInfo : INotifyPropertyChanged
{
    public ImageFileInfo(ImageProperties properties,
        StorageFile imageFile,
        string name,
        string type)
    {
        ImageProperties = properties;
        ImageName = name;
        ImageFileType = type;
        ImageFile = imageFile;
        var rating = (int)properties.Rating;
        var random = new Random();
        ImageRating = rating == 0 ? random.Next(1, 5) : rating;
    }

    public StorageFile ImageFile { get; }

    public ImageProperties ImageProperties { get; }

    public async Task<BitmapImage> GetImageSourceAsync()
    {
        using IRandomAccessStream fileStream = await ImageFile.OpenReadAsync();

        // Create a bitmap to be the image source.
        BitmapImage bitmapImage = new();
        bitmapImage.SetSource(fileStream);

        return bitmapImage;
    }

    public async Task<BitmapImage> GetImageThumbnailAsync()
    {
        StorageItemThumbnail thumbnail =
            await ImageFile.GetThumbnailAsync(ThumbnailMode.PicturesView);
        // Create a bitmap to be the image source.
        var bitmapImage = new BitmapImage();
        bitmapImage.SetSource(thumbnail);
        thumbnail.Dispose();

        return bitmapImage;
    }

    public string ImageName { get; }

    public string ImageFileType { get; }

    public string ImageDimensions => $"{ImageProperties.Width} x {ImageProperties.Height}";

    public string ImageTitle
    {
        get => string.IsNullOrEmpty(ImageProperties.Title) ? ImageName : ImageProperties.Title;
        set
        {
            if (ImageProperties.Title != value)
            {
                ImageProperties.Title = value;
                _ = ImageProperties.SavePropertiesAsync();
                OnPropertyChanged();
            }
        }
    }

    public int ImageRating
    {
        get => (int)ImageProperties.Rating;
        set
        {
            if (ImageProperties.Rating != value)
            {
                ImageProperties.Rating = (uint)value;
                _ = ImageProperties.SavePropertiesAsync();
                OnPropertyChanged();
            }
        }
    }

    public event PropertyChangedEventHandler? PropertyChanged;

    protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) =>
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

Cette classe servira de modèle pour représenter les fichiers image dans GridView. Même si, à ce stade, il est techniquement possible d’exécuter l’application, il se peut qu’elle ne puisse pas assurer un rendu correct des images ou en afficher les propriétés. Dans les prochaines sections, nous allons apporter un ensemble de modifications à ces fichiers copiés pour les rendre compatibles dans un contexte multiplateforme.

Utilisation de directives de préprocesseur

Dans le projet bureau du tutoriel précédent, le fichier MainPage.xaml.cs contient une méthode GetItemsAsync qui énumère les éléments d’un StorageFolder représentant l’emplacement du package installé. Cet emplacement n’étant pas disponible sur certaines plateformes comme WebAssembly, nous devons apporter des modifications à cette méthode pour la rendre compatible avec toutes les plateformes. Nous allons donc apporter des modifications à la classe ImageFileInfo afin de garantir la compatibilité.

Tout d’abord, apportez les modifications nécessaires à la méthode GetItemsAsync. Remplacez la méthode GetItemsAsync dans le fichier MainPage.xaml.cs par le code suivant :

private async Task GetItemsAsync()
{
#if WINDOWS
    StorageFolder appInstalledFolder = Package.Current.InstalledLocation;
    StorageFolder picturesFolder = await appInstalledFolder.GetFolderAsync("UnoSimplePhotos\\Assets\\Samples");

    var result = picturesFolder.CreateFileQueryWithOptions(new QueryOptions());

    IReadOnlyList<StorageFile> imageFiles = await result.GetFilesAsync();
#else
    var imageFileNames = Enumerable.Range(1, 20).Select(i => new Uri($"ms-appx:///UnoSimplePhotos/Assets/Samples/{i}.jpg"));
    var imageFiles = new List<StorageFile>();

    foreach (var file in imageFileNames)
    {
        imageFiles.Add(await StorageFile.GetFileFromApplicationUriAsync(file));
    }
#endif
    foreach (StorageFile file in imageFiles)
    {
        Images.Add(await LoadImageInfoAsync(file));
    }
}

Cette méthode utilise désormais une directive de préprocesseur pour déterminer le code à exécuter en fonction de la plateforme. Sur Windows, la méthode obtient le StorageFolder représentant l’emplacement du package installé et retourne le dossier Samples à partir de celui-ci. Sur les autres plateformes, la méthode compte jusqu’à 20, obtenant les fichiers image du dossier Samples en utilisant un Uri pour représenter le fichier image.

Ensuite, ajustez la méthode LoadImageInfoAsync pour tenir compte des modifications que nous avons apportées à la méthode GetItemsAsync. Remplacez la méthode LoadImageInfoAsync dans le fichier MainPage.xaml.cs par le code suivant :

public async static Task<ImageFileInfo> LoadImageInfoAsync(StorageFile file)
{
#if WINDOWS
    var properties = await file.Properties.GetImagePropertiesAsync();
    ImageFileInfo info = new(properties,
                                file, file.DisplayName, $"{file.FileType} file");
#else
    ImageFileInfo info = new(file, file.DisplayName, $"{file.FileType} file");
#endif
    return info;
}

À l’instar de la méthode GetItemsAsync, cette méthode utilise à présent une directive de préprocesseur pour déterminer le code à exécuter en fonction de la plateforme. Sur Windows, la méthode obtient ImageProperties à partir de StorageFile et s’en sert pour créer un objet ImageFileInfo. Sur les autres plateformes, la méthode construit un objet ImageFileInfo sans le paramètre ImageProperties. Par la suite, des modifications seront apportées à la classe ImageFileInfo pour prendre en compte cette modification.

Certains contrôles comme GridView autorisent le chargement progressif du contenu du conteneur d’éléments mis à jour à mesure qu’ils défilent dans la fenêtre d’affichage. Pour cela, l’événement ContainerContentChanging est utilisé. Dans le projet bureau du tutoriel précédent, la méthode ImageGridView_ContainerContentChanging se sert de cet événement pour charger les fichiers image dans GridView. Sachant que certains aspects de cet événement ne sont pas pris en charge sur toutes les plateformes, nous devons apporter des modifications à cette méthode pour la rendre compatible avec lesdites plateformes.

Diagram of collection control viewport.

Par exemple, la propriété ContainerContentChangingEventArgs.Phase n’est actuellement pas prise en charge sur les plateformes autres que Windows. Nous devons apporter des modifications à la méthode ImageGridView_ContainerContentChanging pour prendre en compte cette modification. Remplacez la méthode ImageGridView_ContainerContentChanging dans le fichier MainPage.xaml.cs par le code suivant :

private void ImageGridView_ContainerContentChanging(
ListViewBase sender,
ContainerContentChangingEventArgs args)
{

    if (args.InRecycleQueue)
    {
        var templateRoot = args.ItemContainer.ContentTemplateRoot as Grid;
        var image = templateRoot?.FindName("ItemImage") as Image;
        if (image is not null)
        {
            image.Source = null;
        }
    }

#if WINDOWS
        if (args.Phase == 0)
        {
            args.RegisterUpdateCallback(ShowImage);
            args.Handled = true;
        }
#else
    ShowImage(sender, args);
#endif
}

Le rappel spécialisé est désormais inscrit uniquement à l’aide de ContainerContentChangingEventArgs.RegisterUpdateCallback() si la plateforme est Windows. Sinon, la méthode ShowImage est appelée directement. Nous devons également apporter des modifications à la méthode ShowImage pour l’accommoder à celles qui ont été apportées à la méthode ImageGridView_ContainerContentChanging. Remplacez la méthode ShowImage dans le fichier MainPage.xaml.cs par le code suivant :

private async void ShowImage(ListViewBase sender, ContainerContentChangingEventArgs args)
{
    if (
#if WINDOWS
            args.Phase == 1
#else
        true
#endif
        )
    {

        // It's phase 1, so show this item's image.
        var templateRoot = args.ItemContainer.ContentTemplateRoot as Grid;
        var image = templateRoot?.FindName("ItemImage") as Image;
        var item = args.Item as ImageFileInfo;
#if WINDOWS
        if (image is not null && item is not null)
        {
            image.Source = await item.GetImageThumbnailAsync();
        }
#else
        if (item is not null)
        {
            await item.GetImageSourceAsync();
        }
#endif
    }
}

Là encore, les directives de préprocesseur garantissent que la propriété ContainerContentChangingEventArgs.Phase est utilisée uniquement sur les plateformes qui la prennent en charge. Nous utilisons la méthode GetImageSourceAsync() non utilisée précédemment pour charger les fichiers image dans GridView sur les plateformes autres que Windows. À ce stade, nous allons prendre en compte les modifications apportées ci-dessus en modifiant la classe ImageFileInfo.

Création d’un chemin de code distinct pour les autres plateformes

Mettez à jour ImageFileInfo.cs pour inclure une nouvelle propriété appelée ImageSource qui servira à charger le fichier image.

public BitmapImage? ImageSource { get; private set; }

Sachant que les plateformes comme le web ne prennent pas en charge les propriétés avancées des fichiers image qui sont facilement accessibles sur Windows, nous allons ajouter une surcharge de constructeur qui ne nécessite pas de paramètre typé ImageProperties. Ajoutez la nouvelle surcharge après celle qui existe déjà en utilisant le code suivant :

public ImageFileInfo(StorageFile imageFile,
    string name,
    string type)
{
    ImageName = name;
    ImageFileType = type;
    ImageFile = imageFile;
}

Cette surcharge de constructeur sert à construire un objet ImageFileInfo sur les plateformes autres que Windows. La suite logique est de rendre la propriété ImageProperties nullable. Mettez à jour la propriété ImageProperties pour la rendre nullable en utilisant le code suivant :

public ImageProperties? ImageProperties { get; }

Mettez à jour la méthode GetImageSourceAsync pour utiliser la propriété ImageSource au lieu de retourner un objet BitmapImage. Remplacez la méthode GetImageSourceAsync dans le fichier ImageFileInfo.cs par le code suivant :

public async Task<BitmapImage> GetImageSourceAsync()
{
    using IRandomAccessStream fileStream = await ImageFile.OpenReadAsync();

    // Create a bitmap to be the image source.
    BitmapImage bitmapImage = new();
    bitmapImage.SetSource(fileStream);

    ImageSource = bitmapImage;
    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ImageSource)));

    return bitmapImage;
}

Pour éviter d’obtenir la valeur de ImageProperties quand elle est null, apportez les modifications suivantes :

  • Modifiez la propriété ImageDimensions pour utiliser l’opérateur conditionnel Null :

    public string ImageDimensions => $"{ImageProperties?.Width} x {ImageProperties?.Height}";
    
  • Rectifiez la propriété ImageTitle pour utiliser l’opérateur conditionnel Null :

    public string ImageTitle
    {
        get => string.IsNullOrEmpty(ImageProperties?.Title) ? ImageName : ImageProperties?.Title;
        set
        {
            if (ImageProperties is not null)
            {
                if (ImageProperties.Title != value)
                {
                    ImageProperties.Title = value;
                    _ = ImageProperties.SavePropertiesAsync();
                    OnPropertyChanged();
                }
            }
        }
    }
    
  • Rectifiez ImageRating pour ne pas utiliser ImageProperties en générant une évaluation (étoiles) aléatoire à des fins de démonstration :

    public int ImageRating
    {
        get => (int)((ImageProperties?.Rating == null || ImageProperties.Rating == 0) ? (uint)Random.Shared.Next(1, 5) : ImageProperties.Rating);
        set
        {
            if (ImageProperties is not null)
            {
                if (ImageProperties.Rating != value)
                {
                    ImageProperties.Rating = (uint)value;
                    _ = ImageProperties.SavePropertiesAsync();
                    OnPropertyChanged();
                }
            }
        }
    }
    
  • Mettez à jour le constructeur pour l’empêcher de générer un entier aléatoire :

    public ImageFileInfo(ImageProperties properties,
        StorageFile imageFile,
        string name,
        string type)
    {
        ImageProperties = properties;
        ImageName = name;
        ImageFileType = type;
        ImageFile = imageFile;
    }
    

À la suite de ces modifications, la classe ImageFileInfo devrait contenir le code suivant. Il dispose désormais d’un chemin de code distinct pour les plateformes autres que Windows :

using Microsoft.UI.Xaml.Media.Imaging;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Windows.Storage;
using Windows.Storage.FileProperties;
using Windows.Storage.Streams;
using ThumbnailMode = Windows.Storage.FileProperties.ThumbnailMode;

namespace UnoSimplePhotos;

public class ImageFileInfo : INotifyPropertyChanged
{
    public BitmapImage? ImageSource { get; private set; }

    public ImageFileInfo(ImageProperties properties,
        StorageFile imageFile,
        string name,
        string type)
    {
        ImageProperties = properties;
        ImageName = name;
        ImageFileType = type;
        ImageFile = imageFile;
    }

    public ImageFileInfo(StorageFile imageFile,
        string name,
        string type)
    {
        ImageName = name;
        ImageFileType = type;
        ImageFile = imageFile;
    }

    public StorageFile ImageFile { get; }

    public ImageProperties? ImageProperties { get; }

    public async Task<BitmapImage> GetImageSourceAsync()
    {
        using IRandomAccessStream fileStream = await ImageFile.OpenReadAsync();

        // Create a bitmap to be the image source.
        BitmapImage bitmapImage = new();
        bitmapImage.SetSource(fileStream);

        ImageSource = bitmapImage;
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ImageSource)));

        return bitmapImage;
    }

    public async Task<BitmapImage> GetImageThumbnailAsync()
    {
        StorageItemThumbnail thumbnail =
            await ImageFile.GetThumbnailAsync(ThumbnailMode.PicturesView);
        // Create a bitmap to be the image source.
        var bitmapImage = new BitmapImage();
        bitmapImage.SetSource(thumbnail);
        thumbnail.Dispose();

        return bitmapImage;
    }

    public string ImageName { get; }

    public string ImageFileType { get; }

    public string ImageDimensions => $"{ImageProperties?.Width} x {ImageProperties?.Height}";

    public string ImageTitle
    {
        get => string.IsNullOrEmpty(ImageProperties?.Title) ? ImageName : ImageProperties.Title;
        set
        {
            if (ImageProperties is not null)
            {
                if (ImageProperties.Title != value)
                {
                    ImageProperties.Title = value;
                    _ = ImageProperties.SavePropertiesAsync();
                    OnPropertyChanged();
                }
            }
        }
    }

    public int ImageRating
    {
        get => (int)((ImageProperties?.Rating == null || ImageProperties.Rating == 0) ? (uint)Random.Shared.Next(1, 5) : ImageProperties.Rating);
        set
        {
            if (ImageProperties is not null)
            {
                if (ImageProperties.Rating != value)
                {
                    ImageProperties.Rating = (uint)value;
                    _ = ImageProperties.SavePropertiesAsync();
                    OnPropertyChanged();
                }
            }
        }
    }

    public event PropertyChangedEventHandler? PropertyChanged;

    protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) =>
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

Cette classe ImageFileInfo sert à représenter les fichiers image dans GridView. Enfin, nous allons apporter des modifications au fichier MainPage.xaml pour prendre en compte les modifications apportées au modèle.

Utilisation du balisage XAML propre à la plateforme

Le balisage de la vue contient quelques éléments qui ne doivent être évalués que sur Windows. Ajoutez un nouvel espace de noms dans l’élément Page du MainPage.xaml fichier comme ceci :

...
xmlns:win="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

À présent, dans MainPage.xaml, remplacez la méthode setter de la propriété ItemsPanel dans l’élément GridView par le code suivant :

win:ItemsPanel="{StaticResource ImageGridView_ItemsPanelTemplate}"

Le fait de faire précéder le nom de propriété de win: est l’assurance que la propriété n’est définie que sur Windows. Faites la même chose dans la ressource ImageGridView_ItemTemplate. L’objectif est ici de charger uniquement les éléments qui utilisent la propriété ImageDimensions sur Windows. Remplacez l’élément TextBlock qui utilise la propriété ImageDimensions par le code suivant :

<win:TextBlock Text="{x:Bind ImageDimensions}"
               HorizontalAlignment="Center"
               Style="{StaticResource CaptionTextBlockStyle}"
               Margin="8,0,0,0" />

Le fichier MainPage.xaml doit maintenant se présenter comme ceci :

<Page x:Class="UnoSimplePhotos.MainPage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:local="using:UnoSimplePhotos"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      xmlns:win="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      mc:Ignorable="d"
      Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">

    <Grid>
        <Grid.Resources>
            <DataTemplate x:Key="ImageGridView_ItemTemplate"
                          x:DataType="local:ImageFileInfo">
                <Grid Height="300"
                      Width="300"
                      Margin="8">
                    <Grid.RowDefinitions>
                        <RowDefinition />
                        <RowDefinition Height="Auto" />
                    </Grid.RowDefinitions>

                    <Image x:Name="ItemImage"
                           Source="{x:Bind ImageSource}"
                           Stretch="Uniform" />

                    <StackPanel Orientation="Vertical"
                                Grid.Row="1">
                        <TextBlock Text="{x:Bind ImageTitle}"
                                   HorizontalAlignment="Center"
                                   Style="{StaticResource SubtitleTextBlockStyle}" />
                        <StackPanel Orientation="Horizontal"
                                    HorizontalAlignment="Center">
                            <TextBlock Text="{x:Bind ImageFileType}"
                                       HorizontalAlignment="Center"
                                       Style="{StaticResource CaptionTextBlockStyle}" />
                            <win:TextBlock Text="{x:Bind ImageDimensions}"
                                           HorizontalAlignment="Center"
                                           Style="{StaticResource CaptionTextBlockStyle}"
                                           Margin="8,0,0,0" />
                        </StackPanel>

                        <RatingControl Value="{x:Bind ImageRating}"
                                       IsReadOnly="True" />
                    </StackPanel>
                </Grid>
            </DataTemplate>
            
            <Style x:Key="ImageGridView_ItemContainerStyle"
                   TargetType="GridViewItem">
                <Setter Property="Background"
                        Value="Gray" />
                <Setter Property="Margin" 
                        Value="8"/>
            </Style>

            <ItemsPanelTemplate x:Key="ImageGridView_ItemsPanelTemplate">
                <ItemsWrapGrid Orientation="Horizontal"
                               HorizontalAlignment="Center"/>
            </ItemsPanelTemplate>
        </Grid.Resources>

        <GridView x:Name="ImageGridView"
                  ItemsSource="{x:Bind Images, Mode=OneWay}"
                  win:ItemsPanel="{StaticResource ImageGridView_ItemsPanelTemplate}"
                  ContainerContentChanging="ImageGridView_ContainerContentChanging"
                  ItemContainerStyle="{StaticResource ImageGridView_ItemContainerStyle}"
                  ItemTemplate="{StaticResource ImageGridView_ItemTemplate}" />
    </Grid>
</Page>

Exécution de l’application

Lancez la cible UnoSimplePhotos.Windows. Notez que cette application WinUI ressemble beaucoup à celle du tutoriel précédent.

Vous pouvez maintenant créer et exécuter votre application sur n’importe quelle plateforme prise en charge. Pour ce faire, vous pouvez utiliser la liste déroulante de la barre d’outils de débogage pour sélectionner la plateforme cible du déploiement :

  • Pour exécuter l’en-tête WebAssembly (Wasm) :

    • Cliquez avec le bouton droit sur le projet UnoSimplePhotos.Wasm, sélectionnez Définir comme projet de démarrage
    • Appuyez sur le bouton UnoSimplePhotos.Wasm pour déployer l’application
    • Si vous le souhaitez, vous pouvez ajouter et utiliser le projet UnoSimplePhotos.Server en guise d’alternative
  • Pour déboguer pour iOS :

    • Cliquez avec le bouton droit sur le projet UnoSimplePhotos.Mobile, puis sélectionnez Définir comme projet de démarrage

    • Dans la liste déroulante de la barre d’outils de débogage, sélectionnez un appareil iOS actif ou le simulateur. Vous devez être associé à un Mac pour que cela fonctionne.

      Screenshot of the Visual Studio dropdown to select a target framework to deploy.

  • Pour déboguer pour Mac Catalyst :

    • Cliquez avec le bouton droit sur le projet UnoSimplePhotos.Mobile, puis sélectionnez Définir comme projet de démarrage
    • Dans la liste déroulante de la barre d’outils de débogage, sélectionnez un appareil macOS distant. Vous devez être associé à un appareil de ce type pour que cela fonctionne.
  • Pour déboguer la plateforme Android :

    • Cliquez avec le bouton droit sur le projet UnoSimplePhotos.Mobile, puis sélectionnez Définir comme projet de démarrage
    • Dans la liste déroulante de la barre d’outils de débogage, sélectionnez un appareil Android actif ou l’émulateur
      • Sélectionnez un appareil actif dans le sous-menu « Appareil »
  • Pour déboguer sur Linux avec Skia GTK :

    • Cliquez avec le bouton droit sur le projet UnoSimplePhotos.Skia.Gtk, puis sélectionnez Définir comme projet de démarrage
    • Appuyez sur le bouton UnoSimplePhotos.Skia.Gtk pour déployer l’application

Voir aussi