ViewModelはどのようにフォームを閉じる必要がありますか?


247

WPFとMVVMの問題を学習しようとしていますが、問題が発生しました。この質問は似ていますが、これとまったく同じではありません(handling-dialogs-in-wpf-with-mvvm) ...

MVVMパターンを使用して書かれた「ログイン」フォームがあります。

このフォームには、通常のデータバインディングを使用してXAMLのビューにバインドされているユーザー名とパスワードを保持するViewModelがあります。また、通常のデータバインディングを使用して、フォームの[ログイン]ボタンにバインドされている[ログイン]コマンドもあります。

「Login」コマンドが起動すると、ViewModelの関数が呼び出され、ネットワークを介してデータが送信されてログインします。この関数が完了すると、2つのアクションがあります。

  1. ログインが無効でした-メッセージボックスを表示するだけで問題ありません

  2. ログインは有効でした。ログインフォームを閉じ、trueを返す必要がありDialogResultます...

問題は、ViewModelが実際のビューについて何も知らないため、ビューを閉じて特定のDialogResultを返すように指示するにはどうすればよいですか?CodeBehindにコードを貼り付けたり、ViewをViewModelに渡したりすることはできますが、MVVMのポイント全体を完全に無効にするようです...


更新

結局、MVVMパターンの「純粋さ」に違反し、ビューにClosedイベントを発行させ、Closeメソッドを公​​開させました。その後、ViewModelは単にを呼び出しますview.Close。ビューは、インターフェースを介してのみ認識され、IOCコンテナーを介して配線されるため、テスト性や保守性が失われることはありません。

受け入れられた回答が-5票であるのはかなりばかげているようです!「純粋」でありながら問題を解決することで得られる良い気持ちはよく知っていますが、1行のメソッドを回避するためだけに200行のイベント、コマンド、および動作を考えているのは、私だけではありません。 「パターン」と「純度」の名前は少しばかげています...


2
私は受け入れられた回答に反対票を投じなかったが、反対票決の理由は、たとえそれが1つのケースでうまくいくかもしれないとしても、一般的に役に立たないということだと思います。「ログインフォームは「2つのフィールド」ダイアログですが、もっと複雑な(したがってMVVMを保証する)が他にもたくさんありますが、それでも閉じる必要がある...」
Joeホワイト

1
あなたの意見はわかりますが、個人的には、一般的なケースでも単純なClose方法が最善の解決策であると私は思います。他のより複雑なダイアログの他のすべてはMVVMとデータバインドですが、単純な方法ではなく、ここで巨大な「ソリューション」を実装するのはばかげているように見えました...
Orion Edwards

2
次のリンクでダイアログの結果を確認できます。imsajjad.blogspot.com/ 2010/10 /…。ダイアログの結果を返し、viewModelからビューを閉じます
Asim Sajjad

3
この質問に対する承認済みの回答を変更してください。この機能のためにMVVMの使用を疑問視する人よりもはるかに優れた優れたソリューションがたくさんあります。それは答えではありません、それは回避です。
ScottCher

2
@OrionEdwards私はここでパターンを破るのは正しかったと思います。設計パターンの主な目的は、チーム全体が同じルールに従うことで、開発サイクルのスピードアップ、保守性の向上、コードの簡素化です。これは、外部ライブラリへの依存関係を追加してタスクを実行するために数百のコード行を実装することによっては達成されません。パターンの「純度」を犠牲にするために頑固にする必要があるからといって、はるかに単純なソリューションがあることを完全に無視します。ただ、行ってyour've何の文書に確保し、KISS(あなたのコードをk個の EEP I T SホルトとS imple)。
M463 2015年

回答:


324

シンプルな添付プロパティを記述するというThejuanの回答に触発されました。スタイルもトリガーもありません。代わりに、これを行うことができます:

<Window ...
        xmlns:xc="clr-namespace:ExCastle.Wpf"
        xc:DialogCloser.DialogResult="{Binding DialogResult}">

これは、WPFチームが正しく理解し、そもそもDialogResultを依存関係プロパティにしたのと同じくらいクリーンです。bool? DialogResultプロパティをViewModelに配置してINotifyPropertyChangedを実装するだけで、ViewModelはプロパティを設定するだけでウィンドウを閉じる(そしてそのDialogResultを設定する)ことができます。本来あるべきMVVM。

DialogCloserのコードは次のとおりです。

using System.Windows;

namespace ExCastle.Wpf
{
    public static class DialogCloser
    {
        public static readonly DependencyProperty DialogResultProperty =
            DependencyProperty.RegisterAttached(
                "DialogResult",
                typeof(bool?),
                typeof(DialogCloser),
                new PropertyMetadata(DialogResultChanged));

        private static void DialogResultChanged(
            DependencyObject d,
            DependencyPropertyChangedEventArgs e)
        {
            var window = d as Window;
            if (window != null)
                window.DialogResult = e.NewValue as bool?;
        }
        public static void SetDialogResult(Window target, bool? value)
        {
            target.SetValue(DialogResultProperty, value);
        }
    }
}

これもブログに投稿しました


3
これが私が一番好きな答えです!その添付プロパティを書く良い仕事。
ホルヘヴァルガス

2
素晴らしいオプションですが、このソリューションには微妙なバグがあります。ダイアログのビューモデルがシングルトンの場合、DialogResult値は、ダイアログの次の使用に持ち越されます。つまり、それ自体が表示される前に即座にキャンセルまたは受け入れられるため、ダイアログが再度表示されることはありません。
コーディングが

13
@HiTech Magic、バグはそもそもシングルトンViewModelの使用にあるようです。(ニヤリと)真面目に、なぜ一体、シングルトンのViewModelが欲しいのですか?グローバル変数で変更可能な状態を維持することは悪い考えです。テストは悪夢になり、テストはそもそもMVVMを使用する理由の1つです。
ジョーホワイト

3
MVVMの目的は、ロジックを特定のUIに密結合しないことではありませんか?この場合、bool?WinFormなどの別のUIではほとんどの場合使用できません。DialogCloserはWPFに固有です。では、これはソリューションとしてどのように適合するのでしょうか?また、なぜバインディングを介してウィンドウを閉じるためだけに2x-10xコードを書くのですか?
David Anderson、

2
@DavidAnderson、私はどんな場合でもWinFormsでMVVMを試しません。そのデータバインディングサポートは弱すぎ、MVVMは十分に検討されたバインディングシステムに依存しています。そして、それは2x-10xコードにほど遠いです。そのコード、ウィンドウごとに1回ではなく1 記述します。その後は、1行のバインディングと通知プロパティであり、ビューの他のすべてに既に使用しているのと同じメカニズムを使用します(たとえば、窓)。あなたは他のトレードオフをすることを歓迎しますが、それは私にとって一般的に良い取引のようです。
ジョーホワイト

64

私の観点からは、同じ方法が「ログイン」ウィンドウだけでなく、あらゆる種類のウィンドウに使用されるため、質問はかなり良いです。多くの提案を確認しましたが、どれも問題ありません。MVVM設計パターンの記事からの私の提案を確認してください。

各ViewModelクラスは、タイプのイベントとプロパティWorkspaceViewModelを持つクラスを継承する必要がRequestCloseありCloseCommandますICommandCloseCommandプロパティのデフォルト実装はRequestCloseイベントを発生させます。

ウィンドウを閉じるには、ウィンドウのOnLoadedメソッドをオーバーライドする必要があります。

void CustomerWindow_Loaded(object sender, RoutedEventArgs e)
{
    CustomerViewModel customer = CustomerViewModel.GetYourCustomer();
    DataContext = customer;
    customer.RequestClose += () => { Close(); };
}

またはOnStartupあなたのアプリの方法:

    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);

        MainWindow window = new MainWindow();
        var viewModel = new MainWindowViewModel();
        viewModel.RequestClose += window.Close;
        window.DataContext = viewModel;

        window.Show();
    }

私は推測するRequestCloseイベントとCloseCommandしてプロパティの実装をWorkspaceViewModelかなり明確ですが、私は彼らが一貫して表示されます:

public abstract class WorkspaceViewModel : ViewModelBase
// There's nothing interesting in ViewModelBase as it only implements the INotifyPropertyChanged interface
{
    RelayCommand _closeCommand;
    public ICommand CloseCommand
    {
        get
        {
            if (_closeCommand == null)
            {
                _closeCommand = new RelayCommand(
                   param => Close(),
                   param => CanClose()
                   );
            }
            return _closeCommand;
        }
    }

    public event Action RequestClose;

    public virtual void Close()
    {
        if ( RequestClose != null )
        {
            RequestClose();
        }
    }

    public virtual bool CanClose()
    {
        return true;
    }
}

そしてのソースコードRelayCommand

public class RelayCommand : ICommand
{
    #region Constructors

    public RelayCommand(Action<object> execute, Predicate<object> canExecute)
    {
        if (execute == null)
            throw new ArgumentNullException("execute");

        _execute = execute;
        _canExecute = canExecute;
    }
    #endregion // Constructors

    #region ICommand Members

    [DebuggerStepThrough]
    public bool CanExecute(object parameter)
    {
        return _canExecute == null ? true : _canExecute(parameter);
    }

    public event EventHandler CanExecuteChanged
    {
        add { CommandManager.RequerySuggested += value; }
        remove { CommandManager.RequerySuggested -= value; }
    }

    public void Execute(object parameter)
    {
        _execute(parameter);
    }

    #endregion // ICommand Members

    #region Fields

    readonly Action<object> _execute;
    readonly Predicate<object> _canExecute;

    #endregion // Fields
}

PSそれらのソースのために私を酷く扱わないでください!昨日持っていたら、数時間節約できたでしょう...

PPSコメントや提案は大歓迎です。


2
ええと、customer.RequestCloseXAMLファイルのコードビハインドのイベントハンドラーにフックしたという事実は、MVVMパターンに違反していませんか?Clickとにかく、背後のコードに触れてthis.Close()!を実行したことを確認して、閉じるボタンのイベントハンドラーにバインドすることもできます。正しい?
GONeale 2012年

1
イベントアプローチに関してあまり問題はありませんが、RequestCloseという言葉は好きではありません。私にとっては、ビューの実装に関する多くの知識を意味するためです。私は、IsCancelledなどのプロパティを公開することを好みます。このプロパティは、コンテキストを考えるとより意味があり、ビューが応答で何を行うことになっているのかをほのめかしがちです。
jpierson 2013

18

付属のビヘイビアーを使用してウィンドウを閉じました。ViewModelの「シグナル」プロパティをアタッチされたビヘイビアーにバインドします(実際にはトリガーを使用します)。trueに設定されている場合、ビヘイビアーはウィンドウを閉じます。

http://adammills.wordpress.com/2009/07/01/window-close-from-xaml/


これは、これまでのところ、ウィンドウ内に分離コードを必要としない唯一の答えです(実際には、別のアプローチを提案する代わりに、モーダルウィンドウを閉じます)。残念ながら、StyleとTriggerなど、非常に複雑な処理が必要です。これは、1行のアタッチされた動作で実行できるはずです。
ジョーホワイト

4
これで、1行の添付動作で実行できるようになりました。:私の答えを参照してくださいstackoverflow.com/questions/501886/...
ジョー・ホワイト

15

ここには、MVVMの長所と短所を論じるコメントがたくさんあります。私にとって、Nirに同意します。それはパターンを適切に使用することの問題であり、MVVMは常に適合しません。人々は、ソフトウェア設計の最も重要な原則のすべてを犠牲にして、MVVMに適合させるだけで構わないと思っているようです。

そうは言っても、少しリファクタリングすることで、あなたのケースはうまく適合すると思います。

私が遭遇したほとんどの場合、WPFを使用すると、複数Windowのなしで取得できます。Windowsの代わりにFramesとPages を使用してみてくださいDialogResult

あなたの場合、私の提案はLoginFormViewModelハンドルでLoginCommandあり、ログインが無効な場合は、プロパティをLoginFormViewModel適切な値(falseまたはのような列挙値UserAuthenticationStates.FailedAuthentication)に設定します。成功したログイン(trueまたは他の列挙値)についても同じようにします。次にDataTrigger、さまざまなユーザー認証状態に応答するを使用し、を使用してSetterSourceプロパティを変更できますFrame

ログインウィンドウを返すと、DialogResult混乱します。これDialogResultは実際にはViewModelのプロパティです。私のWPFでの確かに限られた経験では、WinFormsで同じことをどのように実行するかという観点から考えているため、通常は何かが正しくないと感じています。

お役に立てば幸いです。


10

ログインダイアログが作成される最初のウィンドウであると想定して、LoginViewModelクラス内でこれを試してください。

    void OnLoginResponse(bool loginSucceded)
    {
        if (loginSucceded)
        {
            Window1 window = new Window1() { DataContext = new MainWindowViewModel() };
            window.Show();

            App.Current.MainWindow.Close();
            App.Current.MainWindow = window;
        }
        else
        {
            LoginError = true;
        }
    }

男性これはシンプルで、うまく機能します。現在、私はこのアプローチを使用しています。
Erre Efe、2011

MAINウィンドウでのみ機能します。他のウィンドウには使用しないでください。
Oleksii 2015年

7

これはシンプルでクリーンなソリューションです。ViewModelにイベントを追加し、そのイベントが発生したときにウィンドウを閉じるようにウィンドウに指示します。

詳細については、私のブログ記事、ViewModelからウィンドウを閉じるを参照してください。

XAML:

<Window
  x:Name="this"
  xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"  
  xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions">
  <i:Interaction.Triggers>
    <i:EventTrigger SourceObject="{Binding}" EventName="Closed">
      <ei:CallMethodAction
        TargetObject="{Binding ElementName=this}"
        MethodName="Close"/>
    </i:EventTrigger>
  </i:Interaction.Triggers>
<Window>

ViewModel:

private ICommand _SaveAndCloseCommand;
public ICommand SaveAndCloseCommand
{
  get
  {
    return _SaveAndCloseCommand ??
      (_SaveAndCloseCommand = new DelegateCommand(SaveAndClose));
  }
}
private void SaveAndClose()
{
  Save();
  Close();
}

public event EventHandler Closed;
private void Close()
{
  if (Closed != null) Closed(this, EventArgs.Empty);
}

注:この例ではPrism DelegateCommandPrism:Commandingを参照)をICommand使用していますが、そのために任意の実装を使用できます。

この公式パッケージのビヘイビアーを使用できます。


2
+1ただし、回答自体に詳細を入力する必要があります。たとえば、このソリューションでは、Expression Blend Intertivityアセンブリへの参照が必要です。
2011

6

これを処理する方法は、ViewModelにイベントハンドラーを追加することです。ユーザーが正常にログインすると、イベントが発生します。私のビューでは、このイベントにアタッチし、イベントが発生するとウィンドウを閉じます。


2
それも私が普段していることです。新しいすべてのwpf-commandingのものを考えると、それは少し汚いように見えますが。
Botz3000 2009

4

これが私が最初にやったことですが、うまくいきますが、かなり時間がかかり、醜いようです(グローバルな静的なものは決して良いことではありません)

1:App.xaml.cs

public partial class App : Application
{
    // create a new global custom WPF Command
    public static readonly RoutedUICommand LoggedIn = new RoutedUICommand();
}

2:LoginForm.xaml

// bind the global command to a local eventhandler
<CommandBinding Command="client:App.LoggedIn" Executed="OnLoggedIn" />

3:LoginForm.xaml.cs

// implement the local eventhandler in codebehind
private void OnLoggedIn( object sender, ExecutedRoutedEventArgs e )
{
    DialogResult = true;
    Close();
}

4:LoginFormViewModel.cs

// fire the global command from the viewmodel
private void OnRemoteServerReturnedSuccess()
{
    App.LoggedIn.Execute(this, null);
}

その後、このコードをすべて削除しLoginFormViewModel、そのビューでCloseメソッドを呼び出しました。それは結局ずっと素晴らしくて簡単に追跡できるようになりました。パターンのポイントは、アプリが何をしているかをより簡単に理解できるようにすることです。この場合、MVVMは、私がそれを使用しなかった場合よりも理解をはるかに難しくし、現在はアンチパターンになりました。


3

参考までに、私はこれと同じ問題に遭遇し、グローバルまたは静的を必要としない回避策を見つけたと思いますが、それは最善の答えではないかもしれません。私はあなた達にあなた自身でそれを決定させます。

私の場合、表示するウィンドウをインスタンス化するViewModel(これをViewModelMainと呼ぶことにします)は、LoginFormViewModelについても認識しています(上記の状況を例として使用しています)。

したがって、ICommandタイプのLoginFormViewModelにプロパティを作成しました(これをCloseWindowCommandと呼びましょう)。次に、Windowで.ShowDialog()を呼び出す前に、LoginFormViewModelのCloseWindowCommandプロパティを、インスタンス化したWindowのwindow.Close()メソッドに設定しました。次に、LoginFormViewModel内で、CloseWindowCommand.Execute()を呼び出してウィンドウを閉じます。

これは私が想定している少しの回避策/ハックですが、MVVMパターンを実際に壊すことなくうまく機能します。

このプロセスを自由に批評してください、私はそれを受け入れます!:)


完全に理解したかどうかはわかりませんが、これは、LoginWindowの前にMainWindowをインスタンス化する必要があるという意味ではありませんか?それは私が可能な場合は避けたいのですが何か
オリオンエドワーズ

3

これはおそらく非常に遅いですが、同じ問題に遭遇し、私にとってはうまくいく解決策を見つけました。

ダイアログなしでアプリを作成する方法がわかりません(たぶんそれは単なるマインドブロックです)。そのため、MVVMに行き詰まり、ダイアログを表示していました。だから私はこのCodeProjectの記事に出くわしました:

http://www.codeproject.com/KB/WPF/XAMLDialog.aspx

これは、基本的にウィンドウが別のウィンドウのビジュアルツリー内にあることを許可するUserControlです(xamlでは許可されていません)。また、IsShowingと呼ばれるブールDependencyPropertyを公開します。

次のようなスタイルを設定できます。通常は、resourcedictionaryで、トリガーのコントロールの!= nullのContentプロパティが常にダイアログを表示するようにします。

<Style TargetType="{x:Type d:Dialog}">
    <Style.Triggers>
        <Trigger Property="HasContent"  Value="True">
            <Setter Property="Showing" Value="True" />
        </Trigger>
    </Style.Triggers>
</Style>

ダイアログを表示したいビューでは、次のようにします。

<d:Dialog Content="{Binding Path=DialogViewModel}"/>

また、ViewModelでは、プロパティを値に設定するだけです(注:ViewModelクラスは、発生したことをビューが知るためにINotifyPropertyChangedをサポートする必要があります)。

そのようです:

DialogViewModel = new DisplayViewModel();

ViewModelをViewと一致させるには、resourcedictionaryに次のようなものが必要です。

<DataTemplate DataType="{x:Type vm:DisplayViewModel}">
    <vw:DisplayView/>
</DataTemplate>

これらすべてを使用して、ダイアログを表示するための1行のコードを取得します。あなたが得る問題は、上記のコードだけではダイアログを本当に閉じることができないということです。そのため、DisplayViewModelが継承するViewModel基本クラスにイベントを配置する必要があるのは上記のコードの代わりに、次のように記述します

        var vm = new DisplayViewModel();
        vm.RequestClose += new RequestCloseHandler(DisplayViewModel_RequestClose);
        DialogViewModel = vm;

その後、コールバックを介してダイアログの結果を処理できます。

これは少し複雑に見えるかもしれませんが、土台が整えば、それはかなり簡単です。これも私の実装ですが、他にもあると思います:)

これがお役に立てば幸いです。


3

わかりました。この質問は6歳近くになっていて、ここでは正しい答えだと思うものがまだ見つからないので、「2セント」を共有させてください...

私は実際にそれを行う2つの方法があります。1つ目は単純な方法です... 2つ目は右の方法です。したがって、適切な方法を探している場合は、1をスキップして2にジャンプしてください。

1.すばやく簡単(ただし完全ではない)

小さなプロジェクトしかない場合は、ViewModelにCloseWindowActionを作成することがあります。

        public Action CloseWindow { get; set; } // In MyViewModel.cs

そして、ビューを作成する人、またはビューの背後にあるコードで、アクションが呼び出すメソッドを設定するだけです。

(MVVMはViewとViewModelの分離に関するものであることを思い出してください... Viewの背後にあるコードは依然としてViewであり、適切な分離が存在する限り、パターンに違反していません)

ViewModelが新しいウィンドウを作成する場合:

private void CreateNewView()
{
    MyView window = new MyView();
    window.DataContext = new MyViewModel
                             {
                                 CloseWindow = window.Close,
                             }; 
    window.ShowDialog();
}

または、メインウィンドウに配置する場合は、ビューのコンストラクタの下に配置します。

public MyView()
{
    InitializeComponent();           
    this.DataContext = new MainViewModel
                           {
                                CloseWindow = this.Close
                           };
}

ウィンドウを閉じたいときは、ViewModelのActionを呼び出すだけです。


2.正しい方法

これを行う適切な方法は、Prism(IMHO)を使用することであり、それに関するすべてはここ見つけることができます

インタラクションリクエストを作成し、新しいウィンドウで必要なデータを入力して、ランチし、閉じて、データを受け取ることさえできます。このすべてがカプセル化され、MVVMで承認されています。あなたも、ウィンドウが閉じられたかのステータスを取得、ユーザー場合のようなCanceledまたはAccepted(OKボタン)ウィンドウとあなたがそれを必要とする場合、データがバックアップ。これは少し複雑で答え#1ですが、はるかに完全で、Microsoftによる推奨パターンです。

私が与えたリンクにはすべてのコードスニペットと例が含まれているので、ここにコードを配置することはありません。Prismクイックスタートのダウンロードの記事を読んで実行するだけです。それを機能させるが、利点は単にウィンドウを閉じるよりも大きい。


良い方法ですが、ViewModelsの解決と割り当ては常に簡単なことではありません。同じビューモデルが多くのウィンドウのDataContextである場合はどうなりますか?
カイロ・レン

次に、すべてのウィンドウを一度に閉じる必要があると思います。アクションは一度に多くのデリゲートをトリガーし、デリゲート+=を追加するために使用し、アクションを呼び出すだけです。すべてのウィンドウを起動します。または、 VMに特別なロジックを作成して、閉じるウィンドウを認識できるようにする必要があります(おそらくCloseアクションのコレクションがあるかもしれません)...しかし、1つのVMに複数のビューをバインドすることはベストプラクティスではないと思います。 1つのビューと1つのVMインスタンスを互いにバインドし、場合によってはすべてのビューにバインドされているすべての子VMを管理する親VMを管理する方が適切です。
mFeinstein

3
public partial class MyWindow: Window
{
    public ApplicationSelection()
    {
      InitializeComponent();

      MyViewModel viewModel = new MyViewModel();

      DataContext = viewModel;

      viewModel.RequestClose += () => { Close(); };

    }
}

public class MyViewModel
{

  //...Your code...

  public event Action RequestClose;

  public virtual void Close()
  {
    if (RequestClose != null)
    {
      RequestClose();
    }
  }

  public void SomeFunction()
  {
     //...Do something...
     Close();
  }
}

2

ViewModelに、Viewが登録するイベントを公開させることができます。次に、ViewModelがビューを閉じる時間を決定すると、そのイベントを起動してビューを閉じます。特定の結果値を返したい場合は、そのためのViewModelにプロパティを設定します。


私はこれに同意します-シンプルさは貴重です。次のジュニア開発者がこのプロジェクトを引き継ぐために雇われたときに何が起こるかについて考えなければなりません。私が思うに、あなたが説明するように、彼はこれを正しく行うチャンスがはるかに良いでしょう。あなたが自分でこのコードを永遠に維持するつもりだと思わない限り?+1
Dean

2

膨大な数の回答に追加するために、以下を追加します。ViewModelにICommandがあり、そのコマンドでウィンドウ(またはそのほかのアクション)を閉じる場合は、次のようなものを使用できます。

var windows = Application.Current.Windows;
for (var i=0;i< windows.Count;i++ )
    if (windows[i].DataContext == this)
        windows[i].Close();

それは完璧ではなく、テストするのが難しいかもしれません(静的を模擬/スタブ化するのが難しいため)が、他のソリューションよりクリーン(IMHO)です。

エリック


あなたの簡単な答えを見たとき、私はとても幸せになりました!しかし、それも機能しません!Visual Basicで開閉する必要があります。VBにおける(windows [i] .DataContext == this)の同等性を知っていますか?
イーサン

やっとできた!:)ありがとう。if windows(i).DataContext Is me
Ehsan

ウィンドウを開く簡単な方法も知っていますか?子ビューモデルでも一部のデータを送受信する必要があり、その逆も同様です。
イーサン

1

私はジョーホワイトのソリューションを実装しましたが、「ウィンドウが作成され、ダイアログとして表示された後にのみ、DialogResultを設定できる」という問題が発生しました。

ビューが閉じられた後も私はViewModelを維持していましたが、後で同じVMを使用して新しいビューを開くこともありました。古いビューがガベージコレクションされる前に新しいビューを閉じると、DialogResultChangedがDialogResultを設定しようとしたようですが閉じたウィンドウにプロパティため、エラーが発生したようです。

私の解決策は、ウィンドウのIsLoadedプロパティをチェックするようにDialogResultChangedを変更することでした:

private static void DialogResultChanged(
    DependencyObject d,
    DependencyPropertyChangedEventArgs e)
{
    var window = d as Window;
    if (window != null && window.IsLoaded)
        window.DialogResult = e.NewValue as bool?;
}

この変更を行った後、閉じられたダイアログへの添付は無視されます。


ありがとうございます。私も同じ問題を抱えていました
DJ Burb 2014年

1

プログラムで作成したウィンドウにユーザーコントロールを表示する必要があったため、結局、Joe Whiteの回答Adam Millsの回答のコードを混ぜ合わせました。したがって、DialogCloserはウィンドウ上にある必要はなく、ユーザーコントロール自体の上に置くことができます

<UserControl ...
    xmlns:xw="clr-namespace:Wpf"
    xw:DialogCloser.DialogResult="{Binding DialogResult}">

そして、DialogCloserは、ウィンドウ自体にアタッチされていない場合、ユーザーコントロールのウィンドウを見つけます。

namespace Wpf
{
  public static class DialogCloser
  {
    public static readonly DependencyProperty DialogResultProperty =
        DependencyProperty.RegisterAttached(
            "DialogResult",
            typeof(bool?),
            typeof(DialogCloser),
            new PropertyMetadata(DialogResultChanged));

    private static void DialogResultChanged(
        DependencyObject d,
        DependencyPropertyChangedEventArgs e)
    {
      var window = d.GetWindow();
      if (window != null)
        window.DialogResult = e.NewValue as bool?;
    }

    public static void SetDialogResult(DependencyObject target, bool? value)
    {
      target.SetValue(DialogResultProperty, value);
    }
  }

  public static class Extensions
  {
    public static Window GetWindow(this DependencyObject sender_)
    {
      Window window = sender_ as Window;        
      return window ?? Window.GetWindow( sender_ );
    }
  }
}

1

ここでは動作が最も便利な方法です。

  • 一方から、それは与えられたビューモデルにバインドすることができます(「フォームを閉じる!」

  • 別の手から、フォーム自体にアクセスできるため、必要なフォーム固有のイベントをサブスクライブしたり、確認ダイアログを表示したりすることができます。

必要な振る舞いを書くことは、初めて退屈に見えるかもしれません。ただし、これからは、必要なすべての単一フォームで正確な1行XAMLスニペットを使用して再利用できます。必要に応じて、別のアセンブリとして抽出し、次のプロジェクトに含めることができます。


0

なぜウィンドウをコマンドパラメータとして渡さないのですか?

C#:

 private void Cancel( Window window )
  {
     window.Close();
  }

  private ICommand _cancelCommand;
  public ICommand CancelCommand
  {
     get
     {
        return _cancelCommand ?? ( _cancelCommand = new Command.RelayCommand<Window>(
                                                      ( window ) => Cancel( window ),
                                                      ( window ) => ( true ) ) );
     }
  }

XAML:

<Window x:Class="WPFRunApp.MainWindow"
        x:Name="_runWindow"
...
   <Button Content="Cancel"
           Command="{Binding Path=CancelCommand}"
           CommandParameter="{Binding ElementName=_runWindow}" />

VMをウィンドウタイプに制限することは良い考えではないと思います。
Shimmy Weitzhandler、2011年

2
私は、VMをWindow「純粋な」MVVMではないタイプに制限することは良い考えではないと思います。VMがオブジェクトに制限されていない、この回答を参照してくださいWindow
Shimmy Weitzhandler、2011年

このようにして、依存関係がボタンに置​​かれているのは確かに常に状況ではありません。また、UIタイプをViewModelに渡すことは悪い習慣です。
カイロ・レン

0

別の解決策は、DialogResultのようなビューモデルでINotifyPropertyChangedを使用してプロパティを作成し、コードビハインドで次のように記述します。

public class SomeWindow: ChildWindow
{
    private SomeViewModel _someViewModel;

    public SomeWindow()
    {
        InitializeComponent();

        this.Loaded += SomeWindow_Loaded;
        this.Closed += SomeWindow_Closed;
    }

    void SomeWindow_Loaded(object sender, RoutedEventArgs e)
    {
        _someViewModel = this.DataContext as SomeViewModel;
        _someViewModel.PropertyChanged += _someViewModel_PropertyChanged;
    }

    void SomeWindow_Closed(object sender, System.EventArgs e)
    {
        _someViewModel.PropertyChanged -= _someViewModel_PropertyChanged;
        this.Loaded -= SomeWindow_Loaded;
        this.Closed -= SomeWindow_Closed;
    }

    void _someViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName == SomeViewModel.DialogResultPropertyName)
        {
            this.DialogResult = _someViewModel.DialogResult;
        }
    }
}

最も重要なフラグメントは_someViewModel_PropertyChangedです。 DialogResultPropertyNameのパブリックconst文字列にすることができますSomeViewModel

これをViewModelで行うのが難しい場合に備えて、この種のトリックを使用してビューコントロールにいくつかの変更を加えます。ViewModelのOnPropertyChangedを使用すると、Viewでやりたいことが何でもできます。ViewModelはまだ「単体テスト可能」であり、コードビハインドのコードのいくつかの小さな行は違いがありません。


0

私はこのように行きます:

using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;    
using GalaSoft.MvvmLight.Messaging; 

// View

public partial class TestCloseWindow : Window
{
    public TestCloseWindow() {
        InitializeComponent();
        Messenger.Default.Register<CloseWindowMsg>(this, (msg) => Close());
    }
}

// View Model

public class MainViewModel: ViewModelBase
{
    ICommand _closeChildWindowCommand;

    public ICommand CloseChildWindowCommand {
        get {
            return _closeChildWindowCommand?? (_closeChildWindowCommand = new RelayCommand(() => {
                Messenger.Default.Send(new CloseWindowMsg());
        }));
        }
    }
}

public class CloseWindowMsg
{
}

0

私はすべての回答を読みましたが、私は言わなければなりません。それらのほとんどは、十分によくないか、さらに悪いだけです。

ダイアログウィンドウを表示してダイアログの結果を返す責任があるDialogServiceクラスでこれを美しく処理できます。実装と使用法を示すサンプルプロジェクトを作成しました。

ここに最も重要な部分があります:

//we will call this interface in our viewmodels
public interface IDialogService
{
    bool? ShowDialog(object dialogViewModel, string caption);
}

//we need to display logindialog from mainwindow
public class MainWindowViewModel : ViewModelBase
{
    public string Message {get; set;}
    public void ShowLoginCommandExecute()
    {
        var loginViewModel = new LoginViewModel();
        var dialogResult = this.DialogService.ShowDialog(loginViewModel, "Please, log in");

        //after dialog is closed, do someting
        if (dialogResult == true && loginViewModel.IsLoginSuccessful)
        {
            this.Message = string.Format("Hello, {0}!", loginViewModel.Username);
        }
    }
}


public class DialogService : IDialogService
{
    public bool? ShowDialog(object dialogViewModel, string caption)
    {
        var contentView = ViewLocator.GetView(dialogViewModel);
        var dlg = new DialogWindow
        {
            Title = caption
        };
        dlg.PART_ContentControl.Content = contentView;

        return dlg.ShowDialog();
    }
}

これはもっと単純ではないですか?EventAggregatorや他の同様のソリューションよりも、より単純で、より読みやすく、そして最後ですが、少なくともデバッグが簡単ですか?

ご覧のとおり、私のビューモデルでは、ここの私の投稿で説明されているViewModelの最初のアプローチを使用しています。 ます。WPFでViewModelからViewを呼び出すためのベストプラクティス

もちろん、実際にはDialogService.ShowDialog、ボタンやコマンドなど、ダイアログを構成するためのオプションがさらに多くなければなりません。その方法はいくつかありますが、範囲外です:)


0

これは、viewmodelを使用してこれを行う方法の質問には答えませんが、XAML +ブレンドSDKのみを使用して行う方法を示しています。

私はBlend SDKから2つのファイルをダウンロードして使用することを選択しました。どちらもMicrosoftからNuGetを通じてパッケージとして使用できます。ファイルは次のとおりです。

System.Windows.Interactivity.dllおよびMicrosoft.Expression.Interactions.dll

Microsoft.Expression.Interactions.dllを使用すると、ビューモデルやその他のターゲットでプロパティを設定したり、メソッドを呼び出したり、その他のウィジェットを内部に組み込んだりすることができます。

一部のXAML:

<Window x:Class="Blah.Blah.MyWindow"
    ...
    xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
    xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
  ...>
 <StackPanel>
    <Button x:Name="OKButton" Content="OK">
       <i:Interaction.Triggers>
          <i:EventTrigger EventName="Click">
             <ei:ChangePropertyAction
                      TargetObject="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}"
                      PropertyName="DialogResult"
                      Value="True"
                      IsEnabled="{Binding SomeBoolOnTheVM}" />                                
          </i:EventTrigger>
    </Button>
    <Button x:Name="CancelButton" Content="Cancel">
       <i:Interaction.Triggers>
          <i:EventTrigger EventName="Click">
             <ei:ChangePropertyAction
                      TargetObject="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}"
                      PropertyName="DialogResult"
                      Value="False" />                                
          </i:EventTrigger>
    </Button>

    <Button x:Name="CloseButton" Content="Close">
       <i:Interaction.Triggers>
                <i:EventTrigger EventName="Click">
                    <!-- method being invoked should be void w/ no args -->
                    <ei:CallMethodAction
                        TargetObject="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}"
                        MethodName="Close" />
                </i:EventTrigger>
            </i:Interaction.Triggers>
    </Button>
 <StackPanel>
</Window>

単純なOK / Cancel動作を行う場合は、ウィンドウがWindow.ShowDialog()で表示されている限り、IsDefaultプロパティとIsCancelプロパティを使用して回避できます。
IsDefaultプロパティがtrueに設定されているボタンで個人的に問題が発生しましたが、ページが読み込まれるとボタンが非表示になりました。表示された後はうまく再生したくなかったので、代わりに上に示したようにWindow.DialogResultプロパティを設定するだけで機能します。


0

これは、バグのない簡単な解決策(ソースコードを含む)です。

  1. からViewModelを派生させる INotifyPropertyChanged

  2. ViewModelで監視可能なプロパティCloseDialogを作成する

    public void Execute()
    {
        // Do your task here
    
        // if task successful, assign true to CloseDialog
        CloseDialog = true;
    }
    
    private bool _closeDialog;
    public bool CloseDialog
    {
        get { return _closeDialog; }
        set { _closeDialog = value; OnPropertyChanged(); }
    }
    
    public event PropertyChangedEventHandler PropertyChanged;
    
    private void OnPropertyChanged([CallerMemberName]string property = "")
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(property));
        }
    }

    }

  3. このプロパティ変更のハンドラーをビューにアタッチします

        _loginDialogViewModel = new LoginDialogViewModel();
        loginPanel.DataContext = _loginDialogViewModel;
        _loginDialogViewModel.PropertyChanged += OnPropertyChanged;
  4. これでほぼ完了です。イベントハンドラでDialogResult = true

    protected void OnPropertyChanged(object sender, PropertyChangedEventArgs args)
    {
        if (args.PropertyName == "CloseDialog")
        {
            DialogResult = true;
        }
    }

0

/ anyにを作成Dependency Propertyします(または、閉じます)。以下のように:ViewUserControlWindow

 public bool CloseTrigger
        {
            get { return (bool)GetValue(CloseTriggerProperty); }
            set { SetValue(CloseTriggerProperty, value); }
        }

        public static readonly DependencyProperty CloseTriggerProperty =
            DependencyProperty.Register("CloseTrigger", typeof(bool), typeof(ControlEventBase), new PropertyMetadata(new PropertyChangedCallback(OnCloseTriggerChanged)));

        private static void OnCloseTriggerChanged(DependencyObject dp, DependencyPropertyChangedEventArgs e)
        {
            //write Window Exit Code
        }

そして、それをViewModelのプロパティからバインドします。

<Window x:Class="WpfStackOverflowTempProject.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow"  Width="525"
        CloseTrigger="{Binding Path=CloseWindow,Mode=TwoWay}"

プロパティインVeiwModel

private bool closeWindow;

    public bool CloseWindow
    {
        get { return closeWindow; }
        set 
        { 
            closeWindow = value;
            RaiseChane("CloseWindow");
        }
    }

次にCloseWindow、ViewModel の値を変更して、閉じる操作をトリガーします。:)


-2

ウィンドウを閉じる必要がある場合は、これをビューモデルに配置するだけです。

ただ

  foreach (Window window in Application.Current.Windows)
        {
            if (window.DataContext == this)
            {
                window.Close();
                return;
            }
        }

AのViewModelは含んではならないのUIElementを、これはバグの作成することができますので、どのような方法で
WiiMaxx

複数のウィンドウでDataContextが継承されている場合はどうなりますか?
カイロ・レン

ta-da、これは完全にMVVMではありません。
Alexandru Dicu

-10
Application.Current.MainWindow.Close() 

もういい!


3
-1閉じたいウィンドウがメインウィンドウの場合にのみ当てはまります...ログインダイアログの想定はほとんどありません...
サーペン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.