Praktik yang baik atau buruk untuk Dialog di WPF dengan MVVM?

148

Saya akhir-akhir ini memiliki masalah membuat tambah dan edit dialog untuk aplikasi WPF saya.

Yang ingin saya lakukan dalam kode saya adalah sesuatu seperti ini. (Saya kebanyakan menggunakan pendekatan viewmodel pertama dengan mvvm)

ViewModel yang memanggil jendela dialog:

var result = this.uiDialogService.ShowDialog("Dialogwindow Title", dialogwindowVM);
// Do anything with the dialog result

Bagaimana cara kerjanya?

Pertama, saya membuat layanan dialog:

public interface IUIWindowDialogService
{
    bool? ShowDialog(string title, object datacontext);
}

public class WpfUIWindowDialogService : IUIWindowDialogService
{
    public bool? ShowDialog(string title, object datacontext)
    {
        var win = new WindowDialog();
        win.Title = title;
        win.DataContext = datacontext;

        return win.ShowDialog();
    }
}

WindowDialogadalah jendela khusus tetapi sederhana. Saya membutuhkannya untuk menyimpan konten saya:

<Window x:Class="WindowDialog"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    Title="WindowDialog" 
    WindowStyle="SingleBorderWindow" 
    WindowStartupLocation="CenterOwner" SizeToContent="WidthAndHeight">
    <ContentPresenter x:Name="DialogPresenter" Content="{Binding .}">

    </ContentPresenter>
</Window>

Masalah dengan dialog di wpf adalah dialogresult = truehanya dapat dicapai dalam kode. Itu sebabnya saya membuat antarmuka untuk dialogviewmodelmengimplementasikannya.

public class RequestCloseDialogEventArgs : EventArgs
{
    public bool DialogResult { get; set; }
    public RequestCloseDialogEventArgs(bool dialogresult)
    {
        this.DialogResult = dialogresult;
    }
}

public interface IDialogResultVMHelper
{
    event EventHandler<RequestCloseDialogEventArgs> RequestCloseDialog;
}

Kapan pun ViewModel saya menganggap sudah saatnya dialogresult = true, maka naikkan acara ini.

public partial class DialogWindow : Window
{
    // Note: If the window is closed, it has no DialogResult
    private bool _isClosed = false;

    public DialogWindow()
    {
        InitializeComponent();
        this.DialogPresenter.DataContextChanged += DialogPresenterDataContextChanged;
        this.Closed += DialogWindowClosed;
    }

    void DialogWindowClosed(object sender, EventArgs e)
    {
        this._isClosed = true;
    }

    private void DialogPresenterDataContextChanged(object sender,
                              DependencyPropertyChangedEventArgs e)
    {
        var d = e.NewValue as IDialogResultVMHelper;

        if (d == null)
            return;

        d.RequestCloseDialog += new EventHandler<RequestCloseDialogEventArgs>
                                    (DialogResultTrueEvent).MakeWeak(
                                        eh => d.RequestCloseDialog -= eh;);
    }

    private void DialogResultTrueEvent(object sender, 
                              RequestCloseDialogEventArgs eventargs)
    {
        // Important: Do not set DialogResult for a closed window
        // GC clears windows anyways and with MakeWeak it
        // closes out with IDialogResultVMHelper
        if(_isClosed) return;

        this.DialogResult = eventargs.DialogResult;
    }
 }

Sekarang setidaknya saya harus membuat DataTemplatedi file sumber saya ( app.xamlatau sesuatu):

<DataTemplate DataType="{x:Type DialogViewModel:EditOrNewAuswahlItemVM}" >
        <DialogView:EditOrNewAuswahlItem/>
</DataTemplate>

Baiklah itu saja, sekarang saya dapat memanggil dialog dari viewmodels saya:

 var result = this.uiDialogService.ShowDialog("Dialogwindow Title", dialogwindowVM);

Sekarang pertanyaan saya, apakah Anda melihat ada masalah dengan solusi ini?

Edit: untuk kelengkapan. ViewModel harus menerapkan IDialogResultVMHelperdan kemudian dapat meningkatkannya dalam OkCommandatau sesuatu seperti ini:

public class MyViewmodel : IDialogResultVMHelper
{
    private readonly Lazy<DelegateCommand> _okCommand;

    public MyViewmodel()
    {
         this._okCommand = new Lazy<DelegateCommand>(() => 
             new DelegateCommand(() => 
                 InvokeRequestCloseDialog(
                     new RequestCloseDialogEventArgs(true)), () => 
                         YourConditionsGoesHere = true));
    }

    public ICommand OkCommand
    { 
        get { return this._okCommand.Value; } 
    }

    public event EventHandler<RequestCloseDialogEventArgs> RequestCloseDialog;
    private void InvokeRequestCloseDialog(RequestCloseDialogEventArgs e)
    {
        var handler = RequestCloseDialog;
        if (handler != null) 
            handler(this, e);
    }
 }

EDIT 2: Saya menggunakan kode dari sini untuk membuat register EventHandler saya lemah:
http://diditwith.net/2007/03/23/SolvingTheProblemWithEventsWeakEventHandlers.aspx
(Situs web tidak lagi ada, WebArchive Mirror )

public delegate void UnregisterCallback<TE>(EventHandler<TE> eventHandler) 
    where TE : EventArgs;

public interface IWeakEventHandler<TE> 
    where TE : EventArgs
{
    EventHandler<TE> Handler { get; }
}

public class WeakEventHandler<T, TE> : IWeakEventHandler<TE> 
    where T : class 
    where TE : EventArgs
{
    private delegate void OpenEventHandler(T @this, object sender, TE e);

    private readonly WeakReference mTargetRef;
    private readonly OpenEventHandler mOpenHandler;
    private readonly EventHandler<TE> mHandler;
    private UnregisterCallback<TE> mUnregister;

    public WeakEventHandler(EventHandler<TE> eventHandler,
                                UnregisterCallback<TE> unregister)
    {
        mTargetRef = new WeakReference(eventHandler.Target);

        mOpenHandler = (OpenEventHandler)Delegate.CreateDelegate(
                           typeof(OpenEventHandler),null, eventHandler.Method);

        mHandler = Invoke;
        mUnregister = unregister;
    }

    public void Invoke(object sender, TE e)
    {
        T target = (T)mTargetRef.Target;

        if (target != null)
            mOpenHandler.Invoke(target, sender, e);
        else if (mUnregister != null)
        {
            mUnregister(mHandler);
            mUnregister = null;
        }
    }

    public EventHandler<TE> Handler
    {
        get { return mHandler; }
    }

    public static implicit operator EventHandler<TE>(WeakEventHandler<T, TE> weh)
    {
        return weh.mHandler;
    }
}

public static class EventHandlerUtils
{
    public static EventHandler<TE> MakeWeak<TE>(this EventHandler<TE> eventHandler, 
                                                    UnregisterCallback<TE> unregister)
        where TE : EventArgs
    {
        if (eventHandler == null)
            throw new ArgumentNullException("eventHandler");

        if (eventHandler.Method.IsStatic || eventHandler.Target == null)
            throw new ArgumentException("Only instance methods are supported.",
                                            "eventHandler");

        var wehType = typeof(WeakEventHandler<,>).MakeGenericType(
                          eventHandler.Method.DeclaringType, typeof(TE));

        var wehConstructor = wehType.GetConstructor(new Type[] 
                             { 
                                 typeof(EventHandler<TE>), typeof(UnregisterCallback<TE>) 
                             });

        IWeakEventHandler<TE> weh = (IWeakEventHandler<TE>)wehConstructor.Invoke(
                                        new object[] { eventHandler, unregister });

        return weh.Handler;
    }
}
blindmeis
sumber
1
Anda mungkin melewatkan rujukan xmlns: x = " schemas.microsoft.com/winfx/2006/xaml " di WindowAMialog XAML Anda.
Adiel Yaacov
Sebenarnya namespace adalah xmlns: x = "[http: //] schemas.microsoft.com/winfx/2006/xaml" tanpa tanda kurung
reggaeguitar
1
Hai! Latecomer di sini. Saya tidak mengerti bagaimana Layanan Anda memiliki referensi ke WindowDialog. Apa hierarki model Anda? Dalam pikiran saya, View memegang referensi ke rakitan Viewmodel dan Viewmodel ke majelis Layanan dan Model. Dengan demikian, lapisan Layanan tidak akan memiliki pengetahuan tentang tampilan WindowDialog. Apa yang saya lewatkan?
Moe45673
2
Hai @blindmeis, hanya mencoba membungkus kepala saya di sekitar konsep ini, saya kira tidak ada beberapa contoh proyek online yang bisa saya pilih? Ada beberapa hal yang membuat saya bingung.
Hank

Jawaban:

48

Ini adalah pendekatan yang baik dan saya menggunakan yang serupa di masa lalu. Lakukan itu!

Satu hal kecil yang saya pasti akan lakukan adalah membuat acara menerima boolean ketika Anda perlu mengatur "false" di DialogResult.

event EventHandler<RequestCloseEventArgs> RequestCloseDialog;

dan kelas EventArgs:

public class RequestCloseEventArgs : EventArgs
{
    public RequestCloseEventArgs(bool dialogResult)
    {
        this.DialogResult = dialogResult;
    }

    public bool DialogResult { get; private set; }
}
Julian Dominguez
sumber
Bagaimana jika alih-alih menggunakan layanan, seseorang menggunakan semacam Callback untuk memfasilitasi interaksi dengan ViewModel dan View? Sebagai contoh, View mengeksekusi Command di ViewModel, maka ketika semua dikatakan dan dilakukan, ViewModel mem-panggil Callback untuk View untuk menampilkan hasil dari Command. Saya masih belum bisa membawa tim saya menggunakan Layanan untuk menangani interaksi Dialog di ViewModel.
Matius S
15

Saya telah menggunakan pendekatan yang hampir identik selama beberapa bulan sekarang, dan saya sangat senang dengan itu (yaitu saya belum merasakan keinginan untuk menulis ulang sepenuhnya ...)

Dalam implementasi saya, saya menggunakan IDialogViewModelyang memaparkan hal-hal seperti judul, tombol standar untuk ditampilkan (agar memiliki kemunculan yang konsisten di semua dialog), RequestCloseacara, dan beberapa hal lain untuk dapat mengontrol ukuran jendela dan tingkah laku

Thomas Levesque
sumber
thx, judulnya harus benar-benar masuk dalam IDialogViewModel saya. properti lain seperti ukuran, tombol standar saya akan pergi, karena ini semua setidaknya berasal dari datatemplate.
blindmeis
1
Itulah yang saya lakukan pada awalnya juga, cukup gunakan SizeToContent untuk mengontrol ukuran jendela. Tetapi dalam satu kasus saya perlu membuat jendela resizable, jadi saya harus sedikit mengubah ...
Thomas Levesque
@ThomasLevesque tombol-tombol yang terdapat dalam ViewModel Anda, apakah mereka benar-benar objek Tombol UI atau objek yang mewakili tombol?
Thomas
3
@ Thomas, benda yang mewakili tombol. Anda seharusnya tidak pernah merujuk objek UI di ViewModel.
Thomas Levesque
2

Jika Anda berbicara tentang jendela dialog dan bukan hanya tentang kotak pesan munculan, harap pertimbangkan pendekatan saya di bawah ini. Poin kuncinya adalah:

  1. Saya meneruskan referensi Module Controllerke konstruktor masing-masing ViewModel(Anda dapat menggunakan injeksi).
  2. Itu Module Controllermemiliki metode publik / internal untuk membuat jendela dialog (hanya membuat, tanpa mengembalikan hasil). Maka dari itu untuk membuka jendela dialog di ViewModelsaya menulis:controller.OpenDialogEntity(bla, bla...)
  3. Setiap jendela dialog memberitahukan tentang hasilnya (seperti OK , Simpan , Batalkan , dll.) Melalui Acara Lemah . Jika Anda menggunakan PRISM, maka lebih mudah untuk mempublikasikan pemberitahuan menggunakan EventAggregator ini .
  4. Untuk menangani hasil dialog, saya menggunakan langganan pemberitahuan (lagi-lagi Acara Lemah dan EventAggregator jika PRISM). Untuk mengurangi ketergantungan pada pemberitahuan semacam itu, gunakan kelas independen dengan pemberitahuan standar.

Pro:

  • Kode lebih sedikit. Saya tidak keberatan menggunakan antarmuka, tetapi saya telah melihat terlalu banyak proyek di mana kelebihan menggunakan antarmuka dan lapisan abstraksi menyebabkan lebih banyak masalah daripada bantuan.
  • Buka jendela dialog melalui Module Controlleradalah cara sederhana untuk menghindari referensi yang kuat dan masih memungkinkan untuk menggunakan mock-up untuk pengujian.
  • Pemberitahuan melalui peristiwa lemah mengurangi jumlah kebocoran memori potensial.

Cons:

  • Tidak mudah untuk membedakan notifikasi yang diperlukan dari yang lain di handler. Dua solusi:
    • kirim token unik saat membuka jendela dialog dan periksa token itu di langganan
    • menggunakan kelas notifikasi umum di <T>mana Tenumerasi entitas (atau untuk kesederhanaan itu bisa menjadi tipe ViewModel).
  • Untuk suatu proyek harus ada perjanjian tentang penggunaan kelas notifikasi untuk mencegah duplikasi mereka.
  • Untuk proyek yang sangat besar, Module Controllerdapat dibanjiri oleh metode untuk membuat windows. Dalam hal ini lebih baik membaginya dalam beberapa modul.

PS Saya telah menggunakan pendekatan ini untuk waktu yang cukup lama sekarang dan siap untuk mempertahankan kelayakannya dalam komentar dan memberikan beberapa contoh jika diperlukan.

Alex Klaus
sumber