Tworzenie dostępnego i adaptacyjnego interfejsu

Teraz poproś asystenta o utworzenie strony. Nazwij żądane kontrolki WinUI, aby nie zastępowały kontrolki z innej platformy interfejsu użytkownika.

Poproś o kod XAML

Użyj tego monitu:

Create MainPage.xaml for TaskTally.

- Use a TextBox with a visible Header and an Add task Button.
- Use ListView for the task collection. Don't wrap it in ScrollViewer.
- In each row, use a CheckBox, task title, and delete Button.
- Use InfoBar for save and error status.
- Use x:Bind and specify Mode for every binding that changes.
- For TextBox.Text, use Mode=TwoWay and
  UpdateSourceTrigger=PropertyChanged.
- Add accessible names and tooltips for icon-only commands.
- Add an empty state.
- At widths below 640 effective pixels, move the Add task button
  below the TextBox and stretch it.
- Use theme resources instead of hard-coded colors.
- Build and fix all XAML compiler and analyzer warnings.

Sprawdź te szczegóły w wygenerowanych kodach XAML.

Wprowadzanie danych

<TextBox
    x:Name="NewTaskTextBox"
    Header="New task"
    PlaceholderText="For example, review the generated XAML"
    Text="{x:Bind ViewModel.NewTaskTitle, Mode=TwoWay,
                  UpdateSourceTrigger=PropertyChanged}" />
<Button
    x:Name="AddTaskButton"
    Grid.Column="1"
    VerticalAlignment="Bottom"
    AutomationProperties.Name="Add task"
    Command="{x:Bind ViewModel.AddTaskCommand}"
    Content="Add task" />

Widoczne Header oznacza, że dane wejściowe nie opierają się na tekście zastępczym jako jedynej etykiecie. UpdateSourceTrigger=PropertyChanged aktualizuje model widoku podczas wpisywania przez użytkownika, dzięki czemu polecenie może zostać natychmiast włączone.

Kolekcja zadań

Zdefiniuj typowany szablon danych:

<DataTemplate x:Key="TaskTemplate" x:DataType="models:TaskItem">
    <Grid Padding="8" ColumnSpacing="12">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="Auto" />
        </Grid.ColumnDefinitions>

        <CheckBox
            VerticalAlignment="Center"
            AutomationProperties.Name="{x:Bind Title}"
            Click="TaskCheckBox_Click"
            IsChecked="{x:Bind IsComplete, Mode=TwoWay}" />
        <TextBlock
            Grid.Column="1"
            VerticalAlignment="Center"
            Text="{x:Bind Title}" />
        <Button
            Grid.Column="2"
            VerticalAlignment="Center"
            AutomationProperties.Name="{x:Bind local:MainPage.DeleteTaskName(Title),
                                                Mode=OneWay}"
            Click="DeleteButton_Click"
            DataContext="{x:Bind}"
            ToolTipService.ToolTip="Delete task">
            <FontIcon Glyph="&#xE74D;" />
        </Button>
    </Grid>
</DataTemplate>

Użyj funkcji statycznej, aby uwzględnić tytuł zadania w nazwie automatyzacji przycisku usuwania, aby czytnik zawartości ekranu mógł odróżnić wiersze:

public static string DeleteTaskName(string title) => $"Delete {title}";

Następnie powiąż kolekcję:

<ListView
    AutomationProperties.Name="Tasks"
    ItemTemplate="{StaticResource TaskTemplate}"
    ItemsSource="{x:Bind ViewModel.Tasks, Mode=OneWay}"
    SelectionMode="None" />

ListView Zapewnia wirtualizację przewijania i interfejsu użytkownika. Opakowanie tego w ScrollViewer zakłóca oba.

Dowiedz się więcej: ListView i GridView oraz powiązanie danych za pomocą języka XAML.

Stan i pusty stan

Użyj InfoBar dla nieblokującego komunikatu o stanie:

<InfoBar
    IsClosable="True"
    IsOpen="{x:Bind ViewModel.IsStatusOpen, Mode=TwoWay}"
    Message="{x:Bind ViewModel.StatusMessage, Mode=OneWay}"
    Severity="{x:Bind local:MainPage.StatusSeverity(ViewModel.HasError),
                      Mode=OneWay}" />

Użyj funkcji statycznej, aby przekonwertować wartość logiczną stanu pustego na Visibility bez dodawania klasy konwertera:

public static Visibility BoolToVisibility(bool value) =>
    value ? Visibility.Visible : Visibility.Collapsed;

public static InfoBarSeverity StatusSeverity(bool hasError) =>
    hasError ? InfoBarSeverity.Error : InfoBarSeverity.Success;
<TextBlock
    HorizontalAlignment="Center"
    VerticalAlignment="Center"
    Text="Add a task to get started."
    Visibility="{x:Bind local:MainPage.BoolToVisibility(ViewModel.HasNoTasks),
                        Mode=OneWay}" />

Uruchom aplikację przed dodaniem danych i upewnij się, że pusty stan jest jasny, a przycisk Dodaj zadanie jest wyłączony:

Task Tally w pustym stanie, z nowym polem tekstowym do wprowadzania zadania, wyłączonym przyciskiem Dodaj zadanie oraz wskazówką, aby dodać pierwsze zadanie.

Układ adaptacyjny

Użyj stanów wizualnych, aby przenieść przycisk poniżej pola wprowadzania:

<VisualStateManager.VisualStateGroups>
    <VisualStateGroup>
        <VisualState x:Name="Narrow">
            <VisualState.StateTriggers>
                <AdaptiveTrigger MinWindowWidth="0" />
            </VisualState.StateTriggers>
            <VisualState.Setters>
                <Setter Target="AddTaskButton.(Grid.Row)" Value="1" />
                <Setter Target="AddTaskButton.(Grid.Column)" Value="0" />
                <Setter Target="AddTaskButton.HorizontalAlignment" Value="Stretch" />
            </VisualState.Setters>
        </VisualState>
        <VisualState x:Name="Standard">
            <VisualState.StateTriggers>
                <AdaptiveTrigger MinWindowWidth="640" />
            </VisualState.StateTriggers>
        </VisualState>
    </VisualStateGroup>
</VisualStateManager.VisualStateGroups>

Zmień rozmiar uruchomionej aplikacji w punkcie przerwania. Stwierdzenie w poleceniu, że układ jest responsywny, nie stanowi dowodu na to, że działa poprawnie.

Dowiedz się więcej: Dynamiczne układy za pomocą języka XAML.

Łączenie zdarzeń specyficznych dla widoku

Strona ładuje utrwalane dane raz i deleguje zmiany zadania do modelu wyświetlania:

public sealed partial class MainPage : Page
{
    public MainPageViewModel ViewModel { get; } = new();

    public MainPage()
    {
        InitializeComponent();
        Loaded += MainPage_Loaded;
    }

    private async void MainPage_Loaded(object sender, RoutedEventArgs e)
    {
        Loaded -= MainPage_Loaded;
        await ViewModel.InitializeAsync();
        NewTaskTextBox.Focus(FocusState.Programmatic);
    }

    private async void TaskCheckBox_Click(object sender, RoutedEventArgs e)
    {
        await ViewModel.TaskCompletionChangedAsync();
    }

    private async void DeleteButton_Click(object sender, RoutedEventArgs e)
    {
        if (sender is Button { DataContext: TaskItem task })
        {
            await ViewModel.DeleteTaskCommand.ExecuteAsync(task);
        }
    }
}

Procedury obsługi zdarzeń z typem zwracanym async void są odpowiednie w tym miejscu, ponieważ zdarzenia XAML wymagają void. Inne metody asynchroniczne w aplikacji zwracają wartość Task.

Budowanie i uruchamianie

Skompiluj aplikację, uruchom ją za pomocą pakietu i przetestuj każde kryterium akceptacji, które jest teraz zaimplementowane:

  1. Dodaj zadanie za pomocą myszy.
  2. Dodaj zadanie tylko za pomocą klawiatury.
  3. Ukończ i usuń zadanie.
  4. Zamknij i uruchom ponownie aplikację, aby zweryfikować trwałość.
  5. Zmień rozmiar okna poniżej i powyżej 640 pikseli efektywnych.
  6. Przełączaj się między jasnym i ciemnym motywem oraz motywem o wysokim kontraście.