引っ張って更新

引っ張って更新を使うと、ユーザーは、タッチ操作でデータのリストを下に引っ張ることで、より多くのデータを取得できるようになります。 引っ張って更新は、タッチ スクリーン機能があるデバイスで広く使用されています。 ここで示す API を使用すると、アプリで引っ張って更新を実装できます。

引っ張って更新 gif

これは適切なコントロールですか?

ユーザーが定期的に更新する可能性のあるデータの一覧またはグリッドがあり、アプリがタッチ優先デバイスで実行されている可能性がある場合は、引っ張って更新を使用します。

また、RefreshVisualizer を使用して、更新ボタンなどの他の方法で呼び出される一貫した更新エクスペリエンスを作成することもできます。

コントロールの更新

引っ張って更新は、2 つのコントロールによって有効になります。

  • RefreshContainer - 引っ張って更新エクスペリエンスのラッパーを提供する ContentControl。 タッチ操作を処理し、内部更新ビジュアライザーの状態を管理します。
  • RefreshVisualizer - 次のセクションで説明する更新の視覚化をカプセル化します。

メイン コントロールは RefreshContainer であり、ユーザーが更新をトリガーするために引っ張るコンテンツのラッパーとして配置します。 RefreshContainer はタッチでのみ機能するため、タッチ インターフェイスを持たないユーザーも更新ボタンを使用できるようにすることをお勧めします。 更新ボタンは、アプリ内の適切な場所 、コマンド バー、または更新されるサーフェスに近い場所に配置できます。

視覚化の更新

既定の更新視覚化は、更新がいつ行われるか、および更新が開始された後の更新の進行状況を伝えるために使用される循環型進行状況インジケータです。 更新ビジュアライザーには 5 つの状態があります。

更新を開始するためにユーザーがリストにプル ダウンする必要がある距離をしきい値と呼びます。 ビジュアライザーの状態は、このしきい値に関連するプル状態によって決まります。 指定できる値は、RefreshVisualizerState 列挙体に含まれています。

アイドル

ビジュアライザーの既定の状態は [アイドル] です。 ユーザーがタッチを介して RefreshContainer と対話していないので、進行中の更新はありません。

視覚的には、更新ビジュアライザーの証拠はありません。

対話

ユーザーが PullDirection プロパティで指定された方向にリストをプルし、しきい値に達する前に、ビジュアライザーが [対話] 状態になります。

  • ユーザーがこの状態の間にコントロールを解放すると、コントロールは [アイドル] 状態に戻ります。

    引っ張って更新の事前しきい値

    視覚的には、アイコンは無効 (60% の不透明度) として表示されます。 さらに、アイコンはスクロール アクションで 1 回転します。

  • ユーザーがしきい値を超えてリストをプルすると、ビジュアライザーは [対話] から [保留中] に切り替わります。

    しきい値での引っ張って更新

    視覚的には、アイコンは 100% の不透明度に切り替え、150% までのサイズでパルスを切り替え、遷移中に 100% のサイズに戻ります。

保留中

ユーザーがしきい値を超えてリストを引っ張ると、ビジュアライザーは、[保留中] 状態になります。

  • ユーザーがリストを解放せずにしきい値を超えてリストを戻すと、[対話] 状態に戻ります。
  • ユーザーがリストを解放すると、更新要求が開始され、[更新] 状態に遷移します。

引っ張って更新の事前しきい値

視覚的には、アイコンはサイズと不透明度の両方で 100% です。 この状態では、アイコンはスクロール アクションと共に下に移動し続けますが、回転しなくなります。

最新の情報に更新しています

ユーザーがしきい値を超えてビジュアライザーを解放すると、[更新] 状態になります。

この状態が入力されると、RefreshRequested イベントが発生します。 これは、アプリのコンテンツの更新を開始するための合図です。 イベント引数 (RefreshRequestedEventArgs) には Deferral オブジェクトが含まれています。このオブジェクトは、イベント ハンドラーでハンドルを受け取る必要があります。 その後、更新を実行するコードが完了したら、遅延を完了としてマークする必要があります。

更新が完了すると、ビジュアライザーは [アイドル] 状態に戻ります。

視覚的には、アイコンはしきい値の場所に戻り、更新中に回転します。 この回転は、更新の進行状況を示すために使用され、受信コンテンツのアニメーションに置き換えられます。

ピーク

ユーザーが更新が許可されていない開始位置から更新方向にプルすると、ビジュアライザーは [ピーク] 状態になります。 これは通常、ユーザーがプルを開始したときに ScrollViewer が 0 の位置にない場合に発生します。

  • ユーザーがこの状態の間にコントロールを解放すると、コントロールは [アイドル] 状態に戻ります。

プル方向

既定では、ユーザーはリストを上から下にプルして更新を開始します。 向きが異なるリストまたはグリッドがある場合は、更新コンテナーのプル方向を一致するように変更する必要があります。

PullDirection プロパティは、RefreshPullDirection 値であるBottomToTopTopToBottomRightToLeft またはLeftToRight のひとつを受け取ります。

プル方向を変更すると、ビジュアライザーの進行状況編集インジケーターの開始位置が自動的に回転し、矢印がプル方向の適切な位置から開始されます。 必要に応じて、RefreshVisualizer.Orientation プロパティを変更すると自動動作をオーバーライドできます。 ほとんどの場合、既定値の [自動] のままにしておくことをお勧めします。

UWP と WinUI 2

重要

この記事の情報と例は、Windows アプリ SDKWinUI 3 を使用するアプリ向けに最適化されていますが、一般に WinUI 2 を使用する UWP アプリに適用されます。 プラットフォーム固有の情報と例については、UWP API リファレンスを参照してください。

このセクションには、UWP または WinUI 2 アプリでコントロールを使用するために必要な情報が含まれています。

UWP アプリの更新コントロールは、WinUI 2 の一部として含まれています。 インストール手順などの詳細については、「WinUI 2」を参照してください。 このコントロールの API は、Windows.UI.Xaml.Controls (UWP) と Microsoft.UI.Xaml.Controls (WinUI) 名前空間の両方に存在します。

最新の WinUI 2 を使用して、すべてのコントロールの最新のスタイル、テンプレート、および機能を取得することをお勧めします。

WinUI 2 でこの記事のコードを使用するには、XAML のエイリアスを使って (ここでは muxc を使用)、プロジェクトに含まれる Windows UI ライブラリ API を表します。 詳細については、「WinUI 2 の概要」を参照してください。

xmlns:muxc="using:Microsoft.UI.Xaml.Controls"

<muxc:RefreshContainer />

引っ張って更新を実装する

WinUI 3 ギャラリー アプリには、ほとんどの WinUI 3 コントロールと機能の対話型の例が含まれています。 Microsoft Store からアプリを入手するか、GitHub でソース コードを取得します。

引っ張って更新機能を一覧に追加するには、いくつかの手順を実行する必要があります。

  1. リストを RefreshContainer コントロールでラップします。
  2. RefreshRequested イベントを処理してコンテンツを更新します。
  3. 必要に応じて、RequestRefresh を呼び出して更新を開始します (たとえば、ボタン クリックから)。

Note

RefreshVisualizer は単独でインスタンス化できます。 ただし、タッチ以外のシナリオでも、RefreshContainer でコンテンツをラップし、RefreshContainer.Visualizer プロパティが指定した RefreshVisualizer を使用することをお勧めします。 この記事では、ビジュアライザーは常に更新コンテナーから取得されることを前提としています。

さらに、便宜上、更新コンテナーの RequestRefresh メンバーと RefreshRequested メンバーを使用します。 refreshContainer.RequestRefresh()refreshContainer.Visualizer.RequestRefresh() に相当し、いずれかで RefreshContainer.RefreshRequested イベントと RefreshVisualizer.RefreshRequested イベントの両方が発生します。

更新を要求する

更新コンテナーは、タッチ操作を処理して、ユーザーがタッチを介してコンテンツを更新できるようにします。 更新ボタンや音声コントロールなど、タッチ以外のインターフェイスには他のアフォーダンスを提供することをお勧めします。

更新を開始するには、RequestRefresh メソッドを呼び出します。

// See the Examples section for the full code.
private void RefreshButtonClick(object sender, RoutedEventArgs e)
{
    RefreshContainer.RequestRefresh();
}

RequestRefresh を呼び出すと、ビジュアライザーの状態は [アイドル] 状態から [更新] 状態に直接戻ります。

更新要求を処理する

必要に応じた新しいコンテンツを取得するには、RefreshRequested イベントを処理します。 イベント ハンドラーでは、新しいコンテンツを取得するためにアプリに固有のコードが必要です。

イベント引数 (RefreshRequestedEventArgs) には Deferral オブジェクトが含まれています。 イベント ハンドラーで遅延へのハンドルを取得します。 その後、更新を実行するコードが完了したら、遅延を完了としてマークします。

// See the Examples section for the full code.
private async void RefreshContainer_RefreshRequested(RefreshContainer sender, RefreshRequestedEventArgs args)
{
    // Respond to a request by performing a refresh and using the deferral object.
    using (var RefreshCompletionDeferral = args.GetDeferral())
    {
        // Do some async operation to refresh the content

         await FetchAndInsertItemsAsync(3);

        // The 'using' statement ensures the deferral is marked as complete.
        // Otherwise, you'd call
        // RefreshCompletionDeferral.Complete();
        // RefreshCompletionDeferral.Dispose();
    }
}

状態の変更に対応する

必要に応じて、ビジュアライザーの状態の変化に対応できます。 たとえば、複数の更新要求を防ぐために、ビジュアライザーの更新中に更新ボタンを無効にすることができます。

// See the Examples section for the full code.
private void Visualizer_RefreshStateChanged(RefreshVisualizer sender, RefreshStateChangedEventArgs args)
{
    // Respond to visualizer state changes.
    // Disable the refresh button if the visualizer is refreshing.
    if (args.NewState == RefreshVisualizerState.Refreshing)
    {
        RefreshButton.IsEnabled = false;
    }
    else
    {
        RefreshButton.IsEnabled = true;
    }
}

RefreshContainer での ScrollViewer の使用

Note

RefreshContainer のコンテンツは、ScrollViewer、GridView、ListView などのスクロール可能なコントロールである必要があります。コンテンツを Grid などのコントロールに設定すると、未定義の動作が発生します。

この例では、スクロール ビューアーで引っ張って更新を使用する方法を示します。

<RefreshContainer>
    <ScrollViewer VerticalScrollMode="Enabled"
                  VerticalScrollBarVisibility="Auto"
                  HorizontalScrollBarVisibility="Auto">
 
        <!-- Scrollviewer content -->

    </ScrollViewer>
</RefreshContainer>

ListView に引っ張って更新を追加する

この例では、リスト ビューで引っ張って更新を使用する方法を示します。

<StackPanel Margin="0,40" Width="280">
    <CommandBar OverflowButtonVisibility="Collapsed">
        <AppBarButton x:Name="RefreshButton" Click="RefreshButtonClick"
                      Icon="Refresh" Label="Refresh"/>
        <CommandBar.Content>
            <TextBlock Text="List of items" 
                       Style="{StaticResource TitleTextBlockStyle}"
                       Margin="12,8"/>
        </CommandBar.Content>
    </CommandBar>

    <RefreshContainer x:Name="RefreshContainer">
        <ListView x:Name="ListView1" Height="400">
            <ListView.ItemTemplate>
                <DataTemplate x:DataType="local:ListItemData">
                    <Grid Height="80">
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="*" />
                        </Grid.RowDefinitions>
                        <TextBlock Text="{x:Bind Path=Header}"
                                   Style="{StaticResource SubtitleTextBlockStyle}"
                                   Grid.Row="0"/>
                        <TextBlock Text="{x:Bind Path=Date}"
                                   Style="{StaticResource CaptionTextBlockStyle}"
                                   Grid.Row="1"/>
                        <TextBlock Text="{x:Bind Path=Body}"
                                   Style="{StaticResource BodyTextBlockStyle}"
                                   Grid.Row="2"
                                   Margin="0,4,0,0" />
                    </Grid>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </RefreshContainer>
</StackPanel>
public sealed partial class MainPage : Page
{
    public ObservableCollection<ListItemData> Items { get; set; } 
        = new ObservableCollection<ListItemData>();

    public MainPage()
    {
        this.InitializeComponent();

        Loaded += MainPage_Loaded;
        ListView1.ItemsSource = Items;
    }

    private async void MainPage_Loaded(object sender, RoutedEventArgs e)
    {
        Loaded -= MainPage_Loaded;
        RefreshContainer.RefreshRequested += RefreshContainer_RefreshRequested;
        RefreshContainer.Visualizer.RefreshStateChanged += Visualizer_RefreshStateChanged;

        // Add some initial content to the list.
        await FetchAndInsertItemsAsync(2);
    }

    private void RefreshButtonClick(object sender, RoutedEventArgs e)
    {
        RefreshContainer.RequestRefresh();
    }

    private async void RefreshContainer_RefreshRequested(RefreshContainer sender, RefreshRequestedEventArgs args)
    {
        // Respond to a request by performing a refresh and using the deferral object.
        using (var RefreshCompletionDeferral = args.GetDeferral())
        {
            // Do some async operation to refresh the content

            await FetchAndInsertItemsAsync(3);

            // The 'using' statement ensures the deferral is marked as complete.
            // Otherwise, you'd call
            // RefreshCompletionDeferral.Complete();
            // RefreshCompletionDeferral.Dispose();
        }
    }

    private void Visualizer_RefreshStateChanged(RefreshVisualizer sender, RefreshStateChangedEventArgs args)
    {
        // Respond to visualizer state changes.
        // Disable the refresh button if the visualizer is refreshing.
        if (args.NewState == RefreshVisualizerState.Refreshing)
        {
            RefreshButton.IsEnabled = false;
        }
        else
        {
            RefreshButton.IsEnabled = true;
        }
    }

    // App specific code to get fresh data.
    private async Task FetchAndInsertItemsAsync(int updateCount)
    {
        for (int i = 0; i < updateCount; ++i)
        {
            // Simulate delay while we go fetch new items.
            await Task.Delay(1000);
            Items.Insert(0, GetNextItem());
        }
    }

    private ListItemData GetNextItem()
    {
        return new ListItemData()
        {
            Header = "Header " + DateTime.Now.Second.ToString(),
            Date = DateTime.Now.ToLongDateString(),
            Body = DateTime.Now.ToLongTimeString()
        };
    }
}

public class ListItemData
{
    public string Header { get; set; }
    public string Date { get; set; }
    public string Body { get; set; }
}

サンプル コードの入手