MS_XAMLWriting2 - NetDevInfraWGinOSSConsortium/NetDevInfraWiki GitHub Wiki

XAMLの書き方(2)

概要

XAMLの書き方(1)の続き。

補足(本ページの位置付け): XAMLの書き方(1)
XAML の言語仕様と描画の仕組み(名前空間・構文・リソース・バインディング・
レイアウト・スタイル・テンプレート・トリガ)を扱ったのに対し、
本ページはアプリケーションを組み立てる側の話題を扱う。

【本ページの構成】
   ① ビルディング ブロック クラス
        Application / Window / ナビゲーション / Win32 ダイアログ
        → 【アプリの骨格】をどう作るか
   ② 入力支援
        メニュー・コマンド / ツールチップ / IME 制御
        → 【業務アプリで必ず要る】UI 部品
   ③ デザイナ向け機能
        シェイプ / グラデーション / トランスフォーム / アニメーション
        → 【WPF ならでは】のベクタ描画
   ④ MVVM デザイン パターン
   ⑤ バリデーション

ビルティング ブロック クラス

WPF / Silverlight は、Windows Forms とは
全く異なる UI サブシステムであるが、
類似のビルティング ブロック クラスが使用されているため、
よく似たコードでプログラムを記述することができる。

Applicationオブジェクト

WPF / Silverlight には Windows フォームと同様に
Application オブジェクトが存在する。

画面の起動

  • WPF では、Application オブジェクトの StartupUri 属性に
    起動時に呼び出される XAML を指定する。
  • Application オブジェクトはメッセージ ループを構築し、
    アプリケーションが終了するまでの間、ユーザにイベントを問い合わせる。
  • なお、この処理は、起動時に呼び出される XAML のパーシャル クラスに
    自動生成されるので通常は確認できない。
<Application x:Class="WpfApplication1.App"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  StartupUri="Window1.xaml">
  <Application.Resources></Application.Resources>
</Application>

補足(StartupUri を捨てる構成が主流になった): 原文の記述は今も有効だが、
実務では別の書き方が定着している

【StartupUri 方式の限界】
   ・起動画面を【XAML に文字列で書く】ため、
     条件分岐(ログイン画面 → メイン画面)ができない
   ・【DI コンテナ】から Window を解決できない
   ・起動前の初期化(設定読込・接続確認)を挟みにくい

【現在の定番】★
   App.xaml から StartupUri を【削除】し、
   OnStartup をオーバーライドする

     protected override void OnStartup(StartupEventArgs e) {
       base.OnStartup(e);
       var w = _serviceProvider.GetRequiredService<MainWindow>();
       w.Show();
     }

   → .NET 6 以降は【Generic Host】
     (Microsoft.Extensions.Hosting)と組み合わせ、
     設定・ログ・DI を ASP.NET Core と同じ形で扱うのが一般的
【ShutdownMode も併せて押さえる】★
   既定は OnLastWindowClose(最後のウィンドウを閉じたら終了)
     ・スプラッシュ画面 → メイン画面 の構成では
       【OnExplicitShutdown】にしないと
       スプラッシュを閉じた時点でアプリが落ちる

イベント

Application オブジェクトには様々なイベント処理を実装することができる。

  • 開始イベント
    WPF / Silverlight での共通初期化処理を実装する場合、
    Application.Startup イベントを使用する。

  • 共通エラー ハンドリング イベント
    WPF / Silverlight での共通エラー ハンドリング処理を実装する場合、
    Application.DispatcherUnhandledException イベントで例外をハンドルする
    (Open 棟梁の例外処理方式(OTR_ExceptionHandling.md)を参照)。

  • 終了イベント
    WPF / Silverlight での共通終了処理を実装する場合、
    Application.Exit イベントを使用する。

  • イベントの実装サンプル
    以下、それぞれのイベントの実装サンプルである。

    • XAML
<Application x:Class="WpfApplication1.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    StartupUri="Window1.xaml"
    Startup="Application_Startup" Exit="Application_Exit"
    DispatcherUnhandledException="Application_DispatcherUnhandledException">
    <Application.Resources></Application.Resources>
</Application>
  • コード ビハインド
private void Application_Startup(object sender, StartupEventArgs e) {
  System.Diagnostics.Debug.WriteLine("開始処理");
}

private void Application_DispatcherUnhandledException(
  object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e) {
  System.Diagnostics.Debug.WriteLine("エラー処理");
}

private void Application_Exit(object sender, ExitEventArgs e) {
  System.Diagnostics.Debug.WriteLine("終了処理");
}

補足(DispatcherUnhandledException だけでは足りない): 共通エラー
ハンドリングとしてこのイベントを挙げるのは正しいが、捕まえられる範囲が限定的である。

【WPF で仕掛けるべき「最後の砦」は 3 つ】★
 ① Application.DispatcherUnhandledException
      → 【UI スレッド上】の未処理例外
      → e.Handled = true で【アプリを落とさず継続できる】

 ② AppDomain.CurrentDomain.UnhandledException
      → 【UI スレッド以外】(Thread, Timer 等)の未処理例外
      → 【継続はできない】(ログを残して落ちるのみ)

 ③ TaskScheduler.UnobservedTaskException
      → 【await されなかった Task】の中の例外
      → GC 時に発火するため、【遅れて】来る
【async void の落とし穴】★
   async void なイベント ハンドラの中の例外は
   ① で捕まる(SynchronizationContext 経由で再送出されるため)

   ただし【async void なメソッドを自分で呼んだ】場合は
   どこにも捕まらず【プロセスが落ちる】ことがある
     → イベント ハンドラ以外で async void を書かない ★

Application.Propertiesプロパティ

  • WPF では ASP.NET などの Web アプリケーションと同様の Session 変数ライクな
    Key - Value ストアとして、Application.Properties プロパティを利用可能である。

  • 以下のサンプルで、任意の個所で(画面間を跨って)
    Application.Properties プロパティが取得できることを確認できる。

    • Application.xaml.cs
      • コード ビハインド
private void Application_Startup(object sender, StartupEventArgs e) {
  Application.Current.Properties["Key"] = "Value1";
}
  • Window1.xaml / Window1.xaml.cs
    • XAML
<Window x:Class="WpfApplication1.Window1"
  ・・・
  Loaded="Window_Loaded">
<Grid>
  <Button Click="Button_Click">ボタン</Button>
</Grid>
- コード ビハインド
private void Window_Loaded(object sender, RoutedEventArgs e) {
  MessageBox.Show((string)Application.Current.Properties["Key"]);
  Application.Current.Properties["Key"] = "Value2";
}

private void Button_Click(object sender, RoutedEventArgs e) {
  Window2 w2 = new Window2();
  w2.Show();
}
  • Window2.xaml.cs
    • コード ビハインド
private void Window_Loaded(object sender, RoutedEventArgs e) {
  MessageBox.Show((string)Application.Current.Properties["Key"]);
}

移行メモ(見出しの誤り): 移行元では、Application.xaml.cs と
Window1.xaml.cs の**コード ビハインドの見出しがいずれも「XAML」**と
なっていたため、内容に合わせて修正した
(Window1 側は XAML とコード ビハインドの 2 つに分けた)。

補足(Application.Properties は「使わない」が定石): 動作としては
原文の通りだが、現在は推奨されない

【問題点】
 ① 【型が object】 → 毎回キャストが要る
 ② 【キーが文字列】 → 打ち間違いがコンパイルで検出されない
 ③ 【グローバル変数そのもの】
      → どこから書かれたか追えない
      → 単体テストで差し替えられない
 ④ Session 変数の比喩は誤解を招く
      → Web の Session は【ユーザ単位】だが、
        これは【プロセス単位】の単なる静的辞書 ★
【代替】★
   ・アプリ設定の永続化 → Properties.Settings / appsettings.json
   ・画面間の値の受け渡し → コンストラクタ引数 / メッセンジャ
   ・アプリ全体で共有する状態
     → 【DI で注入するシングルトン サービス】
       (型安全・差し替え可能・テスト可能)

Window画面

ウィンドウの起動

WPF では Windows Forms と同様に Window 画面を、
以下のように起動できる(「XBAP」を除く)。

  • モードレス(通常画面)の起動
    モードレス(通常画面)として起動。
Window2 win2 = new Window2();
win2.Show();
  • モーダル ダイアログの起動
    モーダル ダイアログとして起動。
Window2 win2 = new Window2();
win2.Owner = this;
win2.ShowDialog();

補足(Owner の設定は「作法」ではなく必須に近い): 上の 2 つの違いは
ShowDialog() だけではなく、Owner の有無も重要である。

【Owner を設定すると起きること】★
   ・親の【上に必ず表示される】(裏に回り込まない)
   ・親を最小化すると【一緒に最小化される】
   ・親を閉じると【一緒に閉じる】
   ・WindowStartupLocation="CenterOwner" が効くようになる

【設定しないと】
   ・タスクバーに別項目として並ぶ
   ・Alt+Tab で親の裏に隠れ、【操作不能に見える】
     → 「画面が固まった」という問い合わせの典型原因 ★
【ShowDialog の戻り値】
   bool? (true / false / null)
     ・子側で this.DialogResult = true; とすると
       【その時点でウィンドウが閉じる】★
     ・× ボタンで閉じた場合は null

   var dlg = new Window2 { Owner = this };
   if (dlg.ShowDialog() == true) { /* OK された */ }

プロパティ

Window には、以下のようなプロパティを設定できる。

# プロパティ 説明
1 Title ウィンドウのタイトルを取得 / 設定する。
2 WindowStartupLocation ウィンドウ起動時の表示位置を取得 / 設定する。
3 ResizeMode ウィンドウのサイズ変更モードを取得 / 設定する(ResizeMode 列挙体)。
4 WindowStyle ウィンドウの境界線スタイルを取得 / 設定する。
5 ShowInTaskbar タスクバーのアイコンの表示・非表示を取得 / 設定する。

移行メモ(正誤・体裁): 移行元では Window. WindowStartUpLocation
Window. ResizeMode のようにクラス名とプロパティ名の間に空白が入り、
また WindowStartUpLocationU が大文字(正しくは WindowStartupLocation
となっていたため、修正した。
併せて、箇条書きを表に整理した。

補足(業務アプリで実際に触るのはこの辺り): 上表の 5 つに加え、
知らないと困るものを挙げておく。

【組み合わせの定石】
 ・【固定サイズのダイアログ】★
     ResizeMode="NoResize"
     WindowStartupLocation="CenterOwner"
     SizeToContent="WidthAndHeight"   ← 中身に合わせて自動サイズ

 ・【枠なしのカスタム ウィンドウ】
     WindowStyle="None" AllowsTransparency="True"
     → 自前でドラッグ移動・リサイズを実装する必要がある
     → AllowsTransparency="True" は
       【描画がソフトウェア レンダリングに落ちる】ことがあり重い ★

 ・【最大化サイズがおかしい】
     WindowState="Maximized" だと
     タスクバーに被る / 画面外にはみ出すことがある
     → MaxHeight を SystemParameters から算出するか、
       WindowChrome を使う
【DPI(高解像度ディスプレイ)】★
   .NET Framework 4.6.2 以降 / .NET Core 3.0 以降で
   【Per-Monitor DPI 対応】が入った
     → app.manifest の dpiAwareness 設定が必要
     → 未対応だと 4K ディスプレイで【ぼやける】

ナビゲーション フレームワーク

  • WPF / Silverlight では「ナビゲーション フレームワーク」により
    ASP.NET などの Web アプリケーションと同様に、
    WWW ブラウザの単一ウィンドウ、複数ページから構成される画面を提供できる。

  • この機能は、「ナビゲーション フレームワーク」の NavigationWindow / Page により提供され、
    ウィザードや、ハイパー テキスト ソリューションに有用である。

  • なお、以下のような機能もサポートされている。

    • WWW ブラウザ型の単一ウィンドウ、複数ページから構成される画面を提供
    • WWW ブラウザ同様、画面遷移の履歴が残り [進む]・[戻る] 操作に対応
    • ハイパーリンクによる画面遷移、HTTP コンテンツの参照をサポート
  • 以下、NavigationWindow / Page の使用方法を説明する。

NavigationWindow画面を作成

  • WPF の場合、初めに、NavigationWindow (を継承した)画面
    (ここでは NavWindow クラス)を作成する。

  • なお、NavigationWindow は、Window クラスを継承するため、
    前述のプロパティを設定可能である。

  • コード

    • XAML
<NavigationWindow x:Class="WpfApplication1.NavWindow"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Title="NavWindow" Height="300" Width="300">
</NavigationWindow>
  • コード ビハインド
public partial class NavWindow : NavigationWindow {
  public NavWindow() {
    InitializeComponent();
  }
}

NavigationWindow画面を起動

WPF で NavigationWindow 画面を起動する方法について説明する。

  • モードレス(通常画面)
    NavigationWindow 画面をモードレス(通常画面)として起動できる。
    なお、Application オブジェクトの StartupUri 属性から直接起動することもできる。
NavWindow normalWindow = new NavWindow();
normalWindow.Show();
  • モーダル ダイアログ
    NavigationWindow 画面をモーダル ダイアログとして起動することもできる。
NavWindow dialogWindow = new NavWindow(); 
dialogWindow.Owner = this; 
bool returnValue = dialogWindow.ShowDialog() ?? false;

Page画面のロード (1)

  • WPF で NavigationWindow 画面から、Page 画面をロードする方法について説明する。

  • なお、Page は FrameworkElement からの派生クラスであって、
    NavigationWindow・Frame コントロールでのみホスト・ナビゲーションができる。

  • Page 画面をロードする方法

    • Loaded イベントからロード
      NavigationWindow 画面のコード ビハインドの Loaded イベントから Page 画面をロードする。
      • XAML
<NavigationWindow x:Class="WpfApplication1.NavWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="NavWindow" Height="300" Width="300" Loaded="NavigationWindow_Loaded">
</NavigationWindow>
- コード ビハインド
public partial class NavWindow : NavigationWindow {
  public NavWindow() {
    InitializeComponent();
  }
  private void NavigationWindow_Loaded(object sender, RoutedEventArgs e) {
    // Page1 を表示します。
    this.Navigate(new Page1());
  }
}
  • Source プロパティからロード
    NavigationWindow 画面の Source プロパティに指定して Page 画面をロードすることもできる。
    • XAML
<NavigationWindow x:Class="WpfApplication1.NavWindow"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Title="NavWindow" Height="300" Width="300" Source="Page1.xaml">
</NavigationWindow>
  • Frame コントロールからロード
    Page 内を複数の Frame に分割し、その中に複数のページをロードすることもできる。
    • XAML
<StackPanel Orientation="Vertical">
  <Frame Source="Page2.xaml" Navigated="Frame1_Navigated"/>
  <Frame Source="Page3.xaml" Navigated="Frame2_Navigated"/>
</StackPanel>

Page画面のロード (2)

XBAP」、「Silverlight」では、
NavigationWindow / Window が使用出来ないため、
以下の方法で、直接 Page 画面をロードする。

移行メモ(正誤): 移行元では「WindowNavigation / Window が使用出来ない」と
記載されていたが、正しくは NavigationWindow である(誤記と判断し修正)。

  • XBAP
    Application オブジェクトの StartupUri 属性から直接 Page 画面をロードする。
<Application x:Class="WpfBrowserApplication1.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    StartupUri="Page1.xaml">
    <Application.Resources>
    </Application.Resources>
</Application>
  • Silverlight
    Application オブジェクトの StartupUri 属性がサポートされないので、
    Application.Startup イベントから Page 画面をロードする。
    • XAML
<Application xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
             x:Class="SilverlightApplication1.App">
    <Application.Resources> 
    </Application.Resources>
</Application>
  • コード ビハインド
public App(){
  this.Startup += this.Application_Startup;
  this.Exit += this.Application_Exit;
  this.UnhandledException += this.Application_UnhandledException;

  InitializeComponent();
}

private void Application_Startup(object sender, StartupEventArgs e) {
  // this.RootVisual = new MainPage(); デフォルトは、ユーザコントロール
  this.RootVisual = new Page1();
}

なお、「Silverlight」では、
デフォルトの初期画面(Application.RootVisual)が、ユーザ コントロールとなっているため、
通常、ユーザ コントロールに Frame を追加し、Frame でページ遷移させる方法を取る。

Page画面の画面遷移

WPF / Silverlight における Page 画面の画面遷移方法について説明する。

  • Hyperlink タグによる画面遷移
    • WPF の Page 画面では、Hyperlink タグ使用して、画面遷移できる。
      ※ なお、Hyperlink タグは、TextBlock タグなどで囲う必要がある。
<Hyperlink NavigateUri="Page2.xaml">次のページへ移動します。</Hyperlink>
<Hyperlink NavigateUri="Page1.xaml">前のページへ移動します。</Hyperlink>
  • Silverlight の Page 画面では、HyperlinkButton タグ使用して、画面遷移できる。
<HyperlinkButton NavigateUri="/Page2.xaml">次のページへ移動します。</HyperlinkButton>
<HyperlinkButton NavigateUri="/Page1.xaml">前のページへ移動します。</HyperlinkButton>
  • コードビハインドからの画面遷移
    コード ビハインドからの画面遷移も可能である。
    • WPF の場合は、Page 画面を直接生成可能である。
this.NavigationService.Navigate(new Page2());
  • Silverlight の場合は、Page 画面を Uri で指定する必要がある。
    • 以下は、相対パス指定:
this.NavigationService.Navigate(new Uri("/Page2.xaml", UriKind.Relative));
- 以下は、絶対パス指定:
this.NavigationService.Navigate(
     new Uri("pack://application:,,,/Page2.xaml", UriKind.Absolute));

Page画面の進む・戻る操作への対応

  • WPF / Silverlight では、WWW ブラウザ同様、
    進む・戻るボタンが常備されており、進む・戻る操作に対応している
    (Web アプリケーションと同様に、戻った際は、前ページの入力の状態は保持されている)。

  • XBAP」、「Silverlight」については、
    この進む・戻るボタンは、WWW ブラウザのものと統合されている(同じボタンを利用する)。

進む・戻るボタン

  • コードビハインドからの進む・戻る操作
    コードビハインドからは、次の処理で進む・戻る操作に対応できる。
    • 進む
this.NavigationService.GoForward();
  • 戻る
this.NavigationService.GoBack();
  • 履歴の管理
    • 進む・戻る操作が行えるということは、画面遷移の履歴を保持しているということである。
      • これを以下のコードで画面遷移の Page 履歴を消去できる。
this.NavigationService.RemoveBackEntry();
- Page 履歴の消去処理を自動化する場合は、\
  NavigationWindow の Navigated イベントで実行すると良い。
private void NavigationWindow_Navigated(object sender, NavigationEventArgs e) {
  this.NavigationService.RemoveBackEntry();
}
  • XBAP」、「Silverlight」では NavigationWindow が
    存在しないため、Frame の Navigated イベントで実行する。
    ※ Frame を使用しない場合は、Page の Loaded イベントなどで履歴を消去する。
private void Frame1_Navigated(object sender, NavigationEventArgs e) {
  if (NavigationService !=null) {
    if (NavigationService.CanGoBack) {
      this.NavigationService.RemoveBackEntry();
    }
  }
}

private void Frame2_Navigated(object sender, NavigationEventArgs e) {
  if (NavigationService != null) {
    if (NavigationService.CanGoBack) {
      this.NavigationService.RemoveBackEntry();
    }
  }
}
  • なお、Page 履歴の管理はブラック ボックスとなっているため、
    詳細は不明であるが、前 Page に戻った際に入力状態が保持されていることから、
    Page 履歴により、ある程度メモリが消費されているものと考える。
    メモリ消費量が拡大するような場合は Page 履歴の消去を検討する。

補足(「ブラック ボックス」の中身): 原文が「詳細は不明」とした箇所は、
仕組みが公開されているので補っておく。

【ジャーナル(Journal)】★
   ナビゲーション履歴は【ジャーナル】として保持される
     NavigationService.RemoveBackEntry()
     NavigationService.CanGoBack / CanGoForward

【何が保持されるか】
   ・Navigate(new Page1()) …【Page インスタンスそのもの】が保持される
       → 入力状態が残るのはこのため
       → 【メモリを食う】という原文の推測は【正しい】★
   ・Navigate(uri)         … Uri のみが保持され、
                             戻る際に【再生成】される
       → 入力状態は消える
       → JournalEntry.KeepAlive で挙動を制御できる

【Page.KeepAlive プロパティ】
   既定は false(Uri ナビゲーション時)
     true にすると【インスタンスが保持される】
     → メモリ消費と状態保持のトレードオフを明示的に選べる ★
【現在の位置付け】
   ナビゲーション フレームワークは【今も動く】が、
   新規の業務アプリでは
     ・【ContentControl + DataTemplate】で View を差し替える
     ・Prism / MVVM フレームワークの
       【Region ナビゲーション】を使う
   方が主流である(MVVM と噛み合わせやすいため)★

Page画面の進む・戻る操作の注意点

  • [新規画面遷移] → [戻る] → [進む] の一連の操作で、
    表示される Page の再初期化が発生するので、
    必要に応じてこれを考慮した実装とするか、[進む]・[戻る] 操作を抑止する必要がある。

  • 再初期化とは?
    コンストラクタや、Page の Loaded イベントの処理の実行

    • コンストラクタが実行されるのは、最初の Page のみである。
    • Loaded イベントはすべての Page で実行される。
  • [進む]・[戻る] 操作を抑止する。
    先頭 Page で Page.ShowsNavigationUI プロパティを false に設定にし、
    以降の Page の [進む]・[戻る] ボタンを非表示にするか、前述の履歴消去で対応する。

  • [進む]・[戻る] ボタンを非表示にする。

    • XAML
<Page x:Class="WpfApplication1.Page1"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Title="Page1" ShowsNavigationUI="false">
  • BackSpace キーによる [戻る] を抑止
    • [進む]・[戻る] ボタンを非表示にする場合も BackSpace キーで戻ることができるので、
      必要に応じて BackSpace キーを抑止する。
    • また、UI 要素にフォーカスの当たっていない状態では、
      BackSpace キーを抑止することができないので、
      Page.Loaded イベントなどで Page 上の入力 UI 要素にフォーカスを当てるようにする。
      • XAML
<Page x:Class="WpfApplication1.Page1"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Title="Page1" ShowsNavigationUI="false"
  Loaded="Page_Loaded" KeyDown="Page_KeyDown">
  <StackPanel>
    <TextBlock>
      <Hyperlink NavigateUri="Page2.xaml">Page2</Hyperlink>
    </TextBlock>
    <TextBox Name="textBox1"/>
  </StackPanel>
</Page>
- コード ビハインド
public partial class Page1 : Page {
  public Page1() {
    InitializeComponent();
  }
  private void Page_Loaded(object sender, RoutedEventArgs e) {
    this.textBox1.Focus();
  }
  private void Page_KeyDown(object sender, KeyEventArgs e) {
    if (e.Key == Key.Back) e.Handled = true;
  }
}

補足(KeyDown では抑止しきれないことがある): BackSpace 抑止は
Web アプリでも同じ問題があり、本質的に「戻り」を止める設計は難しい。

【KeyDown で漏れる理由】★
   ・KeyDown は【バブリング】イベント
     → 子コントロールが e.Handled = true にすると届かない
     → 【PreviewKeyDown】(トンネリング)で先に捕まえる方が確実

   ・原文の指摘どおり、【フォーカスがない】と Page に届かない
     → Focusable="True" + Focus() で受け皿を作る

【より確実な手】
   NavigationCommands.BrowseBack を無効化する
     <Page.CommandBindings>
       <CommandBinding Command="NavigationCommands.BrowseBack"
                       CanExecute="Never_CanExecute" />
     </Page.CommandBindings>
   → キーでもボタンでも【まとめて塞げる】★
【設計としての助言】
   「戻れると困る」画面(確定処理の後など)は、
   【戻ることを禁止する】のではなく
   【戻っても壊れない】ようにするのが本筋である
     → 二重登録は【サーバ側で冪等性キー】により防ぐ
     → 画面側の抑止は【補助】と位置付ける ★

その他

NavigationWindow / Page のその他のトピック

  • UriMapper クラス

    • UriMapper クラスは「Silverlight」専用のコントロールになるが、
      Hyperlink タグの NavigateUri 属性などで使用する Page.xaml へのパスを簡素化することができる。

    • UriMapper クラスは複数の UriMapping クラスから構成され、
      UriMapping クラスの MappedUri プロパティに実際の Page.xaml へのパス(のパターン)を指定、
      Uri プロパティに簡素化したパス(のパターン)を指定する。

    • 作成した UriMapper オブジェクトを Frame.UriMapper プロパティに
      追加することで、Frame 内で簡素化したパスを使用することができる。

  • ChildWindow コントロール

    • ChildWindow コントロールは、「Silverlight」専用のコントロールであるが、
      Page 上に、子ウィンドウ(モーダル ダイアログ相当)を表示できる。

    • Silverlight」アプリケーションのプロジェクトでは、
      ChildWindow のクラステンプレートが用意されており、
      これを新規作成し、以下のコードで ChildWindow を呼び出すことができる。

    • ChildWindow は、若干のアニメーションのエフェクトを伴い表示される。
      また、ChildWindow から、さらに子の ChildWindow を表示させることもできる。

ChildWindow1 childWindow = new ChildWindow1();
childWindow.Show();

移行メモ(リンク切れ / 廃止): 本節の UriMapper・UriMapping・ChildWindow は
いずれも Silverlight 専用であり、Silverlight 自体が
2021 年 10 月にサポート終了しているため、
Microsoft Learn 側に対応するページが存在しない(旧 MSDN のリンクも切れている)。
記録として原文の記述を残したが、新規開発での参照価値はない
上記の Learn へのリンクも、Silverlight 版ではなく WPF 版のものである。

Win32ダイアログ

  • WPF / Silverlight は、従来からの種々の Win32 ダイアログを
    サポートしている(一部サポートされないダイアログもある)。

  • このため、WPF / Silverlight アプリケーションであるからと言って、
    「外観や、操作方法の異なる標準ダイアログに慣れる必要がある」・・・といった問題は無い。

メッセージ ボックス

下記の MessageBox クラスの名前空間は、Windows Forms ではなく
WPF のものだが、
この MessageBox クラスのクラス階層を確認すると
WPF のアーキテクチャに基づいていないことが確認できる。

補足(「WPF のアーキテクチャに基づいていない」の意味): 鋭い指摘なので、
何が言いたいのかを明示しておく。

【WPF の通常のコントロールのクラス階層】
   DispatcherObject → DependencyObject → Visual
     → UIElement → FrameworkElement → Control

【MessageBox のクラス階層】★
   System.Object → MessageBox      ← 【これだけ】

 → MessageBox は【static メソッドの集まり】であり、
   内部で Win32 API の MessageBox() を呼んでいるだけ

【そこから来る実害】
 ・【スタイル・テンプレートが一切効かない】★
 ・DPI / テーマが WPF の見た目と揃わない
 ・【ボタンの文言を変えられない】(OS 言語に従う)
 ・単体テストで差し替えられない(static のため)
【実務での扱い】★
 ・MVVM では【IDialogService を挟む】
     → ViewModel は MessageBox.Show を直接呼ばない
     → テスト時はモックに差し替える
 ・見た目を揃えたいなら【自前の Window】か
   OSS のダイアログ(MahApps の MetroDialog 等)
 ・Windows 標準の新しい見た目が欲しいなら
   【TaskDialog】(Vista 以降、.NET 標準にはない)

コモン ダイアログ ボックス

  • OpenFileDialog クラス

    • OpenFileDialog は、部分信頼で実行されている。
    • XBAP」・「Silverlight」でもファイル名を取得できる。
  • SaveFileDialog クラス

    • SaveFileDialog は、部分信頼で実行されている。
    • XBAP」・「Silverlight」でもファイルを保存できる。
  • PrintDialog クラス

    • このクラスの名前空間は、Windows Forms ではなく WPF のものだが、
      クラス階層を確認すると WPF のアーキテクチャに基づいていないことが確認できる。
    • Silverlight では、PrintDocument クラス

移行メモ(正誤・体裁): 移行元では PrintDialog クラスの説明の階層が
1 段深く---)なっていたため、他の項目に揃えた。
また Printdocument の表記を PrintDocument に修正した。

補足(OpenFolderDialog が標準で入った): 長らく WPF には
「フォルダ選択ダイアログ」が標準で存在しなかったが、状況が変わっている。

【歴史】
 ・.NET Framework 時代
     → Microsoft.Win32 には【OpenFolderDialog がない】
     → System.Windows.Forms.FolderBrowserDialog を
       参照追加して使う(見た目が古い)か、
       Windows API Code Pack(非公式)を使うのが定番だった

 ・【.NET 8 で OpenFolderDialog が追加された】★
     var dlg = new Microsoft.Win32.OpenFolderDialog();
     if (dlg.ShowDialog() == true) { var p = dlg.FolderName; }
     → 現代的な IFileDialog ベースの見た目
【部分信頼について】★
   原文の「部分信頼で実行されている」は
   【XBAP / Silverlight のサンドボックス】を前提とした記述。
   .NET Framework 4.0 で【CAS(部分信頼)は非推奨】となり、
   .NET Core 以降は【部分信頼という概念自体がない】。
   → 現在のデスクトップ アプリでは常に完全信頼で動く

カスタム ダイアログ ボックス

  • 前述の「ウィンドウの起動」のモーダル ダイアログの起動を参照のこと。
  • XBAP」では使用できないので注意する。
  • なお、「Silverlight」の場合は、
    前述の ChildWindow コントロールで代替可能である。

参考

入力支援

入力支援機能に関する技術要素について説明する。

メニュー・タスクバーとコマンド

  • メニュー・タスクバーと「コマンド」の関連付けも、宣言的プロパティにより実装・カスタマイズ可能。
  • 例えば、以下の XAML でメニュー・タスクバーと「コマンド」の概要を理解することができ、
    また、メニュー・タスクバーと「コマンド」の関連付けが容易に実装可能であることが分かる。

移行メモ(用語): 移行元の見出しは「メニュー・タスクバーとコマンド」だが、
本文・サンプルで扱っているのは Windows のタスクバーではなく
**ツールバー(ToolBar / ToolBarTray)**である。
作者の見出しをそのまま残したが、読み替えて理解されたい。

実装

  • 画面

メニュー・タスクバーとコマンド

  • XAML
<Window.CommandBindings>
  <!--ApplicationCommands.Saveのカスタム動作(実行可否、イベント処理)を実装する-->
  <CommandBinding x:Name="SaveCommand"
    Command="ApplicationCommands.Save"
    CanExecute="SaveCommand_CanExecute"
    Executed="SaveCommand_Executed"/>
  <!--ApplicationCommands.Closeのカスタム動作(実行可否、イベント処理)を実装する-->
  <CommandBinding x:Name="CloseCommand"
    Command="ApplicationCommands.Close"
    CanExecute="CloseCommand_CanExecute"
    Executed="CloseCommand_Executed"/>
</Window.CommandBindings>

<DockPanel>
  <Menu DockPanel.Dock="Top">
    <MenuItem Header="ファイル(_F)">
      <!--ApplicationCommands.Save → カスタム動作 → CommandBinding-->
      <MenuItem Header="保存(_S)" Command="ApplicationCommands.Save">
        <MenuItem.Icon><Image Source=".\icons\save.png"/></MenuItem.Icon>
      </MenuItem>
      <Separator/>
      <!--ApplicationCommands.Close → カスタム動作 → CommandBinding-->
      <!--MenuItem Header="終了(_X)" Command="ApplicationCommands.Close"-->
      <!--ApplicationCommands.Close → カスタム動作 → カスタムイベント-->
      <MenuItem Name="Exit" Click="Exit_Click"  Header="終了(_X)">
        <MenuItem.Icon><Image Source=".\icons\exit.png"/></MenuItem.Icon>
      </MenuItem>
    </MenuItem>
    <MenuItem Header="編集(_E)">
      <!--ApplicationCommands.Undo → フォーカスのあるコントロール(RichTextBox)の既定の動作を実行-->
      <MenuItem Header="元に戻す(_U)" Command="ApplicationCommands.Undo">
        <MenuItem.Icon><Image Source=".\icons\undo.png"/></MenuItem.Icon>
      </MenuItem>
      <!--ApplicationCommands.Redo → フォーカスのあるコントロール(RichTextBox)の既定の動作を実行-->
      <MenuItem Header="やり直し(_R)" Command="ApplicationCommands.Redo">
        <MenuItem.Icon><Image Source=".\icons\redo.png"/></MenuItem.Icon>
      </MenuItem>
      <Separator/>
      <!--ApplicationCommands.Cut → フォーカスのあるコントロール(RichTextBox)の既定の動作を実行-->
      <MenuItem Header="切り取り(_T)" Command="ApplicationCommands.Cut">
        <MenuItem.Icon><Image Source=".\icons\cut.png"/></MenuItem.Icon>
      </MenuItem>
      <!--ApplicationCommands.Copy → フォーカスのあるコントロール(RichTextBox)の既定の動作を実行-->
      <MenuItem Header="コピー(_C)" Command="ApplicationCommands.Copy">
        <MenuItem.Icon><Image Source=".\icons\copy.png"/></MenuItem.Icon>
      </MenuItem>
      <!--ApplicationCommands.Paste → フォーカスのあるコントロール(RichTextBox)の既定の動作を実行-->
      <MenuItem Header="貼り付け(_P)" Command="ApplicationCommands.Paste">
        <MenuItem.Icon><Image Source=".\icons\paste.png"/></MenuItem.Icon>
      </MenuItem>
      <!--ApplicationCommands.Delete → フォーカスのあるコントロール(RichTextBox)の既定の動作を実行-->
      <MenuItem Header="削除(_D)" Command="ApplicationCommands.Delete">
        <MenuItem.Icon><Image Source=".\icons\delete.png"/></MenuItem.Icon>
      </MenuItem>
      <MenuItem Header="配置(_A)">
        <!--EditingCommands.AlignLeft → フォーカスのあるコントロール(RichTextBox)の既定の動作を実行-->
        <MenuItem Header="左揃え" Command="EditingCommands.AlignLeft">
          <MenuItem.Icon><Image Source=".\icons\text_align_left.png"/></MenuItem.Icon>
        </MenuItem>
        <!--EditingCommands.AlignCenter → フォーカスのあるコントロール(RichTextBox)の既定の動作を実行-->
        <MenuItem Header="中央揃え" Command="EditingCommands.AlignCenter">
          <MenuItem.Icon><Image Source=".\icons\text_align_center.png"/></MenuItem.Icon>
        </MenuItem>
        <!--EditingCommands.AlignRight → フォーカスのあるコントロール(RichTextBox)の既定の動作を実行-->
        <MenuItem Header="右揃え" Command="EditingCommands.AlignRight">
          <MenuItem.Icon><Image Source=".\icons\text_align_right.png"/></MenuItem.Icon>
        </MenuItem>
        <!--EditingCommands.AlignJustify → フォーカスのあるコントロール(RichTextBox)の既定の動作を実行-->
        <MenuItem Header="両端揃え" Command="EditingCommands.AlignJustify">
          <MenuItem.Icon><Image Source=".\icons\text_align_justify.png"/></MenuItem.Icon>
        </MenuItem>
      </MenuItem>
      <MenuItem Header="スタイル(_S)">
        <!--EditingCommands.ToggleBold → フォーカスのあるコントロール(RichTextBox)の既定の動作を実行-->
        <MenuItem Header="太字" Command="EditingCommands.ToggleBold">
          <MenuItem.Icon><Image Source=".\icons\text_bold.png"/></MenuItem.Icon>
        </MenuItem>
        <!--EditingCommands.ToggleItalic → フォーカスのあるコントロール(RichTextBox)の既定の動作を実行-->
        <MenuItem Header="斜体" Command="EditingCommands.ToggleItalic">
          <MenuItem.Icon><Image Source=".\icons\text_italic.png"/></MenuItem.Icon>
        </MenuItem>
        <!--EditingCommands.ToggleUnderline → フォーカスのあるコントロール(RichTextBox)の既定の動作を実行-->
        <MenuItem Header="下線" Command="EditingCommands.ToggleUnderline">
          <MenuItem.Icon><Image Source=".\icons\text_underline.png"/></MenuItem.Icon>
        </MenuItem>
        <!--EditingCommands.ToggleBullets → フォーカスのあるコントロール(RichTextBox)の既定の動作を実行-->
        <MenuItem Header="箇条書き" Command="EditingCommands.ToggleBullets">
          <MenuItem.Icon><Image Source=".\icons\text_list_bullets.png"/></MenuItem.Icon>
        </MenuItem>
        <!--EditingCommands.ToggleNumbering → フォーカスのあるコントロール(RichTextBox)の既定の動作を実行-->
        <MenuItem Header="段落番号" Command="EditingCommands.ToggleNumbering">
          <MenuItem.Icon><Image Source=".\icons\text_list_numbers.png"/></MenuItem.Icon>
        </MenuItem>
        <!--EditingCommands.IncreaseIndentation → フォーカスのあるコントロール(RichTextBox)の既定の動作を実行-->
        <MenuItem Header="インデントを増やす" Command="EditingCommands.IncreaseIndentation">
          <MenuItem.Icon><Image Source=".\icons\text_indent.png"/></MenuItem.Icon>
        </MenuItem>
        <!--EditingCommands.DecreaseIndentation → フォーカスのあるコントロール(RichTextBox)の既定の動作を実行-->
        <MenuItem Header="インデントを減らす" Command="EditingCommands.DecreaseIndentation">
          <MenuItem.Icon><Image Source=".\icons\text_indent_remove.png"/></MenuItem.Icon>
        </MenuItem>
      </MenuItem>
      <MenuItem Header="テキスト(_T)">
        <!--EditingCommands.IncreaseFontSize → フォーカスのあるコントロール(RichTextBox)の既定の動作を実行-->
        <MenuItem Header="フォントの拡大" Command="EditingCommands.IncreaseFontSize"/>
        <!--EditingCommands.DecreaseFontSize → フォーカスのあるコントロール(RichTextBox)の既定の動作を実行-->
        <MenuItem Header="フォントの縮小" Command="EditingCommands.DecreaseFontSize"/>
      </MenuItem>
    </MenuItem>
    <MenuItem Header="ヘルプ(_H)">
      <!--RoutedCommandをコードビハインドから自作しカスタム動作(実行可否、イベント処理)を実装する-->
      <MenuItem Header="バージョン情報(_A)" Name="About">
        <MenuItem.Icon><Image Source=".\icons\about.png"/></MenuItem.Icon>
      </MenuItem>
    </MenuItem>
  </Menu>
  <ToolBarTray DockPanel.Dock="Top">
    <ToolBar Header="編集:">
      <Button ToolTip="元に戻す" Command="ApplicationCommands.Undo">
        <Image Source=".\icons\undo.png" Stretch="Fill"/></Button>
      <Button ToolTip="やり直し" Command="ApplicationCommands.Redo">
        <Image Source=".\icons\redo.png" Stretch="Fill"/></Button>
      <Button ToolTip="切り取り" Command="ApplicationCommands.Cut">
        <Image Source=".\icons\cut.png" Stretch="Fill"/></Button>
      <Button ToolTip="コピー" Command="ApplicationCommands.Copy">
        <Image Source=".\icons\copy.png" Stretch="Fill"/></Button>
      <Button ToolTip="貼り付け" Command="ApplicationCommands.Paste">
        <Image Source=".\icons\paste.png" Stretch="Fill"/></Button>
      <Button ToolTip="削除" Command="ApplicationCommands.Delete">
        <Image Source=".\icons\delete.png" Stretch="Fill"/>
      </Button>
    </ToolBar>
    <ToolBar Header="配置:">
      <Button ToolTip="左揃え" Command="EditingCommands.AlignLeft">
        <Image Source=".\icons\text_align_left.png" Stretch="Fill"/></Button>
      <Button ToolTip="中央揃え" Command="EditingCommands.AlignCenter">
        <Image Source=".\icons\text_align_center.png" Stretch="Fill"/></Button>
      <Button ToolTip="右揃え" Command="EditingCommands.AlignRight">
        <Image Source=".\icons\text_align_right.png" Stretch="Fill"/></Button>
      <Button ToolTip="両端揃え" Command="EditingCommands.AlignJustify">
        <Image Source=".\icons\text_align_justify.png" Stretch="Fill"/></Button>
    </ToolBar>
    <ToolBar Header="スタイル:">
      <Button ToolTip="太字" Command="EditingCommands.ToggleBold">
        <Image Source=".\icons\text_bold.png" Stretch="Fill"/></Button>
      <Button ToolTip="斜体" Command="EditingCommands.ToggleItalic">
        <Image Source=".\icons\text_italic.png" Stretch="Fill"/></Button>
      <Button ToolTip="下線" Command="EditingCommands.ToggleUnderline">
        <Image Source=".\icons\text_underline.png" Stretch="Fill"/></Button>
      <Button ToolTip="箇条書き" Command="EditingCommands.ToggleBullets">
        <Image Source=".\icons\text_list_bullets.png" Stretch="Fill"/></Button>
      <Button ToolTip="段落番号" Command="EditingCommands.ToggleNumbering">
        <Image Source=".\icons\text_list_numbers.png" Stretch="fill"/></Button>
      <Button ToolTip="インデントを増やす" Command="EditingCommands.IncreaseIndentation">
        <Image Source=".\icons\text_indent.png" Stretch="Fill"/></Button>
      <Button ToolTip="インデントを減らす" Command="EditingCommands.DecreaseIndentation">
        <Image Source=".\icons\text_indent_remove.png" Stretch="Fill"/></Button>
    </ToolBar>
  </ToolBarTray>
  <RichTextBox DockPanel.Dock="Top" Width="Auto" Height="100"
    IsDocumentEnabled="True" AcceptsTab="True" FontSize="24"/>
  <RichTextBox DockPanel.Dock="Top" Width="Auto" Height="100"
    IsDocumentEnabled="True" AcceptsTab="True" FontSize="24"/>
</DockPanel>
  • コード ビハインド
/// <summary>Window1.xaml の相互作用ロジック</summary>
public partial class Window1 : Window {
  public Window1() {
    InitializeComponent();

    // UIElementにRoutedCommandをバインドし、メニューに関連付ける。
    InitRoutedCommands_about();
  }

  // AboutCommand(カスタムのRoutedCommand)
  public static readonly RoutedCommand AboutCommand = new RoutedCommand();

  private void InitRoutedCommands_about() {

    // CommandBindingの生成
    CommandBinding binding = 
      new CommandBinding(AboutCommand, AboutCommand_Executed, AboutCommand_CanExecute);

    // UIElementにRoutedCommandをバインド
    this.CommandBindings.Add(binding);

    // メニューに関連付け
    About.Command = AboutCommand;
  }

  // 【情報メニュー+コマンド】UIElementにRoutedCommandをバインド:カスタム動作(実行可否)
  private void AboutCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e) { 
    e.CanExecute = true;
  }
  // 【情報メニュー+コマンド】UIElementにRoutedCommandをバインド:カスタム動作(イベント処理)
  private void AboutCommand_Executed(object sender, ExecutedRoutedEventArgs e) {
    // 情報表示処理
  }

  // 【保存メニュー+コマンド】UIElementにRoutedCommandをバインド:カスタム動作(実行可否)
  private void SaveCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e) { 
    e.CanExecute = true;
  }
  // 【保存メニュー+コマンド】UIElementにRoutedCommandをバインド:カスタム動作(イベント処理)
  private void SaveCommand_Executed(object sender, ExecutedRoutedEventArgs e) {
    // 保存処理
  }

  // 【終了メニュー+コマンド】UIElementにRoutedCommandをバインド:カスタム動作(実行可否)
  private void CloseCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e) { 
    e.CanExecute = true;
  }
  // 【終了メニュー+コマンド】UIElementにRoutedCommandをバインド:カスタム動作(イベント処理)
  private void CloseCommand_Executed(object sender, ExecutedRoutedEventArgs e) { 
    this.Close();
  }

  // 【終了メニュー】メニューのクリックイベントのみの実装で済ます場合。
  private void Exit_Click(object sender, RoutedEventArgs e) { 
    this.Close();
  }
}

移行メモ(正誤): 移行元では AboutCommand_Executed のコメントが
AboutCommand_CanExecute と同じ「カスタム動作(実行可否)」と
なっていたが、Executed 側はイベント処理であるため
(他の Save / Close 側の記述に揃えて)修正した。

説明

  • メニュー・タスクバーと「コマンド」の関連付け
    • MenuItem

      • MenuItem(MenuItem クラス)は入れ子が可能であり、
      • MenuItem のアクセスキーは、MenuItem クラスの Header 属性に指定する文字列中に
        _+英数字」と設定することで(_ の直後の文字をアクセスキーとして)容易に設定可能
      • また、MenuItem からは Command 属性を指定して指定の「Command」を発生させることができる。
    • ToolBarTray

      • ToolBarTray には ToolBar を格納でき、ToolBar には Button を格納できる。
      • Button からは指定の「Command」を発生させることができる。
      • ToolBarTray 内の ToolBar は、実行時に D&D にて配置を変更できる。
    • Command

      • これらのコントロールから発生させた「Command」は、フォーカスのあるコントロールに受信されるが、
      • CommandTarget="{Binding ElementName=要素名}" という記述により、
        対象のコントロールを明示することもできる。
      • 対象のコントロールは、各種 RoutedCommand クラスの CanExecute、Execute メソッドが
        発生させるイベントを処理するイベント ハンドラを実装している必要がある。

移行メモ(正誤): 移行元では

  • MenuItem のアクセスキーが「Header 属性に指定する文字列中に**「英数字」**と設定することで
    英数字の先頭文字をアクセスキーとして)」と記載されていたが、
    実際にはサンプルのとおり アンダースコア(_)の直後の文字がアクセスキーになるため修正した。
  • 「ToolBarTray には**、を格納でき**」と目的語が欠落していたため、
    文脈から「ToolBar を格納でき」と補った。
  • 標準コマンド

    • 標準コマンド(ApplicationCommands, EditingCommands, etc.)に対応した処理を
      実装しているコントロールには、

      • 「コマンド」に対応する処理の UOC は不要であり、既定の処理が実行される。
      • また、「コマンド」には、既定のショートカットも割り当てられている。
    • コントロールの実装する標準コマンド
      (上記サンプルでは、ApplicationCommands.Save・Close)の処理をカスタマイズしたい場合、

      • 「コマンド」に対応したイベント ハンドラの UOC を UIElement
        (上記サンプルでは、Window)に定義・実装する必要がある。

      • 標準コマンドのカスタム処理の定義・実装には、標準コマンド(ApplicationCommands)を
        UIElement の CommandBindings プロパティに追加し、
        ApplicationCommands.CanExecute、Execute が発生させるカスタム イベントを処理する
        イベント ハンドラの UOC を UIElement 上に実装する必要がある。

      • また、上記の方式は、カスタム コマンド(RoutedCommand)に対応した
        イベント ハンドラの UOC を UIElement に定義・実装する場合にも利用できる。

    • その他にも、以下の標準コマンドが System.Windows.Input 名前空間に用意されている。

      • ComponentCommands
      • MediaCommands
      • NavigationCommands

補足(RoutedCommand と MVVM の ICommand は別物): 本節の
RoutedCommandWPF 独自の「ルーティングされるコマンド」であり、
MVVM で使う ICommand の実装(RelayCommand 等)とは性格が異なる

RoutedCommand RelayCommand(ICommand の自作実装)
処理の置き場所 CommandBinding(View 側) ViewModel 側
探し方 Visual Tree を遡って CommandBinding を探す バインドされた ViewModel を直接呼ぶ
実行対象 フォーカスのある要素に依存する 依存しない
実行可否 CanExecute イベント CanExecute デリゲート
用途 編集系の標準コマンド(Cut / Copy / Undo)★ 業務ロジックの起動
【なぜ標準コマンドが「何も書かずに動く」のか】★
   TextBox / RichTextBox は
   自分の CommandBindings に
   Cut / Copy / Paste / Undo の実装を【最初から持っている】

   メニューの Command="ApplicationCommands.Cut" は
     → フォーカスのある要素から Visual Tree を遡り、
     → 最初に見つかった CommandBinding を実行する
     → だから【フォーカスのある TextBox】が切り取られる

 ※ 逆に言うと、フォーカスが外れると
   メニューが【勝手にグレーアウトする】のもこの仕組みのため
【CanExecute が呼ばれるタイミング】★
   CommandManager.RequerySuggested により
   【フォーカス移動・キー入力・マウス操作のたび】に再評価される
     → CanExecute に【重い処理を書いてはいけない】
     → 逆に、任意のタイミングで再評価させたいときは
       CommandManager.InvalidateRequerySuggested()
【実務での使い分け】
   ・テキスト編集系(Cut/Copy/Paste/Undo)→ 標準コマンドをそのまま ★
   ・保存・検索・実行などの業務操作
     → ViewModel の ICommand にバインドする ★
       <MenuItem Header="保存" Command="{Binding SaveCommand}" />
     → CommunityToolkit.Mvvm の [RelayCommand] で自動生成できる

参考

ツールチップ

  • WPF では、「ツールチップ」を任意の UI コントロールに設定可能である。
  • これには FrameworkElement、FrameworkContentElement に ToolTip プロパティが
    実装されているためである。

実装

  • 画面

ツールチップ

  • XAML
    • UserControl
<UserControl x:Class="WpfApplication1.UserControl1"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Width="250" Height="120">
  <StackPanel>
    <TextBlock Margin="4,4,4,4" Text="タイトル"
      TextWrapping="Wrap" FontWeight="Bold" FontSize="14"/>
    <Path Width="Auto" Height="1" 
      Fill="Black" Stretch="Fill" 
      Stroke="Black" Data="M20,45 L250,45" 
      Margin="4,4,4,4"/>
    <TextBlock Margin="4,4,4,4" Text="説明文" 
      TextWrapping="Wrap"/>
  </StackPanel>
</UserControl>
  • Window
<Window x:Class="WpfApplication1.Window1"
  ・・・
    xmlns:my="clr-namespace:WpfApplication1"
  ・・・>

  ・・・

<RichTextBox DockPanel.Dock="Top" Width="Auto" Height="100" 
             IsDocumentEnabled="True" AcceptsTab="True" FontSize="24">
  <RichTextBox.ToolTip>
    <ToolTip>
      <my:UserControl1/>
    </ToolTip>
  </RichTextBox.ToolTip>
</RichTextBox>

説明

この ToolTip プロパティには、ToolTip 属性に直接文字列で説明文を記述することも可能であるが、
「プロパティ要素構文」を使用して、説明文などの文字列だけではなく、
(image などの)任意の UI 要素も設定可能である。
例えば、上記は、ユーザ コントロールを使用して ToolTip を表示した例である。

移行メモ(体裁): 移行元では、この「説明」節が「実装」節のにあるにも
かかわらず「以下は、ユーザ コントロールを使用して…」と
前方参照になっていたため、「上記は」に改めた。

参考

移行メモ(リンクの誤り): 移行元では FrameworkElement.ToolTip プロパティ
URL が EditingCommands クラスのものになっていた
(前節からのコピー時の誤りと思われる)ため、正しい URL に修正した。

補足(ツールチップで実際に困るのはここ): 実装は簡単だが、
既定の挙動が業務アプリに合わないことが多い

【よく変更する設定(ToolTipService の添付プロパティ)】★
   ToolTipService.InitialShowDelay  … 表示までの待ち時間(既定 1 秒)
   ToolTipService.ShowDuration      … 表示し続ける時間(既定 5 秒)★
   ToolTipService.BetweenShowDelay  … 連続表示時の待ち無し時間
   ToolTipService.Placement         … 表示位置
   ToolTipService.ShowOnDisabled    … 【無効なコントロールでも出す】★

   <Button IsEnabled="False"
           ToolTipService.ShowOnDisabled="True"
           ToolTip="権限がないため実行できません" />

   → 「なぜ押せないか」を伝えられる。
     既定では【無効なコントロールにツールチップが出ない】ため、
     これを知らないと理由を伝える手段がなくなる
【ToolTip の中身がバインドできない】★
   ToolTip の中身は【Visual Tree の外】(Popup)にあるため、
   親の DataContext が【継承されない】ことがある
     → PlacementTarget 経由で辿る

     <ToolTip DataContext="{Binding PlacementTarget.DataContext,
                            RelativeSource={RelativeSource Self}}">
【エラー表示との組み合わせ】
   バリデーション エラーの内容を
   ツールチップに出すのが WPF の定番
     ToolTip="{Binding (Validation.Errors)[0].ErrorContent,
               RelativeSource={RelativeSource Self}}"
   → 後述の「バリデーション」を参照

IME制御

  • WPF / Silverlight では、InputMethod クラスを利用することで IME 制御が可能である
    (XAML から直接指定することも可能)。

  • ただし、

    • IME2003 以外では、IME のオン・オフの切り替え(ひらがな ⇔ 直接入力)はできるが、
    • 入力モードの切り替え(カタカナ ⇔ 全角英字など)はできないので注意が必要。
  • 切り替え方法

    • IME オン・オフ
      • InputMethod.SetIsInputMethodEnabled メソッド
      • InputMethod.Current.ImeState プロパティ
    • 入力モード
      • InputMethod.SetPreferredImeConversionMode メソッド
      • InputMethod.Current.ImeConversionMode プロパティ

補足(Windows FormsImeMode との比較は別ページ):
UI サブシステム横断での IME 制御の考え方は
IME制御」を参照のこと。
なお、上記の「IME2003 以外では入力モードの切り替えができない」という制約は
当時の Windows XP + Office IME 2003 環境での事象であり、
現在の Microsoft IME では状況が異なる(下記参照)。

実装

以下は、IME 制御のサンプルである。

  • 画面

IME制御

  • XAML
    イベント ハンドラで IME 制御する方法と、XAML で IME 制御する方法がある。

    • イベント ハンドラで IME 制御
<StackPanel Orientation="Vertical">
  <StackPanel Orientation="Horizontal">
    <TextBlock Height="23" Width="80" Text="制御なし:"/>
    <TextBox Name="textBox0" Height="23" Width="120"/>
  </StackPanel>
  <StackPanel Orientation="Horizontal">
    <TextBlock Height="23" Text="以下、イベントで制御"/>
  </StackPanel>
  <StackPanel Orientation="Horizontal">
    <TextBlock Height="23" Width="80" Text="OFF:"/>
    <TextBox Name="textBox1" Height="23" Width="120"
      GotFocus="textBox1_GotFocus" LostFocus="textBox1_LostFocus"/>
  </StackPanel>
  <StackPanel Orientation="Horizontal">
    <TextBlock Height="23" Width="80" Text="ON:"/>
    <TextBox Name="textBox2" Height="23" Width="120"
      GotFocus="textBox2_GotFocus" LostFocus="textBox2_LostFocus"/>
  </StackPanel>
  <StackPanel Orientation="Horizontal">
    <TextBlock Height="23" Width="80" Text="全角片仮名:"/>
    <TextBox Name="textBox3" Height="23" Width="120"
      GotFocus="textBox3_GotFocus" LostFocus="textBox3_LostFocus"/>
  </StackPanel>
  • XAML で IME 制御
  <StackPanel Orientation="Horizontal">
    <TextBlock Height="23" Text="以下、XAMLで制御"/>
  </StackPanel>
  <StackPanel Orientation="Horizontal">
    <TextBlock Height="23" Width="80" Text="OFF:"/>
    <TextBox Name="textBox4" Height="23" Width="120"
      InputMethod.PreferredImeState="Off"/>
  </StackPanel>
  <StackPanel Orientation="Horizontal">
    <TextBlock Height="23" Width="80" Text="ON:"/>
    <TextBox Name="textBox5" Height="23" Width="120"
      InputMethod.PreferredImeState="On"/>
  </StackPanel>
  <StackPanel Orientation="Horizontal">
    <TextBlock Height="23" Width="80" Text="半角片仮名:"/>
    <TextBox Name="textBox6" Height="23" Width="120"
      InputMethod.PreferredImeState="On" InputMethod.PreferredImeConversionMode="Native,Fixed,Katakana"/>
  </StackPanel>
</StackPanel>
  • コード ビハインド
    下記はイベント ハンドラによる IME 制御である。
/// <summary>Window1.xaml の相互作用ロジック</summary>
public partial class Window1 : Window {
  public Window1() {
    InitializeComponent();
  }

  InputMethodState ims = InputMethodState.DoNotCare;
  ImeConversionModeValues imc = ImeConversionModeValues.DoNotCare;

  private void textBox1_GotFocus(object sender, RoutedEventArgs e) {
    ims = InputMethod.Current.ImeState;
    InputMethod.Current.ImeState = InputMethodState.Off;
  }
  private void textBox1_LostFocus(object sender, RoutedEventArgs e) {
    InputMethod.Current.ImeState = ims;
  }

  private void textBox2_GotFocus(object sender, RoutedEventArgs e) {
    ims = InputMethod.Current.ImeState;
     InputMethod.Current.ImeState = InputMethodState.On;
  }
  private void textBox2_LostFocus(object sender, RoutedEventArgs e) {
    InputMethod.Current.ImeState = ims;
  }

  private void textBox3_GotFocus(object sender, RoutedEventArgs e) {
    ims = InputMethod.Current.ImeState;
    imc = InputMethod.Current.ImeConversionMode;

    InputMethod.Current.ImeState = InputMethodState.On;
    InputMethod.Current.ImeConversionMode = ImeConversionModeValues.Native
      | ImeConversionModeValues.FullShape | ImeConversionModeValues.Katakana;
  }
  private void textBox3_LostFocus(object sender, RoutedEventArgs e) {
    InputMethod.Current.ImeState = ims;
    InputMethod.Current.ImeConversionMode = imc;
  }
}

移行メモ(正誤): XAML 側の textBox6 は「半角片仮名」というラベルだが、
指定は Native,Fixed,Katakana であり、FullShape / Fixed の別で言えば
全角・半角を決めるのは FullShape(全角)の有無である
Fixed は「固定入力」を意味し、全角・半角の別ではない)。
移行元の記述をそのまま残したが、半角片仮名を意図するなら
FullShape を含めない指定
が必要である。

説明

イベント ハンドラでの IME 制御の動作は、
以下の点が、XAML による IME 制御の場合と異なる。

  • GotFocus、LostFocus イベントを扱っており、
    GotFocus イベントで現状の IME 設定を保存し、
    LostFocus イベントで IME 設定を復元している。

  • このため、入力時の IME 設定変更は LostFocus イベントで無効になる。

参考

移行メモ(リンク切れ): 上記の Microsoft Connect は
2018 年にサービス終了しており、リンクは到達しない
記録として残す。

補足(今の推奨と、当時からの変化): 本節の記述は
Windows XP + Office IME 2003 時代のものである。現在の状況を補っておく。

【① イベント ハンドラ方式は使わない】★
   InputMethod.Current は【アプリ全体で共有される】ため、
   GotFocus / LostFocus で保存・復元する実装は
     ・フォーカス遷移が入れ子になると【値が壊れる】
     ・非同期処理が挟まると復元されない

   → 【XAML の添付プロパティ方式】を使う ★
       InputMethod.PreferredImeState="On"
       InputMethod.PreferredImeConversionMode="Native,FullShape"
     → フォーカスの出入りは WPF が面倒を見る

   ※ 完全に IME を禁じたいなら
      InputMethod.IsInputMethodEnabled="False"
      (数値・コード入力欄で有効)★
【② 入力モードの切り替えは「今は効く」】
   原文の「IME2003 以外では入力モードの切り替えができない」は
   【当時の制約】であり、
   現在の Microsoft IME(Windows 10 / 11)では
   PreferredImeConversionMode は【概ね機能する】★

   ただし、
     ・【Google 日本語入力】等のサードパーティ IME では
       無視されることがある
     ・Windows 10 1809 以降の【新しい Microsoft IME】は
       挙動が変わった時期があり、
       バージョン差で結果が揺れる
   → 【対象環境で必ず実機確認する】ことが前提 ★
【③ ImeConversionModeValues の組み合わせ】
   Native      … かな入力(日本語入力オン)
   Alphanumeric… 英数
   Katakana    … 片仮名
   FullShape   … 【全角】★(付けなければ半角)
   Roman       … ローマ字入力
   Fixed       … 固定(ユーザによる変更を抑止)

   全角カタカナ → "Native,FullShape,Katakana"
   半角カタカナ → "Native,Katakana"
   全角英数     → "Alphanumeric,FullShape"

デザイナ向け機能

デザイナ向けの技術要素について説明する。

様々なシェイプ

  • WPFのアーキテクチャ の「クラス階層」の Visual クラスで説明したように、
    WPF / Silverlight では、Shape クラスから派生する
    様々なベクタ グラフィックスを記述できる。

  • Shape クラスから派生するベクタ グラフィックス描画クラスには、次のものがある。

# クラス 説明
1 Rectangle クラス 四角形を描画
2 Ellipse クラス 楕円を描画
3 Line クラス 直線を描画
4 Polyline クラス 一連の直線を描画
5 Path クラス 一連の直線と曲線を描画

移行メモ(体裁): 箇条書きを表に整理した。

補足(Shape の一覧はもう少しある): 原文の 5 つに加え、
**Polygon(閉じた多角形)**がある。
Polyline が「開いた折れ線」なのに対し、Polygon は始点と終点が自動的に閉じる。

【Shape の描画コストに注意】★
   Shape は【FrameworkElement の派生】=レイアウトに参加する
     → 数が増えると【重い】

   【大量に描くなら】
     ・DrawingVisual + VisualCollection(レイアウトを通らない)
     ・Geometry を 1 つの Path に統合(GeometryGroup)★
     ・WriteableBitmap にピクセルで描く
     → 数千個の点をプロットするグラフなどでは必須の判断

実装

  • ただし、Visual Studio では Path 要素の編集が困難である。
    このため Expression Blend のデザイナでは、[ペン] や [鉛筆] など、
    Path 要素の編集用ツールを使用すると良い。
    以下は、Expression Blend デザイナの [ペン] や [鉛筆] を使用して記述した Path 要素の描画である。

    • 画面

様々なシェイプ(Path)

  • XAML
<Grid>
  <Path Fill="White" Stretch="Fill" Stroke="Black" StrokeThickness="5"
    Data="M208,68 C182.37169,68 155.97712,69 131,69 ・・・"/>
  <Path Fill="White" Stretch="Fill" Stroke="Black" StrokeThickness="5" Margin="50"
    Data="M88,132 C88,132 87.5,250.5 120.5,234.5 ・・・"/>
</Grid>
  • さらに、VS や Expression Blend を用い、Ellipse・TextBlock などの描画コンテンツを作成し、
    これを Expression Blend で、これらを Path に変換するといった機能も用意されている。

    • 画面

様々なシェイプ(Ellipse・TextBlock)

  • XAML
<Grid>
  <Viewbox Height="400" Width="400">
    <Grid>
      <Ellipse Stroke="Black" StrokeThickness="5"
        Height="400" Width="400"/>
      <TextBlock Text="WPF" FontSize="150"
        HorizontalAlignment="Center" VerticalAlignment="Center"/>
    </Grid>
  </Viewbox>
</Grid>
  • この変換を行うには、Expression Blend の [オブジェクトとタイムライン] で
    Ellipse・TextBlock を選択した状態で、
    メニューの [オブジェクト] → [パス] → [複合パスの作成] を選択する。
    以下は、上記の Ellipse・TextBlock を Path に変換した XAML である。

    • XAML
<Grid>
  <Path Data="M397.5,200 C397.5,309.07624 309.07624,397.5 200,397.5 C90.923762,397.5 2.5,309.07624 2.5,200 C2.5,90.923762 90.923762,2.5 200,2.5 C309.07624,2.5 397.5,90.923762 397.5,200 z M59.0835,149.3165 L71.974126,149.3165 L87.794437,223.73042 L107.13037,149.3165 L120.021,149.3165 L139.35694,223.73042 L155.17725,149.3165 L168.06787,149.3165 L145.80225,250.6835 L134.0835,250.6835 L113.57569,169.23834 L93.067874,250.6835 L81.349124,250.6835 z M192.08984,160.44929 L192.08984,199.1211 L222.55859,199.1211 C228.02734,199.1211 232.32422,197.5586 235.44922,194.4336 C238.96484,190.91799 240.72266,186.23049 240.72266,180.37113 C240.72266,173.73052 239.16016,168.84771 236.03516,165.72272 C232.51953,162.2071 228.02734,160.44929 222.55859,160.44929 z M178.61329,149.3165 L225.48828,149.3165 C234.08203,149.31651 241.11328,152.05088 246.58203,157.51961 C252.05078,162.98835 254.78515,170.60552 254.78515,180.37113 C254.78515,190.13674 252.24609,197.5586 247.16797,202.63671 C242.08984,207.71483 234.86328,210.25389 225.48828,210.25389 L192.08984,210.25389 L192.08984,249.51163 L178.61329,249.51163 z M273.5337,149.3165 L340.9165,149.3165 L340.9165,160.44929 L287.01027,160.44929 L287.01027,193.26173 L329.19775,193.26173 L329.19775,204.39452 L287.01027,204.39452 L287.01027,249.51163 L273.5337,249.51163 z" Stretch="Fill" Stroke="Black" StrokeThickness="5"/>
</Grid>

補足(Expression Blend の現在): 本節は Expression Blend を前提としているが、
製品としての Expression シリーズはすでに存在しない

【経緯】★
   Expression Studio(Blend / Design / Web / Encoder)
     → 2012 年に【製品ラインが終息】
     → Blend だけが【Blend for Visual Studio】として
       Visual Studio に【同梱】された

   現在(Visual Studio 2022)
     ・ワークロード「.NET デスクトップ開発」に含まれる
     ・XAML ファイルを右クリック →
       【Blend for Visual Studio で デザイン】で起動
     ・Path の [ペン]/[鉛筆]、複合パスの作成、
       アニメーションのタイムラインは【今も使える】★
【今のワークフロー】
   ・アイコン・図形は【SVG から変換】するのが主流
     → SVG の path の d 属性は
       WPF の Path.Data と【ほぼ同じ文法】★
     → 変換ツール(SvgToXaml 等)で流用できる
   ・デザイナとの受け渡しは
     Figma / Illustrator → SVG → XAML
   ・大量のアイコンは【フォント アイコン】
     (Segoe MDL2 Assets / Segoe Fluent Icons)で済ませる
【Viewbox は覚えておく価値がある】★
   上のサンプルで使われている Viewbox は、
   中身を【指定サイズに拡大縮小】するコンテナ。
   ベクタなので【拡大してもぼやけない】
     → アイコンを 1 つ描いて、
       Viewbox で任意サイズに使い回すのが定番

参考

グラデーション

WPF / Silverlight では、様々なグラデーションをデザイン可能。

実装

  • 簡単なグラデーション
    以下の簡素なグラデーションであれば、XAML で直接記述することも可能。

    • 画面

簡単なグラデーション

  • XAML
<Window.Background>
  <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
    <GradientStop Color="Black" Offset="0"/>
    <GradientStop Color="White" Offset="1"/>
  </LinearGradientBrush>
</Window.Background>
  • 複雑なグラデーション
    しかし、以下の複雑なグラデーションであれば、
    Expression Blend のデザイナを使用した方が、容易にグラデーションをデザインできる。

    • 画面

複雑なグラデーション

  • XAML
<Window.Background>
  <RadialGradientBrush GradientOrigin="0.3,0.3">
    <GradientStop Color="Aqua" Offset="0.1"/>
    <GradientStop Color="Black" Offset="0.2"/>
    <GradientStop Color="Aqua" Offset="0.3"/>
    <GradientStop Color="Black" Offset="0.4"/>
    <GradientStop Color="Aqua" Offset="0.5"/>
    <GradientStop Color="Black" Offset="0.6"/>
    <GradientStop Color="Aqua" Offset="0.7"/>
    <GradientStop Color="Black" Offset="0.8"/>
    <GradientStop Color="Aqua" Offset="0.9"/>
  </RadialGradientBrush>
</Window.Background>

補足(Brush と Freeze(): グラデーションは手軽だが、
性能面で知っておくべきことがある。

【Brush は Freezable】★
   Brush / Geometry / Transform は Freezable の派生。
   Freeze() すると
     ・【変更不可】になる代わりに
     ・変更通知の購読が不要になり【軽くなる】
     ・【スレッドを跨いで共有できる】

   リソースに置いた Brush は
   【自動的に Freeze される】ことが多いが、
   コードで大量に生成する場合は明示的に Freeze する ★

     var b = new SolidColorBrush(Colors.Red);
     b.Freeze();
【グラデーションの描画コスト】
   ・LinearGradientBrush … 【GPU で処理される】ため軽い
   ・RadialGradientBrush … やや重い
   ・大きな面(Window 全体)に
     複雑なグラデーション+リサイズ を重ねると目に見えて重くなる

   → 一覧の【各行の背景】に凝ったグラデーションを敷くと
     スクロールが引っかかる。行の背景は単色が無難 ★
【他の Brush】
   ImageBrush        … 画像を敷く
   VisualBrush       … 【他の UI 要素をそのまま描く】★
                       (鏡像・反射・サムネイル表現に使う)
   DrawingBrush      … Drawing を敷く(タイル模様)

トランスフォーム処理

  • WPF で 2D 平面での変換を行う「トランスフォーム処理」を使用して以下の効果を追加できる。

    • 「回転、拡大縮小、傾斜、平行移動」
    • 「アニメーションなど一時効果」
  • 「トランスフォーム処理」に使用できるクラスには、
    System.Windows.Media 名前空間の Transform クラスから派生する、下記クラスがある。

# クラス 変換の内容
1 MatrixTransform 任意の行列による変換
2 RotateTransform 回転
3 ScaleTransform 拡大縮小
4 SkewTransform 傾斜(せん断)
5 TranslateTransform 平行移動
6 TransformGroup 上記の組み合わせ

移行メモ(体裁): 箇条書きを表に整理し、各クラスの変換内容を補った。

  • なお、TransformGroup を使用すれば、
    複数の Transform クラスを組み合わせて適用することができる。

  • 以下、下記の図形に対する、上記2つの「トランスフォーム処理」の例。

    • 画面

トランスフォーム前

  • XAML
<Grid>
  <Border Height="100" Width="100" BorderBrush="Black" BorderThickness="1" >
    <Rectangle Height="100" Width="100" Fill="Blue"/>
  </Border>
</Grid>

回転、拡大縮小、傾斜、平行移動

  • FrameworkElement.LayoutTransform プロパティに設定された Transform クラスを使用し、
    レイアウト時に「トランスフォーム処理」を実行する。

    • このため、「トランスフォーム処理」後も UI 要素がレイアウト時の描画領域内に収まる。
    • このような特徴から、この「トランスフォーム処理」は、
      表示を垂直から水平に切り替えたり、ズームしたりするなどの用途で利用可能である。
  • 以下は、LayoutTransform プロパティに RotateTransform クラスを指定して
    UI 要素を回転させた例である。

    • 画面

回転、拡大縮小、傾斜、平行移動

  • XAML
<Grid>
  <Border Height="100" Width="100" BorderBrush="Black" BorderThickness="1" >
    <Rectangle Height="100" Width="100" Fill="Blue">
      <Rectangle.LayoutTransform>
        <RotateTransform CenterX="50" CenterY="50" Angle="45"/>
      </Rectangle.LayoutTransform>
    </Rectangle>
  </Border>
</Grid>

アニメーションなど一時効果の追加

  • UIElement.RenderTransform プロパティに設定された Transform クラスを使用し、
    レイアウト後に「トランスフォーム処理」しレンダリングする。

    • このため、場合によっては UI 要素がレイアウトの描画領域の外にでてしまう場合がある。
    • このような特徴から、この「トランスフォーム処理」は、
      「アニメーション」(後述)と組み合わせるなどして
      UI 要素に注目を集めるなどの用途で利用可能である。
  • 以下は、RenderTransform プロパティに RotateTransform クラスを指定して
    UI 要素を回転させた例である。

    • 画面

アニメーションなど一時効果の追加

  • XAML
<Grid>
  <Border Height="100" Width="100" BorderBrush="Black" BorderThickness="1" >
    <Rectangle Height="100" Width="100" Fill="Blue">
      <Rectangle.RenderTransform>
        <RotateTransform CenterX="50" CenterY="50" Angle="45"/>
      </Rectangle.RenderTransform>
    </Rectangle>
  </Border>
</Grid>

※ なお、こちらの「トランスフォーム処理」は「Silverlight」でもサポートされる。

補足(LayoutTransformRenderTransform の使い分け): 原文の説明は正確だが、
図で押さえると迷わなくなる。

【WPF のレイアウトの 2 パス】★
   ① Measure(測る) → ② Arrange(配置する) → ③ Render(描く)

   LayoutTransform  … 【① と ② の間】で効く
     → 変換後の大きさで【場所を取り直す】
     → 周りの要素が【押しのけられる】
     → 【レイアウトが再計算される=重い】★

   RenderTransform  … 【③ の直前】に効く
     → 場所は変換前のまま
     → 周りの要素は【動かない】
     → はみ出す・重なる
     → 【GPU で処理され、極めて軽い】★
【使い分けの結論】
   ・アニメーション → 必ず【RenderTransform】★
      (LayoutTransform でアニメすると
       毎フレーム レイアウトが走り、確実にカクつく)
   ・縦書きラベル、90 度回転した列見出し
     → 【LayoutTransform】(幅と高さを入れ替えたい)★
   ・ズーム機能
     → スクロールと連動させたいなら LayoutTransform
       見た目だけ拡大なら RenderTransform
【RenderTransformOrigin が便利】★
   CenterX / CenterY はピクセル指定だが、
   RenderTransformOrigin は【0〜1 の相対値】で指定できる

     RenderTransformOrigin="0.5,0.5"   ← 【中心を軸に回す】

   → サイズが変わっても軸がずれない
   → 上のサンプルの CenterX="50" CenterY="50" は
     Width/Height が 100 だから中心になっているだけで、
     サイズを変えると【軸がずれる】

参考

アニメーション

移行メモ(未執筆): 移行元では本節(「実装」「説明」「参考」の 3 つの小見出し)が
見出しのみで本文が存在しない
XAMLの書き方(1) の「イベント トリガ」から
本節への参照があるため、見出しは残したうえで、
以下に最小限の補足を追記した。

補足(WPF のアニメーションの要点): 本節が未執筆のため、
参照元から辿った読者のために骨子を示す。

【中心となる概念】★
   Storyboard          … アニメーションの入れ物(時間軸)
   XxxAnimation        … 型ごとのアニメーション
                         DoubleAnimation / ColorAnimation /
                         PointAnimation / ThicknessAnimation
   Storyboard.TargetName / TargetProperty
                       … 【何の、どのプロパティを】動かすか
   EventTrigger        … 【いつ】始めるか
<Rectangle Width="100" Height="100" Fill="Blue">
  <Rectangle.RenderTransform>
    <RotateTransform x:Name="rt" />          <!-- 名前が要る -->
  </Rectangle.RenderTransform>
  <Rectangle.Triggers>
    <EventTrigger RoutedEvent="Loaded">      <!-- いつ -->
      <BeginStoryboard>
        <Storyboard>
          <DoubleAnimation
            Storyboard.TargetName="rt"
            Storyboard.TargetProperty="Angle"
            From="0" To="360" Duration="0:0:2"
            RepeatBehavior="Forever" />
        </Storyboard>
      </BeginStoryboard>
    </EventTrigger>
  </Rectangle.Triggers>
</Rectangle>
【押さえどころ】
 ① 【アニメーションは最優先】★
      依存関係プロパティの優先順位で【最上位】にある
      → アニメ中はローカル値の代入が効かない
      → 終わった後も FillBehavior="HoldEnd"(既定)だと
        【値を握り続ける】
        → 以後 SetValue が効かず「なぜか色が変わらない」
        → FillBehavior="Stop" にするか、
          BeginAnimation(prop, null) で解除する ★

 ② 【RenderTransform を動かす】
      Width / Height / Margin を動かすと
      毎フレーム レイアウトが走って重い
      → Scale / Translate / Rotate と Opacity を動かす ★

 ③ 【イージング】
      EasingFunction(QuadraticEase, BackEase 等)で
      等速でない自然な動きになる

 ④ 【VisualStateManager】
      ボタンの押下・ホバーなどコントロールの状態遷移は
      トリガーより VisualStateManager で書く方が現代的 ★
【業務アプリでの節度】
   ・処理待ちの表現(ProgressRing の回転)
   ・エラー箇所への注意喚起(短い点滅・シェイク)
   程度に留める。
   画面遷移のたびに派手に動くと【操作が遅く感じる】★
   OS の「アニメーションを表示しない」設定
   (SystemParameters.ClientAreaAnimation)も尊重する

MVVMデザイン パターン

概要

MVVM(Model - View - View Model)モデルとは、
前述の「データ バインディング」の仕組みを活用したデザイン パターンであり、
従来の3層デザイン パターンの「モデル オブジェクト」、「ビュー オブジェクト」に加えて、
「バインディング ソース」である「ビュー・モデル オブジェクト」を新設する
デザイン パターンである。

メリット

このデザイン パターンのメリットは、
「モデル オブジェクト」、「ビュー オブジェクト」間の結合部を、
「データ バインディング」による「ビュー・モデル オブジェクト」に限定し、
疎結合を実現できる点である。

ガイドライン

なお、MVVM デザイン パターンのためのガイドラインには、以下のものがある。

  1. XAML で実装できるものは、コードビハインドに実装しない。
  2. 「ビュー・モデル オブジェクト」は DataContext として「ビュー オブジェクト」に渡す。
  3. 「ビュー・モデル オブジェクト」と「モデル オブジェクト」は、
    「データ バインディング」により結合されるため、「ビュー オブジェクト」には、一切アクセスしない。
  4. 「ビュー・モデル オブジェクト」は、INotifyPropertyChanged インターフェイスを実装するべき。
  5. 「ビュー・モデル オブジェクト」は、「モデル オブジェクト」をカプセル化することで、
    「ビュー オブジェクト」から、データ ストレージの複雑さを隠蔽する。

参考

Model - View - ViewModelデザイン パターン

移行メモ(リンク切れ): 上記の MSDN マガジンの記事
(Josh Smith, "WPF Apps With The Model-View-ViewModel Design Pattern",
MSDN Magazine 2009年2月号)は MVVM の古典として知られるが、
MSDN マガジンのアーカイブ移行に伴い当該 URL は到達しない
現在は Microsoft Learn のアーカイブ
https://learn.microsoft.com/en-us/archive/msdn-magazine/2009/february/patterns-wpf-apps-with-the-model-view-viewmodel-design-pattern )で読める。

補足(ガイドライン 5 項目の現在の評価): この 5 項目は
今読んでも妥当である。現在の実装手段を対応付けておく。

# 原文のガイドライン 現在の実装手段
1 XAML で書けるものはコードビハインドに書かない バインディング / トリガー / Behavior で置き換える
2 ViewModel は DataContext で渡す DI + DataTemplate(暗黙) で自動解決 ★
3 ViewModel は View に一切アクセスしない メッセンジャ / IDialogService で間接化 ★
4 INotifyPropertyChanged を実装する [ObservableProperty](ソース ジェネレータ)
5 ViewModel が Model をカプセル化する 変わらず
【原文が触れていない、今なら必須の 2 点】★
 ① 【コマンド(ICommand)】
      ボタン クリックをコードビハインドに書かないための要。
      ガイドライン 1 を実現する【中心的な手段】だが
      原文には登場しない。
        [RelayCommand] 属性で自動生成できる

 ② 【非同期】
      ViewModel から async でサービスを呼ぶのが標準。
      ただし ICommand.Execute は void を返すため、
      【AsyncRelayCommand】等の非同期対応コマンドを使う
      (二重実行の抑止・キャンセル・例外の受け取りが要る)
【MVVM の実装ライブラリ】
   ・【CommunityToolkit.Mvvm】★(旧 MvvmLight の後継、Microsoft 製)
       → 軽量。ソース ジェネレータで定型コードが消える
       → 【まずこれを選ぶ】
   ・Prism … Region ナビゲーション、モジュール分割
             大規模向け
   ・ReactiveUI … Rx ベース。学習コストは高いが強力
【MVVM の「やり過ぎ」に注意】★
   ガイドライン 3(View に一切アクセスしない)を
   厳格に守ろうとすると、
   「フォーカス移動」「スクロール位置」といった
   【純粋に View の関心事】まで
   ViewModel から操作しようとして破綻する。

   → そうした処理は【添付ビヘイビア】で View 側に置く
   → 後述の「フォーカス制御」で作者が
     「問題が多い」と書いているのは、まさにこの領域である

バリデーション

単項目のバリデーション

https://github.com/OpenTouryoProject/SampleProgram/tree/master/UISubsystem/WPF/Validation/InputField

一覧のバリデーション

https://github.com/OpenTouryoProject/SampleProgram/tree/master/UISubsystem/WPF/Validation/DataGrid

フォーカス制御

フォーカス制御については、いろいろ試しているが
問題が多いので使わない方向で考えた方が良さそう。

補足(バリデーションの選択肢と、フォーカス制御の難しさ): 本章は
サンプル コードへのリンクのみで本文がないため、要点を補う。

【WPF のバリデーションの 4 つのやり方】★
 ① 【ValidationRule】(WPF 独自)
      <Binding.ValidationRules> に置く。
      → View 側に検証ロジックが来るので MVVM と相性が悪い

 ② 【IDataErrorInfo】
      ViewModel が this[string columnName] を実装。
      → ValidatesOnDataErrors=True で有効化
      → 単純だが【1 項目 1 エラー】しか返せない

 ③ 【INotifyDataErrorInfo】★(.NET 4.5 以降・推奨)
      → 【1 項目に複数エラー】を返せる
      → 【非同期検証】ができる(サーバ問い合わせ等)
      → ValidatesOnNotifyDataErrors=True(既定で True)
      → CommunityToolkit.Mvvm の
        ObservableValidator が実装済みで、
        【DataAnnotations 属性】と組み合わせられる ★

           [Required(ErrorMessage = "必須です")]
           [MaxLength(10)]
           [ObservableProperty] private string _name;

 ④ 【例外を投げる】(ValidatesOnExceptions)
      → セッターで throw する。非推奨 ★
【エラーの見せ方】
   既定では【赤い枠】が出るだけで理由が見えない。
     → Validation.ErrorTemplate を差し替えるか、
       ツールチップにエラー内容を出す(前述)

     ToolTip="{Binding (Validation.Errors)/ErrorContent,
               RelativeSource={RelativeSource Self}}"
【「一覧のバリデーション」が難しい理由】★
   DataGrid は
     ・行単位(IEditableObject / RowValidationRules)
     ・セル単位(列の Binding の検証)
   の【2 層】で検証が走り、
   「行を抜けるまでコミットされない」ため
   エラー表示のタイミングが直感に反する。
     → CommittingEdit / RowEditEnding を理解しないと
       【エラーのまま次の行に進める】等の穴ができる
【フォーカス制御が「問題が多い」理由】★
   作者の結論(使わない方向)は【妥当】である。

   ・WPF のフォーカスは【論理フォーカス】と
     【キーボード フォーカス】の【2 種類】がある
       → FocusManager.FocusedElement と Keyboard.Focus() が
         別物で、片方だけ設定しても効かないことがある
   ・Loaded の時点では【まだフォーカスできない】ことがある
       → Dispatcher.BeginInvoke(DispatcherPriority.Input, ...)
         で遅延させる回避策が必要
   ・テンプレート内の実体(PART_EditableTextBox 等)に
     当てないと効かないコントロールがある
   ・仮想化された一覧では【要素が存在しない】ことがある

   【現実的な落としどころ】
     ・初期フォーカスだけ設定する(FocusManager.FocusedElement)
     ・Tab 順は【TabIndex と KeyboardNavigation.TabNavigation】
       で宣言的に整える ★
     ・「エラー箇所へ自動でフォーカス」は
       【添付ビヘイビア】に隔離し、
       効かない環境があることを前提に設計する

Tags: 移行, .NET開発, UIサブシステム, WPF/Silverlight, XAML

⚠️ **GitHub.com Fallback** ⚠️