Bagaimana cara ViewModel menutup formulir?

248

Saya mencoba mempelajari WPF dan masalah MVVM, tetapi menemui hambatan. Pertanyaan ini mirip tetapi tidak persis sama dengan pertanyaan ini (menangani-dialog-in-wpf-with-mvvm) ...

Saya memiliki formulir "Login" yang ditulis menggunakan pola MVVM.

Formulir ini memiliki ViewModel yang menyimpan Username dan Password, yang terikat ke tampilan di XAML menggunakan data binding normal. Ia juga memiliki perintah "Login" yang terikat ke tombol "Login" pada formulir, agan menggunakan penyatuan data normal.

Saat perintah "Login" diaktifkan, ini memanggil fungsi di ViewModel yang berbunyi dan mengirim data melalui jaringan untuk masuk. Saat fungsi ini selesai, ada 2 tindakan:

  1. Login tidak valid - kami hanya menampilkan MessageBox dan semuanya baik-baik saja

  2. Login itu valid, kita perlu menutup formulir Login dan mengembalikannya benar sebagai DialogResult...

Masalahnya adalah, ViewModel tidak tahu apa-apa tentang tampilan aktual, jadi bagaimana cara menutup tampilan dan menyuruhnya mengembalikan DialogResult tertentu ?? Saya bisa menempelkan beberapa kode di CodeBehind, dan / atau meneruskan View ke ViewModel, tapi sepertinya itu akan mengalahkan keseluruhan poin MVVM sepenuhnya ...


Memperbarui

Pada akhirnya saya hanya melanggar "kemurnian" pola MVVM dan meminta View mempublikasikan Closedacara, dan mengekspos Closemetode. ViewModel kemudian akan memanggil view.Close. Tampilan ini hanya diketahui melalui antarmuka dan dihubungkan melalui wadah IOC, jadi tidak ada kemampuan pengujian atau pemeliharaan yang hilang.

Tampaknya agak konyol bahwa jawaban yang diterima adalah -5 suara! Sementara saya sangat menyadari perasaan baik yang didapat seseorang dengan memecahkan masalah sambil menjadi "murni", Tentunya saya bukan satu-satunya yang berpikir bahwa 200 baris kejadian, perintah dan perilaku hanya untuk menghindari metode satu baris dalam nama "pola" dan "kemurnian" agak konyol ....

Orion Edwards
sumber
2
Saya tidak memberikan suara negatif untuk jawaban yang diterima, tetapi saya menduga alasan downvote tersebut adalah karena itu tidak membantu secara umum, meskipun mungkin berhasil dalam satu kasus. Anda mengatakannya sendiri di komentar lain: "Meskipun formulir login adalah dialog 'dua bidang', saya memiliki banyak lainnya yang jauh lebih kompleks (dan karenanya menjamin MVVM), tetapi masih perlu ditutup ..."
Joe Putih
1
Saya mengerti maksud Anda, tetapi saya pribadi berpikir bahwa bahkan untuk kasus umum Closemetode sederhana masih merupakan solusi terbaik. Segala sesuatu yang lain pada dialog lain yang lebih kompleks adalah MVVM dan databound, tetapi tampaknya konyol untuk menerapkan "solusi" besar di sini, bukan hanya metode sederhana ...
Orion Edwards
2
Anda dapat memeriksa tautan berikut untuk hasil dialog asimsajjad.blogspot.com/2010/10/… , yang akan mengembalikan dialog resutl dan menutup tampilan dari viewModel
Asim Sajjad
3
Harap ubah jawaban yang diterima untuk pertanyaan ini. Ada banyak solusi bagus yang jauh lebih baik daripada seseorang yang mempertanyakan penggunaan MVVM untuk fungsi ini. Itu bukan jawaban, itu penghindaran.
ScottCher
2
@OrionEdwards Saya pikir Anda benar untuk memecahkan pola di sini. Tujuan utama pola desain adalah untuk mempercepat siklus pengembangan, meningkatkan pemeliharaan, dan menyederhanakan kode Anda dengan membuat seluruh tim mengikuti aturan yang sama. Hal ini tidak dicapai dengan menambahkan dependensi pada perpustakaan eksternal dan menerapkan ratusan baris kode untuk menyelesaikan tugas, sama sekali mengabaikan bahwa ada solusi yang jauh lebih sederhana, hanya karena seseorang terlalu keras kepala untuk mengorbankan "kemurnian" pola. Pastikan saja untuk mendokumentasikan apa yang telah Anda lakukan dan KISS kode Anda ( k eep i t s hort and s imple).
M463

Jawaban:

326

Saya terinspirasi oleh jawaban Thejuan untuk menulis properti terlampir yang lebih sederhana. Tanpa gaya, tidak ada pemicu; sebaliknya, Anda dapat melakukan ini:

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

Ini hampir sebersih jika tim WPF telah melakukannya dengan benar dan menjadikan DialogResult sebagai properti dependensi sejak awal. Cukup letakkan bool? DialogResultproperti di ViewModel Anda dan implementasikan INotifyPropertyChanged, dan voilà, ViewModel Anda bisa menutup Jendela (dan menyetel DialogResult) hanya dengan menyetel properti. MVVM sebagaimana mestinya.

Berikut kode untuk 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);
        }
    }
}

Saya juga memposting ini di blog saya .

Joe White
sumber
3
Yang ini adalah jawaban yang paling saya suka! Pekerjaan bagus menulis properti terlampir itu.
Jorge Vargas
2
Pilihan bagus, tetapi ada bug halus dalam solusi ini. Jika Model Tampilan untuk dialog tersebut adalah tunggal, nilai DialogResult akan diteruskan ke penggunaan dialog berikutnya. Artinya, dialog akan langsung membatalkan atau menerima sebelum menampilkan dirinya sendiri sehingga dialog tidak akan muncul untuk kedua kalinya.
Gone Coding
13
@HiTech Magic, kedengarannya seperti bug pertama kali menggunakan ViewModel tunggal. (menyeringai) Serius, mengapa Anda menginginkan ViewModel tunggal? Merupakan ide yang buruk untuk mempertahankan status yang bisa berubah dalam variabel global. Membuat pengujian menjadi mimpi buruk, dan pengujian adalah salah satu alasan Anda menggunakan MVVM sejak awal.
Joe White
3
Bukankah tujuan MVVM untuk tidak menggabungkan logika Anda dengan UI tertentu? Dalam hal ini, bool? pasti tidak dapat digunakan oleh UI lain seperti WinForm, dan DialogCloser khusus untuk WPF. Jadi bagaimana ini cocok sebagai solusi? Juga, mengapa menulis kode 2x-10x hanya untuk menutup Window melalui Binding?
David Anderson
3
@DavidAnderson, saya tidak akan mencoba MVVM dengan WinForms dalam hal apapun; dukungan penyatuan data terlalu lemah, dan MVVM mengandalkan sistem pengikatan yang dipikirkan dengan matang. Dan itu jauh dari kode 2x-10x. Anda menulis kode itu sekali , tidak sekali untuk setiap jendela. Setelah itu, ini adalah pengikatan satu baris ditambah properti pemberitahuan, menggunakan mekanisme yang sama yang sudah Anda gunakan untuk semua hal lain dalam tampilan Anda (jadi, misalnya, Anda tidak perlu memasukkan antarmuka tampilan tambahan hanya untuk menangani penutupan jendela). Anda dipersilakan untuk melakukan pengorbanan lain, tetapi secara umum tampaknya kesepakatan yang baik bagi saya.
Joe White
64

Dari sudut pandang saya, pertanyaannya cukup bagus karena pendekatan yang sama akan digunakan tidak hanya untuk jendela "Login", tetapi untuk semua jenis jendela. Saya telah meninjau banyak saran dan tidak ada yang OK untuk saya. Silakan tinjau saran saya yang diambil dari artikel pola desain MVVM .

Setiap kelas ViewModel harus mewarisi dari WorkspaceViewModelyang memiliki RequestCloseacara dan CloseCommandproperti ICommandtipe. Implementasi default CloseCommandproperti akan memunculkan RequestCloseacara tersebut.

Untuk menutup jendela, OnLoadedmetode jendela Anda harus diganti:

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

atau OnStartupmetode aplikasi Anda:

    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();
    }

Saya kira implementasi RequestCloseacara dan CloseCommandproperti di WorkspaceViewModelcukup jelas, tetapi saya akan menunjukkannya agar konsisten:

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;
    }
}

Dan kode sumber dari 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 Jangan perlakukan saya dengan buruk untuk sumber-sumber itu! Jika saya memilikinya kemarin, itu akan menyelamatkan saya beberapa jam ...

PPS Setiap komentar atau saran dipersilakan.

Budda
sumber
2
Umm, fakta Anda terhubung ke event handler customer.RequestClosedi kode di belakang file XAML Anda bukankah itu melanggar pola MVVM? Anda bisa saja mengikat ke Clickevent handler pada tombol tutup Anda di tempat pertama melihat Anda telah menyentuh kode di belakangnya dan melakukan this.Close()! Baik?
GONeale
2
Saya tidak memiliki terlalu banyak masalah dengan pendekatan acara tetapi saya tidak suka kata RequestClose karena bagi saya kata itu masih menyiratkan banyak pengetahuan tentang implementasi View. Saya lebih suka mengekspos properti seperti IsCancelled yang cenderung lebih bermakna mengingat konteksnya dan kurang menyiratkan tentang apa yang seharusnya dilakukan tampilan sebagai tanggapan.
jpierson
18

Saya menggunakan perilaku terlampir untuk menutup jendela. Ikat properti "sinyal" pada ViewModel Anda ke perilaku terlampir (saya sebenarnya menggunakan pemicu). Jika disetel ke true, perilaku tersebut menutup jendela.

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

Adam Mills
sumber
Sejauh ini, ini adalah satu-satunya jawaban yang tidak memerlukan codebehind apa pun di Window (dan sebenarnya menutup jendela modal, alih-alih menyarankan pendekatan lain). Sayangnya itu membutuhkan begitu banyak kerumitan, dengan Style dan Trigger dan semua kotoran itu - sepertinya ini benar-benar harus bisa dilakukan dengan perilaku satu baris yang melekat.
Joe White
4
Sekarang ini bisa dilakukan dengan perilaku satu baris yang terpasang. Lihat jawaban saya: stackoverflow.com/questions/501886/…
Joe White
15

Ada banyak komentar yang memperdebatkan pro dan kontra MVVM di sini. Bagi saya, saya setuju dengan Nir; ini masalah menggunakan pola dengan tepat dan MVVM tidak selalu cocok. Orang-orang tampaknya rela mengorbankan semua prinsip terpenting desain perangkat lunak HANYA agar sesuai dengan MVVM.

Yang mengatakan, .. saya pikir kasus Anda bisa cocok dengan sedikit refactoring.

Dalam kebanyakan kasus yang saya temui, WPF memungkinkan Anda bertahan TANPA beberapa Windowdetik. Mungkin Anda bisa mencoba menggunakan Frames dan Pages daripada Windows dengan DialogResults.

Dalam kasus Anda, saran saya akan LoginFormViewModelmenangani LoginCommanddan jika login tidak valid, setel properti LoginFormViewModelke nilai yang sesuai ( falseatau beberapa nilai enum seperti UserAuthenticationStates.FailedAuthentication). Anda akan melakukan hal yang sama untuk login yang berhasil ( trueatau nilai enum lainnya). Anda kemudian akan menggunakan a DataTriggeryang merespons berbagai status otentikasi pengguna dan dapat menggunakan sederhana Setteruntuk mengubah Sourceproperti file Frame.

Setelah Jendela login Anda kembali, DialogResultmenurut saya itulah yang membuat Anda bingung; yang DialogResultsebenarnya adalah properti ViewModel Anda. Dalam pengalaman saya, yang diakui terbatas dengan WPF, ketika ada sesuatu yang terasa tidak benar biasanya karena saya berpikir dalam hal bagaimana saya akan melakukan hal yang sama di WinForms.

Semoga membantu.

EightyOne Unite
sumber
10

Dengan asumsi dialog login Anda adalah jendela pertama yang dibuat, coba ini di dalam kelas LoginViewModel Anda:

    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;
        }
    }
Jim Wallace
sumber
Pria ini sederhana dan bekerja dengan baik. Saat ini saya menggunakan pendekatan ini.
Erre Efe
Ini hanya berfungsi untuk jendela MAIN. Jadi jangan gunakan untuk jendela lain.
Oleksii
7

Ini adalah solusi yang sederhana dan bersih - Anda menambahkan acara ke ViewModel dan menginstruksikan Jendela untuk menutup sendiri saat acara tersebut diaktifkan.

Untuk lebih jelasnya lihat posting blog saya, Tutup jendela dari 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);
}

Catatan: Contoh ini menggunakan Prism's DelegateCommand(lihat Prism: Commanding ), tetapi ICommandimplementasi apa pun dapat digunakan dalam hal ini.

Anda dapat menggunakan perilaku dari paket resmi ini .

Shimmy Weitzhandler
sumber
2
+1 tetapi Anda harus memberikan lebih banyak detail dalam jawaban itu sendiri, misalnya bahwa solusi ini memerlukan referensi ke rakitan Interaktivitas Campuran Ekspresi.
Surfen
6

Cara saya menanganinya adalah dengan menambahkan event handler di ViewModel saya. Ketika pengguna berhasil masuk, saya akan mengaktifkan acara tersebut. Dalam View saya, saya akan melampirkan ke acara ini dan ketika diaktifkan, saya akan menutup jendela.


sumber
2
Itu yang biasanya saya lakukan juga. Meskipun itu tampak agak kotor mengingat semua hal berwpf-memerintah bermodel.
Botz3000
4

Inilah yang awalnya saya lakukan, yang berhasil, namun tampaknya agak bertele-tele dan jelek (statis global apa pun tidak pernah baik)

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);
}

Saya kemudian menghapus semua kode ini, dan baru saja LoginFormViewModelmemanggil metode Tutup pada tampilan itu. Itu akhirnya menjadi jauh lebih bagus dan lebih mudah diikuti. IMHO Inti dari pola adalah memberi orang cara yang lebih mudah untuk memahami apa yang dilakukan aplikasi Anda, dan dalam hal ini, MVVM membuatnya jauh lebih sulit untuk dipahami daripada jika saya tidak menggunakannya, dan sekarang menjadi anti- pola.

Orion Edwards
sumber
3

FYI, saya mengalami masalah yang sama ini dan saya pikir saya menemukan solusi yang tidak memerlukan global atau statika, meskipun itu mungkin bukan jawaban terbaik. Aku membiarkan kalian memutuskannya sendiri.

Dalam kasus saya, ViewModel yang memberi contoh Jendela untuk ditampilkan (sebut saja ViewModelMain) juga tahu tentang LoginFormViewModel (menggunakan situasi di atas sebagai contoh).

Jadi yang saya lakukan adalah membuat properti di LoginFormViewModel yang bertipe ICommand (Sebut saja CloseWindowCommand). Kemudian, sebelum saya memanggil .ShowDialog () di Window, saya mengatur properti CloseWindowCommand pada LoginFormViewModel ke metode window.Close () dari Window yang saya contohkan. Kemudian di dalam LoginFormViewModel yang harus saya lakukan adalah memanggil CloseWindowCommand.Execute () untuk menutup jendela.

Saya kira ini sedikit solusi / peretasan, tetapi berfungsi dengan baik tanpa benar-benar merusak pola MVVM.

Jangan ragu untuk mengkritik proses ini sebanyak yang Anda suka, saya bisa menerimanya! :)


sumber
Saya tidak yakin saya sepenuhnya melakukannya, tetapi bukankah ini berarti Jendela Utama Anda harus dibuat sebelum Jendela Masuk Anda? Itu adalah sesuatu yang ingin saya hindari jika memungkinkan
Orion Edwards
3

Ini mungkin sudah sangat terlambat, tetapi saya menemukan masalah yang sama dan saya menemukan solusi yang berhasil untuk saya.

Saya tidak tahu cara membuat aplikasi tanpa dialog (mungkin itu hanya mind block). Jadi saya menemui jalan buntu dengan MVVM dan menunjukkan dialog. Jadi saya menemukan artikel CodeProject ini:

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

Yang merupakan UserControl yang pada dasarnya memungkinkan sebuah jendela berada di dalam pohon visual jendela lain (tidak diizinkan di xaml). Ini juga mengekspos DependencyProperty boolean yang disebut IsShowing.

Anda dapat menyetel gaya seperti, biasanya dalam sebuah sumber-sumber, yang pada dasarnya menampilkan dialog setiap kali properti Konten kontrol! = Null melalui pemicu:

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

Dalam tampilan di mana Anda ingin menampilkan dialog, cukup miliki ini:

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

Dan di ViewModel, yang harus Anda lakukan adalah menyetel properti ke nilai (Catatan: kelas ViewModel harus mendukung INotifyPropertyChanged agar tampilan mengetahui sesuatu telah terjadi).

seperti ini:

DialogViewModel = new DisplayViewModel();

Untuk mencocokkan ViewModel dengan View, Anda harus memiliki sesuatu seperti ini di sebuah sumber bacaan:

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

Dengan semua itu Anda mendapatkan kode satu baris untuk menampilkan dialog. Masalah yang Anda dapatkan adalah Anda tidak dapat menutup dialog hanya dengan kode di atas. Jadi itulah mengapa Anda harus meletakkan acara di kelas dasar ViewModel yang diwarisi oleh DisplayViewModel dan alih-alih kode di atas, tulis ini

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

Kemudian Anda dapat menangani hasil dialog melalui callback.

Ini mungkin tampak sedikit rumit, tetapi begitu landasannya diletakkan, itu cukup mudah. Sekali lagi ini adalah implementasi saya, saya yakin ada yang lain :)

Semoga ini bisa membantu, ini menyelamatkan saya.

Jose
sumber
3

Oke, jadi pertanyaan ini hampir berusia 6 tahun dan saya masih tidak dapat menemukan di sini apa yang menurut saya merupakan jawaban yang tepat, jadi izinkan saya untuk membagikan "2 sen" saya ...

Saya sebenarnya memiliki 2 cara untuk melakukannya, yang pertama adalah yang sederhana ... yang kedua di yang benar, jadi jika Anda mencari yang benar, lewati saja # 1 dan lompat ke # 2 :

1. Cepat dan Mudah (tapi belum lengkap)

Jika saya hanya memiliki proyek kecil, terkadang saya hanya membuat CloseWindowAction di ViewModel:

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

Dan siapa pun yang memasukkan Tampilan, atau dalam kode Tampilan di belakang, saya baru saja mengatur Metode yang akan dipanggil oleh Tindakan:

(ingat MVVM adalah tentang pemisahan View dan ViewModel ... kode View tetaplah View dan selama ada pemisahan yang tepat Anda tidak melanggar polanya)

Jika beberapa ViewModel membuat jendela baru:

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

Atau jika Anda menginginkannya di Jendela Utama, cukup letakkan di bawah konstruktor View Anda:

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

bila Anda ingin menutup jendela, cukup panggil Action di ViewModel Anda.


2. Cara yang benar

Sekarang cara yang tepat untuk melakukannya adalah dengan menggunakan Prism (IMHO), dan semua tentangnya dapat ditemukan di sini .

Anda dapat membuat Permintaan Interaksi , mengisinya dengan data apa pun yang Anda perlukan di Jendela baru Anda, makan siang, menutupnya, dan bahkan menerima data kembali . Semua ini dikemas dan MVVM disetujui. Anda bahkan mendapatkan status bagaimana Jendela ditutup , seperti jika Pengguna Canceledatau Accepted(tombol OK) Jendela dan data kembali jika Anda membutuhkannya . Ini sedikit lebih rumit dan Jawaban # 1, tetapi jauh lebih lengkap, dan Pola yang Direkomendasikan oleh Microsoft.

Tautan yang saya berikan memiliki semua cuplikan kode dan contoh, jadi saya tidak akan repot-repot menempatkan kode apa pun di sini, cukup baca artikel mengunduh Prism Quick Start dan menjalankannya, sangat mudah untuk memahami hanya sedikit lebih verbose membuatnya berhasil, tetapi manfaatnya lebih besar dari sekadar menutup jendela.

Michel Feinstein
sumber
Cara yang bagus, tetapi resolusi & penetapan ViewModels tidak bisa selalu lurus ke depan. Bagaimana jika viewmodel yang sama adalah DataContext dari banyak Windows?
Kylo Ren
Maka saya kira Anda harus menutup semua jendela sekaligus, ingat Tindakan dapat memicu banyak delegasi sekaligus, cukup gunakan +=untuk menambahkan delegasi, dan panggil Tindakan, itu akan memecat semuanya .... Atau Anda akan melakukannya harus membuat logika khusus pada VM Anda sehingga akan mengetahui jendela mana yang akan ditutup (mungkin memiliki kumpulan Tindakan Tutup) .... Namun menurut saya, memiliki beberapa tampilan yang diikat ke satu VM bukanlah praktik terbaik, itu lebih baik untuk mengelola agar memiliki satu Lihat dan satu instance VM, yang terikat satu sama lain dan mungkin VM induk yang mengelola semua VM turunan yang terikat ke semua Tampilan.
Michel Feinstein
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();
  }
}
Amir Touitou
sumber
2

Anda bisa membuat ViewModel mengekspos peristiwa yang didaftarkan oleh View. Kemudian, saat ViewModel memutuskan waktunya untuk menutup tampilan, ia mengaktifkan peristiwa yang menyebabkan tampilan ditutup. Jika Anda ingin nilai hasil tertentu dikembalikan, Anda akan memiliki properti di ViewModel untuk itu.

Abdulla Al-Qawasmeh
sumber
Saya setuju dengan ini - kesederhanaan itu berharga. Saya harus memikirkan tentang apa yang terjadi ketika pengembang junior berikutnya dipekerjakan untuk mengambil alih proyek ini. Dugaan saya adalah dia akan memiliki kesempatan yang jauh lebih baik untuk mendapatkan hak ini seperti yang Anda gambarkan. Kecuali jika Anda pikir Anda akan mempertahankan kode ini selamanya? +1
Dean
2

Hanya untuk menambah sejumlah besar jawaban, saya ingin menambahkan yang berikut ini. Dengan asumsi bahwa Anda memiliki ICommand pada ViewModel Anda, dan Anda ingin perintah itu menutup jendelanya (atau tindakan lain dalam hal ini), Anda dapat menggunakan sesuatu seperti berikut ini.

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

Ini tidak sempurna, dan mungkin sulit untuk diuji (karena sulit untuk meniru / menghentikan statis) tetapi lebih bersih (IMHO) daripada solusi lainnya.

Erick

Erick T
sumber
Saya menjadi sangat senang ketika melihat jawaban sederhana Anda! tapi itu juga tidak berhasil! Saya perlu membuka dan menutup dengan visual basic. Apakah Anda tahu kesetaraan (windows [i] .DataContext == this) di VB?
Ehsan
Saya akhirnya mendapatkannya! :) Terima kasih. Jika windows (i) .DataContext Is me
Ehsan
Tahukah Anda cara sederhana yang sama untuk membuka jendela juga? Saya perlu mengirim dan menerima beberapa data juga di viewmodel anak dan sebaliknya.
Ehsan
1

Saya menerapkan solusi Joe White, tetapi mengalami masalah dengan sesekali " DialogResult dapat disetel hanya setelah Window dibuat dan ditampilkan sebagai kesalahan dialog ".

Saya menyimpan ViewModel setelah Tampilan ditutup dan kadang-kadang saya kemudian membuka Tampilan baru menggunakan VM yang sama. Tampaknya menutup Tampilan baru sebelum Tampilan lama dikumpulkan sampah mengakibatkan DialogResultChanged mencoba menyetel properti DialogResult pada jendela tertutup, sehingga memicu kesalahan.

Solusi saya adalah mengubah DialogResultChanged untuk memeriksa properti IsLoaded jendela :

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

Setelah membuat perubahan ini, semua lampiran ke dialog tertutup akan diabaikan.

Jim Hansen
sumber
Terima kasih Pak. Saya memiliki masalah yang sama
DJ Burb
1

Saya akhirnya memadukan jawaban Joe White dan beberapa kode dari jawaban Adam Mills , karena saya perlu menunjukkan kontrol pengguna di jendela yang dibuat secara terprogram. Jadi DialogCloser tidak perlu berada di jendela, bisa di kontrol pengguna itu sendiri

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

Dan DialogCloser akan menemukan jendela kontrol pengguna jika tidak dilampirkan ke jendela itu sendiri.

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_ );
    }
  }
}
Anuroopa Shenoy
sumber
1

Perilaku adalah cara paling nyaman di sini.

  • Dari satu sisi, ini dapat diikat ke model tampilan yang diberikan (yang dapat menandakan "tutup formulir!")

  • Dari sisi lain, ia memiliki akses ke formulir itu sendiri sehingga dapat berlangganan acara khusus formulir yang diperlukan, atau menampilkan dialog konfirmasi, atau apa pun.

Menulis perilaku yang diperlukan bisa terlihat membosankan untuk pertama kalinya. Namun, mulai sekarang, Anda dapat menggunakannya kembali di setiap formulir yang Anda butuhkan dengan cuplikan XAML satu baris yang tepat. Dan jika perlu, Anda dapat mengekstraknya sebagai rakitan terpisah sehingga dapat dimasukkan ke dalam proyek berikutnya yang Anda inginkan.

Yury Schkatula
sumber
0

Mengapa tidak melewatkan jendela sebagai parameter perintah?

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}" />
chrislarson
sumber
Saya tidak berpikir itu ide yang baik untuk membatasi VM ke tipe Window.
Shimmy Weitzhandler
2
Menurut saya bukan ide yang baik untuk membatasi VM ke Windowjenis yang agak bukan MVVM "murni". Lihat jawaban ini , di mana VM tidak terbatas pada suatu Windowobjek.
Shimmy Weitzhandler
Dengan cara ini ketergantungan diletakkan pada sebuah Tombol yang tentunya tidak bisa selalu menjadi situasinya. Juga meneruskan tipe UI ke ViewModel adalah praktik yang buruk.
Kylo Ren
0

Solusi lain adalah membuat properti dengan INotifyPropertyChanged dalam Model Tampilan seperti DialogResult, lalu di Code Behind tulis ini:

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;
        }
    }
}

Fragmen terpenting adalah _someViewModel_PropertyChanged. DialogResultPropertyNamebisa menjadi beberapa string const publik di SomeViewModel.

Saya menggunakan trik semacam ini untuk membuat beberapa perubahan dalam Kontrol Tampilan jika hal ini sulit dilakukan di ViewModel. OnPropertyChanged di ViewModel Anda dapat melakukan apa pun yang Anda inginkan dalam View. ViewModel masih 'unit testable' dan beberapa baris kecil kode di belakang tidak ada bedanya.

sliwinski.lukas
sumber
0

Saya akan melakukannya dengan cara ini:

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
{
}
romanoza
sumber
0

Saya telah membaca semua jawaban tetapi saya harus mengatakan, kebanyakan dari mereka tidak cukup baik atau bahkan lebih buruk.

Anda bisa menangani ini dengan cantik dengan kelas DialogService yang tanggung jawabnya adalah menampilkan jendela dialog dan mengembalikan hasil dialog. Saya telah membuat proyek sampel yang mendemonstrasikan implementasi dan penggunaannya.

berikut adalah bagian terpenting:

//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();
    }
}

Bukankah ini lebih sederhana? lebih kaku, lebih mudah dibaca dan terakhir tetapi tidak kalah mudahnya untuk di-debug daripada EventAggregator atau solusi serupa lainnya?

seperti yang Anda lihat, Dalam model tampilan saya, saya telah menggunakan pendekatan ViewModel pertama yang dijelaskan dalam posting saya di sini: Praktik terbaik untuk memanggil View dari ViewModel di WPF

Tentu saja, dalam dunia nyata, DialogService.ShowDialogharus memiliki lebih banyak opsi untuk mengkonfigurasi dialog, misalnya tombol dan perintah yang harus mereka jalankan. Ada cara berbeda untuk melakukannya, tetapi di luar ruang lingkup :)

Liero
sumber
0

Meskipun ini tidak menjawab pertanyaan tentang bagaimana melakukan ini melalui viewmodel, ini menunjukkan bagaimana melakukannya hanya dengan menggunakan XAML + SDK campuran.

Saya memilih untuk mengunduh dan menggunakan dua file dari Blend SDK, yang keduanya Anda dapat sebagai paket dari Microsoft melalui NuGet. File-file tersebut adalah:

System.Windows.Interactivity.dll dan Microsoft.Expression.Interactions.dll

Microsoft.Expression.Interactions.dll memberi Anda kemampuan bagus seperti kemampuan untuk mengatur properti atau menjalankan metode pada viewmodel Anda atau target lain dan memiliki widget lain di dalamnya juga.

Beberapa 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>

Perhatikan bahwa jika Anda hanya menggunakan perilaku OK / Cancel sederhana, Anda dapat menggunakan properti IsDefault dan IsCancel selama jendela ditampilkan dengan Window.ShowDialog ().
Saya pribadi mengalami masalah dengan tombol yang memiliki properti IsDefault disetel ke true, tetapi disembunyikan ketika halaman dimuat. Tampaknya tidak ingin bermain dengan baik setelah itu ditampilkan, jadi saya hanya mengatur properti Window.DialogResult seperti yang ditunjukkan di atas dan berfungsi untuk saya.

Wes
sumber
0

Ini adalah solusi bug gratis sederhana (dengan kode sumber), Ini bekerja untuk saya.

  1. Dapatkan ViewModel Anda dari INotifyPropertyChanged

  2. Buat properti yang dapat diamati CloseDialog di ViewModel

    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. Lampirkan Handler di View untuk perubahan properti ini

        _loginDialogViewModel = new LoginDialogViewModel();
        loginPanel.DataContext = _loginDialogViewModel;
        _loginDialogViewModel.PropertyChanged += OnPropertyChanged;
    
  4. Sekarang Anda hampir selesai. Dalam acara handler makeDialogResult = true

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

Buat Dependency Propertydi Anda View/ apa saja UserControl(atau WindowAnda ingin menutup). Seperti dibawah ini:

 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
        }

Dan ikat dari properti ViewModel Anda :

<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}"

Properti Di VeiwModel:

private bool closeWindow;

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

Sekarang picu operasi penutupan dengan mengubah CloseWindownilai di ViewModel. :)

Kylo Ren
sumber
-2

Di mana Anda perlu menutup jendela, cukup letakkan ini di viewmodel:

ta-da

  foreach (Window window in Application.Current.Windows)
        {
            if (window.DataContext == this)
            {
                window.Close();
                return;
            }
        }
Cătălin Rădoi
sumber
ViewModel tidak boleh berisi UIElement dengan cara apa pun, karena ini dapat membuat bug
WiiMaxx
Bagaimana jika DataContext diwarisi menjadi beberapa jendela?
Kylo Ren
ta-da, ini sama sekali bukan MVVM.
Alexandru Dicu
-9
Application.Current.MainWindow.Close() 

Cukup!

Alexey
sumber
4
-1 Hanya benar jika jendela yang ingin Anda tutup adalah jendela utama ... Asumsi yang sangat tidak mungkin untuk dialog Login ...
surfen