Obsługa okien dialogowych w WPF z MVVM


235

We wzorcu MVVM dla WPF obsługa okien dialogowych jest jedną z bardziej złożonych operacji. Ponieważ model widoku nie wie nic o widoku, komunikacja w dialogu może być interesująca. Mogę ujawnić, ICommandże gdy widok go wywołuje, może pojawić się okno dialogowe.

Czy ktoś zna dobry sposób obsługi wyników z okien dialogowych? Mówię o oknach dialogowych systemu Windows, takich jak MessageBox.

Jednym ze sposobów, w jaki to zrobiliśmy, było zdarzenie w viewmodel, które widok subskrybowałby, gdy wymagane byłoby okno dialogowe.

public event EventHandler<MyDeleteArgs> RequiresDeleteDialog;

To jest OK, ale oznacza, że ​​widok wymaga kodu, od którego chciałbym się trzymać.


Dlaczego nie powiązać z obiektem pomocnika w widoku?
Paul Williams

1
Nie wiem co masz na myśli.
Ray Booysen,

1
Jeśli rozumiem pytanie, nie chcesz, aby maszyna wirtualna pojawiała się w oknach dialogowych i nie chcesz, aby kod był opóźniony w widoku. Ponadto wydaje się, że wolisz polecenia od zdarzeń. Zgadzam się z nimi wszystkimi, więc używam klasy pomocnika w Widoku, która udostępnia polecenie obsługi okna dialogowego. Odpowiedziałem na to pytanie w innym wątku tutaj: stackoverflow.com/a/23303267/420400 . Jednak ostatnie zdanie brzmi, jakbyś nie chciał w ogóle żadnego kodu w dowolnym miejscu w Widoku. Rozumiem tę obawę, ale kod, o którym mowa, jest jedynie warunkowy i prawdopodobnie nie ulegnie zmianie.
Paul Williams,

4
Ten model widoku powinien zawsze odpowiadać za logikę tworzenia okna dialogowego, to jest cały powód jego istnienia. To powiedziawszy, że nie robi (i nie powinien) ciężkiego podnoszenia tworzenia samego widoku. Napisałem artykuł na ten temat na codeproject.com/Articles/820324/..., gdzie pokazuję, że całym cyklem życia okien dialogowych można zarządzać poprzez regularne wiązanie danych WPF i bez naruszania wzorca MVVM.
Mark Feldman

Odpowiedzi:


131

Proponuję zrezygnować z dialogów modalnych z lat 90. i zamiast tego wdrożyć formant jako nakładkę (płótno + pozycjonowanie bezwzględne) z widocznością powiązaną z wartością logiczną z powrotem na maszynie wirtualnej. Bliżej kontrolki typu ajax.

Jest to bardzo przydatne:

<BooleanToVisibilityConverter x:Key="booltoVis" />

jak w:

<my:ErrorControl Visibility="{Binding Path=ThereWasAnError, Mode=TwoWay, Converter={StaticResource booltoVis}, UpdateSourceTrigger=PropertyChanged}"/>

Oto jak mam jeden zaimplementowany jako kontrola użytkownika. Kliknięcie „x” zamyka kontrolkę w wierszu kodu w kodzie kontrolki użytkownika z tyłu. (Ponieważ mam moje widoki w pliku .exe i ViewModels w bibliotece dll, nie czuję się źle z powodu kodu, który manipuluje interfejsem użytkownika.)

Okno dialogowe Wpf


20
Tak, podoba mi się również ten pomysł, ale chciałbym zobaczyć jakiś przykład tej kontroli pod względem sposobu jej pokazania, pobierania wyników dialogu itp. Szczególnie w scenariuszu MVVM w Silverlight.
Roboblob

16
Jak uniemożliwić użytkownikowi interakcję z elementami sterującymi pod tą nakładką okna dialogowego?
Andrew Garrison

16
Problem z tym podejściem polega na tym, że nie można otworzyć drugiego modalnego okna dialogowego od pierwszego, przynajmniej nie bez poważnych modyfikacji systemu nakładek ...
Thomas Levesque

6
Innym problemem związanym z tym podejściem jest to, że „dialogu” nie można przenieść. W naszych aplikacjach musimy mieć ruchome okno dialogowe, aby użytkownik mógł zobaczyć, co się za nim kryje.
JAB

12
To podejście wydaje mi się okropne. czego mi brakuje? Jak to jest lepsze niż prawdziwe okno dialogowe?
Jonathan Wood,

51

W tym celu powinieneś użyć mediatora. Mediator to wspólny wzorzec projektowy, znany również jako Messenger w niektórych jego implementacjach. Jest to paradygmat typu Zarejestruj / Powiadom i umożliwia Twojemu ViewModel i Widokom komunikowanie się za pośrednictwem nisko sprzężonego mechanizmu przesyłania wiadomości.

Powinieneś sprawdzić grupę Google WPF Disciples i po prostu poszukać Mediatora. Będziesz bardzo zadowolony z odpowiedzi ...

Możesz jednak zacząć od tego:

http://joshsmithonwpf.wordpress.com/2009/04/06/a-mediator-prototype-for-wpf-apps/

Cieszyć się !

Edycja: odpowiedź na ten problem z zestawem narzędzi MVVM Light Toolkit można znaleźć tutaj:

http://mvvmlight.codeplex.com/Thread/View.aspx?ThreadId=209338


2
Marlon Grech właśnie opublikował nową implementację mediatora: marlongrech.wordpress.com/2009/04/16/…
Roubachof

21
Tylko uwaga: wzorzec Mediator nie został wprowadzony przez uczniów WPF, to klasyczny wzorzec GoF ... ( dofactory.com/Patterns/PatternMediator.aspx ). W przeciwnym razie fajna odpowiedź;)
Thomas Levesque

10
Boże, nie używaj pośrednika ani przeklętego posłańca. Tego rodzaju kod z dziesiątkami latających wiadomości staje się bardzo trudny do debugowania, chyba że w jakiś sposób pamiętasz wszystkie punkty w całej bazie kodu, które subskrybują i obsługują każde zdarzenie. Staje się koszmarem dla nowych deweloperów. W rzeczywistości uważam całą bibliotekę MvvMLight za potężny anty-wzorzec dla jej wszechobecnego i niepotrzebnego wykorzystywania asynchronicznego przesyłania wiadomości. Rozwiązanie jest proste: zadzwoń do oddzielnego serwisu dialogowego (tj. IDialogService) swojego projektu. Interfejs ma metody i zdarzenia dla wywołań zwrotnych.
Chris Bordeman

34

Dobre okno dialogowe MVVM powinno:

  1. Zadeklaruj tylko XAML.
  2. Uzyskaj wszystkie jego zachowania od wiązania danych.

Niestety WPF nie zapewnia tych funkcji. Wyświetlenie okna dialogowego wymaga wywołania kodowego ShowDialog(). Klasa Window, która obsługuje okna dialogowe, nie może być zadeklarowana w XAML, więc nie może być łatwo powiązana z danymi DataContext.

Aby rozwiązać ten problem, napisałem kontrolkę kodu pośredniczącego XAML, która znajduje się w drzewie logicznym i przekazuje wiązanie danych do a Windoworaz uchwyty pokazujące i ukrywające okno dialogowe. Można go znaleźć tutaj: http://www.codeproject.com/KB/WPF/XAMLDialog.aspx

Jest naprawdę prosty w użyciu i nie wymaga żadnych dziwnych zmian w ViewModel i nie wymaga zdarzeń ani wiadomości. Podstawowe połączenie wygląda następująco:

<dialog:Dialog Content="{Binding Path=DialogViewModel}" Showing="True" />

Prawdopodobnie chcesz dodać styl, który określa Showing. Wyjaśniam to w moim artykule. Mam nadzieję, że to Ci pomoże.


2
To naprawdę interesujące podejście do problemu wyświetlania okien dialogowych w MVVM.
dthrasher

2
"Showing a dialog requires a code-behind"mmm, możesz to nazwać w ViewModel
Brock Hensley,

Dodałbym punkt 3 - możesz swobodnie łączyć się z innymi obiektami w widoku. Pozostawienie kodu okna dialogowego za pustym oznacza, że ​​w widoku nie ma kodu C #, a wiązanie danych nie oznacza wiązania z maszyną wirtualną.
Paul Williams

25

Używam tego podejścia do dialogów z MVVM.

Teraz muszę tylko wywołać następujące z mojego modelu widoku.

var result = this.uiDialogService.ShowDialog("Dialogwindow title goes here", dialogwindowVM);

z jakiej biblioteki pochodzi uiDialogService?
aggietech

1
brak biblioteki. Jest tylko mały interfejs i realizacja: stackoverflow.com/questions/3801681/... . żeby być uczciwym, ma więcej przeciążeń dla moich potrzeb :) (wysokość, szerokość, ustawienia właściwości itd.)
blindmeis,

16

Moje obecne rozwiązanie rozwiązuje większość problemów, o których wspomniałeś, ale jest całkowicie oderwane od rzeczy specyficznych dla platformy i można je ponownie wykorzystać. Również nie użyłem żadnego wiązania tylko kodem z DelegateCommands, które implementują ICommand. Okno dialogowe to w zasadzie widok - oddzielna kontrolka, która ma swój własny ViewModel i jest pokazywana z ViewModel ekranu głównego, ale wyzwalana z interfejsu użytkownika poprzez powiązanie DelagateCommand.

Zobacz pełne rozwiązanie Silverlight 4 Modalne okna dialogowe z MVVM i Silverlight 4


Podobnie jak podejście @Elad Katz, w Twojej odpowiedzi brakuje powiązanej treści - popraw ją, wstawiając ją, ponieważ jest to dobra odpowiedź tutaj na SO. Niemniej jednak dziękuję za Twój wkład! :)
Yoda

6

Naprawdę zmagałem się z tą koncepcją przez jakiś czas, kiedy uczyłem się (wciąż się uczę) MVVM. To, co zdecydowałem i myślę, że inni już zdecydowali, ale nie było dla mnie jasne, to:

Moją pierwotną myślą było to, że ViewModel nie powinien mieć możliwości bezpośredniego wywoływania okna dialogowego, ponieważ nie ma biznesu decydującego o sposobie wyświetlania okna dialogowego. Z tego powodu zacząłem myśleć o tym, jak mogę przekazywać wiadomości tak, jak w MVP (tj. View.ShowSaveFileDialog ()). Myślę jednak, że to niewłaściwe podejście.

Jest OK, aby ViewModel bezpośrednio wywoływał okno dialogowe. Jednak podczas testowania ViewModel oznacza to, że okno dialogowe albo wyskoczy podczas testu, albo zakończy się niepowodzeniem (nigdy tak naprawdę nie próbowałem tego).

Tak więc to, co musi się zdarzyć, polega na tym, aby podczas testowania użyć „testowej” wersji okna dialogowego. Oznacza to, że w każdym oknie dialogowym, które masz, musisz utworzyć interfejs i albo wykpić odpowiedź okna dialogowego, albo stworzyć próbny test, który będzie miał domyślne zachowanie.

Powinieneś już używać jakiegoś Lokalizatora usług lub IoC, który możesz skonfigurować, aby zapewnić poprawną wersję w zależności od kontekstu.

Korzystając z tego podejścia, Twój ViewModel jest nadal testowalny i zależnie od tego, jak wykpisz swoje okna dialogowe, możesz kontrolować zachowanie.

Mam nadzieję że to pomoże.


6

Są dwa dobre sposoby, aby to zrobić: 1) usługa dialogu (łatwa, czysta) i 2) pomoc w wyświetlaniu. Wspomaganie widoku zapewnia kilka ciekawych funkcji, ale zwykle nie jest tego warte.

USŁUGA DIALOGOWA

a) interfejs usługi okna dialogowego, taki jak przez konstruktora lub jakiś kontener zależności:

interface IDialogService { Task ShowDialogAsync(DialogViewModel dlgVm); }

b) Wdrożenie IDialogService powinno otworzyć okno (lub wstrzyknąć trochę kontroli do aktywnego okna), stworzyć widok odpowiadający nazwie danego typu dlgVm (użyj rejestracji lub konwencji kontenera lub ContentPresenter z typem DataTemplates). ShowDialogAsync powinien utworzyć TaskCompletionSource i zwrócić jego właściwość .Task. Sama klasa DialogViewModel potrzebuje zdarzenia, które można wywołać w klasie pochodnej, gdy chce się zamknąć, i oglądać w oknie dialogowym, aby faktycznie zamknąć / ukryć okno dialogowe i ukończyć TaskCompletionSource.

b) Aby użyć, po prostu wywołaj polecenie wyczekuj this.DialogService.ShowDialog (myDlgVm) w twojej instancji klasy wywodzącej się z DialogViewModel. Po oczekiwaniu na powrót sprawdź właściwości dodane do maszyny Wirtualnej okna dialogowego, aby ustalić, co się stało; nawet nie potrzebujesz oddzwonienia.

WIDOK Z POMOCĄ

Dzięki temu twój widok nasłuchuje zdarzenia na viewmodel. Można to wszystko zawrzeć w zachowaniu mieszania, aby uniknąć opóźnień w korzystaniu z kodu i zasobów, jeśli masz takie skłonności (FMI, podklasę klasy „Zachowanie”, aby zobaczyć coś w rodzaju mieszanej właściwości dołączanej na sterydach). Na razie zrobimy to ręcznie dla każdego widoku:

a) Utwórz OpenXXXXXDialogEvent z niestandardowym ładunkiem (klasa pochodna DialogViewModel).

b) Poproś widok, aby zasubskrybował to wydarzenie w swoim zdarzeniu OnDataContextChanged. Pamiętaj, aby ukryć i anulować subskrypcję, jeśli stara wartość! = Null oraz w zdarzeniu okna rozładowanego.

c) Gdy zdarzenie się uruchomi, otwórz widok, który może znajdować się w zasobie na Twojej stronie, lub możesz go zlokalizować zgodnie z konwencją w innym miejscu (np. w podejściu do usługi dialogu).

To podejście jest bardziej elastyczne, ale wymaga więcej pracy. Nie używam tego dużo. Jedną miłą zaletą jest na przykład fizyczne umieszczenie widoku wewnątrz karty. Użyłem algorytmu, aby umieścić go w granicach bieżącego formantu użytkownika, a jeśli nie jest wystarczająco duży, przejdź w górę drzewa wizualnego, aż znajdzie się wystarczająco duży kontener.

Dzięki temu okna dialogowe mogą znajdować się w pobliżu miejsca, w którym faktycznie są używane, przyciemnić tylko część aplikacji związaną z bieżącą aktywnością i pozwolić użytkownikowi poruszać się po aplikacji bez konieczności ręcznego odsuwania okien dialogowych, a nawet mieć wiele quasi- modalne okna dialogowe otwierają się na różnych kartach lub widokach podrzędnych.


Usługa dialogu jest zdecydowanie łatwiejsza i zwykle to robię. Ułatwia także zamknięcie okna dialogowego widoku z nadrzędnego modelu widoku, co jest konieczne, gdy nadrzędny model widoku jest zamykany lub anulowany.
Chris Bordeman

4

Użyj polecenia zamrażania

<Grid>
        <Grid.DataContext>
            <WpfApplication1:ViewModel />
        </Grid.DataContext>


        <Button Content="Text">
            <Button.Command>
                <WpfApplication1:MessageBoxCommand YesCommand="{Binding MyViewModelCommand}" />
            </Button.Command>
        </Button>

</Grid>
public class MessageBoxCommand : Freezable, ICommand
{
    public static readonly DependencyProperty YesCommandProperty = DependencyProperty.Register(
        "YesCommand",
        typeof (ICommand),
        typeof (MessageBoxCommand),
        new FrameworkPropertyMetadata(null)
        );


    public static readonly DependencyProperty OKCommandProperty = DependencyProperty.Register(
        "OKCommand",
        typeof (ICommand),
        typeof (MessageBoxCommand),
        new FrameworkPropertyMetadata(null)
        );


    public static readonly DependencyProperty CancelCommandProperty = DependencyProperty.Register(
        "CancelCommand",
        typeof (ICommand),
        typeof (MessageBoxCommand),
        new FrameworkPropertyMetadata(null)
        );


    public static readonly DependencyProperty NoCommandProperty = DependencyProperty.Register(
        "NoCommand",
        typeof (ICommand),
        typeof (MessageBoxCommand),
        new FrameworkPropertyMetadata(null)
        );


    public static readonly DependencyProperty MessageProperty = DependencyProperty.Register(
        "Message",
        typeof (string),
        typeof (MessageBoxCommand),
        new FrameworkPropertyMetadata("")
        );

    public static readonly DependencyProperty MessageBoxButtonsProperty = DependencyProperty.Register(
        "MessageBoxButtons",
        typeof(MessageBoxButton),
        typeof(MessageBoxCommand),
        new FrameworkPropertyMetadata(MessageBoxButton.OKCancel)
        );

    public ICommand YesCommand
    {
        get { return (ICommand) GetValue(YesCommandProperty); }
        set { SetValue(YesCommandProperty, value); }
    }

    public ICommand OKCommand
    {
        get { return (ICommand) GetValue(OKCommandProperty); }
        set { SetValue(OKCommandProperty, value); }
    }

    public ICommand CancelCommand
    {
        get { return (ICommand) GetValue(CancelCommandProperty); }
        set { SetValue(CancelCommandProperty, value); }
    }

    public ICommand NoCommand
    {
        get { return (ICommand) GetValue(NoCommandProperty); }
        set { SetValue(NoCommandProperty, value); }
    }

    public MessageBoxButton MessageBoxButtons
    {
        get { return (MessageBoxButton)GetValue(MessageBoxButtonsProperty); }
        set { SetValue(MessageBoxButtonsProperty, value); }
    }

    public string Message
    {
        get { return (string) GetValue(MessageProperty); }
        set { SetValue(MessageProperty, value); }
    }

    public void Execute(object parameter)
    {
        var messageBoxResult = MessageBox.Show(Message);
        switch (messageBoxResult)
        {
            case MessageBoxResult.OK:
                OKCommand.Execute(null);
                break;
            case MessageBoxResult.Yes:
                YesCommand.Execute(null);
                break;
            case MessageBoxResult.No:
                NoCommand.Execute(null);
                break;
            case MessageBoxResult.Cancel:
                if (CancelCommand != null) CancelCommand.Execute(null); //Cancel usually means do nothing ,so can be null
                break;

        }
    }

    public bool CanExecute(object parameter)
    {
        return true;
    }

    public event EventHandler CanExecuteChanged;


    protected override Freezable CreateInstanceCore()
    {
        throw new NotImplementedException();
    }
}

Ten kod wymaga trochę pracy, ale jest to jak dotąd najlepszy pomysł, szczególnie w przypadku okien dialogowych systemu, takich jak okna plików lub drukarek. Dialogi należą do widoku, jeśli cokolwiek. W przypadku okien dialogowych pliku wynik (wybrana nazwa pliku) można przekazać do polecenia wewnętrznego jako jego parametr.
Anton Tykhyy

3

Uważam, że obsługa okna dialogowego powinna odpowiadać za widok, a widok musi mieć kod do obsługi tego widoku.

Jeśli zmienisz interakcję ViewModel - View w celu obsługi okien dialogowych, wówczas ViewModel jest zależny od tej implementacji. Najprostszym sposobem rozwiązania tego problemu jest uczynienie Widoku odpowiedzialnym za wykonanie zadania. Jeśli oznacza to wyświetlenie okna dialogowego, to dobrze, ale może to być również komunikat o stanie na pasku stanu itp.

Chodzi mi o to, że cały punkt wzorca MVVM polega na oddzieleniu logiki biznesowej od GUI, więc nie powinieneś mieszać logiki GUI (aby wyświetlić okno dialogowe) w warstwie biznesowej (ViewModel).


2
Maszyna wirtualna nigdy nie obsługiwałaby okna dialogowego, w moim przykładzie mogłoby to mieć zdarzenie, które wymagałoby uruchomienia okna dialogowego i przekazania informacji w jakiejś formie EventArgs. Jeśli widok jest odpowiedzialny, w jaki sposób przekazuje informacje do maszyny wirtualnej?
Ray Booysen

Powiedz, że maszyna wirtualna musi coś usunąć. Maszyna wirtualna wywołuje metodę w widoku Usuń, która zwraca wartość logiczną. Widok może następnie albo usunąć element bezpośrednio i zwrócić wartość true, albo wyświetlić okno dialogowe potwierdzenia i zwrócić wartość true / false, w zależności od odpowiedzi użytkownika.
Cameron MacFarland

Maszyna wirtualna nic nie wie o oknie dialogowym, ale poprosiła widok tylko o usunięcie czegoś, co widok potwierdził lub odrzucił.
Cameron MacFarland

Zawsze myślałem, że punktem MVVM jest Model: logika biznesowa, ViewModel: Logika GUI i Widok: brak logiki. Który jest w jakiś sposób zaprzeczony przez twój ostatni akapit. Proszę wytłumacz!
David Schmitt

2
Najpierw należy ustalić, czy prośba o potwierdzenie przed usunięciem jest logiką biznesową czy logiką przeglądania. Jeśli jest to logika biznesowa, metoda DeleteFile w modelu nie może tego robić, a raczej zwraca obiekt pytania potwierdzającego. Obejmuje to odniesienie do delegata, który dokonuje faktycznego usunięcia. Jeśli nie jest to logika biznesowa, maszyna wirtualna musi zbudować maszynę wirtualną pytania w DeleteFileCommand z dwoma członkami ICommand. Jeden za tak, a drugi za nie. Prawdopodobnie istnieją argumenty dla obu widoków, aw RL większość zastosowań prawdopodobnie napotka oba.
Guge


3

Dlaczego po prostu nie zgłosić zdarzenia na maszynie wirtualnej i zasubskrybować to wydarzenie w widoku? Pozwoliłoby to zachować logikę aplikacji i widok osobno i nadal pozwalało na korzystanie z okna potomnego do okien dialogowych.


3

Zaimplementowałem zachowanie, które nasłuchuje komunikatu z ViewModel. Opiera się na rozwiązaniu Laurent Bugnion, ale ponieważ nie wykorzystuje kodu z tyłu i jest bardziej wielokrotnego użytku, myślę, że jest bardziej elegancki.

Jak sprawić, aby WPF zachowywał się tak, jakby MVVM był obsługiwany po wyjęciu z pudełka


1
Powinieneś dołączyć tutaj pełny kod, ponieważ tego SO wymaga dobrych odpowiedzi. Niemniej jednak połączone podejście jest całkiem fajne, więc dziękuję za to! :)
Yoda

2
@yoda pełny kod jest dość długi i dlatego wolę link do niego. Zredagowałem swoją odpowiedź, aby odzwierciedlić zmiany i wskazać link, który nie jest zepsuty
Elad Katz,

Dzięki za ulepszenie. Niemniej jednak lepiej jest zapewnić przewijanie całostronicowych kodów 3 długo tutaj na SO, niż łącze, które może kiedyś być offline. Dobre artykuły na skomplikowane tematy są zawsze dość długie - i nie widzę żadnej korzyści z otwierania nowej karty, przełączania się do niej i przewijania tam przewijania na tej samej stronie / karcie, na której byłem wcześniej. ;)
Yoda,

@EladKatz Widziałem, że udostępniłeś część swojej implementacji WPF pod podanym linkiem. Czy masz jakieś rozwiązanie do otwierania nowego okna z ViewModel? Zasadniczo mam dwie formy, a każda z nich ma jeden ViewModel. Jeden użytkownik klika przycisk, pojawia się inny formularz i viewmodel1 wysyła swój obiekt do viewmodel2. W formularzu 2 użytkownik może zmienić obiekt, a po zamknięciu okna zaktualizowany obiekt zostanie odesłany z powrotem do pierwszego modelu ViewModel. Czy masz na to jakieś rozwiązanie?
Ehsan

2

Myślę, że widok może mieć kod do obsługi zdarzenia z modelu widoku.

W zależności od zdarzenia / scenariusza może również mieć wyzwalacz zdarzenia, który subskrybuje, aby wyświetlić zdarzenia modelowe, i jedną lub więcej akcji do wywołania w odpowiedzi.




1

Karl Shifflett stworzył przykładową aplikację do wyświetlania okien dialogowych przy użyciu podejścia serwisowego i podejścia Prism InteractionRequest.

Podoba mi się podejście do usługi - jest mniej elastyczne, więc użytkownicy są mniej podatni na uszkodzenie :) Jest to również zgodne z częścią mojej aplikacji WinForm (MessageBox.Show) Ale jeśli planujesz wyświetlać wiele różnych okien dialogowych, to InteractionRequest jest lepszy sposób.

http://karlshifflett.wordpress.com/2010/11/07/in-the-box-ndash-mvvm-training/


1

Wiem, że to stare pytanie, ale kiedy przeprowadziłem to wyszukiwanie, znalazłem wiele powiązanych pytań, ale nie znalazłem naprawdę jasnej odpowiedzi. Tworzę więc własną implementację okna dialogowego / skrzynki wiadomości / popin i udostępniam to!
Myślę, że jest to „dowód MVVM” i staram się, aby było to proste i właściwe, ale jestem nowy w WPF, więc nie krępuj się komentować, a nawet wysyłać żądania ściągania.

https://github.com/Plasma-Paris/Plasma.WpfUtils

Możesz użyć tego w następujący sposób:

public RelayCommand YesNoMessageBoxCommand { get; private set; }
async void YesNoMessageBox()
{
    var result = await _Service.ShowMessage("This is the content of the message box", "This is the title", System.Windows.MessageBoxButton.YesNo);
    if (result == System.Windows.MessageBoxResult.Yes)
        // [...]
}

Lub jeśli chcesz bardziej wyrafinowanego popina:

var result = await _Service.ShowCustomMessageBox(new MyMessageBoxViewModel { /* What you want */ });

I pokazuje takie rzeczy:

2)


1

Standardowe podejście

Po latach spędzonych na rozwiązywaniu tego problemu w WPF, w końcu wymyśliłem standardowy sposób wdrażania dialogów w WPF. Oto zalety tego podejścia:

  1. CZYSTY
  2. Nie narusza wzorca projektowego MVVM
  3. ViewModal nigdy nie odwołuje się do żadnej z bibliotek interfejsu użytkownika (WindowBase, PresentationFramework itp.)
  4. Idealny do zautomatyzowanych testów
  5. Dialogi można łatwo wymienić.

Więc jaki jest klucz. Jest to DI + IoC .

Oto jak to działa. Używam MVVM Light, ale to podejście może zostać rozszerzone również na inne frameworki:

  1. Dodaj projekt aplikacji WPF do swojego rozwiązania. Nazwij to aplikacją .
  2. Dodaj bibliotekę klas ViewModal. Nazwij to VM .
  3. Aplikacja odwołuje się do projektu VM. Projekt VM nic nie wie o aplikacji.
  4. Dodaj odniesienie NuGet do MVVM Light do obu projektów . Używam MVVM Light Standard , ale nie masz nic przeciwko pełnej wersji Framework.
  5. Dodaj interfejs IDialogService do projektu VM:

    public interface IDialogService
    {
      void ShowMessage(string msg, bool isError);
      bool AskBooleanQuestion(string msg);
      string AskStringQuestion(string msg, string default_value);
    
      string ShowOpen(string filter, string initDir = "", string title = "");
      string ShowSave(string filter, string initDir = "", string title = "", string fileName = "");
      string ShowFolder(string initDir = "");
    
      bool ShowSettings();
    }
  6. Ujawnij publiczną właściwość statyczną IDialogServicetypu w swoim ViewModelLocator, ale pozostaw część rejestracyjną do wykonania dla warstwy Widok. To jest klucz :

    public static IDialogService DialogService => SimpleIoc.Default.GetInstance<IDialogService>();
  7. Dodaj implementację tego interfejsu w projekcie aplikacji.

    public class DialogPresenter : IDialogService
    {
        private static OpenFileDialog dlgOpen = new OpenFileDialog();
        private static SaveFileDialog dlgSave = new SaveFileDialog();
        private static FolderBrowserDialog dlgFolder = new FolderBrowserDialog();
    
        /// <summary>
        /// Displays a simple Information or Error message to the user.
        /// </summary>
        /// <param name="msg">String text that is to be displayed in the MessageBox</param>
        /// <param name="isError">If true, Error icon is displayed. If false, Information icon is displayed.</param>
        public void ShowMessage(string msg, bool isError)
        {
                if(isError)
                        System.Windows.MessageBox.Show(msg, "Your Project Title", MessageBoxButton.OK, MessageBoxImage.Error);
                else
                        System.Windows.MessageBox.Show(msg, "Your Project Title", MessageBoxButton.OK, MessageBoxImage.Information);
        }
    
        /// <summary>
        /// Displays a Yes/No MessageBox.Returns true if user clicks Yes, otherwise false.
        /// </summary>
        /// <param name="msg"></param>
        /// <returns></returns>
        public bool AskBooleanQuestion(string msg)
        {
                var Result = System.Windows.MessageBox.Show(msg, "Your Project Title", MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes;
                return Result;
        }
    
        /// <summary>
        /// Displays Save dialog. User can specify file filter, initial directory and dialog title. Returns full path of the selected file if
        /// user clicks Save button. Returns null if user clicks Cancel button.
        /// </summary>
        /// <param name="filter"></param>
        /// <param name="initDir"></param>
        /// <param name="title"></param>
        /// <param name="fileName"></param>
        /// <returns></returns>
        public string ShowSave(string filter, string initDir = "", string title = "", string fileName = "")
        {
                if (!string.IsNullOrEmpty(title))
                        dlgSave.Title = title;
                else
                        dlgSave.Title = "Save";
    
                if (!string.IsNullOrEmpty(fileName))
                        dlgSave.FileName = fileName;
                else
                        dlgSave.FileName = "";
    
                dlgSave.Filter = filter;
                if (!string.IsNullOrEmpty(initDir))
                        dlgSave.InitialDirectory = initDir;
    
                if (dlgSave.ShowDialog() == DialogResult.OK)
                        return dlgSave.FileName;
                else
                        return null;
        }
    
    
        public string ShowFolder(string initDir = "")
        {
                if (!string.IsNullOrEmpty(initDir))
                        dlgFolder.SelectedPath = initDir;
    
                if (dlgFolder.ShowDialog() == DialogResult.OK)
                        return dlgFolder.SelectedPath;
                else
                        return null;
        }
    
    
        /// <summary>
        /// Displays Open dialog. User can specify file filter, initial directory and dialog title. Returns full path of the selected file if
        /// user clicks Open button. Returns null if user clicks Cancel button.
        /// </summary>
        /// <param name="filter"></param>
        /// <param name="initDir"></param>
        /// <param name="title"></param>
        /// <returns></returns>
        public string ShowOpen(string filter, string initDir = "", string title = "")
        {
                if (!string.IsNullOrEmpty(title))
                        dlgOpen.Title = title;
                else
                        dlgOpen.Title = "Open";
    
                dlgOpen.Multiselect = false;
                dlgOpen.Filter = filter;
                if (!string.IsNullOrEmpty(initDir))
                        dlgOpen.InitialDirectory = initDir;
    
                if (dlgOpen.ShowDialog() == DialogResult.OK)
                        return dlgOpen.FileName;
                else
                        return null;
        }
    
        /// <summary>
        /// Shows Settings dialog.
        /// </summary>
        /// <returns>true if User clicks OK button, otherwise false.</returns>
        public bool ShowSettings()
        {
                var w = new SettingsWindow();
                MakeChild(w); //Show this dialog as child of Microsoft Word window.
                var Result = w.ShowDialog().Value;
                return Result;
        }
    
        /// <summary>
        /// Prompts user for a single value input. First parameter specifies the message to be displayed in the dialog 
        /// and the second string specifies the default value to be displayed in the input box.
        /// </summary>
        /// <param name="m"></param>
        public string AskStringQuestion(string msg, string default_value)
        {
                string Result = null;
    
                InputBox w = new InputBox();
                MakeChild(w);
                if (w.ShowDialog(msg, default_value).Value)
                        Result = w.Value;
    
                return Result;
        }
    
        /// <summary>
        /// Sets Word window as parent of the specified window.
        /// </summary>
        /// <param name="w"></param>
        private static void MakeChild(System.Windows.Window w)
        {
                IntPtr HWND = Process.GetCurrentProcess().MainWindowHandle;
                var helper = new WindowInteropHelper(w) { Owner = HWND };
        }
    }
  8. Chociaż niektóre z tych funkcji są generyczne ( ShowMessage, AskBooleanQuestionetc.), inne są specyficzne dla tego projektu i wykorzystanie niestandardowych Windows. Możesz dodać więcej niestandardowych okien w ten sam sposób. Kluczem jest utrzymanie elementów specyficznych dla interfejsu użytkownika w warstwie widoku i po prostu odsłonięcie zwróconych danych za pomocą POCO w warstwie maszyny wirtualnej .
  9. Wykonaj rejestrację IoC swojego interfejsu w warstwie widoku za pomocą tej klasy. Możesz to zrobić w konstruktorze głównego widoku (po InitializeComponent()wywołaniu):

    SimpleIoc.Default.Register<IDialogService, DialogPresenter>();
  10. Proszę bardzo. Masz teraz dostęp do wszystkich funkcji okna dialogowego na warstwach VM i View. Twoja warstwa maszyny wirtualnej może wywoływać następujące funkcje:

    var NoTrump = ViewModelLocator.DialogService.AskBooleanQuestion("Really stop the trade war???", "");
  11. Tak czysty, widzisz. Warstwa maszyny wirtualnej nie wie nic o tym, jak warstwa interfejsu użytkownika będzie przedstawiać użytkownikowi pytanie Tak / Nie i nadal może z powodzeniem pracować ze zwróconym wynikiem z okna dialogowego.

Inne bezpłatne profity

  1. Aby napisać test jednostkowy, możesz podać niestandardową implementację IDialogServicew swoim projekcie testowym i zarejestrować tę klasę w IoC w konstruktorze swojej klasy testowej.
  2. Musisz zaimportować niektóre przestrzenie nazw, takie jak Microsoft.Win32dostęp do okien dialogowych Otwórz i Zapisz. Zostawiłem je, ponieważ dostępna jest również wersja tych okien dialogowych WinForm, a także ktoś może chcieć stworzyć własną wersję. Zauważ też, że niektóre z używanych identyfikatorów DialogPresenterto nazwy własnych okien (np SettingsWindow.). Musisz usunąć je zarówno z interfejsu, jak i z implementacji, lub podać własne okna.
  3. Jeśli maszyna wirtualna wykonuje wielowątkowość, zadzwoń do MVVM Light na DispatcherHelper.Initialize()wczesnym etapie cyklu życia aplikacji.
  4. Z wyjątkiem sytuacji, w DialogPresenterktórej wstrzykiwany jest do warstwy View, inne ViewModals powinny zostać zarejestrowane, ViewModelLocatora następnie publiczna właściwość statyczna tego typu powinna zostać ujawniona, aby warstwa View mogła zużyć. Coś takiego:

    public static SettingsVM Settings => SimpleIoc.Default.GetInstance<SettingsVM>();
  5. W przeważającej części twoje okna dialogowe nie powinny mieć żadnych opóźnień w kodowaniu dla takich rzeczy jak wiązanie lub ustawianie DataContext itp. Nie powinieneś nawet przekazywać rzeczy jako parametrów konstruktora. XAML może zrobić to wszystko za Ciebie:

    <Window x:Class="YourViewNamespace.SettingsWindow"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:local="clr-namespace:YourViewProject"
      xmlns:vm="clr-namespace:YourVMProject;assembly=YourVMProject"
      DataContext="{x:Static vm:ViewModelLocator.Settings}"
      d:DataContext="{d:DesignInstance Type=vm:SettingsVM}" />
  6. Ustawienie w DataContextten sposób zapewnia wszystkie korzyści w czasie projektowania, takie jak Intellisense i automatyczne uzupełnianie.

Mam nadzieję, że pomaga wszystkim.


0

Zastanawiałem się nad podobnym problemem, pytając, jak powinien wyglądać model widoku dla zadania lub okna dialogowego .

Moje obecne rozwiązanie wygląda następująco:

public class SelectionTaskModel<TChoosable> : ViewModel
    where TChoosable : ViewModel
{
    public SelectionTaskModel(ICollection<TChoosable> choices);
    public ReadOnlyCollection<TChoosable> Choices { get; }
    public void Choose(TChoosable choosen);
    public void Abort();
}

Gdy model widoku decyduje, że dane wejściowe użytkownika są wymagane, wyświetla instancję SelectionTaskModelz możliwymi opcjami dla użytkownika. Infrastruktura zajmuje się wyświetlaniem odpowiedniego widoku, który w odpowiednim czasie wywołaChoose() funkcję z wyborem użytkownika.


0

Walczyłem z tym samym problemem. Wymyśliłem sposób na komunikację między View a ViewModel. Możesz zainicjować wysłanie wiadomości z ViewModel do View, aby kazać mu wyświetlić okno komunikatu, a on zgłosi się z wynikiem. Następnie ViewModel może odpowiedzieć na wynik zwrócony z widoku.

Wykazuję to na moim blogu :



0

Przepraszam, ale muszę się włączyć. Przed znalezieniem przestrzeni nazw Prism.Wpf.Interactivity w projekcie Prism zapoznałem się z kilkoma sugerowanymi rozwiązaniami. Możesz użyć żądań interakcji i akcji wyskakującego okienka, aby rzucić okno niestandardowe lub dla prostszych potrzeb wbudowane są wyskakujące okienka powiadomień i potwierdzeń. Tworzą one prawdziwe okna i jako takie są zarządzane. możesz przekazać obiekt kontekstowy z dowolnymi zależnościami potrzebnymi w oknie dialogowym. Korzystamy z tego rozwiązania w mojej pracy, odkąd go znalazłem. Mamy tutaj wielu starszych deweloperów i nikt nie wymyślił nic lepszego. Naszym poprzednim rozwiązaniem była usługa dialogu w nakładce i użycie klasy prezentera, aby to się stało, ale musieliście mieć fabryki dla wszystkich modeli okien dialogowych itp.

To nie jest trywialne, ale też nie jest bardzo skomplikowane. I jest wbudowany w Pryzmat i dlatego jest najlepszą (lub lepszą) praktyką IMHO.

Moje 2 centy!


-1

EDYCJA: tak Zgadzam się, że to nie jest poprawne podejście MVVM i teraz używam czegoś podobnego do tego, co sugerują blindmeis.

Jednym ze sposobów na to jest

W głównym widoku modelu (w którym otwierasz modal):

void OpenModal()
{
    ModalWindowViewModel mwvm = new ModalWindowViewModel();
    Window mw = new Window();
    mw.content = mwvm;
    mw.ShowDialog()
    if(mw.DialogResult == true)
    { 
        // Your Code, you can access property in mwvm if you need.
    }
}

A w twoim oknie modalnym View / ViewModel:

XAML:

<Button Name="okButton" Command="{Binding OkCommand}" CommandParameter="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}">OK</Button>
<Button Margin="2" VerticalAlignment="Center" Name="cancelButton" IsCancel="True">Cancel</Button>

ViewModel:

public ICommand OkCommand
{
    get
    {
        if (_okCommand == null)
        {
            _okCommand = new ActionCommand<Window>(DoOk, CanDoOk);
        }
        return _okCommand ;
    }
}

void DoOk(Window win)
{
    <!--Your Code-->
    win.DialogResult = true;
    win.Close();
}

bool CanDoOk(Window win) { return true; }

lub podobny do zamieszczonego tutaj WPF MVVM: Jak zamknąć okno


2
Nie byłem przegłosowany, ale podejrzewam, że dzieje się tak, ponieważ model widoku ma bezpośrednie odniesienie do widoku.
Brian Gideon

@BrianGideon, dzięki za komentarz. Zgadzam się, że nie jest to rozwiązanie niezwiązane z wielkością produkcji. W rzeczywistości nie używam czegoś podobnego do whar zasugerowanego przez blindmeis. Dzięki jeszcze raz.
Simone

Nie jest łatwo dotrzeć do widoku, gdy tak łatwo nie jest.
Chris Bordeman
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.