Compartilhar via


Tutorial: Criar um visualizador de fotos simples direcionado a várias plataformas

Depois de criar um aplicativo WinUI 3 visualizador de fotos simples inicial, você vai querer alcançar mais usuários sem precisar reescrever o aplicativo. Este tutorial usa a Uno Platform para expandir o alcance do aplicativo WinUI 3 C# existente, permitindo a reutilização da lógica de negócios e da camada de interface do usuário em dispositivos nativos móveis, da Web e da área de trabalho. Com apenas alterações mínimas no aplicativo visualizador de fotos simples, podemos executar uma cópia perfeita de pixels do aplicativo portado para essas plataformas.

Captura de tela do aplicativo UnoSimplePhoto que direciona Web e área de trabalho WinUI.

Pré-requisitos

  • Visual Studio 2022 17.4 ou posterior

  • Configurar seu computador de desenvolvimento (consulte Introdução ao WinUI)

  • Carga de trabalho de desenvolvimento para Web e ASP.NET (de desenvolvimento para WebAssembly)

    Captura de tela da carga de trabalho de desenvolvimento para a Web no Visual Studio.

  • Desenvolvimento do .NET Multi-Platform App UI instalado (para desenvolvimento iOS, Android e Mac Catalyst).

    Captura de tela da carga de trabalho do dotnet para dispositivos móveis no Visual Studio.

  • Desenvolvimento para área de trabalho do .NET instalado (de desenvolvimento para Gtk, Wpf e Linux Framebuffer)

    Captura de tela da carga de trabalho do dotnet para área de trabalho no Visual Studio.

Finalizar o ambiente

  1. Abra um prompt de linha de comando, o Terminal do Windows se estiver instalado, o Prompt de Comando ou o Windows Powershell no menu Iniciar.

  2. Instale ou atualize a ferramenta uno-check:

    • Use o seguinte comando:

      dotnet tool install -g uno.check
      
    • Para atualizar a ferramenta, se você já tiver instalado uma versão mais antiga:

      dotnet tool update -g uno.check
      
  3. Execute a ferramenta com o seguinte comando:

    uno-check
    
  4. Siga as instruções indicadas pela ferramenta. Como ele precisa modificar o sistema, podem ser solicitadas permissões elevadas.

Instalar os modelos de solução da Uno Platform

Inicie o Visual Studio e clique em Continue without code. Clique em Extensions –>Manage Extensions na barra de menus.

Captura de tela do item da barra de menus do Visual Studio para gerenciar extensões.

No Gerenciador de Extensões, expanda o nó Online e procure Uno, instale a extensão Uno Platform ou baixe-a e instale-a no Visual Studio Marketplace e reinicie o Visual Studio.

Captura de tela da janela Gerenciar extensões no Visual Studio com a Uno Platform como um resultado da pesquisa.

Criar um aplicativo

Agora que estamos prontos para criar um aplicativo multiplataforma, a abordagem será criar um aplicativo da Uno Platform. Copiaremos o código do projeto SimplePhotos WinUI 3 do tutorial anterior para o projeto multiplataforma. Isso é possível porque a Uno Platform permite reutilizar a base de código existente. Ao longo do tempo, você pode colocar em funcionamento recursos que dependem de APIs do sistema operacional fornecidas pela plataforma. Essa abordagem é muito útil quando você tem um aplicativo existente que deseja portar para outras plataformas.

Em breve, você poderá colher os benefícios dessa abordagem, pois poderá direcionar um tipo XAML familiar e a base de código que você já tem a mais plataformas.

Abra o Visual Studio e crie um projeto por meio de File>New>Project:

Captura de tela da caixa de diálogo criar um projeto.

Pesquise Uno e selecione o modelo de projeto de aplicativo da Uno Platform:

Captura de tela da caixa de diálogo criar um projeto com o aplicativo da Uno Platform como o tipo de projeto selecionado.

Crie uma solução em C# usando o tipo aplicativo da Uno Platform na Página Inicial do Visual Studio. Para evitar conflitos com o código do tutorial anterior, daremos a essa solução um nome diferente: "UnoSimplePhotos". Especifique o nome do projeto, o nome da solução e o diretório. Neste exemplo, o projeto multiplataforma UnoSimplePhotos pertence a uma solução UnoSimplePhotos, que residirá em C:\Projects:

Captura de tela da especificação de detalhes do novo projeto da Uno Platform.

Agora você escolherá um modelo base para o aplicativo de galeria de fotos simples multiplataforma.

O modelo aplicativo da Uno Platform vem com duas opções predefinidas que permitem começar a usar rapidamente uma solução Em branco ou a configuração Padrão que inclui referências às bibliotecas Uno.Material e Uno.Toolkit. A configuração padrão também inclui Uno.Extensions, que é usado para injeção de dependência, configuração, navegação e registro em log. Além disso, ela usa MVUX em vez de MVVM e, portanto, é um ótimo ponto de partida para a criação rápida de aplicativos do mundo real.

Captura de tela do modelo de solução Uno para o tipo de projeto de inicialização.

Para simplificar, selecione a predefinição Em branco. Em seguida, clique no botão Criar. Aguarde até que os projetos sejam criados e as dependências sejam restauradas.

Um banner na parte superior do editor pode solicitar o recarregamento dos projetos, clique em Recarregar projetos:

Captura de tela do banner do Visual Studio oferecendo para recarregar seus projetos a fim de concluir as alterações.

Você verá a seguinte estrutura de arquivo padrão no Gerenciador de Soluções:

Captura de tela da estrutura de arquivos padrão no Gerenciador de Soluções.

Adicionar ativos de imagem ao projeto

O aplicativo precisará de algumas imagens para exibição. Você pode usar as mesmas imagens do tutorial anterior.

No projeto UnoSimplePhotos, crie uma pasta chamada Assets e copie os arquivos de imagem JPG em uma subpasta Samples. A estrutura de pasta Assets ficará assim:

Captura de tela do painel Gerenciador de Soluções no Visual Studio com os novos arquivos e pastas adicionados.

Para obter mais informações sobre como criar a pasta Assets e adicionar imagens a ela, confira a documentação da Uno Platform sobre Ativos e exibição de imagem.

Preparar o aplicativo

Agora que você gerou o ponto de partida funcional do aplicativo WinUI multiplataforma, copie nele o código do projeto de área de trabalho.

Copiar a exibição

Como a Uno Platform permite que você use o tipo XAML com o qual já está familiarizado, copie o mesmo código sobre o que você criou no tutorial anterior.

Retorne ao projeto SimplePhotos do tutorial anterior. No Gerenciador de Soluções, localize o arquivo chamado MainWindow.xaml e abra-o. Observe que o conteúdo da exibição é definido dentro de um elemento Window, não em um Page. Isso ocorre porque o projeto de área de trabalho é um aplicativo WinUI 3, que pode usar elementos Window para definir o conteúdo da exibição:

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

A implementação multiplataforma da Uno Platform dos controles encontrados no elemento Window, como GridView, Image e RatingControl, garante que a exibição em funcionará em todas as plataformas com suporte exigindo poucas ações. Copie o conteúdo deste Window e cole-o no elemento Page do arquivo MainPage.xaml no projeto UnoSimplePhotos da Uno Platform. A exibição XAML MainPage é assim:

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

A solução da área de trabalho também tinha um arquivo MainWindow.xaml.cs que continha um code-behind correspondente à exibição. No projeto da Uno Platform, o code-behind da exibição MainPage de destino da cópia está contido no arquivo MainPage.xaml.cs.

Para ativar esse code-behind multiplataforma, primeiro devemos colocar o seguinte no arquivo MainPage.xaml.cs:

  • Propriedade Images: fornece o GridView com uma coleção observável de arquivos de imagem

  • Conteúdo do construtor: chama GetItemsAsync() para preencher a coleção Images com itens que representam arquivos de imagem

  • Remover a modificação manual da propriedade ItemsSource do controle ImageGridView

  • Método ImageGridView_ContainerContentChanging: usado em uma estratégia para carregar os itens GridView progressivamente conforme eles entram na exibição

  • Método ShowImage: carrega os arquivos de imagem no GridView

  • Método GetItemsAsync: obtém os arquivos de ativo de imagem da pasta Samples

  • Método LoadImageInfoAsync: constrói um objeto ImageFileInfo por meio de um StorageFile criado

Depois que tudo for colocado no MainPage.xaml.cs, ele ficará assim:

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

Observação

Os arquivos no projeto de aplicativo Uno devem usar UnoSimplePhotos como o namespace.

Até agora, os arquivos da exibição principal com a qual estamos trabalhando contêm todas as funcionalidades da solução de área de trabalho. Depois de copiar o arquivo de modelo ImageFileInfo.cs, aprenderemos a modificar os blocos de código direcionados à área de trabalho para garantir a compatibilidade multiplataforma.

Copie ImageFileInfo do projeto de área de trabalho e cole-o no arquivo ImageFileInfo.cs. Faça as seguintes alterações:

  • Renomeie o namespace como UnoSimplePhotos, em vez de SimplePhotos:

    // Found towards the top of the file
    namespace UnoSimplePhotos;
    
  • Altere o tipo de parâmetro do método OnPropertyChanged para anulável:

    // string -> string?
    protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
    ...
    
  • Altere PropertyChangedEventHandler para anulável:

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

Em conjunto, o arquivo deve ficar assim:

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

Essa classe funcionará como um modelo para representar os arquivos de imagem no GridView. Embora seja tecnicamente possível executar o aplicativo neste ponto, ele pode não renderizar as imagens ou não exibir as respectivas propriedades corretamente. Nas próximas seções, faremos um conjunto de alterações nesses arquivos copiados para garantir a compatibilidade multiplataforma.

Usar diretivas de pré-processador

No projeto de área de trabalho do tutorial anterior, o arquivo MainPage.xaml.cs continha um método GetItemsAsync que enumera itens de um StorageFolder que representa o local do pacote instalado. Como esse local não está disponível em determinadas plataformas, como WebAssembly, precisaremos fazer alterações nesse método para que ele fique compatível com todas as plataformas. Faremos algumas alterações na classe ImageFileInfo para garantir a compatibilidade.

Primeiro, faça as alterações necessárias no método GetItemsAsync. Substitua o método GetItemsAsync no arquivo MainPage.xaml.cs pelo seguinte código:

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

Esse método agora usa uma diretiva de pré-processador para determinar qual código executar com base na plataforma. No Windows, o método obtém o StorageFolder que representa o local do pacote instalado e retorna a pasta Samples dele. Em outras plataformas, o método conta até 20, obtendo os arquivos de imagem da pasta Samples usando um Uri para representar o arquivo de imagem.

Em seguida, ajuste o método LoadImageInfoAsync para incluir as alterações feitas no método GetItemsAsync. Substitua o método LoadImageInfoAsync no arquivo MainPage.xaml.cs pelo seguinte código:

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

Como no método GetItemsAsync, esse método agora usa uma diretiva de pré-processador para determinar qual código será executado com base na plataforma. No Windows, o método obtém o ImageProperties do StorageFile e o usa para criar um objeto ImageFileInfo. Em outras plataformas, o método constrói um objeto ImageFileInfo sem o parâmetro ImageProperties. Mais tarde, serão feitas modificações na classe ImageFileInfo para incluir essa alteração.

Controles como GridView permitem o carregamento progressivo do conteúdo do contêiner de item atualizado à medida que ele chega no visor. Isso é feito usando o evento ContainerContentChanging. No projeto de área de trabalho do tutorial anterior, o método ImageGridView_ContainerContentChanging usa esse evento para carregar os arquivos de imagem no GridView. Como determinados aspectos desse evento não são compatíveis com todas as plataformas, precisaremos fazer alterações nesse método para torná-lo compatível.

Diagrama do visor de controle de coleta.

Por exemplo, no momento, a propriedade ContainerContentChangingEventArgs.Phase não é compatível com plataformas que não sejam Windows. Precisaremos fazer alterações no método ImageGridView_ContainerContentChanging para incluir essa alteração. Substitua o método ImageGridView_ContainerContentChanging no arquivo MainPage.xaml.cs pelo seguinte código:

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
}

O retorno de chamada especializado agora só será registrado usando ContainerContentChangingEventArgs.RegisterUpdateCallback() se a plataforma for Windows. Caso contrário, o método ShowImage será chamado diretamente. Também precisaremos fazer alterações no método ShowImage para trabalhar junto com as alterações feitas no método ImageGridView_ContainerContentChanging. Substitua o método ShowImage no arquivo MainPage.xaml.cs pelo seguinte código:

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

Novamente, as diretivas de pré-processador garantem que a propriedade ContainerContentChangingEventArgs.Phase seja usada apenas em plataformas com as quais seja compatível. Usamos o método GetImageSourceAsync() que não foi usado antes para carregar os arquivos de imagem em GridView em plataformas que não sejam Windows. Neste ponto, incluiremos as alterações feitas acima editando a classe ImageFileInfo.

Criar um caminho de código separado para outras plataformas

Atualize ImageFileInfo.cs para incluir uma nova propriedade chamada ImageSource que será usada para carregar o arquivo de imagem.

public BitmapImage? ImageSource { get; private set; }

Como plataformas como a Web não são compatíveis com propriedades avançadas de arquivo de imagem prontamente disponíveis no Windows, adicionaremos uma sobrecarga de construtor que não requer um parâmetro tipado ImageProperties. Adicione a nova sobrecarga após a existente usando o seguinte código:

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

Essa sobrecarga de construtor é usada para construir um objeto ImageFileInfo em plataformas que não sejam Windows. Como isso foi feito, é melhor alterar a propriedade ImageProperties para anulável. Atualize a propriedade ImageProperties para anulável usando o seguinte código:

public ImageProperties? ImageProperties { get; }

Atualize o método GetImageSourceAsync para usar a propriedade ImageSource em vez de retornar apenas um objeto BitmapImage. Substitua o método GetImageSourceAsync no arquivo ImageFileInfo.cs pelo seguinte código:

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

Para evitar obter o valor de ImageProperties quando ele é nulo, faça as seguintes alterações:

  • Modifique a propriedade ImageDimensions para usar o operador condicional nulo:

    public string ImageDimensions => $"{ImageProperties?.Width} x {ImageProperties?.Height}";
    
  • Altere a propriedade ImageTitle para usar o operador condicional nulo:

    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();
                }
            }
        }
    }
    
  • Altere ImageRating para não depender de ImageProperties gerando uma classificação por estrelas aleatória para fins de demonstração:

    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();
                }
            }
        }
    }
    
  • Atualize o construtor que gera um inteiro aleatório para não fazer mais isso:

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

Com essas edições, a classe ImageFileInfo deve conter o código a seguir. Agora há um novo caminho de código separado para plataformas que não sejam 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));
}

Essa classe ImageFileInfo é usada para representar os arquivos de imagem no GridView. Por fim, faremos alterações no arquivo MainPage.xaml para incluir as alterações no modelo.

Usar a marcação XAML específica da plataforma

Há alguns itens na marcação de exibição que só devem ser avaliados no Windows. Adicione um novo namespace ao elemento Page do arquivo MainPage.xaml da seguinte maneira:

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

Agora, em MainPage.xaml, substitua o setter de propriedade ItemsPanel no elemento GridView pelo seguinte código:

win:ItemsPanel="{StaticResource ImageGridView_ItemsPanelTemplate}"

Quando win: é acrescentado ao nome da propriedade, ela é definida apenas no Windows. Faça isso novamente no recurso ImageGridView_ItemTemplate. Queremos carregar apenas elementos que usam a propriedade ImageDimensions no Windows. Substitua o elemento TextBlock que usa a propriedade ImageDimensions pelo seguinte código:

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

O arquivo MainPage.xaml deve estar assim agora:

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

Executar o aplicativo

Inicie o destino UnoSimplePhotos.Windows. Observe que esse aplicativo WinUI é muito semelhante ao do tutorial anterior.

Agora você pode criar e executar o aplicativo em qualquer plataforma com suporte. Para fazer isso, você pode usar a lista suspensa da barra de ferramentas de depuração para selecionar uma plataforma de destino de implantação:

  • Para executar o cabeçalho WebAssembly (Wasm):

    • Clique com o botão direito do mouse no projeto UnoSimplePhotos.Wasm, selecione Definir como projeto de inicialização
    • Pressione o botão UnoSimplePhotos.Wasm para implantar o aplicativo
    • Se desejar, adicione e use o projeto UnoSimplePhotos.Server como alternativa
  • Como fazer a depuração para iOS:

    • Clique com o botão direito do mouse no projeto UnoSimplePhotos.Mobile e selecione Definir como projeto de inicialização

    • Na lista suspensa da barra de ferramentas de depuração, selecione um dispositivo iOS ativo ou o simulador. Você precisará fazer o emparelhamento com um Mac para que isso funcione.

      Captura de tela do menu suspenso do Visual Studio para selecionar uma estrutura de destino de implantação.

  • Como depurar para Mac Catalyst:

    • Clique com o botão direito do mouse no projeto UnoSimplePhotos.Mobile e selecione Definir como projeto de inicialização
    • Na lista suspensa da barra da ferramentas de depuração, selecione um dispositivo macOS remoto. Você precisará estar emparelhado com um deles para que isso funcione.
  • Como depurar a plataforma Android:

    • Clique com o botão direito do mouse no projeto UnoSimplePhotos.Mobile e selecione Definir como projeto de inicialização
    • Na lista suspensa da barra de ferramentas de depuração, selecione um dispositivo Android ativo ou o emulador
      • Selecione um dispositivo ativo no submenu "Dispositivo"
  • Como depurar no Linux com Skia GTK:

    • Clique com o botão direito do mouse no projeto UnoSimplePhotos.Skia.Gtk e selecione Definir como projeto de inicialização
    • Pressione o botão UnoSimplePhotos.Skia.Gtk para implantar o aplicativo

Confira também