WPFでMVVMを組むと必ず通るのが「ICommandにラムダ式を直で入れたい」問題です。短く書ければ生産性が上がりそうですが、実際にはCS1660で止まってしまう……。本記事はその理由を型レベルから丁寧にひも解き、実運用可能なRelayCommand(DelegateCommand)実装、非同期対応、型安全化、落とし穴回避までを網羅的に解説します。
結論:ICommandにラムダ式は直接代入できない(CS1660)
まず結論から。以下のようなコードはコンパイルできません。
public ICommand MyCommand { get; }
public MainViewModel()
{
// コンパイル エラー CS1660:
// “匿名メソッドブロック” を delegate 以外の型に変換できない
MyCommand = (o, e) => { /* 処理 */ };
}
理由は単純で、ICommandはインターフェースであり、ラムダ式はdelegate(デリゲート)型にしか暗黙変換されないためです。ICommandはdelegateではないので、暗黙変換の対象外です。さらに、上記のラムダが(o, e)の2引数になっている点も誤りです。WPFのICommand.Executeは引数が1つ(object? parameter)です。2引数スタイルはイベントハンドラー(object sender, EventArgs e)のシグネチャであり、コマンド実行のシグネチャではありません。
型の違いを俯瞰
| 要素 | 概要 | 代表的シグネチャ | 備考 |
|---|---|---|---|
| ラムダ式 | 匿名メソッド。コンパイラが適切なdelegate型に変換 | param => ... / (p1, p2) => ... | 代入先がdelegate型である必要 |
| delegate | メソッド参照を保持する型 | Action<T>, Func<TResult> | イベントやコールバックで多用 |
| ICommand | WPFコマンドの契約(インターフェース) | bool CanExecute(object?) / void Execute(object?) | delegateではないためラムダを直接代入不可 |
解決策の王道:ICommandを実装する「ラッパー」を使う
WPF本体にはMAUIのCommandのような実装クラスがありません。したがって、RelayCommand(またはDelegateCommand)などの「Action/Funcを包むICommand実装」を用意して使います。以下は実運用を意識した最小・堅牢実装です。
基本形:非ジェネリック版 RelayCommand
using System;
using System.Windows.Input;
public sealed class RelayCommand : ICommand
{
private readonly Action _execute;
private readonly Predicate? _canExecute;
public RelayCommand(Action<object?> execute,
Predicate<object?>? canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public bool CanExecute(object? parameter)
=> _canExecute?.Invoke(parameter) ?? true;
public void Execute(object? parameter)
=> _execute(parameter);
// WPFのコマンド更新機構に委譲
public event EventHandler? CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
// 必要に応じてUIの再評価を促すユーティリティ
public void RaiseCanExecuteChanged()
=> CommandManager.InvalidateRequerySuggested();
}
型安全を高める:ジェネリック版 RelayCommand<T>
コマンドパラメーターの型を厳密に扱いたい場合はジェネリック版を使います。キャスト例外やBoxingを避けやすくなります。
public sealed class RelayCommand<T> : ICommand
{
private readonly Action<T?> _execute;
private readonly Predicate<T?>? _canExecute;
public RelayCommand(Action<T?> execute,
Predicate<T?>? canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public bool CanExecute(object? parameter)
{
if (parameter is null && default(T) is not null)
return _canExecute?.Invoke(default) ?? true;
if (parameter is T value)
return _canExecute?.Invoke(value) ?? true;
// 型不一致時は実行不可扱い
return false;
}
public void Execute(object? parameter)
{
if (parameter is null && default(T) is not null)
{
_execute(default);
return;
}
if (parameter is T value)
{
_execute(value);
}
}
public event EventHandler? CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
public void RaiseCanExecuteChanged()
=> CommandManager.InvalidateRequerySuggested();
}
非同期処理にも強い:AsyncRelayCommand
UIスレッドをブロックしないために、コマンド実行はasync/awaitに対応させるのが実務的です。例外を飲み込まないよう注意し、連打抑止(実行中はCanExecute=false)も入れておくと快適です。
public sealed class AsyncRelayCommand : ICommand
{
private readonly Func<object?, Task> _executeAsync;
private readonly Predicate<object?>? _canExecute;
private bool _isExecuting;
public AsyncRelayCommand(Func<object?, Task> executeAsync,
Predicate<object?>? canExecute = null)
{
_executeAsync = executeAsync ?? throw new ArgumentNullException(nameof(executeAsync));
_canExecute = canExecute;
}
public bool CanExecute(object? parameter)
=> !_isExecuting && (_canExecute?.Invoke(parameter) ?? true);
public async void Execute(object? parameter)
{
if (!CanExecute(parameter)) return;
try
{
_isExecuting = true;
RaiseCanExecuteChanged();
await _executeAsync(parameter).ConfigureAwait(true);
}
finally
{
_isExecuting = false;
RaiseCanExecuteChanged();
}
}
public event EventHandler? CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
public void RaiseCanExecuteChanged()
=> CommandManager.InvalidateRequerySuggested();
}
注意:WPFのICommand.Executeはvoidのため、非同期はasync voidになりがちです。上の実装では例外がUIスレッドに伝播してプロセス終了…を避けるため、可能であればViewModel側でtry-catchしログやダイアログ通知へ逃がしてください。
ViewModelでの具体例(ラムダを活用しつつ型安全)
public sealed class MainViewModel
{
public ICommand SaveCommand { get; }
public ICommand DeleteCommand { get; }
public ICommand ToggleCommand { get; }
public ICommand EditUserCommand { get; } // パラメーター型あり
public ICommand LoadCommand { get; } // 非同期
private bool _canSave;
public bool CanSave
{
get => _canSave;
set
{
if (_canSave == value) return;
_canSave = value;
// 条件変化時にCanExecuteを再評価
(SaveCommand as RelayCommand)?.RaiseCanExecuteChanged();
}
}
public MainViewModel()
{
SaveCommand = new RelayCommand(_ => Save(), _ => CanSave);
DeleteCommand = new RelayCommand(_ => Delete(), _ => CanDelete());
ToggleCommand = new RelayCommand(_ => Toggle());
// 型安全なコマンド(ユーザーIDを受け取る)
EditUserCommand = new RelayCommand<int>(id => EditUser(id), id => id > 0);
// 非同期ロード。連打抑止とエラーハンドリングを含む
LoadCommand = new AsyncRelayCommand(async _ =>
{
try
{
await LoadAsync();
}
catch (Exception ex)
{
// ログやUI通知へ
HandleException(ex);
}
});
}
private void Save() { /* 保存処理 */ }
private bool CanDelete() => /* 条件 */ true;
private void Delete() { /* 削除処理 */ }
private void Toggle() { /* ON/OFF 切替 */ }
private void EditUser(int userId) { /* 画面遷移など */ }
private Task LoadAsync() => Task.Delay(250);
private void HandleException(Exception ex) { /* ログ・通知 */ }
}
XAMLでのバインディング例
<StackPanel> <Button Content="保存" Command="{Binding SaveCommand}" /> <Button Content="削除" Command="{Binding DeleteCommand}" /> <Button Content="切替" Command="{Binding ToggleCommand}" />
「ラムダだけで完結」に近づける書き方(糖衣構文)
最終的にはICommand実装クラスが必要ですが、呼び出し側を簡潔にする工夫は可能です。拡張メソッドやヘルパーで「ラムダをコマンド化」しましょう。
public static class CommandExtensions
{
public static ICommand ToCommand(this Action execute)
=> new RelayCommand(_ => execute());
public static ICommand ToCommand(this Action execute, Func<bool> canExecute)
=> new RelayCommand(_ => execute(), _ => canExecute());
public static ICommand ToCommand<T>(this Action<T?> execute)
=> new RelayCommand<T>(execute);
public static ICommand ToCommand<T>(this Action<T?> execute, Predicate<T?> canExecute)
=> new RelayCommand<T>(execute, canExecute);
}
これでViewModel側は次のように書けます。
public MainViewModel()
{
SaveCommand = new Action(Save).ToCommand(() => CanSave);
EditUserCommand = new Action<int>(EditUser).ToCommand(id => id > 0);
}
見た目は「ラムダだけ」を連想させますが、実体はRelayCommandであり、ICommandの契約を満たしています。
CanExecute と CommandManager:UIの更新を正しく起こす
WPFはコマンドの有効/無効をUIフレームワーク側が管理します。CanExecuteの結果が変わったのにボタンのIsEnabledが変化しない場合、CanExecuteChangedを発火させる必要があります。上の実装ではWPFの既定機構CommandManager.RequerySuggestedへイベントを委譲しているため、入力フォーカスの移動やタイマーに合わせて自動再評価されます。
ただし、即時反映したい場合(プロパティ変更直後など)は、RaiseCanExecuteChanged()からCommandManager.InvalidateRequerySuggested()を呼ぶのが手軽です。頻発させるとコストがかかるため、必要な箇所に限定するのがコツです。
よくある落とし穴と回避策
| 症状 | 原因 | 対策 |
|---|---|---|
| (o, e)と書いてしまう | イベントハンドラーの癖。ICommand.Executeは1引数 | _ => ...または(obj) => ...に修正 |
| ボタンが無効化のまま | CanExecuteは変わったが通知していない | 状態変更時にRaiseCanExecuteChanged()を呼び出す |
| 非同期で例外が握りつぶされる/落ちる | async voidの例外伝播 | ViewModel側でtry-catchし、ログ・通知・再試行へ誘導 |
| パラメーターの無効なキャスト | CommandParameterの型と受け側が不一致 | RelayCommand<T>で型安全化。XAML側の型を揃える |
| メモリリークが心配 | イベント購読の保持 | RequerySuggestedは弱参照ベース。長寿命オブジェクトでは解除も意識 |
| UIがカクつく | 重い処理を同期実行 | AsyncRelayCommandでバックグラウンド処理+進捗表示 |
「最小記述」で現場導入するためのテンプレート
プロジェクトに次の3ファイルを入れるだけで、MVVMのコマンド基盤が完成します。
- RelayCommand.cs(上掲の非ジェネリック版)
- RelayCommandOfT.cs(上掲のジェネリック版)
- AsyncRelayCommand.cs(上掲の非同期版)
これでViewModelは「コマンドプロパティをreadonlyで公開し、コンストラクターでラムダを渡す」という統一スタイルにできます。レビュー観点も明確になり、保守性が飛躍的に上がります。
MAUI/XamarinのCommandとの違いと移植のコツ
.NET MAUIにはCommandクラスが標準で存在し、new Command(() => ...)のように書けます。WPFには標準実装がないため、自作RelayCommandを足すか、MVVM系ライブラリ(PrismのDelegateCommand、CommunityToolkit.MvvmのRelayCommand など)を使うのが一般的です。
| 項目 | .NET MAUI | WPF |
|---|---|---|
| 標準コマンド実装 | あり(Command) | なし(自作 or ライブラリ) |
| 非同期対応 | ラッパーで対応が一般的 | AsyncRelayCommandなどを自前で用意 |
| 型安全性 | Command<T>の利用 | RelayCommand<T>を用意 |
移植時は「ICommandの契約に合わせる」「非同期はコマンド内で連打抑止」「CanExecuteChangedの通知」の3点を満たせばスムーズです。
テスト戦略:コマンドはユニットテストしやすい
コマンドのユニットテストは副作用を切り出しやすく、MVVMの要。以下はxUnit想定の簡易例です。
[Fact]
public void SaveCommand_Respects_CanExecute()
{
var vm = new MainViewModel();
vm.CanSave = false;
Assert.False(vm.SaveCommand.CanExecute(null));
vm.CanSave = true;
Assert.True(vm.SaveCommand.CanExecute(null));
}
[Fact]
public async Task LoadCommand_Disables_While_Executing()
{
var vm = new MainViewModel();
Assert.True(vm.LoadCommand.CanExecute(null));
vm.LoadCommand.Execute(null);
Assert.False(vm.LoadCommand.CanExecute(null)); // 実行中は無効
await Task.Delay(300);
Assert.True(vm.LoadCommand.CanExecute(null));
}
非同期コマンドのテストでは完了待ち(Task.Delay)を設け、UI連打抑止が効いているかを確認しましょう。
実務Tips集:現場で効く小ワザ
- CanExecuteの高頻度再評価を避ける:重い検証ロジックはキャッシュ化し、重要なトリガーでのみInvalidate。
- 例外通知の統一:AsyncRelayCommandにエラーフック(
Action<Exception>)を注入し、ViewModelからUI通知へ。 - CommandParameterの型変換:バインディングで
Converterを活用し、XAML側で型を揃える。 - ショートカット対応:コマンドは
InputBindingと相性良好。メニューとボタンで同一コマンドを共有。 - 設計上の分離:ViewModel内でドメイン処理を直接書かず、サービス層へ委譲してテスト容易性を確保。
<Window.InputBindings>
<KeyBinding Command="{Binding SaveCommand}" Modifiers="Ctrl" Key="S" />
</Window.InputBindings>
参考構成:最小MVVMスケルトン
// ViewModelBase(INotifyPropertyChanged)
public abstract class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
protected void Set<T>(ref T field, T value, [CallerMemberName] string? name = null)
{
if (Equals(field, value)) return;
field = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
// MainViewModel(上掲のコマンドを利用)
public sealed class MainViewModel : ViewModelBase
{
// ...(前掲の実装)
}
この骨組みに、RelayCommand / RelayCommand<T> / AsyncRelayCommandを添えるだけで、「ICommandにラムダを渡す」理想的な記法をWPFでも実現できます。
まとめ:最短距離の実装指針
- ラムダ式だけでは不可:CS1660は「ICommandはインターフェース、ラムダはdelegate専用」が原因。
- 実用解:RelayCommand(またはDelegateCommand)を用意し、
new RelayCommand(_ => ...)で渡す。 - 推奨強化:型安全のため
RelayCommand<T>、UXのためAsyncRelayCommand。 - 更新通知:
RaiseCanExecuteChanged()とCommandManagerの使い分けを理解。 - 糖衣構文:拡張メソッドで「ラムダだけ風」に簡潔化できる。
この方針をチーム標準にすれば、WPF MVVMにおけるコマンド実装は短く、読みやすく、テストしやすくなります。

コメント