Dobra czy zła praktyka dla dialogów w WPF z MVVM?

148

Ostatnio miałem problem z tworzeniem okien dialogowych dodawania i edycji dla mojej aplikacji wpf.

Wszystko, co chciałem zrobić w moim kodzie, to coś takiego. (Najczęściej używam pierwszego podejścia Viewmodel z mvvm)

ViewModel, który wywołuje okno dialogowe:

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

Jak to działa?

Najpierw stworzyłem usługę dialogową:

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

WindowDialogto specjalne, ale proste okno. Potrzebuję go do przechowywania treści:

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

Problem z oknami dialogowymi w wpf jest dialogresult = truemożliwy tylko w kodzie. Dlatego stworzyłem interfejs, dialogviewmodelaby go zaimplementować.

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

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

Za każdym razem, gdy mój ViewModel uzna, że ​​nadszedł czas dialogresult = true, podnieś to zdarzenie.

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

Teraz przynajmniej muszę utworzyć DataTemplatew moim pliku zasobów ( app.xamllub czymś):

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

Cóż, to wszystko, mogę teraz wywoływać okna dialogowe z moich modeli widoku:

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

Teraz moje pytanie, czy widzisz jakieś problemy z tym rozwiązaniem?

Edycja: dla kompletności. ViewModel powinien zaimplementować, IDialogResultVMHelpera następnie może podnieść go w ramach OkCommandlub czegoś podobnego:

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

EDYCJA 2: Użyłem kodu z tego miejsca, aby mój rejestr EventHandler był słaby:
http://diditwith.net/2007/03/23/SolvingTheProblemWithEventsWeakEventHandlers.aspx
(Witryna już nie istnieje, 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
źródło
1
prawdopodobnie brakuje refernece xmlns: x = " schemas.microsoft.com/winfx/2006/xaml " w twoim WindowDialog XAML.
Adiel Yaacov
W rzeczywistości przestrzeń nazw to xmlns: x = "[http: //] schemas.microsoft.com/winfx/2006/xaml" bez nawiasów
reggaeguitar
1
Cześć! Spóźniony tutaj. Nie rozumiem, w jaki sposób Twoja usługa ma odwołanie do WindowDialog. Jaka jest hierarchia twoich modeli? Moim zdaniem widok zawiera odniesienie do zestawu Viewmodel i Viewmodel do zestawów Service i Model. W związku z tym warstwa usług nie miałaby wiedzy o widoku WindowDialog. czego mi brakuje?
Moe45673
2
Cześć @blindmeis, po prostu próbuję ogarnąć tę koncepcję, nie sądzę, że istnieje jakiś przykładowy projekt online, który mógłbym wybrać? Jest kilka rzeczy, które mnie zagubiły.
Hank

Odpowiedzi:

48

To dobre podejście i w przeszłości stosowałem podobne. Idź po to!

Jedną drobną rzeczą, którą z pewnością zrobiłbym, jest sprawienie, aby zdarzenie otrzymywało wartość logiczną, gdy trzeba ustawić wartość „false” w DialogResult.

event EventHandler<RequestCloseEventArgs> RequestCloseDialog;

i klasa EventArgs:

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

    public bool DialogResult { get; private set; }
}
Julian Dominguez
źródło
Co jeśli zamiast korzystać z usług, użyje się czegoś w rodzaju Callback, aby ułatwić interakcję z ViewModel i View? Na przykład View wykonuje polecenie w ViewModel, a gdy wszystko jest powiedziane i zrobione, ViewModel wywołuje wywołanie zwrotne dla widoku, aby wyświetlić wyniki polecenia. Nadal nie mogę włączyć mojego zespołu do korzystania z usług do obsługi interakcji w oknie dialogowym w modelu ViewModel.
Matthew S,
15

Od kilku miesięcy stosuję niemal identyczne podejście i jestem z niego bardzo zadowolony (tj. Nie czułem jeszcze ochoty na przepisywanie go całkowicie ...)

W mojej implementacji używam a, IDialogViewModelktóry eksponuje takie rzeczy, jak tytuł, standardowe przyciski do wyświetlania (w celu uzyskania spójnego wyglądu we wszystkich oknach dialogowych), RequestClosezdarzenie i kilka innych rzeczy, aby móc kontrolować rozmiar okna i zachowanie

Thomas Levesque
źródło
thx, tytuł powinien naprawdę znaleźć się w moim IDialogViewModel. inne właściwości, takie jak rozmiar, przycisk standardowy zostawię, ponieważ wszystko to pochodzi przynajmniej z tabliczki znamionowej.
blindmeis
1
Tak też zrobiłem na początku, po prostu użyj SizeToContent do kontrolowania rozmiaru okna. Ale w jednym przypadku potrzebowałem zmienić rozmiar okna, więc musiałem go trochę poprawić ...
Thomas Levesque
@ThomasLevesque przyciski zawarte w Twoim ViewModel, czy w rzeczywistości są to obiekty UI Button czy obiekty reprezentujące przyciski?
Thomas
3
@Thomas, obiekty reprezentujące przyciski. Nigdy nie należy odwoływać się do obiektów interfejsu użytkownika w ViewModel.
Thomas Levesque
2

Jeśli mówisz o oknach dialogowych, a nie tylko o wyskakujących okienkach komunikatów, rozważ moje podejście poniżej. Kluczowe punkty to:

  1. Do Module Controllerkonstruktora każdego przekazuję odwołanie do ViewModel(można użyć iniekcji).
  2. To Module Controllerma publiczne / wewnętrzne metody tworzenia okien dialogowych (po prostu tworzenie, bez zwracania wyniku). Stąd aby otworzyć okno dialogowe w ViewModelpiszę:controller.OpenDialogEntity(bla, bla...)
  3. Każde okno dialogowe powiadamia o swoim wyniku (np. OK , Zapisz , Anuluj itp.) Za pośrednictwem słabych wydarzeń . Jeśli używasz PRISM, łatwiej jest publikować powiadomienia za pomocą tego EventAggregator .
  4. Do obsługi wyników dialogów używam subskrypcji powiadomień (ponownie Weak Events i EventAggregator w przypadku PRISM). Aby zmniejszyć zależność od takich powiadomień, użyj niezależnych klas ze standardowymi powiadomieniami.

Plusy:

  • Mniej kodu. Nie mam nic przeciwko używaniu interfejsów, ale widziałem zbyt wiele projektów, w których nadmierne używanie interfejsów i warstw abstrakcji sprawia więcej kłopotów niż pomaga.
  • Otwarte okna dialogowe Module Controllerto prosty sposób na uniknięcie silnych odniesień i nadal pozwala na używanie makiet do testowania.
  • Powiadomienia o słabych zdarzeniach zmniejszają liczbę potencjalnych wycieków pamięci.

Cons:

  • Nie jest łatwo odróżnić wymagane powiadomienie od innych w programie obsługi. Dwa rozwiązania:
    • wyślij unikalny token po otwarciu okna dialogowego i sprawdź ten token w subskrypcji
    • używaj ogólnych klas powiadomień, w <T>których Tjest wyliczeniem jednostek (lub dla uproszczenia może to być typ ViewModel).
  • Dla projektu należy uzgodnić użycie klas powiadomień, aby zapobiec ich powielaniu.
  • W przypadku bardzo dużych projektów Module Controllermożna przytłoczyć metodami tworzenia okien. W takim przypadku lepiej podzielić go na kilka modułów.

PS Stosuję to podejście już od dłuższego czasu i jestem gotów bronić jego kwalifikowalności w komentarzach i w razie potrzeby podać kilka przykładów.

Alex Klaus
źródło