In W2.1.1 haben Sie Kladde.App angelegt. Die App zeigt noch die Seite der Vorlage. Diese Lektion ersetzt sie durch die ersten eigenen
Seiten: eine Kontaktliste mit einer Detailseite und die Termine des Tages, verbunden durch eine Shell mit zwei Tabs. Die Daten
stehen fest im Code; die Web-API kommt in W2.2.2 dazu. Auch der Code ist noch einfach gehalten, mit Code-Behind. Das ist Absicht: In
W2.2.1 bauen Sie genau diese Seiten auf MVVM um, und dafür brauchen Sie sie zuerst in dieser einfachen Form.
Sie lernen dafür sieben Dinge, die jede MAUI-App braucht:
| Abschnitt | Was Sie danach können |
|---|---|
| XAML | eine Seite lesen und schreiben |
| Layouts | Zeilen, Spalten und Stapel richtig einsetzen |
| Steuerelemente | die Elemente dieser Lektion einordnen |
| CollectionView | Listen anzeigen, auswählen, leer anzeigen |
| Shell | Tabs, Routen, Daten an die nächste Seite |
| Stile und Ressourcen | Gestaltung an einem Ort, helle und dunkle Darstellung |
| Barrierefreiheit | Überschriften, Beschriftungen, Schriftgrößen des Systems |
XAML lesen
XAML ist eine Beschreibung von Objekten in XML. Ein Element ist eine Klasse, ein Attribut ist eine Eigenschaft. Jede Seite besteht aus
zwei Dateien, die zusammen eine einzige Klasse ergeben: der .xaml-Datei mit der Oberfläche und der .xaml.cs-Datei, dem
Code-Behind. So sieht der Kopf einer Seite aus:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Kladde.App.Pages.ContactsPage">
...
</ContentPage>
namespace Kladde.App.Pages;
public partial class ContactsPage : ContentPage
{
public ContactsPage()
{
InitializeComponent();
}
}
Was hier steht, ist in wenigen Punkten erklärt:
x:Classnennt den vollständigen Namen einer .NET-Klasse. Die XAML-Datei definiert damit die KlasseContactsPage, die vonContentPageableitet; deshalb ist die C#-Klassepartial. Alles andere in der Datei sind Objekte bestehender Klassen, die erzeugt und eingestellt werden.x:Classdarf nur am Wurzelelement stehen.- Die zwei Namensräume. Der erste, ohne Präfix, bedeutet: Elemente ohne Präfix sind Klassen von .NET MAUI, also
ContentPageoderLabel. Der zweite hat das Präfixxund gilt für Dinge, die zu XAML selbst gehören, etwax:Class,x:Nameundx:DataType. Die Adressen sind keine Webseiten; unter ihnen steht nichts. Sie dienen als Versionskennung. InitializeComponenterzeugt zur Laufzeit alle Objekte der XAML-Datei, verbindet sie zu einem Baum, hängt Ereignis-Handler aus dem Code an die Ereignisse und setzt den Baum als Inhalt der Seite. Ohne den Aufruf bleibt die Seite leer.- Eigene Klassen aus Ihrem Projekt binden Sie mit einem weiteren Namensraum ein, zum Beispiel
xmlns:models="clr-namespace:Kladde.App.Models". Danach schreiben Siemodels:ContactResponse.
Eigenschaften, Konverter, Markup-Erweiterungen
Einfache Werte schreiben Sie als Attribut: <Label Text="Kontakte" FontSize="24" />. Für Aufzählungen genügt der Name des Mitglieds,
für Farben der Name aus Colors oder ein hexadezimaler RGB-Wert; dafür sorgen Typkonverter, die MAUI mitbringt. Ist der Wert zu
verwickelt für ein Attribut, schreiben Sie die Eigenschaft als eigenes Element (Eigenschaftselement-Syntax):
<Label>
<Label.FontSize>
22
</Label.FontSize>
</Label>
In geschweiften Klammern stehen Markup-Erweiterungen. Sie werden zur Laufzeit ausgewertet. Diese brauchen Sie in dieser Lektion:
| Ausdruck | Bedeutung |
|---|---|
{Binding Name} |
der Wert der Eigenschaft Name des aktuellen BindingContext |
{StaticResource Schlüssel} |
der Wert aus einem Ressourcenwörterbuch, einmal nachgeschlagen |
{DynamicResource Schlüssel} |
wie StaticResource, aber die Verbindung bleibt: wird der Eintrag ersetzt, folgt das Element |
{AppThemeBinding Light=…, Dark=…} |
ein Wert je nach System-Theme |
{DataTemplate pages:ContactsPage} |
eine Vorlage, die eine Seite erst bei Bedarf erzeugt |
Ereignisse hängen Sie mit dem Namen einer Methode im Code-Behind an: <Button Clicked="OnBackClicked" />. Ein Element, auf das Ihr Code
zugreifen muss, bekommt x:Name="ContactList"; danach gibt es im Code-Behind ein Feld gleichen Namens.
Ein Quellgenerator für XAML
MAUI übersetzt XAML beim Bauen (XAML-Kompilierung, standardmäßig an). In .NET 10 gibt es zusätzlich einen Quellgenerator für XAML. Er
erzeugt zur Übersetzungszeit stark typisierten Code für Ihre XAML-Dateien, was nach Learn die Bauzeit verbessert und die Werkzeuge
unterstützt. Sie schalten ihn in der Projektdatei ein, in einer PropertyGroup:
<PropertyGroup>
<MauiXamlInflator>SourceGen</MauiXamlInflator>
</PropertyGroup>
In diesem Kurs schalten Sie ihn in Übung 1 ein und lassen ihn danach an. Learn beschreibt außerdem globale und implizite
Namensräume, die das Wiederholen von xmlns:-Zeilen ersparen; sie sind als Vorschau-Merkmal gekennzeichnet und brauchen eine
Datei GlobalXmlns.cs und einen Schalter für Vorschaufunktionen. Der Kurs verwendet sie nicht.
Layouts
Eine Seite hat ein Kind, meist ein Layout. Ein Layout ordnet seine Kinder an. Drei brauchen Sie ständig:
| Layout | Ordnet an | Wann |
|---|---|---|
Grid |
in Zeilen und Spalten | feste Raster, Formulare, Listeneinträge mit mehreren Teilen |
VerticalStackLayout |
untereinander | eine Spalte von Elementen |
HorizontalStackLayout |
nebeneinander | eine Zeile von Elementen |
Es gibt auch StackLayout mit einer Eigenschaft Orientation. Die beiden Stapel-Layouts mit festem Richtungsnamen sind nach Learn die
leistungsfähigere Wahl. Alle drei haben die Eigenschaft Spacing (Standard 0), den Abstand zwischen den Kindern.
Grid
Ein Grid hat standardmäßig eine Zeile und eine Spalte. Zeilen und Spalten legen Sie mit RowDefinitions und ColumnDefinitions fest. Jede
Größe hat eine von drei Arten:
| Schreibweise | Art | Bedeutung |
|---|---|---|
50 |
Absolute |
genau diese Zahl geräteunabhängiger Einheiten |
Auto |
Auto |
so viel, wie der Inhalt braucht |
*, 2* |
Star |
der Rest, im Verhältnis der Zahlen verteilt |
Der Standard einer Zeile oder Spalte ist *. Kurz schreiben Sie die Definitionen als kommagetrennte Zeichenkette, zum Beispiel
RowDefinitions="Auto, *" und ColumnDefinitions="Auto, *". Die Zelle eines Kindes bestimmen die angehängten Eigenschaften
Grid.Row und Grid.Column, gezählt ab 0 (beide haben den Standard 0). Mit Grid.RowSpan und Grid.ColumnSpan erstreckt sich ein
Kind über mehrere Zellen.
<Grid RowDefinitions="Auto, Auto" ColumnDefinitions="Auto, *" ColumnSpacing="12">
<Label Text="09:00" />
<Label Grid.Row="1" Text="bis 10:00" />
<Label Grid.Column="1" Grid.RowSpan="2" Text="Beratung" />
</Grid>
Learn gibt zwei Warnungen mit. Erstens: Möglichst wenige Zeilen und Spalten auf Auto stellen, denn jede erzwingt zusätzliche
Berechnungen; fest oder * ist günstiger. Zweitens: Ein Grid ist kein Tabellenlayout für tabellarische Daten. Bilden Sie auch kein
Grid mit verschachtelten Stapeln nach: Das ist verschwenderisch, nehmen Sie ein Grid.
Stapel und Abstände
Die MAUI-Stapel „teilen“ den Platz nicht auf: Sie stapeln ihre Kinder in einer Richtung, bis alle gestapelt sind, auch über den
verfügbaren Platz hinaus. Optionen wie FillAndExpand kennen VerticalStackLayout und HorizontalStackLayout nicht; sie behandeln sie
als Fill. Das ist der Unterschied zu Xamarin.Forms, den Sie in alten Beispielen finden.
Für Abstände gibt es drei Eigenschaften mit klar getrennter Bedeutung:
| Eigenschaft | Bedeutung |
|---|---|
Padding |
der Abstand zwischen dem Layout oder der Seite und seinen Kindern |
Margin |
der Abstand zwischen einem Element und den Nachbarn |
Spacing |
der Abstand zwischen den Kindern eines Stapels |
Die Ausrichtung eines Kindes steuern HorizontalOptions und VerticalOptions mit den Werten Start, Center, End und Fill. Ein
Stapel beachtet sie nur quer zur Stapelrichtung: In einem senkrechten Stapel wirkt HorizontalOptions, aber nicht VerticalOptions.
Steuerelemente
Diese Lektion braucht wenige Steuerelemente. Sie sind hier nach dem eingeordnet, was sie können:
| Steuerelement | Aufgabe | Was Sie davon benutzen |
|---|---|---|
Label |
Text anzeigen | Text, FontSize, FontAttributes, TextColor, LineBreakMode, MaxLines |
Button |
eine Aktion auslösen | Text, Clicked |
CollectionView |
eine Sammlung anzeigen | ItemsSource, ItemTemplate, EmptyView, SelectionMode |
ToolbarItem |
ein Eintrag der Titelleiste | Text, Clicked, Order |
ContentPage |
eine Seite | Title, Content, ToolbarItems |
Alle Steuerelemente mit Text haben dieselben vier Eigenschaften für die Schrift: FontFamily, FontAttributes, FontSize und
FontAutoScalingEnabled. Die Schriftgröße steht in geräteunabhängigen Einheiten. Die Vorgabe von MAUI-Apps ist die Schriftart Open
Sans auf jeder Plattform; sie lässt sich ändern.
CollectionView statt ListView
Mit .NET 10 sind ListView und seine Zellen (TextCell, ImageCell, EntryCell, SwitchCell, ViewCell) abgekündigt, ebenso TableView.
Learn nennt für alles CollectionView. Damit sind alte Anleitungen, die ListView mit TextCell benutzen, für diesen Kurs falsch. Die
CollectionView unterscheidet sich in einigem von der ListView:
| Thema | CollectionView |
|---|---|
| Trennlinien | gibt es nicht eingebaut; Sie zeichnen sie in der Vorlage, wenn Sie sie wollen |
| Auswahl | standardmäßig aus (SelectionMode="None"); Single oder Multiple schaltet sie ein |
| Zeilenhöhe | bestimmt ItemSizingStrategy |
| Kopf- und Fußzeile | Header und Footer |
| Gruppen | IsGrouped, GroupHeaderTemplate |
| Wiederverwendung der Zeilen | übernimmt die Virtualisierung der nativen Steuerelemente |
Daten und Vorlage
Die Sammlung setzen Sie mit ItemsSource, und wie ein Eintrag aussieht, bestimmt ItemTemplate, eine DataTemplate. Das Wurzelelement der
Vorlage ist hier ein Grid; der Wurzelknoten darf nie ein ViewCell sein, denn die CollectionView kennt keine Zellen. Das Beispiel
zeigt das Muster:
<CollectionView ItemsSource="{Binding Monkeys}">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:Monkey">
<Grid Padding="10">
<Label Text="{Binding Name}" FontAttributes="Bold" />
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
Das ist die Form aus Learn, gekürzt. Ihre eigene Liste folgt in der Übung. Zwei Hinweise zur Sammlung:
- Soll sich die Liste ändern, wenn Elemente hinzukommen, wegfallen oder sich ändern, braucht sie eine Sammlung, die Änderungen
meldet, wie
ObservableCollection<T>. Für die festen Daten dieser Lektion genügt eine gewöhnliche Liste. In W2.2.1 tauschen Sie sie aus. - Eine
CollectionViewlöst eine Ausnahme aus, wenn ihreItemsSourcevon einem anderen Thread als dem Oberflächenthread geändert wird. Merken Sie sich das für W2.2.2, wenn Daten aus dem Netz kommen.
Zwei Fallen betreffen das Layout um die Liste: Eine CollectionView in einem VerticalStackLayout oder StackLayout kann das
Scrollen verhindern und die Zahl der angezeigten Einträge begrenzen. Stecken Sie sie in ein Grid. Und mit Stapel-Layouts als
Anordnung der Einträge (ItemsLayout) schalten Sie die Virtualisierung ab; die Standardanordnung ist senkrecht und passt.
Leer und Auswahl
Ist ItemsSource null oder leer, zeigt die Liste ihre Leeransicht: der Text in EmptyView, oder eine ganze Ansicht mit mehreren
Elementen. Das ist der Ort für „Keine Kontakte vorhanden.“:
<CollectionView ItemsSource="{Binding Contacts}" EmptyView="Keine Kontakte vorhanden." />
Mit SelectionMode="Single" wird ein Eintrag ausgewählt und hervorgehoben; SelectedItem ist dann der Eintrag und das Ereignis
SelectionChanged wird ausgelöst. Seine Argumente haben PreviousSelection und CurrentSelection als schreibgeschützte Listen. Kehrt
ein Benutzer von einer Detailseite zurück, ist der Eintrag noch ausgewählt und lässt sich nicht erneut antippen. Learn empfiehlt dagegen,
die Auswahl nach dem Auslösen zu löschen:
void OnCollectionViewSelectionChanged(object sender, SelectionChangedEventArgs e)
{
var cv = (CollectionView)sender;
if (cv.SelectedItem == null)
return;
cv.SelectedItem = null;
}
Das Löschen löst SelectionChanged noch einmal aus, mit leerer CurrentSelection; Ihr Handler muss diesen zweiten Aufruf erkennen und
überspringen. Die Übung baut das ein.
Gebundene Typen und Warnungen
Die Bindungen {Binding Name} lösen zur Laufzeit einen Eigenschaftspfad per Reflexion auf, es sei denn, XAML kennt den Typ. Setzen Sie
x:DataType auf den Typ, an den das Element und seine Kinder binden, dann werden die Bindungen schon beim Bauen übersetzt: falsche
Namen sind Baufehler, und die Laufzeit ist schneller. Wer x:DataType weglässt, bekommt die Warnung XC0022; innerhalb einer
DataTemplate brauchen Sie es an der Vorlage selbst, sonst meldet der Build XC0024. Setzen Sie es überall, wo sich der Typ des
BindingContext ändert. Sie können beide Warnungen zu Fehlern machen:
<WarningsAsErrors>$(WarningsAsErrors);XC0022;XC0023</WarningsAsErrors>
Shell: Tabs, Routen, Daten
Die Shell ist die Gliederung der App. Die Vorlage legt sie als AppShell an. Ihre Hauptbausteine:
| Baustein | Bedeutung |
|---|---|
TabBar |
die untere Tab-Leiste; ohne Menü |
FlyoutItem |
ein Eintrag im Menü, das von der Seite hereinfährt |
Tab |
gruppierter Inhalt eines Tabs |
ShellContent |
eine Seite in der Hierarchie, mit Title, Icon, Route und ContentTemplate |
Die Seiten entstehen bei Bedarf, wenn jemand dorthin navigiert. Dafür setzen Sie die ContentTemplate jedes ShellContent mit der
Markup-Erweiterung DataTemplate auf die Seitenklasse. Mehrere ShellContent-Elemente in einer TabBar werden zu unteren Tabs;
mehr als fünf Tabs ergeben einen Tab „More“. ShellContent braucht nicht in einem Tab zu stehen: MAUI packt es selbst ein.
<Shell xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:pages="clr-namespace:Kladde.App.Pages"
x:Class="Kladde.App.AppShell">
<TabBar>
<ShellContent Title="Kontakte" Route="contacts" ContentTemplate="{DataTemplate pages:ContactsPage}" />
<ShellContent Title="Heute" Route="today" ContentTemplate="{DataTemplate pages:AppointmentsPage}" />
</TabBar>
</Shell>
Routen
Navigiert wird mit URIs aus Routen. Jedes Element der Shell-Hierarchie hat eine; fehlt eine, erzeugt MAUI sie zur Laufzeit, aber nicht
beständig von Sitzung zu Sitzung. Zwei gleiche Routen auf derselben Ebene lösen beim Start eine ArgumentException aus. Seiten, die
nicht in der Hierarchie stehen, etwa Detailseiten, melden Sie mit Routing.RegisterRoute an, vor dem ersten Gebrauch. Ihre Route heißt
globale Route:
public partial class AppShell : Shell
{
public AppShell()
{
InitializeComponent();
Routing.RegisterRoute("contactdetails", typeof(ContactDetailPage));
}
}
Navigiert wird mit Shell.Current.GoToAsync. Die Methode gibt eine Task zurück, die endet, wenn die Animation fertig ist, und ist
immer mit await aufzurufen: Ein vergessenes await führt zu Wettläufen, fehlenden Parametern und stillen Fehlern.
| URI | Wirkung |
|---|---|
//today |
absolute Route zu einem Element der Hierarchie |
contactdetails |
relative Route: eine registrierte Seite wird auf den Stapel gelegt |
.. |
eine Seite zurück |
../route |
zurück und dann zu einer Route |
Zwei Grenzen aus Learn: Absolute Routen funktionieren nicht mit Seiten, die mit RegisterRoute angemeldet sind; und relative Routen
funktionieren nicht für Seiten, die in der Shell-Klasse selbst stehen. Wer zu einer Route navigiert, die es nicht gibt, bekommt eine
ArgumentException. Wenn eine Seite aus der Hierarchie angesteuert wird, entsteht kein Navigationsstapel; wird eine Seite angesteuert,
die nicht in der Hierarchie steht, entsteht einer, und man kann zurückgehen.
Daten an die Zielseite
Einfache Werte gehen als Zeichenfolge an die URI: await Shell.Current.GoToAsync($"details?id={item.Id}");. Ganze Objekte übergeben Sie als
zweites Argument. Für Daten, die nur einmal gelten und nach der Navigation gelöscht werden, gibt es ShellNavigationQueryParameters:
var parameters = new ShellNavigationQueryParameters
{
{ "Contact", contact }
};
await Shell.Current.GoToAsync("contactdetails", parameters);
Die Zielseite empfängt sie auf einem von zwei Wegen: mit QueryProperty-Attributen oder durch die Schnittstelle IQueryAttributable mit der
Methode ApplyQueryAttributes. Learn warnt, dass das Attribut nicht sicher für das Kürzen (Trimming) und für Native AOT ist, und
empfiehlt IQueryAttributable. Der Kurs nimmt deshalb die Schnittstelle. Beachten Sie einen Unterschied: Zeichenfolgen, die per
IQueryAttributable ankommen, sind nicht automatisch URL-dekodiert; die per Attribut ankommenden sind es.
Stile und Ressourcen
Alles, was Sie auf mehr als einem Element einstellen, gehört in einen Stil oder eine Ressource. Ein Ressourcenwörterbuch
(ResourceDictionary) enthält Farben, Stile, Vorlagen, Konverter und andere Werte. Es kann an einem Steuerelement, einer Seite oder der
App hängen; je weiter oben, desto weiter reicht die Gültigkeit. Ein Stil (Style) fasst Eigenschaftswerte in Setter zusammen, mit
einem TargetType:
- Impliziter Stil: ohne
x:Key. Er gilt für jedes Element in Reichweite, dessen Typ genau derTargetTypeist, aber nicht für abgeleitete Typen. MitApplyToDerivedTypes="True"gilt er auch dort. - Expliziter Stil: mit
x:Key. Er gilt nur für Elemente, die ihn mitStyle="{StaticResource Schlüssel}"verwenden. - Vererbung:
BasedOnbaut einen Stil auf einem anderen auf; er darf nur von Stilen auf derselben oder einer höheren Ebene erben.
Eigene Dateien binden Sie als Teil der Ressourcen der App ein. In App.xaml steht schon ein Wörterbuch mit zusammengeführten
Wörterbüchern der Vorlage; fügen Sie dort eine Zeile an:
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources/Styles/KladdeStyles.xaml" />
</ResourceDictionary.MergedDictionaries>
Das Beispiel zeigt die Form; der Pfad hängt davon ab, wo Sie die Datei ablegen. Eine Änderung an einem Ressourcenwörterbuch lädt bei Hot Reload alle XAML-Dateien neu.
Hell und dunkel
Die Systeme haben ein helles und ein dunkles Thema, und eine App soll beide beachten. MAUI-Apps reagieren darauf unter iOS 13 und
neuer, Android 10 (API 29) und neuer, macOS 10.14 und neuer und Windows 10 und neuer. Die Markup-Erweiterung AppThemeBinding wählt einen
Wert nach dem Thema; wechselt es, während die App läuft, passen sich alle Elemente an. Werte aus einem Wörterbuch binden Sie mit
StaticResource ein:
<Color x:Key="CardLight">#F2F2F5</Color>
<Color x:Key="CardDark">#2A2A30</Color>
<Style x:Key="CardGrid" TargetType="Grid">
<Setter Property="BackgroundColor"
Value="{AppThemeBinding Light={StaticResource CardLight}, Dark={StaticResource CardDark}}" />
</Style>
Das aktuelle System-Thema liefert Application.Current.RequestedTheme (Light, Dark oder Unspecified). Mit Application.Current.UserAppTheme
legen Sie fest, welches Thema die App benutzt, unabhängig vom System; AppTheme.Unspecified stellt das Verhalten des Systems wieder her.
Das brauchen Sie in der Übung, um beide Darstellungen zu prüfen, ohne das Telefon umzustellen.
Barrierefreiheit
Eine App ist für alle Menschen da, auch für die, die sie mit einem Bildschirmleser, mit vergrößerter Schrift oder ohne Touch bedienen. Was K2 in Lektion 2.1.7 für Webseiten gelehrt hat, gilt dem Sinn nach hier wieder; die Technik ist eine andere. Drei Werkzeuge gehören in jede Seite.
Semantische Eigenschaften. SemanticProperties sind angehängte Eigenschaften, die die Bedienhilfen der Plattform füttern. Sie
erzwingen keine einheitliche Wirkung, sondern nutzen, was die Plattform kann:
| Eigenschaft | Zweck |
|---|---|
Description |
ein kurzer Text, den der Bildschirmleser für das Element sagt |
Hint |
zusätzlicher Zusammenhang, etwa wozu das Element dient |
HeadingLevel |
markiert eine Überschrift, Level1 bis Level9, damit man zwischen Überschriften springen kann |
<Image Source="like.png"
SemanticProperties.Description="Like"
SemanticProperties.Hint="Like this post." />
Das Beispiel ist von Learn. Unter Windows gibt es neun Überschriftsebenen; Android und iOS kennen nur eine, jede gesetzte Ebene wird dort
zur Überschrift. Learn nennt mehrere Fallen. Setzen Sie Description nicht an einem Label: Der Text würde nicht mehr vorgelesen, und
sichtbarer und gesprochener Text sollen gleich sein. Setzen Sie Description unter Android nicht an Entry und Editor, das stört
TalkBack; dort nehmen Sie Placeholder oder Hint. Und setzen Sie Hint nicht anders als Placeholder: Beide landen unter Android in
derselben Plattform-Eigenschaft. Unter iOS kann der Bildschirmleser die Kinder eines Elements nicht erreichen, das selbst eine
Description hat.
Lesereihenfolge. Standardmäßig ist es die Reihenfolge in XAML, in einem Grid zusätzlich nach Zeile und Spalte. Bauen Sie das Layout
so, dass die Reihenfolge im Dokument der Lesereihenfolge entspricht.
Schrift des Systems. Alle Steuerelemente mit Text skalieren ihre Schrift mit der Einstellung des Betriebssystems (FontAutoScalingEnabled
steht auf true). Schalten Sie das nur aus, wenn die Größe exakt feststehen muss. Wichtiger ist, dass Ihre Layouts eine größere Schrift
vertragen: Zeilen mit Auto und * wachsen mit, Zeilen mit fester Höhe schneiden Text ab.
Übung 1
Kontaktliste mit Shell. Ersetzen Sie die Seite der Vorlage durch eine Kontaktliste. Die Daten stehen fest im Code.
- Legen Sie in
src/Kladde.Appdie OrdnerModelsundPagesan. Löschen SieMainPage.xamlundMainPage.xaml.cs. - Legen Sie in
Modelsdie KlassenContactResponse,AppointmentResponseundSampleDataan (Quelltext in der Lösung). Die beiden Datensätze haben dieselben Namen und Felder wie die Antworten der Web-API aus K2; in W2.2.2 füllt die API sie. - Legen Sie in
Pagesdie SeiteContactsPagean (XAML plus Code-Behind) mit einerCollectionView, die die Kontakte anzeigt, mit Name und Firma je Eintrag und der Leeransicht „Keine Kontakte vorhanden.“. - Ändern Sie
AppShell.xamlso, dass sie eineTabBarmit der Kontaktseite hat. Die zweite Seite folgt in Übung 3. - Schalten Sie den Quellgenerator ein (
MauiXamlInflator) und bauen Sie: keine WarnungXC0022. - Starten Sie die App auf dem Android-Emulator und unter Windows.
So prüfen Sie: Die App zeigt die Liste mit sechs Kontakten. Setzen Sie testweise ContactList.ItemsSource = Array.Empty<ContactResponse>();
in den Konstruktor: Dann erscheint der Text der Leeransicht. Entfernen Sie die Zeile wieder. Ändern Sie bei laufender App die Schriftgröße
des Namens in der XAML-Datei und beobachten Sie Hot Reload. Prüfen Sie nach dem Umschalten auf den Quellgenerator, ob Hot Reload noch
greift; was Learn zum Zusammenspiel beider sagt, ist in dieser Lektion nicht belegt.
Lösung 1
Die Modelle. Die Felder entsprechen den Antworten der Web-API (ContactResponse und AppointmentResponse aus K2):
namespace Kladde.App.Models;
public sealed record ContactResponse(
int Id, string Name, string? Email, string? Company, IReadOnlyList<string> Tags);
public sealed record AppointmentResponse(
string Title, DateTimeOffset Start, DateTimeOffset End, IReadOnlyList<int> ContactIds);
Die festen Daten. Die Termine liegen auf dem heutigen Tag; die Hilfsmethode Today baut einen Zeitpunkt in der lokalen Zeitzone:
namespace Kladde.App.Models;
/// <summary>Fixed sample data for W2.1; the web API replaces it in W2.2.2.</summary>
public static class SampleData
{
public static IReadOnlyList<ContactResponse> Contacts { get; } =
[
new(1, "Mira Hoffmann", "mira.hoffmann@example.com", "Hoffmann IT-Beratung", ["Kunde"]),
new(2, "Jonas Weber", "jonas.weber@example.com", "Weber und Partner", ["Kunde", "Wartung"]),
new(3, "Selin Aydin", "selin.aydin@example.com", "Aydin Logistik", ["Interessent"]),
new(4, "Tom Brandt", null, null, []),
new(5, "Lena Krüger", "lena.krueger@example.com", "Krüger Software", ["Partner"]),
new(6, "Paul Neumann", "paul.neumann@example.com", "Neumann Bau", ["Kunde"]),
];
public static IReadOnlyList<AppointmentResponse> TodaysAppointments { get; } =
[
new("Beratung Hoffmann", Today(9, 0), Today(10, 0), [1]),
new("Rückruf Weber", Today(10, 30), Today(11, 0), [2]),
new("Workshop Aydin", Today(13, 0), Today(14, 30), [3]),
new("Jour fixe Team", Today(16, 0), Today(16, 30), []),
];
private static DateTimeOffset Today(int hour, int minute) =>
new(DateTime.Today.AddHours(hour).AddMinutes(minute));
}
Die Seite Pages/ContactsPage.xaml. Der Namensraum models bindet den Ordner Models; x:DataType steht an der DataTemplate:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:models="clr-namespace:Kladde.App.Models"
x:Class="Kladde.App.Pages.ContactsPage"
Title="Kontakte">
<CollectionView x:Name="ContactList"
EmptyView="Keine Kontakte vorhanden.">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:ContactResponse">
<Grid Padding="12" RowDefinitions="Auto, Auto">
<Label Text="{Binding Name}" FontAttributes="Bold" />
<Label Grid.Row="1" Text="{Binding Company}" />
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</ContentPage>
Pages/ContactsPage.xaml.cs:
using Kladde.App.Models;
namespace Kladde.App.Pages;
public partial class ContactsPage : ContentPage
{
public ContactsPage()
{
InitializeComponent();
ContactList.ItemsSource = SampleData.Contacts;
}
}
AppShell.xaml. Der Namensraum pages zeigt auf den Ordner Pages:
<?xml version="1.0" encoding="UTF-8" ?>
<Shell xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:pages="clr-namespace:Kladde.App.Pages"
x:Class="Kladde.App.AppShell">
<TabBar>
<ShellContent Title="Kontakte"
Route="contacts"
ContentTemplate="{DataTemplate pages:ContactsPage}" />
</TabBar>
</Shell>
Der Quellgenerator kommt in die PropertyGroup von Kladde.App.csproj:
<MauiXamlInflator>SourceGen</MauiXamlInflator>
Die Datei AppShell.xaml.cs der Vorlage behalten Sie, wie sie ist, bis Übung 2. Gibt der Build die Warnung XC0022, fehlt x:DataType an
der Vorlage. Ist die Liste leer, obwohl die Daten stehen, fehlt die Zuweisung der ItemsSource im Konstruktor, nach InitializeComponent.
Aufräumen: Beenden Sie die App und den Emulator, wenn Sie fertig sind.
Übung 2
Detailseite und Navigation. Ein Antippen eines Kontakts öffnet eine Detailseite.
- Legen Sie
Pages/ContactDetailPagean (XAML plus Code-Behind), die Name, Firma und E-Mail-Adresse zeigt, mit dem Namen als Überschrift der Ebene 1 (SemanticProperties.HeadingLevel) und einer Schaltfläche „Zurück“. - Melden Sie die Seite in
AppShell.xaml.csals Routecontactdetailsan. - Schalten Sie in der Liste die Einzelauswahl ein und navigieren Sie bei Auswahl zur Detailseite. Übergeben Sie den Kontakt als einmalig geltende Daten. Löschen Sie die Auswahl, damit der Eintrag erneut antippbar ist.
- Nehmen Sie die Daten auf der Detailseite mit
IQueryAttributableentgegen.
So prüfen Sie: Ein Antippen eines Kontakts zeigt die Detailseite mit den Daten dieses Kontakts, und der Zurück-Knopf der Titelleiste und
Ihre Schaltfläche führen zur Liste. Antippen Sie denselben Kontakt noch einmal: Es öffnet sich wieder die Detailseite. Ändern Sie
zur Probe die Route in GoToAsync auf einen Namen, den es nicht gibt: Die Navigation wirft eine ArgumentException. Stellen Sie sie wieder zurück.
Lösung 2
Pages/ContactDetailPage.xaml:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:models="clr-namespace:Kladde.App.Models"
x:Class="Kladde.App.Pages.ContactDetailPage"
x:DataType="models:ContactResponse"
Title="Kontakt">
<VerticalStackLayout Padding="20" Spacing="8">
<Label Text="{Binding Name}"
FontSize="24"
SemanticProperties.HeadingLevel="Level1" />
<Label Text="{Binding Company}" />
<Label Text="{Binding Email}" />
<Button Text="Zurück" Clicked="OnBackClicked" />
</VerticalStackLayout>
</ContentPage>
Pages/ContactDetailPage.xaml.cs. Die Seite bekommt den Kontakt aus dem Wörterbuch und macht ihn zu ihrem BindingContext:
using Kladde.App.Models;
namespace Kladde.App.Pages;
public partial class ContactDetailPage : ContentPage, IQueryAttributable
{
public ContactDetailPage()
{
InitializeComponent();
}
public void ApplyQueryAttributes(IDictionary<string, object> query)
{
if (query.TryGetValue("Contact", out object? value) && value is ContactResponse contact)
{
BindingContext = contact;
}
}
private async void OnBackClicked(object? sender, EventArgs e)
{
await Shell.Current.GoToAsync("..");
}
}
AppShell.xaml.cs meldet die Route an:
using Kladde.App.Pages;
namespace Kladde.App;
public partial class AppShell : Shell
{
public AppShell()
{
InitializeComponent();
Routing.RegisterRoute("contactdetails", typeof(ContactDetailPage));
}
}
In ContactsPage.xaml bekommt die CollectionView zwei Attribute, SelectionMode="Single" und SelectionChanged="OnContactSelected". Im
Code-Behind kommt der Handler dazu. Die Methode ist async void, weil sie ein Ereignis behandelt; Ereignis-Handler sind die Ausnahme
von der Regel, async void zu meiden:
private async void OnContactSelected(object? sender, SelectionChangedEventArgs e)
{
if (e.CurrentSelection.FirstOrDefault() is not ContactResponse contact)
{
return;
}
((CollectionView)sender!).SelectedItem = null;
var parameters = new ShellNavigationQueryParameters { { "Contact", contact } };
await Shell.Current.GoToAsync("contactdetails", parameters);
}
Die erste Bedingung fängt den zweiten Aufruf ab, den das Löschen der Auswahl auslöst: Dann ist CurrentSelection leer. Fehlt die Zeile mit
SelectedItem = null, bleibt der Eintrag nach der Rückkehr markiert und reagiert nicht mehr. Fehlt await, kann der Fehler einer
falschen Route verschwinden, statt sichtbar zu werden. Der Zurück-Knopf der Titelleiste ist Teil der Shell und braucht keinen Code.
Übung 3
Die Termine des Tages. Die zweite Seite zeigt die Termine von heute.
- Legen Sie
Pages/AppointmentsPagean. Die Seite hat oben eine Überschrift der Ebene 1 mit dem heutigen Datum, ausgeschrieben („Montag, 9. November“), darunter eineCollectionViewmit den Terminen ausSampleData.TodaysAppointments. - Ein Eintrag zeigt links Beginn und, darunter, das Ende als „bis 10:00“, rechts den Titel. Benutzen Sie dafür ein
GridmitColumnDefinitionsundStringFormat. - Die Liste liegt in einem
Gridmit den ZeilenAutound*, nicht in einemVerticalStackLayout. - Ergänzen Sie
AppShell.xamlum den zweiten Tab „Heute“.
So prüfen Sie: Der zweite Tab zeigt das heutige Datum und vier Termine mit den Uhrzeiten; die Liste scrollt, wenn Sie ihr in der Höhe
sehr wenig Platz geben (das Fenster unter Windows verkleinern). Ersetzen Sie testweise das äußere Grid durch ein
VerticalStackLayout und beobachten Sie, was sich ändert; stellen Sie es danach wieder zurück.
Lösung 3
Pages/AppointmentsPage.xaml:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:models="clr-namespace:Kladde.App.Models"
x:Class="Kladde.App.Pages.AppointmentsPage"
Title="Heute">
<Grid RowDefinitions="Auto, *" Padding="12">
<Label x:Name="DayLabel"
FontSize="20"
SemanticProperties.HeadingLevel="Level1" />
<CollectionView x:Name="AppointmentList"
Grid.Row="1"
EmptyView="Heute stehen keine Termine an.">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:AppointmentResponse">
<Grid Padding="0, 8" ColumnDefinitions="Auto, *" RowDefinitions="Auto, Auto" ColumnSpacing="16">
<Label Text="{Binding Start, StringFormat='{0:HH:mm}'}" FontAttributes="Bold" />
<Label Grid.Row="1" Text="{Binding End, StringFormat='bis {0:HH:mm}'}" />
<Label Grid.Column="1" Grid.RowSpan="2" Text="{Binding Title}" VerticalOptions="Center" />
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</Grid>
</ContentPage>
Pages/AppointmentsPage.xaml.cs:
using System.Globalization;
using Kladde.App.Models;
namespace Kladde.App.Pages;
public partial class AppointmentsPage : ContentPage
{
public AppointmentsPage()
{
InitializeComponent();
DayLabel.Text = DateTime.Today.ToString("dddd, d. MMMM", CultureInfo.GetCultureInfo("de-DE"));
AppointmentList.ItemsSource = SampleData.TodaysAppointments;
}
}
In AppShell.xaml kommt innerhalb der TabBar ein zweites ShellContent dazu:
<ShellContent Title="Heute"
Route="today"
ContentTemplate="{DataTemplate pages:AppointmentsPage}" />
Die Uhrzeiten in der Liste kommen als DateTimeOffset; das Format HH:mm zeigt sie 24-stündig. Die Namen der Wochentage und Monate hängen
von der Kultur ab, deshalb nennt der Code de-DE ausdrücklich. Beim Test mit dem VerticalStackLayout bekommt die Liste so viel Höhe, wie
ihre Einträge brauchen, und das Scrollen in der Liste ist nicht mehr verlässlich; das ist die Falle, vor der Learn warnt.
Übung 4
Stile, dunkle Darstellung, Schrift und Bedienhilfen. Machen Sie die zwei Seiten einheitlich und zugänglicher.
- Legen Sie
Resources/Styles/KladdeStyles.xamlan, mit zwei Farben für hell und dunkel, einem StilCardGridfür die Zeile eines Eintrags und einem StilCardTitlefür den Namen oder Titel. Binden Sie die Datei inApp.xamlein. - Wenden Sie beide Stile in den Vorlagen beider Listen an. Die Hintergrundfarbe der Zeile folgt dem Thema (
AppThemeBinding). - Fügen Sie der Kontaktseite einen
ToolbarItemhinzu, der zwischen hell und dunkel wechselt (UserAppTheme). - Stellen Sie am Gerät die Schrift auf die größte Stufe und prüfen Sie beide Seiten.
- Schalten Sie, wenn Ihre Plattform einen Bildschirmleser hat, ihn ein und lassen Sie sich eine Seite vorlesen.
So prüfen Sie: In beiden Themen sind Text und Hintergrund gut lesbar; der Wechsel über die Titelleiste ändert beide Seiten sofort. Mit
großer Schrift bleibt jeder Name und jeder Titel ganz sichtbar und wird nicht abgeschnitten. Der Bildschirmleser liest den Namen oder
das Datum als Überschrift und danach die Einträge in der Reihenfolge von oben nach unten. Ändern Sie bei laufender App eine Farbe in
KladdeStyles.xaml: Alle Seiten laden neu, das ist das Verhalten von Hot Reload bei Ressourcenwörterbüchern.
Lösung 4
Resources/Styles/KladdeStyles.xaml. Die Datei hat keine eigene Klasse (kein x:Class):
<?xml version="1.0" encoding="utf-8" ?>
<ResourceDictionary xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
<Color x:Key="CardLight">#F2F2F5</Color>
<Color x:Key="CardDark">#2A2A30</Color>
<Style x:Key="CardGrid" TargetType="Grid">
<Setter Property="Padding" Value="12" />
<Setter Property="BackgroundColor"
Value="{AppThemeBinding Light={StaticResource CardLight}, Dark={StaticResource CardDark}}" />
</Style>
<Style x:Key="CardTitle" TargetType="Label">
<Setter Property="FontAttributes" Value="Bold" />
<Setter Property="FontSize" Value="18" />
</Style>
</ResourceDictionary>
In App.xaml steht die Zeile mit Source="Resources/Styles/KladdeStyles.xaml" zwischen den schon vorhandenen zusammengeführten Wörterbüchern
(siehe oben). In ContactsPage.xaml bekommt das Grid der Vorlage Style="{StaticResource CardGrid}" und das Namens-Label
Style="{StaticResource CardTitle}"; dafür entfallen dort Padding und FontAttributes, die der Stil setzt. In AppointmentsPage.xaml
behält das Grid seine Spalten und Zeilen und bekommt Style="{StaticResource CardGrid}" (das Padding="0, 8" entfällt), und das Label
mit dem Titel bekommt CardTitle. Für die Kontakte sieht die Vorlage dann so aus:
<Grid Style="{StaticResource CardGrid}" RowDefinitions="Auto, Auto">
<Label Text="{Binding Name}" Style="{StaticResource CardTitle}" />
<Label Grid.Row="1" Text="{Binding Company}" />
</Grid>
Der Umschalter. In ContactsPage.xaml zwischen ContentPage und CollectionView:
<ContentPage.ToolbarItems>
<ToolbarItem Text="Hell oder dunkel" Clicked="OnThemeClicked" />
</ContentPage.ToolbarItems>
Im Code-Behind der Seite:
private void OnThemeClicked(object? sender, EventArgs e)
{
if (Application.Current is { } app)
{
app.UserAppTheme = app.UserAppTheme == AppTheme.Dark ? AppTheme.Light : AppTheme.Dark;
}
}
Beim ersten Klick ist UserAppTheme noch Unspecified, der Zweig ergibt Dark. Wollen Sie zum System zurück, setzen Sie
AppTheme.Unspecified. Dieser Umschalter ist ein Werkzeug zum Prüfen; die fertige Kladde-App folgt dem System.
Schrift: Unter Android finden Sie die Einstellung in den Einstellungen des Geräts oder Emulators unter Anzeige; unter Windows unter
Barrierefreiheit, Textgröße. Die Bezeichnungen hängen von der Version ab. Die Schriftgrößen in der Lösung stehen in FontSize; sie
skalieren mit, weil FontAutoScalingEnabled auf true bleibt. Weil die Zeilen Auto sind, wachsen die Einträge mit. Hätte eine Zeile
HeightRequest="40", würde der Text bei großer Schrift abgeschnitten.
Bildschirmleser: Unter Windows ist es die Sprachausgabe, unter Android TalkBack, sofern das Gerät oder Image sie hat; ohne sie lässt sich
dieser Schritt nicht prüfen, und Sie tragen ihn als offen ein. Eine Überschrift sagt der Leser an, weil HeadingLevel gesetzt ist. Eine
Description auf einem Label wäre ein Fehler (siehe Abschnitt Barrierefreiheit).
Die Kladde: Kontakte und Termine in der App
Nach dieser Lektion hat Kladde.App zwei Seiten in einer Shell: die Kontakte mit einer Detailseite und die Termine des Tages, mit festen
Beispieldaten aus SampleData, gestaltet mit einem eigenen Ressourcenwörterbuch, in hellem und dunklem Thema, mit Überschriften für den
Bildschirmleser und mitwachsender Schrift. Die zwei Datensätze ContactResponse und AppointmentResponse haben die Namen und Felder der
Antworten der Web-API aus K2, damit W2.2.2 sie ohne Umbau füllen kann. Die Seiten arbeiten noch mit Code-Behind: Der Konstruktor weist die
Daten zu, der Handler navigiert. In W2.2.1 holen Sie die Logik aus den Seiten heraus und bauen Kontaktliste, Detailseite und Termine auf
ViewModels um, die Sie ohne Gerät testen.
Was Sie jetzt können
- Sie lesen XAML: Wurzelelement,
x:Class, die zwei Namensräume, Eigenschaften, Markup-Erweiterungen und die Rolle vonInitializeComponent. - Sie bauen Seiten mit
Grid,VerticalStackLayoutundHorizontalStackLayoutund unterscheidenPadding,MarginundSpacing. - Sie zeigen Listen mit
CollectionView, mit Vorlage,x:DataType, Leeransicht und Auswahl, und nehmen keinListViewmehr. - Sie navigieren mit Shell: Tabs, Routen,
RegisterRoute,GoToAsyncmitawait, einmalige Daten undIQueryAttributable. - Sie sammeln Gestaltung in Stilen und Ressourcen, unterstützen helle und dunkle Darstellung und setzen
SemanticPropertiesund mitwachsende Schrift ein.
Weiterlesen
- Get started with XAML: Aufbau einer XAML-Datei, Namensräume, Konverter.
- What’s new in .NET MAUI for .NET 10: Quellgenerator, globale Namensräume, Abkündigungen.
- Grid: Zeilen, Spalten, Größenarten.
- StackLayout: Stapel-Layouts, Abstände und Ausrichtung.
- CollectionView: Daten, Vorlagen, Auswahl und Leeransicht.
- Compiled bindings:
x:DataTypeund die Warnungen XC0022 bis XC0025. - .NET MAUI Shell navigation: Routen,
GoToAsync, Daten übergeben. - .NET MAUI Shell tabs: Tabs und ihr Aussehen.
- Style apps using XAML: implizite und explizite Stile, Vererbung.
- Respond to system theme changes:
AppThemeBindingundUserAppTheme. - Build accessible apps with semantic properties: Beschreibung, Hinweis und Überschriften.
- Fonts in .NET MAUI: Schriftgrößen und automatische Skalierung.