日程Fit|「いつ空いてますか?」の往復はもう不要。候補日を選んでURLを送るだけ|登録不要|今すぐ無料で使う →

WPF MVVMのICommandにラムダ式は直接代入できる?CS1660の原因とRelayCommand実装・Async対応まで徹底解説

WPFでMVVMを組むと必ず通るのが「ICommandにラムダ式を直で入れたい」問題です。短く書ければ生産性が上がりそうですが、実際にはCS1660で止まってしまう……。本記事はその理由を型レベルから丁寧にひも解き、実運用可能なRelayCommand(DelegateCommand)実装、非同期対応、型安全化、落とし穴回避までを網羅的に解説します。

日程Fit。無料・登録不要。「いつ空いてる?」を、ひとつのリンクで。リンクを送って、○△×でかんたん日程調整。無料で日程を作る。
目次

結論: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>イベントやコールバックで多用
ICommandWPFコマンドの契約(インターフェース)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(() =&gt; CanSave);
    EditUserCommand = new Action&lt;int&gt;(EditUser).ToCommand(id =&gt; id &gt; 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のコマンド基盤が完成します。

  1. RelayCommand.cs(上掲の非ジェネリック版)
  2. RelayCommandOfT.cs(上掲のジェネリック版)
  3. AsyncRelayCommand.cs(上掲の非同期版)

これでViewModelは「コマンドプロパティをreadonlyで公開し、コンストラクターでラムダを渡す」という統一スタイルにできます。レビュー観点も明確になり、保守性が飛躍的に上がります。

MAUI/XamarinのCommandとの違いと移植のコツ

.NET MAUIにはCommandクラスが標準で存在し、new Command(() => ...)のように書けます。WPFには標準実装がないため、自作RelayCommandを足すか、MVVM系ライブラリ(PrismのDelegateCommand、CommunityToolkit.MvvmのRelayCommand など)を使うのが一般的です。

項目.NET MAUIWPF
標準コマンド実装あり(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内でドメイン処理を直接書かず、サービス層へ委譲してテスト容易性を確保。
&lt;Window.InputBindings&gt;
  &lt;KeyBinding Command="{Binding SaveCommand}" Modifiers="Ctrl" Key="S" /&gt;
&lt;/Window.InputBindings&gt;

参考構成:最小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におけるコマンド実装は短く、読みやすく、テストしやすくなります。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次