Compartir a través de


Tutorial: Creación de un visor de fotos sencillo que tiene como destino varias plataformas

Después de crear una aplicación de winUI 3 de visor de fotos simple de inicio, es posible que se pregunte cómo llegar a más usuarios sin tener que reescribir la aplicación. En este tutorial se usa Uno Platform para ampliar el alcance de la aplicación WinUI 3 de C# existente, lo que permite reutilizar la lógica de negocio y la capa de UI en dispositivos nativos móviles, web y de escritorio. Con cambios mínimos en la aplicación de visor de fotos simple, podemos ejecutar una copia perfecta de la aplicación portada a estas plataformas.

Captura de pantalla de la aplicación UnoSimplePhoto orientada a web y el escritorio de WinUI.

Prerrequisitos

  • Visual Studio 2022 17.4 o posterior

  • Configure el equipo de desarrollo (consulte Introducción a WinUI)

  • Carga de trabajo de ASP.NET y desarrollo web (para desarrollos de WebAssembly

    Captura de pantalla de la carga de trabajo del desarrollo web en Visual Studio.

  • Desarrollo de interfaz de usuario de aplicaciones multiplataforma de .NET instalado (para el desarrollo de iOS, Android, Mac Catalyst)

    Captura de pantalla de la carga de trabajo de dotnet mobile en Visual Studio.

  • Desarrollo de escritorio de .NET instalado (para desarrollo de Gtk, Wpf, y Linux Framebuffer)

    Captura de pantalla de la carga de trabajo de escritorio de dotnet en Visual Studio.

Finalice el entorno

  1. Abra un símbolo del sistema, Terminal Windows si lo tiene instalado, o bien otro símbolo del sistema o Windows Powershell desde el menú Inicio.

  2. Instale o actualice la herramienta uno-check:

    • Use el comando siguiente:

      dotnet tool install -g uno.check
      
    • Para actualizar la herramienta, si ya ha instalado anteriormente una versión anterior:

      dotnet tool update -g uno.check
      
  3. Ejecute la herramienta con el comando siguiente:

    uno-check
    
  4. Siga las instrucciones indicadas por la herramienta. Dado que necesita modificar el sistema, es posible que se le soliciten permisos elevados.

Instalar las plantillas de la solución Uno Platform

Inicie Visual Studio y, a continuación, elija Continue without code. Elija Extensions ->Manage Extensions en la barra de menús.

Captura de pantalla de la barra de menús de Visual Studio que lee administrar extensiones.

En el Administrador de extensiones, expanda el nodo En línea y busque Uno, instale la extensión Uno Platform o descargue e instálela desde Visual Studio Marketplace y finalmente reinicie Visual Studio.

Captura de pantalla de la ventana Administrar extensiones en Visual Studio con la extensión Uno Platform como un resultado de búsqueda.

Crear una aplicación

Ahora que estamos listos para crear una aplicación multiplataforma, el enfoque que adoptaremos es crear una nueva aplicación de Uno Platform. Copiaremos código del proyecto de WinUI 3 SimplePhotos del tutorial anterior en nuestro proyecto multiplataforma. Esto es posible porque Uno Platform le permite reutilizar el código base existente. En el caso de las características que dependen de las API del sistema operativo proporcionadas por cada plataforma, puede hacer que funcionen fácilmente con el tiempo. Este enfoque es especialmente útil si tiene una aplicación existente que desea portar a otras plataformas.

Pronto podrá aprovechar las ventajas de este enfoque, pudiendo orientarse a más plataformas con un tipo de lenguaje XAML familiar y el código base que ya tiene.

Abra Visual Studio y cree un proyecto nuevo a través de File>New>Project:

Captura de pantalla del cuadro de diálogo Nuevo proyecto.

Busque Uno y seleccione la plantilla del proyecto de la aplicación de Uno Platform:

Captura de pantalla del cuadro de diálogo de Crear nuevo proyecto con la aplicación Uno Platform como tipo de proyecto seleccionado.

Cree una nueva solución de C# mediante el tipo de aplicación de Uno Platform desde la página de inicio de Visual Studio. Para evitar conflictos con el código del tutorial anterior, asignaremos a esta solución un nombre diferente, "UnoSimplePhotos". Especifique el nombre del proyecto, el nombre de la solución y el directorio. En este ejemplo, nuestro proyecto multiplataforma UnoSimplePhotos pertenece a una solución UnoSimplePhotos, que residirá en C:\Projects:

Captura de pantalla de la especificación de los detalles del proyecto Uno Platform.

Ahora elegirá una plantilla base a la que llevar la aplicación multiplataforma simple de galería de fotos.

La plantilla de aplicaciones de Uno Platform incluye dos opciones preestablecidas que le permiten empezar a trabajar rápidamente con una solución en blanco o con la configuración predeterminada, que incluye referencias a las bibliotecas Uno.Material y Uno.Toolkit. La configuración predeterminada también incluye Uno.Extensions, que se usa para la inserción de dependencias, la configuración, la navegación y el registro. Además, usa MVUX (modelo-vista-actualización extendido) en lugar de MVVM (modelo-vista-modelo de vista), lo que la convierte en un excelente punto de partida para compilar rápidamente aplicaciones reales.

Captura de pantalla de la plantilla de solución Uno para el tipo de inicio de proyecto.

Para simplificar las cosas, seleccione la configuración predefinida En blanco. Después, elija el botón Crear. Espere a que se creen los proyectos y se restauren sus dependencias.

Un banner en la parte superior del editor puede solicitar que vuelva a cargar proyectos, haga clic en Volver a cargar proyectos:

Captura de pantalla del banner de Visual Studio que ofrece recargar los proyectos para completar los cambios.

Debería ver la siguiente estructura predeterminada de archivos en el Explorador de soluciones:

Captura de pantalla de la estructura de archivo predeterminada en Explorador de soluciones.

Agregar recursos de imagen al proyecto

La aplicación necesitará algunas imágenes para mostrar. Puede usar las mismas imágenes del tutorial anterior.

En el proyecto UnoSimplePhotos, cree una nueva carpeta denominada Assets y copie los archivos de imagen JPG en una subcarpeta Samples. La estructura de la carpeta Assets ahora debería parecerse a esto:

Captura de pantalla del panel de Explorador de soluciones en Visual Studio con los nuevos archivos y carpetas añadidos.

Para más información sobre la creación de la carpeta Assets y la adición de imágenes, consulte la documentación de Uno Platform sobre recursos y visualización de imágenes.

Preparación de la aplicación

Ahora que ha generado el punto de partida funcional de la aplicación WinUI multiplataforma, puede copiar código en ella desde el proyecto de escritorio.

Copiar la vista

Dado que Uno Platform permite usar el tipo de lenguaje XAML que le resulte más familiar, puede copiar el mismo código que creó en el tutorial anterior.

Vuelva al proyecto SimplePhotos del tutorial anterior. En el Explorador de soluciones, busque el archivo denominado MainWindow.xaml y ábralo. Observe que el contenido de la vista se define dentro de un elemento Window en lugar de en un Page. Esto se debe a que el proyecto de escritorio es una aplicación WinUI 3, que puede usar elementos Window para definir los contenidos de la vista:

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

La implementación multiplataforma de Uno Platform de los controles que se encuentran en el elemento Window, tales como GridView, Image y RatingControl, garantiza que la propia vista funcione en todas las plataformas compatibles con solo un pequeño esfuerzo. Copie los contenidos de este Window y péguelos en el elemento Page del archivo MainPage.xaml en el proyecto UnoSimplePhotos de Uno Platform. La vista XAML MainPage debería tener este aspecto:

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

Es posible que recuerde que la solución de escritorio también tenía un archivo MainWindow.xaml.cs que contenía código subyacente que corresponde a la vista. En el proyecto de Uno Platform, el código subyacente de la vista MainPage en la que hemos copiado se encuentra en el archivo MainPage.xaml.cs.

Para hacer multiplataforma este código subyacente, primero deberíamos mover lo siguiente al archivo MainPage.xaml.cs:

  • propiedad Images: proporciona GridView con una colección observable de archivos de imagen

  • Contenidos del constructor: llama a GetItemsAsync() para rellenar la colección Images con elementos que representan archivos de imagen

  • Quitar la modificación manual de la propiedad ItemsSource del control ImageGridView

  • método ImageGridView_ContainerContentChanging: se usa como parte de una estrategia para cargar progresivamente los elementos GridView a medida que se desplazan hacia la vista

  • método ShowImage: carga los archivos de imagen en GridView

  • método GetItemsAsync: obtiene los archivos de recursos de imagen de la carpeta Samples

  • método LoadImageInfoAsync: construye un objeto ImageFileInfo a partir de un StorageFile creado

Después de mover todo, MainPage.xaml.cs ahora debería tener este aspecto:

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

Nota

Los archivos del proyecto de aplicación Uno deberían usar UnoSimplePhotos como espacio de nombres.

Hasta ahora, los archivos de la vista principal con los que estamos trabajando contienen todas las funcionalidades de la solución de escritorio. Después de copiar el archivo modelo ImageFileInfo.cs, aprenderemos a modificar los bloques de código orientados a escritorio para la compatibilidad multiplataforma.

Copie ImageFileInfo desde el proyecto de escritorio y péguelo en el archivo ImageFileInfo.cs. Realice los cambios siguientes:

  • Cambie el nombre del espacio de nombres para que sea UnoSimplePhotos en lugar de SimplePhotos:

    // Found towards the top of the file
    namespace UnoSimplePhotos;
    
  • Cambie el tipo de parámetro del método OnPropertyChanged para que admita valores NULL:

    // string -> string?
    protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
    ...
    
  • Haga que PropertyChangedEventHandler admita valores NULL:

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

En conjunto, el archivo debería parecerse a esto:

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

Esta clase servirá como modelo para representar los archivos de imagen en GridView. Aunque técnicamente sería posible ejecutar la aplicación en este momento, es posible que no represente las imágenes correctamente o muestre sus propiedades. En las secciones siguientes, realizaremos un conjunto de cambios en estos archivos copiados para que sean compatibles en un contexto multiplataforma.

Uso de directivas de preprocesador

En el proyecto de escritorio del tutorial anterior, el archivo MainPage.xaml.cs contiene un método GetItemsAsync que enumera los elementos de StorageFolder que representa la ubicación del paquete instalado. Dado que esa ubicación no está disponible en determinadas plataformas, como WebAssembly, tendremos que realizar cambios en este método para que sea compatible con todas las plataformas. En consecuencia, realizaremos algunos cambios en la clase ImageFileInfo para garantizar la compatibilidad.

En primer lugar, realice los cambios necesarios en el método GetItemsAsync. Reemplace el método GetItemsAsync en el archivo MainPage.xaml.cs por el código siguiente:

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

Este método ahora usa una directiva de preprocesador para determinar qué código se va a ejecutar en función de la plataforma. En Windows, el método obtiene el StorageFolder que representa la ubicación del paquete instalado y devuelve la carpeta Samples. En otras plataformas, el método cuenta hasta 20, obteniendo los archivos de imagen de la carpeta Samples mediante Uri para representar el archivo de imagen.

A continuación, ajuste el método LoadImageInfoAsync para dar cabida a los cambios realizados en el método GetItemsAsync. Reemplace el método LoadImageInfoAsync en el archivo MainPage.xaml.cs por el código siguiente:

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

De forma similar al método GetItemsAsync, este método ahora usa una directiva de preprocesador para determinar qué código se va a ejecutar en función de la plataforma. En Windows, el método obtiene ImageProperties de StorageFile y lo usa para crear un objeto ImageFileInfo. En otras plataformas, el método construye un objeto ImageFileInfo sin el parámetro ImageProperties. Más adelante se realizarán modificaciones en la clase ImageFileInfo para dar cabida a este cambio.

Los controles como GridView permiten la carga progresiva del contenido del contenedor de elementos actualizado a medida que se desplazan al área de visualización. Esto se consigue mediante el evento ContainerContentChanging. En el proyecto de escritorio del tutorial anterior, el método ImageGridView_ContainerContentChanging usa este evento para cargar los archivos de imagen en GridView. Dado que algunos aspectos de este evento no se admiten en todas las plataformas, tendremos que realizar cambios en este método para que sea compatible con ellas.

Diagrama del área de visualización de control de colección.

Por ejemplo, la propiedad ContainerContentChangingEventArgs.Phase no se admite actualmente en plataformas distintas de Windows. Tendremos que realizar cambios en el método ImageGridView_ContainerContentChanging para dar cabida a este cambio. Reemplace el método ImageGridView_ContainerContentChanging en el archivo MainPage.xaml.cs por el código siguiente:

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
}

La devolución de llamada especializada ahora solo se registra mediante ContainerContentChangingEventArgs.RegisterUpdateCallback() si la plataforma es Windows. De lo contrario, se llama directamente al método ShowImage. También es necesario realizar cambios en el método ShowImage para trabajar junto con los cambios realizados en el método ImageGridView_ContainerContentChanging. Reemplace el método ShowImage en el archivo MainPage.xaml.cs por el código siguiente:

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

De nuevo, las directivas de preprocesador garantizan que la propiedad ContainerContentChangingEventArgs.Phase solo se use en plataformas en las que se admita. Usamos el método GetImageSourceAsync() que no usamos previamente para cargar los archivos de imagen en GridView en plataformas distintas de Windows. En este punto, ajustaremos los cambios realizados anteriormente editando la clase ImageFileInfo.

Creación de una ruta de acceso de código independiente para otras plataformas

Actualice ImageFileInfo.cs para incluir una nueva propiedad denominada ImageSource que se usará para cargar el archivo de imagen.

public BitmapImage? ImageSource { get; private set; }

Dado que las plataformas como la web no admiten propiedades avanzadas de archivo de imagen que están disponibles fácilmente en Windows, agregaremos una sobrecarga de constructor que no requiere un parámetro ImageProperties con tipo. Agregue la nueva sobrecarga después de la existente mediante el código siguiente:

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

Esta sobrecarga de constructor se usa para construir un objeto ImageFileInfo en plataformas distintas de Windows. Si lo hicimos, tiene sentido hacer que la propiedad ImageProperties acepte valores NULL. Actualice la propiedad ImageProperties para que admita valores NULL mediante el código siguiente:

public ImageProperties? ImageProperties { get; }

Actualice el método GetImageSourceAsync para usar la propiedad ImageSource en lugar de devolver solo un objeto BitmapImage. Reemplace el método GetImageSourceAsync en el archivo ImageFileInfo.cs por el código siguiente:

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 obtener el valor de ImageProperties cuando es NULL, realice los cambios siguientes:

  • Modifique la propiedad ImageDimensions para usar el operador condicional NULL:

    public string ImageDimensions => $"{ImageProperties?.Width} x {ImageProperties?.Height}";
    
  • Cambie la propiedad ImageTitle para usar el operador condicional 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();
                }
            }
        }
    }
    
  • Cambie ImageRating para que no se base en ImageProperties mediante la generación de una clasificación por estrellas aleatoria como demostración:

    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();
                }
            }
        }
    }
    
  • Actualice el constructor que genera un entero aleatorio para que deje de hacerlo:

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

Con estas ediciones, la clase ImageFileInfo debería contener el código siguiente. Ahora tiene una ruta de acceso de código recién separada para plataformas distintas de 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));
}

Esta clase ImageFileInfo se usa para representar los archivos de imagen en GridView. Por último, realizaremos cambios en el archivo MainPage.xaml para dar cabida a los cambios en el modelo.

Uso del marcado XAML específico de la plataforma

Hay un par de elementos en nuestro marcado de vista que solo deberían evaluarse en Windows. Agregue un nuevo espacio de nombres en el elemento Page del archivo MainPage.xaml así:

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

Ahora, en MainPage.xaml, reemplace el establecedor de propiedad ItemsPanel en el elemento GridView por el código siguiente:

win:ItemsPanel="{StaticResource ImageGridView_ItemsPanelTemplate}"

Anteponer win: al nombre de propiedad asegura que la propiedad solo está establecida en Windows. Vuelva a hacerlo dentro del recurso ImageGridView_ItemTemplate. Solo queremos cargar elementos que usen la propiedad ImageDimensions en Windows. Reemplace el elemento TextBlock que usa la propiedad ImageDimensions por el código siguiente:

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

El archivo MainPage.xaml ahora debería presentar un aspecto similar a este:

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

Ejecución de la aplicación

Inicie el destino UnoSimplePhotos.Windows. Observe que esta aplicación de WinUI es muy similar a la del tutorial anterior.

Ahora puede compilar y ejecutar la aplicación en cualquiera de las plataformas compatibles. Para ello, puede usar la lista desplegable de la barra de herramientas de depuración para seleccionar una plataforma de destino para implementar:

  • Para ejecutar el encabezado webAssembly (Wasm):

    • Elija con el botón derecho el proyecto UnoSimplePhotos.Wasm y seleccione Establecer como proyecto de inicio.
    • Presione el botón UnoSimplePhotos.Wasm para implementar la aplicación.
    • Si lo desea, puede agregar y usar el proyecto UnoSimplePhotos.Server como alternativa.
  • Para depurar para iOS:

    • Elija con el botón derecho el proyecto UnoSimplePhotos.Mobile y seleccione Establecer como proyecto de inicio.

    • En la lista desplegable de la barra de herramientas de depuración, seleccione un dispositivo iOS activo o el simulador. Deberá emparejarse con un equipo Mac para que funcione.

      Captura de pantalla del menú desplegable de Visual Studio para seleccionar un marco de destino a implementar.

  • Para depurar para Mac Catalyst:

    • Elija con el botón derecho el proyecto UnoSimplePhotos.Mobile y seleccione Establecer como proyecto de inicio.
    • En la lista desplegable de la barra de herramientas de depuración, seleccione un dispositivo macOS remoto. Deberá emparejarse con uno para que funcione.
  • Para depurar la plataforma Android:

    • Elija con el botón derecho el proyecto UnoSimplePhotos.Mobile y seleccione Establecer como proyecto de inicio.
    • En la lista desplegable de la barra de herramientas de depuración, seleccione un dispositivo Android activo o el emulador.
      • Seleccionar un dispositivo activo en el submenú "Dispositivo".
  • Para depurar en Linux con Skia GTK:

    • Elija con el botón derecho el proyecto UnoSimplePhotos.Skia.Gtk y seleccione Establecer como proyecto de inicio.
    • Presione el botón UnoSimplePhotos.Skia.Gtk para implementar la aplicación.

Consulte también