sh1’s diary

プログラミング、読んだ本、資格試験、ゲームとか私を記録するところ

WPF UI(UI ライブラリ)の使い方(2)Snackbar 編

WPFGUI ライブラリ「WPF UI」を使ってみる機会があったので、基本的な使い方をメモ。

シリーズの2つ目です。

今回の記事の内容は、公式のコンテンツに掲載がありません。内容に間違いがあるかもしれません。(SnackBar の使い方を説明する記事が全然ない……)

SnackBar

Snackbar は、アプリケーション内で完結する Toast のような機能です。スマートフォンゲームでは、なんらかのイベントやデイリークエストをクリアすると、ゲーム中にポップアップで「〇〇を達成しました」のような表示が出てきます。ああいうやつです。

SnackBar は WPF に(標準で用意されてい)ないコントロールなので、どういった使い方をするかについては、GoogleMaterial Design(の中にある SnackBar)の説明が参考になると思います。

ポイントになりそうなのはここ:

  • When to use snackbars(いつ、使用するのか)
    • Snackbars communicate messages that are minimally interruptive and don’t require user action.(SnackBar は、ユーザーによるアクションを必要としない、最小限の中断メッセージを伝達する)
  • Frequency(表示の頻度)
    • Only one snackbar may be displayed at a time.(ディスプレイ上には、ひとつだけ表示する)
  • Actions(アクション)
    • A snackbar can contain a single action. "Dismiss" or "Cancel" actions are optional.(SnackBar はひとつのアクションを含むことができます。dismiss or cancel アクションはオプションになります)

逆に Toast は v2 の Material Design の説明に少しだけ(SnackBar の比較として)説明があります。

Android also provides a Toast class with a similar API that can be used for displaying system-level notifications. Generally, snackbars are the preferred mechanism for displaying feedback messages to users, as they can be displayed in the context of the UI where the action occurred. Reserve Toast for cases where this cannot be done.

Android も(中略)システムレベルの通知を表示する際に使用することができる。一般的に、SnackBar は、ユーザーへのフィードバックメッセージを表示することに適したメカニズムです。Toast の利用は、SnackBar が使えないケースになります。

XAML の設定

SnackBar を利用するときは、基本となる XAML にコントロールを追加する必要があるみたいです。サンプルでは、MainWindow に追加する形がわかりやすいと思います。

<Window x:Class="Sample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
        xmlns:local="clr-namespace:Sample"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.Resources>
    </Window.Resources>
    <Grid Background="#FBFBFD">
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <ui:Snackbar Grid.Row="1" 
                            x:Name="RootSnackbar"
                            MaxWidth="600"
                            CloseButtonEnabled="False"
                            Timeout="5000"
                            Appearance="Secondary" />
    </Grid>
</Window>

(最適ではないけど)動作する最小サンプル

ポイントは、SnackbarService のインスタンスを作成してから SetSnackbarControl メソッドを通して XAML で用意したコントロール(RootSnackbar)を x:name で渡しています。

この設定は、本来だと初回に1回だけすればいいので、なんらか DI を使って SnackbarService を管理してもらうほうがよいです。(要 singleton 化)

public partial class MainWindow : Window
{
    private readonly ISnackbarService _snackbarService;

    public MainWindow()
    {
        _snackbarService = new SnackbarService();

        InitializeComponent();
    }

    private void CardAction_Click(object sender, RoutedEventArgs e)
    {
        _snackbarService.SetSnackbarControl(RootSnackbar);
        _snackbarService.Show("サンプルタイトル", "デイリークエストを達成しました。", SymbolRegular.Alert32, ControlAppearance.Primary);
    }
}

Singleton によるクラス管理も小さいプロジェクトだと手軽だと思いますが、そうでない場合は素直に DI でいいと思います。(Singleton クラスは大きいプロジェクトになると特にテスト面で不利になりやすい)

Prism の DI を使った例

App で SnackbarService のインスタンスを用意しておけば、サービスをプログラム全体で共有できます。

public partial class App
{
    protected override void RegisterTypes(IContainerRegistry containerRegistry)
    {
        // SnackBar サービスを DI に用意する
        containerRegistry.RegisterInstance<SnackbarService, "name">();
    }
}

次に MainWindow の ViewModel から SnackbarService を取得して、MainWindow に用意しているコントロールの初期化をします。

   public MainWindowViewModel(IUnityContainer container)
    {
        _SnackbarService = container.Resolve<SnackbarService>("name");
    }

Loaded など Window のコントロールの初期化が完了したタイミングで XAML 上の Snackbar コントロールをサービスに設定します。(これはプロジェクト全体で1度やれば OK です)

_snackbarService.SetSnackbarControl(RootSnackbar);

単純にコンストラクタの中で SetSnackbarControl を呼び出して設定できれば楽なのですが、コンストラクタが呼び出されるタイミングは、まだコントロールは未初期化状態のため null を返却します。何らかのやり方でコントロールの初期化完了後に設定する必要があります。

設定ができた状態になっていると、どの ViewModel からでも SnackbarService を DI から取得すれば、Snackbar を表示できるはずです。

_snackbarService.Show("サンプルタイトル", "デイリークエストを達成しました。", SymbolRegular.Alert32, ControlAppearance.Primary);

参考