| name | dotnet-wpf-pdf-preview |
| description | こんなときに使う: Use when adding PDF upload and inline WebView2 preview to a WPF app with MVVM file selection and async initialization.
|
| license | MIT |
| metadata | {"author":"RyoMurakami1983","tags":["dotnet","wpf","csharp","mvvm","webview2","pdf"],"invocable":false} |
WPFアプリケーションへのPDFアップロードとWebView2プレビュー追加
.NET WPFアプリケーションにPDFファイルアップロードとインラインプレビューを追加するためのエンドツーエンドワークフロー:WebView2ベースのPDFレンダリング、CommunityToolkit.MvvmによるMVVMファイル選択、イベントベースのViewModel→View通信、エラーハンドリング付き非同期WebView2初期化。
こんなときに使う
以下の場合にこのスキルを使用してください:
- WPFアプリケーションにPDFアップロードとプレビューパネルを追加するとき
- Microsoft Edge WebView2を使用してPDFファイルをインライン表示するとき
- 左側にPDFプレビュー、右側にコンテンツの分割パネルレイアウトを構築するとき
- ViewModelでファイル選択を実装し、WebView2をcode-behindに保持するとき
- アップロードしたドキュメントを表示する注文書アップロードUIを作成するとき
Related Skills
dotnet-wpf-secure-config — DPAPI暗号化基盤(認証情報の保存用)
dotnet-wpf-dify-api-integration — アップロードしたPDFをDify APIに送信してOCR抽出
dotnet-oracle-wpf-integration — 抽出したPDFデータをOracleデータベースに保存
git-commit-practices — 各ステップをアトミックな変更としてコミット
Core Principles
- MVVM規律 — ViewModelがファイル選択ロジックを所有し、ViewがWebView2レンダリングを所有(基礎と型)
- 最小限のcode-behind — WebView2の初期化とナビゲーションのみをcode-behindに配置(基礎と型)
- イベントベース通信 — ViewModelがイベントでViewに通知。コントロールへの直接アクセスは禁止(ニュートラル)
- デフォルトで非同期 — WebView2の初期化は非同期。UIスレッドのブロック禁止(継続は力)
- グレースフルデグラデーション — WebView2 Runtimeが見つからない場合もクラッシュしない(ニュートラル)
Workflow: Add PDF Preview to WPF
Step 1 — WebView2のインストールとレイアウトセットアップ
WebView2 NuGetパッケージを追加し、分割パネルのXAMLレイアウトを作成するときに使用します。
WebView2パッケージをインストールし、左側にPDFプレビュー、右側にコンテンツエリアの2カラムGridを作成します。
# Install WebView2 NuGet package
Install-Package Microsoft.Web.WebView2
YourApp/
├── Views/
│ └── MainWindow.xaml # 🆕 WebView2付き2カラムレイアウト
│ └── MainWindow.xaml.cs # 🆕 WebView2初期化 + ナビゲーション
└── ViewModels/
└── MainViewModel.cs # 🆕 ファイル選択 + パス管理
XAMLレイアウトテンプレート — 2カラム分割パネル:
<Window x:Class="YourApp.Views.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:wv2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
Title="PDF Preview" Height="700" Width="1200">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1.5*"/>
</Grid.ColumnDefinitions>
<Grid Grid.Column="0" Margin="5">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Button Grid.Row="0" Content="Upload PDF"
Command="{Binding UploadPdfCommand}"
Background="#2196F3" Foreground="White" FontWeight="Bold"/>
<Border Grid.Row="1" BorderBrush="#CCCCCC" BorderThickness="1">
<wv2:WebView2 x:Name="PdfWebView" />
</Border>
</Grid>
<Grid Grid.Column="1" Margin="5">
</Grid>
</Grid>
</Window>
WebView2でx:Nameを使用する理由: WebView2は命令的な初期化(EnsureCoreWebView2Async)とナビゲーション(CoreWebView2.Navigate)が必要です。これらのAPIにはバインド可能な代替がないため、x:NameはMVVMの許容される例外です。
Values: 基礎と型 / 継続は力
Step 2 — ViewModelの実装(ファイル選択 + パス管理)
OpenFileDialogによるPDFファイル選択を処理し、Viewに通知するViewModelを作成するときに使用します。
CommunityToolkit.Mvvmを使用してMainViewModelを作成し、[ObservableProperty]で状態管理、[RelayCommand]でアップロードアクションを実装します。PdfPathChangedイベントがViewModel→View通信を橋渡しします。
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System;
namespace YourApp.ViewModels
{
public partial class MainViewModel : ObservableObject
{
public event EventHandler<string>? PdfPathChanged;
[ObservableProperty]
private string pdfFilePath = string.Empty;
[ObservableProperty]
private bool isPdfLoaded;
[RelayCommand]
private void UploadPdf()
{
var dialog = new Microsoft.Win32.OpenFileDialog
{
Filter = "PDF files (*.pdf)|*.pdf",
Title = "Select PDF file"
};
if (dialog.ShowDialog() == true)
{
PdfFilePath = dialog.FileName;
IsPdfLoaded = true;
PdfPathChanged?.Invoke(this, PdfFilePath);
}
}
}
}
バインディングではなくイベントパターンを使用する理由: WebView2のSourceプロパティはローカルファイルURLに対する信頼性の高い双方向バインディングをサポートしていません。イベントパターンによりナビゲーションのタイミングとエラーハンドリングを明示的に制御できます。
Values: 基礎と型 / ニュートラル
Step 3 — WebView2の初期化(code-behind)
ウィンドウのcode-behindでWebView2の初期化とPDFナビゲーションを配線するときに使用します。
WebView2を非同期で初期化し、ViewModelのPdfPathChangedイベントをサブスクライブしてナビゲーションする最小限のcode-behindを作成します。
using System;
using System.Windows;
namespace YourApp.Views
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
var viewModel = new MainViewModel();
DataContext = viewModel;
InitializeWebView();
viewModel.PdfPathChanged += OnPdfPathChanged;
}
private async void InitializeWebView()
{
try
{
await PdfWebView.EnsureCoreWebView2Async(null);
}
catch (Exception ex)
{
MessageBox.Show(
$"WebView2 Runtime not found.\n\n" +
$"Please install the WebView2 Runtime from:\n" +
$"https://developer.microsoft.com/microsoft-edge/webview2/\n\n" +
$"Error: {ex.Message}",
"WebView2 Error",
MessageBoxButton.OK,
MessageBoxImage.Warning);
}
}
private void OnPdfPathChanged(object? sender, string pdfPath)
{
if (PdfWebView.CoreWebView2 != null)
{
PdfWebView.CoreWebView2.Navigate($"file:///{pdfPath}");
}
}
}
}
ここでasync voidが許容される理由: InitializeWebViewはファイア・アンド・フォーゲットのUI初期化です。try/catchがすべての失敗ケースを処理します。これはasync voidが適切な数少ないケースの一つ — イベント的なUI起動です。
Values: ニュートラル / 基礎と型
Step 4 — XAML名前空間の追加と配線
すべてのXAML名前空間とバインディングが正しく接続されていることを確認するときに使用します。
WebView2のXAML名前空間が宣言され、ボタンコマンドがViewModelにバインドされていることを確認します。
必須XAML名前空間(Windowタグ内):
xmlns:wv2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
バインディングチェックリスト:
<Button Command="{Binding UploadPdfCommand}" Content="Upload PDF" />
<Button Click="OnUploadClick" Content="Upload PDF" />
<wv2:WebView2 x:Name="PdfWebView" />
<wv2:WebView2 Source="{Binding PdfFileUri}" />
Values: 基礎と型 / ニュートラル
Step 5 — エッジケースの処理
デプロイメントと実運用シナリオの堅牢性を追加するときに使用します。
3つの主な障害シナリオを処理します:ランタイム不在、大きなファイル、再アップロード状態。
WebView2 Runtimeが見つからない場合:
private async void InitializeWebView()
{
try
{
await PdfWebView.EnsureCoreWebView2Async(null);
}
catch (Exception)
{
PdfWebView.Visibility = Visibility.Collapsed;
}
}
再アップロード(状態リセット):
[RelayCommand]
private void UploadPdf()
{
var dialog = new Microsoft.Win32.OpenFileDialog
{
Filter = "PDF files (*.pdf)|*.pdf",
Title = "Select PDF file"
};
if (dialog.ShowDialog() == true)
{
PdfFilePath = dialog.FileName;
IsPdfLoaded = true;
PdfPathChanged?.Invoke(this, PdfFilePath);
}
}
大きなPDFファイル — WebView2はChromium内蔵のPDFビューアを通じて大きなPDFをネイティブに処理します。特別な処理は不要ですが、ローディングインジケータの表示を検討してください:
private void OnPdfPathChanged(object? sender, string pdfPath)
{
if (PdfWebView.CoreWebView2 != null)
{
PdfWebView.CoreWebView2.Navigate($"file:///{pdfPath}");
}
}
Values: ニュートラル / 継続は力
Step 6 — アプリケーション固有のカスタマイズ
生成されたコードを本番デプロイ用に準備するときに使用します。
出荷前にこれらのプレースホルダーを置き換えてください:
| 項目 | ファイル | 変更内容 | スキップした場合の影響 |
|---|
| 名前空間 | 全.csファイル | YourApp → 実際の名前空間 | ビルドエラー |
| ウィンドウタイトル | MainWindow.xaml | "PDF Preview" → 実際のタイトル | 汎用的なウィンドウタイトル |
| カラム比率 | MainWindow.xaml | 1* / 1.5* → 希望の比率 | レイアウトの不一致 |
| ボタンスタイル | MainWindow.xaml | テーマに合わせた色とフォント | UIの不統一 |
| アップロードフィルタ | MainViewModel.cs | 他のファイル種別を受け付ける場合のフィルタ | 不正なファイル種別 |
カスタマイズチェックリスト:
# Verify all placeholders are replaced
Select-String -Path "Views/*.xaml","Views/*.cs","ViewModels/*.cs" -Pattern "YourApp" -SimpleMatch
# Expected: 0 matches after customization
Values: 基礎と型 / 成長の複利
Good Practices
1. WebView2コードはcode-behindに保持
What: WebView2の初期化とナビゲーションはMainWindow.xaml.csに配置し、ViewModelには置かない。
Why: WebView2はx:Nameと命令的なAPI呼び出し(EnsureCoreWebView2Async、CoreWebView2.Navigate)が必要です。これはMVVMの許容される例外 — code-behindがViewModelイベントとWebView2 API間の薄いアダプタとして機能します。
Values: 基礎と型(MVVM例外の型)
2. ViewModel→View通信にイベントパターンを使用
What: ViewModelがPdfPathChangedイベントを発行し、code-behindがサブスクライブしてWebView2をナビゲート。
Why: ViewModelをテスト可能に保ちつつ(UI依存なし)、Viewにナビゲーションタイミングの明示的な制御を与えます。代替手段(Messenger、Behavior)はこのユースケースではメリットなく複雑さが増します。
Values: ニュートラル / 基礎と型
3. ウィンドウロード時にWebView2を非同期初期化
What: EnsureCoreWebView2AsyncをコンストラクタまたはLoadedイベントで呼び出し、最初のPDFアップロード時には呼び出さない。
Why: WebView2の初期化には100〜500msかかります。事前に実行することで、ユーザーが最初にアップロードをクリックしたときの目に見える遅延を回避します。
Values: 継続は力(先回りの準備)
4. Quick Language Checklist
- 初出時は Portable Document Format (PDF) と記載し、以降は「PDF」と略す。
- 初出時は Windows Presentation Foundation (WPF) および Model-View-ViewModel (MVVM) と記載する。
- 最初のナビゲーション前に
EnsureCoreWebView2Async でWebView2を初期化する。
- ローカルPDFファイルに
WebView2.Source を直接バインドしない。命令的にナビゲートする。
- WebView2 Runtimeが見つからない場合のユーザー向けメッセージとリンクの表示を検討する。
Common Pitfalls
1. デプロイ先のマシンにWebView2 Runtimeをインストールし忘れる
Problem: WebView2 RuntimeはWindows 11にはプリインストールされていますが、Windows 10やロックダウンされた企業マシンでは欠落している場合があります。
Solution: インストーラにWebView2 Evergreen Bootstrapperを含めるか、ダウンロードリンクを検出してユーザーに提示します。EnsureCoreWebView2Asyncは常にtry/catchで囲みます。
await PdfWebView.EnsureCoreWebView2Async(null);
try { await PdfWebView.EnsureCoreWebView2Async(null); }
catch (Exception ex) { ShowWebView2MissingMessage(ex); }
2. WebView2のSourceプロパティを直接バインドしようとする
Problem: ローカルファイルURLに対して<wv2:WebView2 Source="{Binding PdfUri}" />を使用しても確実に動作しない。
Solution: イベントパターン(Step 2〜3)とCoreWebView2.Navigate()を使用して、ローカルファイルの確実なナビゲーションを実現します。
<wv2:WebView2 Source="{Binding PdfFileUri}" />
PdfWebView.CoreWebView2.Navigate($"file:///{pdfPath}");
3. WebView2初期化の失敗を処理しない
Problem: WebView2 Runtimeが見つからないか破損している場合、EnsureCoreWebView2Asyncが例外をスローし、未処理の例外クラッシュを引き起こす。
Solution: 常にtry/catchで囲み、RuntimeダウンロードURLを含むユーザーフレンドリーなメッセージを表示します。
Anti-Patterns
ファイルダイアログロジックをcode-behindに配置
What: MainWindow.xaml.csのボタンクリックハンドラでOpenFileDialogを直接開く。
Why It's Wrong: MVVM分離に違反。ファイル選択はアプリケーションロジックであり、UIレンダリングではありません。code-behindのファイルダイアログロジックはユニットテスト不可能です。
Better Approach: ViewModelの[RelayCommand]でファイル選択を処理。ダイアログ結果がViewModelプロパティを更新し、Viewがそれを監視します。
WebView2以外のコントロールにx:Nameを使用
What: TextBox、Button、DataGridにx:Nameを追加してcode-behindで操作する。
Why It's Wrong: データバインディングをバイパスし、UIがcode-behindに密結合になります。すべてのx:Name参照はバインディングの機会を逃しています。
Better Approach: すべての標準WPFコントロールに{Binding}を使用。命令的APIが必要なコントロール(WebView2)にのみx:Nameを限定使用します。
StatusLabel.Text = "PDF loaded";
UploadButton.IsEnabled = false;
[ObservableProperty] private string statusText = "Ready";
[ObservableProperty] private bool canUpload = true;
Quick Reference
実装チェックリスト
ファイル構造
| ファイル | 目的 | レイヤー |
|---|
MainWindow.xaml | 2カラムレイアウト + WebView2 | View |
MainWindow.xaml.cs | WebView2初期化 + ナビゲーション | View(code-behind) |
MainViewModel.cs | ファイル選択 + 状態管理 | ViewModel |
WebView2ナビゲーションパターン
| シナリオ | コード |
|---|
| ローカルPDFファイル | CoreWebView2.Navigate($"file:///{path}") |
| 空白ページ | CoreWebView2.Navigate("about:blank") |
| 準備状態の確認 | if (PdfWebView.CoreWebView2 != null) |
Resources
Changelog
バージョン 1.0.0 (2026-02-15)
- 初回リリース: 単一ワークフローPDFアップロード + WebView2プレビューガイド
- 6ステップワークフロー: レイアウト → ViewModel → code-behind → 名前空間 → エッジケース → カスタマイズ
- イベントベースのViewModel→View通信パターン
- エラーハンドリング付きWebView2非同期初期化
- CommunityToolkit.Mvvm統合(
[RelayCommand]と[ObservableProperty])