Share via


Tutorial: Erstellen einer einfachen Fotoanzeige für mehrere Plattformen

Nachdem Sie zum Einstieg eine einfache WinUI 3-App erstellt haben, fragen Sie sich vielleicht, wie Sie mehr Benutzer*innen erreichen können, ohne Ihre App neu schreiben zu müssen. In diesem Tutorial wird Uno Platform verwendet, um die Reichweite Ihrer vorhandenen C#/WinUI 3-Anwendung zu erhöhen und die Wiederverwendung der Geschäftslogik und Benutzeroberflächenebene auf nativen Mobil-, Web- und Desktopplattformen zu ermöglichen. Mit nur minimalen Änderungen an der einfachen Fotoanzeige-App erhalten Sie eine pixelgenaue, zu diesen Plattformen portierte Kopie der App.

Screenshot of UnoSimplePhoto app targeting web and WinUI desktop.

Voraussetzungen

  • Visual Studio 2022 17.4 oder höher
  • Tools für das Windows App SDK
  • Workload „ASP.NET und Webentwicklung“ (für die WebAssembly-Entwicklung) Screenshot of the web development workload in Visual Studio.
  • Installation von „.NET Multi-Platform App UI-Entwicklung“ (für die iOS-, Android- und Mac Catalyst-Entwicklung) Screenshot of the dotnet mobile workload in Visual Studio.
  • Installation von „.NET-Desktopentwicklung“ (für die Gtk-, Wpf- und Linux Framebuffer-Entwicklung) Screenshot of the dotnet desktop workload in Visual Studio.

Fertigstellen Ihrer Umgebung

  1. Öffnen Sie eine Befehlszeile, eine Instanz von Windows-Terminal (sofern installiert) oder auch eine Eingabeaufforderung oder Windows PowerShell über das Startmenü.

  2. Installieren oder aktualisieren Sie das Tool uno-check:

    • Verwenden Sie den folgenden Befehl:

      dotnet tool install -g uno.check
      
    • So aktualisieren Sie das Tool, wenn bei Ihnen bereits eine ältere Version installiert ist:

      dotnet tool update -g uno.check
      
  3. Führen Sie das Tool mithilfe des folgenden Befehls aus:

    uno-check
    
  4. Befolgen Sie die im Tool angegebenen Anweisungen. Da ihr System geändert werden muss, werden Sie möglicherweise aufgefordert, erhöhte Berechtigungen zu erteilen.

Installieren der Projektmappenvorlagen für Uno Platform

Starten Sie Visual Studio, und klicken Sie auf Continue without code. Klicken Sie auf der Menüleiste auf Extensions>Manage Extensions.

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

Erweitern Sie im Erweiterungs-Manager den Knoten Online, suchen Sie nach Uno, und installieren Sie die Erweiterung Uno Platform, oder laden Sie sie aus Visual Studio Marketplace herunter, und installieren Sie sie. Starten Sie dann Visual Studio neu.

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

Erstellen einer Anwendung

Nun sind wir bereit für die Erstellung einer plattformübergreifenden Anwendung. Hierzu erstellen wir eine neue Uno Platform-Anwendung. Wir kopieren Code aus dem WinUI 3-Projekt SimplePhotos des vorherigen Tutorials in unser plattformübergreifendes Projekt. Das ist möglich, da Uno Platform die Wiederverwendung Ihrer bereits vorhandenen Codebasis ermöglicht. Features, die von Betriebssystem-APIs der jeweiligen Plattform abhängig sind, können problemlos nach und nach implementiert werden. Dieser Ansatz ist besonders nützlich, um bereits vorhandene Anwendung zu anderen Plattformen zu portieren.

Schon bald können Sie von den Vorteilen dieses Ansatzes profitieren, da Sie mehr Plattformen mit einer vertrauten XAML-Variante und der bereits vorhandenen Codebasis erreichen können.

Öffnen Sie Visual Studio, und wählen Sie File>New>Project aus, um ein neues Projekt zu erstellen:

Screenshot of the create a new project dialog.

Suchen Sie nach „Uno“, und wählen Sie die Projektvorlage „Uno Platform-App“ aus:

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

Erstellen Sie über die Startseite von Visual Studio eine neue C#-Projektmappe mit dem Typ Uno Platform-App. Geben Sie dieser Projektmappe einen anderen Namen (UnoSimplePhotos), um Konflikte mit dem Code aus dem vorherigen Tutorial zu vermeiden. Geben Sie den Projektnamen, den Projektmappennamen und das Verzeichnis an. In diesem Beispiel gehört das plattformübergreifende Projekt UnoSimplePhotos zu einer Projektmappe namens UnoSimplePhotos, die sich unter „C:\Projects“ befindet:

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

Wählen Sie als Nächstes eine Basisvorlage aus, um Ihre einfache Fotogalerieanwendung zu einer plattformübergreifenden Anwendung zu machen.

Die Vorlage „Uno Platform-App“ enthält zwei voreingestellte Optionen (Leer und Standard) für den schnellen Einstieg – entweder mit einer leeren Projektmappe oder mit der Standardkonfiguration, die Verweise auf die Bibliotheken „Uno.Material“ und „Uno.Toolkit“ enthält. Die Standardkonfiguration beinhaltet auch „Uno.Extensions“ für die Abhängigkeitsinjektion, Konfiguration, Navigation und Protokollierung. Darüber hinaus wird MVUX anstelle von MVVM verwendet, was die Option zu einem guten Ausgangspunkt für die schnelle Erstellung praktischer Anwendungen macht.

Screenshot of the Uno solution template for project startup type.

Wählen Sie der Einfachheit halber die Voreinstellung Leer aus. Klicken Sie anschließend auf die Schaltfläche Erstellen. Warten Sie, bis die Projekte erstellt und ihre Abhängigkeiten wiederhergestellt wurden.

In einem Banner oben im Editor werden Sie u. U. zum Neuladen von Projekten aufgefordert. Klicken Sie auf Projekte neu laden:

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

Im Projektmappen-Explorer sollte die folgende Standarddateistruktur angezeigt werden:

Screenshot of the default file structure in Solution Explorer.

Hinzufügen von Imageressourcen zum Projekt

Ihre App benötigt einige Bilder, die sie anzeigen kann. Sie können die gleichen Bilder wie im vorherigen Tutorial verwenden.

Erstellen Sie im Projekt UnoSimplePhotos einen neuen Ordner mit dem Namen Assets, und kopieren Sie die JPG-Bilddateien in einen Unterordner namens Samples. Die Struktur des Ordners Assets sollte nun wie folgt aussehen:

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

Weitere Informationen zum Erstellen des Ordners Assets und zum Hinzufügen von Bildern finden Sie in der Uno Platform-Dokumentation unter Assets and image display (Ressourcen und Bildanzeige).

Vorbereiten Ihrer App

Nachdem Sie nun über den funktionalen Ausgangspunkt Ihrer plattformübergreifenden WinUI-Anwendung verfügen, können Sie Code aus dem Desktopprojekt hineinkopieren.

Kopieren der Ansicht

Da Uno Platform die Verwendung der XAML-Variante ermöglicht, mit der Sie bereits vertraut sind, können Sie den im vorherigen Tutorial erstellten Code kopieren und hier einfügen.

Kehren Sie zum Projekt SimplePhotos aus dem vorherigen Tutorial zurück. Suchen Sie im Projektmappen-Explorer nach der Datei MainWindow.xaml, und öffnen Sie sie. Beachten Sie, dass der Inhalt der Ansicht nicht in einem Page-Element, sondern in einem Window-Element definiert ist. Das liegt daran, dass es sich bei dem Desktopprojekt um eine WinUI 3-Anwendung handelt, die Window-Elemente verwenden kann, um den Inhalt der Ansicht zu definieren:

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

Dank der von Uno Platform bereitgestellten plattformübergreifende Implementierung der im Window-Element enthaltenen Steuerelemente wie GridView, Image und RatingControl funktioniert die Ansicht mit minimalem Aufwand auf allen unterstützten Plattformen. Kopieren Sie den Inhalt dieses Window-Elements, und fügen Sie ihn im Uno Platform-Projekt UnoSimplePhotos in das Page-Element der Datei MainPage.xaml ein. Der XAML-Code der MainPage-Ansicht sollte wie folgt aussehen:

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

Wie Sie wissen, gab es bei der Desktoplösung auch eine Datei namens MainWindow.xaml.cs mit CodeBehind für die Ansicht. Im Uno Platform-Projekt ist das CodeBehind für die hineinkopierte Ansicht MainPage in der Datei MainPage.xaml.cs enthalten.

Um dieses CodeBehind plattformübergreifend zu machen, muss zunächst Folgendes in der Datei MainPage.xaml.cs platziert werden:

  • Eigenschaft Images: Stellt eine beobachtbare Sammlung von Bilddateien für GridView bereit.

  • Inhalt des Konstruktors: Ruft GetItemsAsync() auf, um die Sammlung Images mit Elementen aufzufüllen, die Bilddateien darstellen.

  • Entfernen Sie die manuelle Änderung der Eigenschaft ItemsSource des Steuerelements ImageGridView.

  • Methode ImageGridView_ContainerContentChanging: Wird als Teil einer Strategie zum progressiven Laden von GridView-Elementen verwendet, die in den sichtbaren Bereich gescrollt werden.

  • Methode ShowImage: Lädt die Bilddateien in GridView.

  • Methode GetItemsAsync: Ruft die Bildressourcendateien aus dem Ordner Samples ab.

  • Methode LoadImageInfoAsync: Erstellt ein ImageFileInfo-Objekt auf der Grundlage einer erstellten Speicherdatei (StorageFile).

Nachdem Sie alles eingefügt haben, sollte MainPage.xaml.cs wie folgt aussehen:

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

Hinweis

Die Dateien in Ihrem Uno-App-Projekt müssen UnoSimplePhotos als Namespace verwenden.

Aktuell enthalten die Dateien für die verwendete Hauptansicht alle Funktionen der Desktoplösung. Nach dem Kopieren der Modelldatei ImageFileInfo.cs erfahren Sie, wie Sie die desktoporientierten Codeblöcke für die Kompatibilität mit mehreren Plattformen ändern.

Kopieren Sie ImageFileInfo aus dem Desktopprojekt, und fügen Sie das Element in die Datei ImageFileInfo.cs ein. Nehmen Sie die folgenden Änderungen vor:

  • Benennen Sie den Namespace von SimplePhotos in UnoSimplePhotos um:

    // Found towards the top of the file
    namespace UnoSimplePhotos;
    
  • Ändern Sie den Parametertyp der Methode OnPropertyChanged so, dass NULL-Werte zulässig sind:

    // string -> string?
    protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
    ...
    
  • Konfigurieren Sie PropertyChangedEventHandler so, dass NULL-Werte zulässig sind:

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

Die Datei sollte nun wie folgt aussehen:

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

Diese Klasse dient als Modell, um die Bilddateien in GridViewdarzustellen. Es sollte nun zwar möglich sein, die App auszuführen, die Bilder werden aber möglicherweise nicht ordnungsgemäß gerendert, oder ihre Eigenschaften werden nicht angezeigt. In den nächsten Abschnitten werden einige Änderungen an den kopierten Dateien vorgenommen, um sie in einem Kontext mit mehreren Plattformen kompatibel zu machen.

Verwenden von Präprozessordirektiven

Im Desktopprojekt aus dem vorherigen Tutorial enthält die Datei MainPage.xaml.cs eine Methode vom Typ GetItemsAsync, die Elemente aus einem Speicherordner (StorageFolder) aufzählt, der den Speicherort des installierten Pakets darstellt. Da dieser Speicherort auf bestimmten Plattformen wie WebAssembly nicht verfügbar ist, muss diese Methode geändert werden, um sie mit allen Plattformen kompatibel zu machen. Wir ändern auch die Klasse ImageFileInfo entsprechend, um die Kompatibilität zu gewährleisten.

Nehmen Sie zunächst die erforderlichen Änderungen an der Methode GetItemsAsync vor. Ersetzen Sie die Methode GetItemsAsync in der Datei MainPage.xaml.cs durch den folgenden Code:

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

Diese Methode verwendet nun eine Präprozessordirektive, um basierend auf der Plattform zu bestimmen, welcher Code ausgeführt werden soll. Unter Windows ruft die Methode den Speicherordner (StorageFolder) ab, der den Speicherort des installierten Pakets darstellt, und gibt daraus den Ordner Samples zurück. Auf anderen Plattformen zählt die Methode bis 20 und ruft die Bilddateien aus dem Ordner Samples ab. Dabei wird ein URI (Uri) verwendet, um die Bilddatei darzustellen.

Passen Sie als Nächstes die Methode LoadImageInfoAsync an die Änderungen an, die Sie an der Methode GetItemsAsync vorgenommen haben. Ersetzen Sie die Methode LoadImageInfoAsync in der Datei MainPage.xaml.cs durch den folgenden Code:

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

Diese Methode verwendet nun ebenfalls eine Präprozessordirektive (ähnlich wie die Methode GetItemsAsync), um basierend auf der Plattform zu bestimmen, welcher Code ausgeführt werden soll. Unter Windows ruft die Methode die Bildeigenschaften (ImageProperties) aus der Speicherdatei (StorageFile) ab und verwendet sie, um ein ImageFileInfo-Objekt zu erstellen. Auf anderen Plattformen erstellt die Methode ein ImageFileInfo-Objekt ohne den Parameter ImageProperties. Später werden Änderungen an der Klasse ImageFileInfo vorgenommen, um diese Änderung zu berücksichtigen.

Steuerelemente wie GridView ermöglichen progressives Laden aktualisierter Elementcontainerinhalte, während sie in den sichtbaren Bereich gescrollt werden. Hierzu wird das Ereignis ContainerContentChanging verwendet. Im Desktopprojekt aus dem vorherigen Tutorial verwendet die Methode ImageGridView_ContainerContentChanging dieses Ereignis, um die Bilddateien in GridView zu laden. Da bestimmte Aspekte dieses Ereignisses nicht auf allen Plattformen unterstützt werden, müssen Änderungen an dieser Methode vorgenommen werden, um sie mit ihnen kompatibel zu machen.

Diagram of collection control viewport.

Beispielsweise wird die Eigenschaft ContainerContentChangingEventArgs.Phase derzeit nur unter Windows unterstützt. Die Methode ImageGridView_ContainerContentChanging muss geändert werden, um diese Änderung zu berücksichtigen. Ersetzen Sie die Methode ImageGridView_ContainerContentChanging in der Datei MainPage.xaml.cs durch den folgenden Code:

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
}

Daraufhin wird der spezialisierte Rückruf nur mit ContainerContentChangingEventArgs.RegisterUpdateCallback() registriert, wenn es sich bei der Plattform um Windows handelt. Andernfalls wird die Methode ShowImage direkt aufgerufen. Es sind auch Änderungen an der Methode ShowImage erforderlich, damit sie mit den Änderungen funktioniert, die an der Methode ImageGridView_ContainerContentChanging vorgenommen wurden. Ersetzen Sie die Methode ShowImage in der Datei MainPage.xaml.cs durch den folgenden Code:

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

Auch hier stellen wieder Präprozessordirektiven sicher, dass die Eigenschaft ContainerContentChangingEventArgs.Phase nur auf Plattformen verwendet wird, auf denen sie unterstützt wird. Wir verwenden die zuvor nicht verwendete Methode GetImageSourceAsync(), um die Bilddateien auf Windows-fremden Plattformen in GridView zu laden. Als Nächstes wird die Klasse ImageFileInfo bearbeitet, um die oben vorgenommenen Änderungen zu berücksichtigen.

Erstellen eines separaten Codepfads für andere Plattformen

Aktualisieren Sie ImageFileInfo.cs, um eine neue Eigenschaft namens ImageSource einzuschließen, die zum Laden der Bilddatei verwendet wird.

public BitmapImage? ImageSource { get; private set; }

Da Plattformen wie das Web keine erweiterten Bilddateieigenschaften unterstützen (die unter Windows problemlos verfügbar sind), wird eine Konstruktorüberladung hinzugefügt, die keinen Parameter vom Typ ImageProperties erfordert. Fügen Sie die neue Überladung mithilfe des folgenden Codes nach der bereits vorhandenen Überladung hinzu:

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

Diese Konstruktorüberladung wird verwendet, um auf Windows-fremden Plattformen ein ImageFileInfo-Objekt zu erstellen. Infolgedessen ist es sinnvoll, die Eigenschaft ImageProperties so zu konfigurieren, dass sie NULL-Werte zulässt. Aktualisieren Sie die Eigenschaft ImageProperties mit dem folgenden Code, damit NULL-Werte für sie zulässig sind:

public ImageProperties? ImageProperties { get; }

Aktualisieren Sie die Methode GetImageSourceAsync so, dass sie die Eigenschaft ImageSource verwendet, anstatt nur ein BitmapImage-Objekt zurückzugeben. Ersetzen Sie die Methode GetImageSourceAsync in der Datei ImageFileInfo.cs durch den folgenden Code:

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

Nehmen Sie die folgenden Änderungen vor, um zu verhindern, dass Sie den Wert von ImageProperties erhalten, wenn er NULL ist:

  • Ändern Sie die Eigenschaft ImageDimensions so, dass der bedingte NULL-Operator verwendet wird:

    public string ImageDimensions => $"{ImageProperties?.Width} x {ImageProperties?.Height}";
    
  • Ändern Sie die Eigenschaft ImageTitle so, dass der bedingte NULL-Operator verwendet wird:

    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();
                }
            }
        }
    }
    
  • Ändern Sie ImageRating so, dass nicht ImageProperties herangezogen wird, indem Sie zu Demonstrationszwecken Bewertungssterne nach dem Zufallsprinzip generieren:

    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();
                }
            }
        }
    }
    
  • Aktualisieren Sie den Konstruktor, der eine zufällige ganze Zahl generiert, damit dies nicht mehr geschieht:

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

Nach diesen Bearbeitungen sollte die Klasse ImageFileInfo den folgenden Code enthalten. Sie verfügt jetzt über einen neuen getrennten Codepfad für Windows-fremde Plattformen:

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

Diese Klasse vom Typ ImageFileInfo wird verwendet, um die Bilddateien in GridView darzustellen. Abschließend ändern wir die Datei MainPage.xaml, um die Änderungen am Modell zu berücksichtigen.

Verwenden von plattformspezifischem XAML-Markup

Es gibt einige Elemente im Ansichtsmarkup, die nur unter Windows ausgewertet werden dürfen. Fügen Sie dem Page-Element der Datei MainPage.xaml einen neuen Namespace hinzu:

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

Ersetzen Sie nun in MainPage.xaml den Eigenschaftensetter ItemsPanel für das GridView-Element durch den folgenden Code:

win:ItemsPanel="{StaticResource ImageGridView_ItemsPanelTemplate}"

Wenn Sie dem Eigenschaftsnamen win: voranstellen, wird die Eigenschaft nur unter Windows festgelegt. Wiederholen Sie dies innerhalb der Ressource ImageGridView_ItemTemplate. Es sollen nur Elemente geladen werden, die die Eigenschaft ImageDimensions unter Windows verwenden. Ersetzen Sie das TextBlock-Element, das die Eigenschaft ImageDimensions verwendet, durch den folgenden Code:

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

Die Datei MainPage.xaml sollte jetzt wie folgt aussehen:

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

Ausführen der App

Starten Sie das Ziel UnoSimplePhotos.Windows. Wie Sie sehen, ist diese WinUI-App dem vorherigen Tutorial sehr ähnlich.

Sie können Ihre App jetzt auf jeder der unterstützten Plattformen erstellen und ausführen. Dazu können Sie über die Dropdownliste auf der Debugsymbolleiste eine Zielplattform für die Bereitstellung auswählen:

  • So führen Sie die WASM-Hauptkomponente (WebAssembly) aus

    • Klicken Sie mit der rechten Maustaste auf das Projekt UnoSimplePhotos.Wasm, und wählen Sie Als Startprojekt festlegen aus.
    • Wählen Sie die Schaltfläche UnoSimplePhotos.Wasm aus, um die App bereitzustellen.
    • Auf Wunsch können Sie das Projekt UnoSimplePhotos.Server als Alternative hinzufügen und verwenden.
  • So debuggen Sie für iOS

    • Klicken Sie mit der rechten Maustaste auf das Projekt UnoSimplePhotos.Mobile, und wählen Sie Als Startprojekt festlegen aus.

    • Wählen Sie über die Dropdownliste auf der Debugsymbolleiste ein aktives iOS-Gerät oder den Simulator aus. Hierzu muss eine Koppelung mit einem Mac bestehen.

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

  • So debuggen Sie für Mac Catalyst

    • Klicken Sie mit der rechten Maustaste auf das Projekt UnoSimplePhotos.Mobile, und wählen Sie Als Startprojekt festlegen aus.
    • Wählen Sie über die Dropdownliste auf der Debugsymbolleiste ein macOS-Remotegerät aus. Hierzu muss eine Koppelung mit einem entsprechenden Gerät bestehen.
  • So debuggen Sie die Android-Plattform

    • Klicken Sie mit der rechten Maustaste auf das Projekt UnoSimplePhotos.Mobile, und wählen Sie Als Startprojekt festlegen aus.
    • Wählen Sie über die Dropdownliste auf der Debugsymbolleiste entweder ein aktives Android-Gerät oder den Emulator aus.
      • Wählen Sie im Untermenü „Gerät“ ein aktives Gerät aus.
  • So debuggen Sie unter Linux mit Skia GTK:

    • Klicken Sie mit der rechten Maustaste auf das Projekt UnoSimplePhotos.Skia.Gtk, und wählen Sie Als Startprojekt festlegen aus.
    • Wählen Sie die Schaltfläche UnoSimplePhotos.Skia.Gtk aus, um die App bereitzustellen.

Siehe auch