W jaki sposób ViewModel powinien zamknąć formularz?

247

Próbuję nauczyć się WPF i problemu MVVM, ale wpadłem w kłopoty. To pytanie jest podobne, ale nie takie samo jak to (obsługa-dialogów-w-wpf-z-mvvm) ...

Mam formularz „Login” napisany przy użyciu wzorca MVVM.

Ten formularz ma ViewModel, który przechowuje nazwę użytkownika i hasło, które są powiązane z widokiem w XAML przy użyciu normalnych powiązań danych. Posiada również polecenie „Login”, które jest powiązane z przyciskiem „Zaloguj się” w formularzu, a także przy użyciu normalnego wiązania danych.

Po uruchomieniu polecenia „Login” wywołuje funkcję w ViewModel, która wyłącza się i wysyła dane przez sieć, aby się zalogować. Po zakończeniu tej funkcji dostępne są 2 działania:

  1. Login był nieprawidłowy - po prostu wyświetlamy MessageBox i wszystko jest w porządku

  2. Login był prawidłowy, musimy zamknąć formularz logowania i zwrócić go, aby był prawdziwy, ponieważ DialogResult...

Problem polega na tym, że ViewModel nie wie nic o rzeczywistym widoku, więc w jaki sposób można go zamknąć i poprosić o zwrócenie określonego DialogResult? Mógłbym przykleić trochę kodu do CodeBehind i / lub przekazać View do ViewModel, ale wygląda na to, że całkowicie pokonałby cały punkt MVVM ...


Aktualizacja

W końcu właśnie naruszyłem „czystość” wzoru MVVM i kazałem Viewowi opublikować Closedzdarzenie i ujawnić Closemetodę. ViewModel po prostu zadzwoniłby view.Close. Widok jest znany tylko przez interfejs i podłączony do kontenera IOC, więc nie można utracić możliwości testowania ani konserwacji.

Wydaje się głupie, że przyjęta odpowiedź to -5 głosów! Chociaż jestem w pełni świadomy dobrych uczuć, które można uzyskać, rozwiązując problem, będąc „czystym”, z pewnością nie jestem jedynym, który uważa, że ​​200 linii zdarzeń, poleceń i zachowań, aby uniknąć metody jednowierszowej w nazwy „wzorów” i „czystości” są nieco śmieszne ...

Orion Edwards
źródło
2
Nie głosowałem za zaakceptowaną odpowiedzią, ale zgaduję, że powodem głosów negatywnych jest to, że ogólnie nie jest to pomocne, nawet jeśli może działać w jednym przypadku. Sam to powiedziałeś w innym komentarzu: „Chociaż formularz logowania jest dialogiem„ dwóch pól ”, mam wiele innych, które są o wiele bardziej złożone (i dlatego uzasadniają MVVM), ale nadal muszą być zamknięte ...”
Joe Biały
1
Rozumiem twój punkt widzenia, ale osobiście uważam, że nawet w ogólnym przypadku prosta Closemetoda jest nadal najlepszym rozwiązaniem. Cała reszta w innych, bardziej skomplikowanych oknach dialogowych to MVVM i baza danych, ale wydawało się głupotą wdrażanie tutaj wielkich „rozwiązań” zamiast prostej metody…
Orion Edwards
2
Możesz sprawdzić następujący link do wyniku okna dialogowego asimsajjad.blogspot.com/2010/10/... , który zwróci okno dialogowe i zamknie widok z viewModel
Asim Sajjad
3
Zmień zaakceptowaną odpowiedź na to pytanie. Istnieje wiele dobrych rozwiązań, które są znacznie lepsze niż ktoś kwestionujący użycie MVVM dla tej funkcjonalności. To nie jest odpowiedź, to unikanie.
ScottCher
2
@OrionEdwards Myślę, że miałeś rację, aby złamać wzór tutaj. Głównym celem wzorca projektowego jest przyspieszenie cykli programistycznych, zwiększenie łatwości konserwacji i uproszczenie kodu, dzięki czemu cały zespół przestrzega tych samych zasad. Nie można tego osiągnąć przez dodanie zależności od bibliotek zewnętrznych i zaimplementowanie setek linii kodu w celu wykonania zadania, całkowicie ignorując to, że istnieje o wiele prostsze rozwiązanie, tylko dlatego, że trzeba się upierać, aby poświęcić „czystość” wzorca. Wystarczy upewnić się, co do dokumentu your've zrobione i KISS kodu ( k EEP i t s hort i s wyko).
M463

Odpowiedzi:

324

Zainspirowała mnie odpowiedź Thejuana, by napisać prostszą załączoną własność. Bez stylów, bez wyzwalaczy; zamiast tego możesz po prostu to zrobić:

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

Jest to prawie tak czyste, jakby zespół WPF dobrze to zrobił i uczynił DialogResult przede wszystkim właściwością zależności. Po prostu umieść bool? DialogResultwłaściwość na swoim ViewModel i zaimplementuj INotifyPropertyChanged, i voila, twój ViewModel może zamknąć okno (i ustawić jego DialogResult) po prostu przez ustawienie właściwości. MVVM tak jak powinno być.

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

Opublikowałem to również na moim blogu .

Joe White
źródło
3
To jest odpowiedź, którą najbardziej lubię! Dobra robota pisania załączonej nieruchomości.
Jorge Vargas,
2
Fajna opcja, ale w tym rozwiązaniu występuje subtelny błąd. Jeśli model widoku dla okna dialogowego jest singletonem, wartość DialogResult jest przenoszona do następnego użycia okna dialogowego. Oznacza to, że natychmiast anuluje lub zaakceptuje przed wyświetleniem się, więc okno dialogowe nie wyświetli się po raz drugi.
Gone Coding
13
@HiTech Magic, wygląda na to, że błąd polega na użyciu singletona ViewModel. (uśmiech) Poważnie, dlaczego, do licha, chcesz mieć singleton ViewModel? Utrzymywanie stanu zmiennego w zmiennych globalnych jest złym pomysłem. Sprawia, że ​​testowanie staje się koszmarem, a testowanie jest jednym z powodów, dla których chciałbyś użyć MVVM.
Joe White
3
Czy celem MVVM nie jest ścisłe powiązanie logiki z konkretnym interfejsem użytkownika? W tym przypadku bool? z pewnością nie jest użyteczny przez inny interfejs użytkownika, taki jak WinForm, a DialogCloser jest specyficzny dla WPF. Jak to pasuje do rozwiązania? Ponadto, dlaczego pisać kod 2x-10x, aby zamknąć okno za pomocą wiązania?
David Anderson
2
@DavidAnderson, w żadnym wypadku nie wypróbowałbym MVVM z WinForms; obsługa wiązania danych jest zbyt słaba, a MVVM opiera się na przemyślanym systemie wiązania. I to nigdzie w pobliżu kodu 2x-10x. Napisz ten kod raz , nie raz dla każdego okna. Następnie jest to jedno-wierszowe wiązanie plus właściwość powiadamiająca, wykorzystująca ten sam mechanizm, którego już używasz do wszystkiego innego w twoim widoku (więc na przykład nie musisz wstrzykiwać dodatkowego interfejsu widoku, aby obsłużyć zamykanie okno). Możesz dokonywać innych kompromisów, ale wydaje mi się, że to ogólnie dobry interes.
Joe White,
64

Z mojego punktu widzenia pytanie jest całkiem dobre, ponieważ to samo podejście byłoby stosowane nie tylko w przypadku okna „Logowanie”, ale w przypadku każdego rodzaju okna. Przejrzałem wiele sugestii i żadne z nich nie jest dla mnie OK. Zapoznaj się z moją sugestią zaczerpniętą z artykułu o wzorze projektu MVVM .

Każda klasa ViewModel powinna dziedziczyć WorkspaceViewModelpo RequestClosezdarzeniu i CloseCommandwłaściwości ICommandtypu. Domyślna implementacja CloseCommandwłaściwości podniesie RequestClosezdarzenie.

Aby zamknąć OnLoadedokno, należy zastąpić metodę okna:

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

lub OnStartupmetoda Twojej aplikacji:

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

Wydaje mi się, że implementacja RequestClosezdarzeń i CloseCommandwłaściwości w WorkspaceViewModelsą dość jasne, ale pokażę, że są spójne:

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

I kod źródłowy 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 Nie traktuj mnie źle dla tych źródeł! Gdybym je miał wczoraj, zaoszczędziłoby mi to kilka godzin ...

PPS Wszelkie uwagi i sugestie są mile widziane.

Budda
źródło
2
Umm, fakt, że podłączyłeś się do procedury obsługi zdarzeń customer.RequestClosew kodzie za plikiem XAML, nie narusza wzorca MVVM? Równie dobrze możesz powiązać Clickmoduł obsługi zdarzeń za pomocą przycisku zamykania, ponieważ i tak dotknąłeś kodu i zrobiłeś to this.Close()! Dobrze?
GONeale
1
Nie mam zbyt wielu problemów z podejściem do zdarzenia, ale nie podoba mi się słowo RequestClose, ponieważ dla mnie wciąż oznacza to dużą wiedzę na temat implementacji widoku. Wolę ujawniać właściwości, takie jak IsCancelled, które wydają się być bardziej znaczące, biorąc pod uwagę kontekst i sugerują mniej o tym, co widok powinien zrobić w odpowiedzi.
jpierson
18

Użyłem dołączonych zachowań, aby zamknąć okno. Powiąż właściwość „signal” w ViewModel z dołączonym zachowaniem (faktycznie używam wyzwalacza) Gdy jest ustawiona na true, zachowanie zamyka okno.

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

Adam Mills
źródło
Jest to jedyna jak dotąd odpowiedź, która nie wymaga żadnego kodu w oknie (i faktycznie zamyka modalne okno, zamiast sugerować inne podejście). Szkoda, że ​​wymaga tyle złożoności, ze Stylem i Wyzwalaczem i tym całym gnojkiem - wydaje się, że to naprawdę powinno być wykonalne z zachowaniem przywiązania do jednej linii.
Joe White
4
Teraz jest to wykonalne przy zachowaniu dołączenia w jednym wierszu. Zobacz moją odpowiedź: stackoverflow.com/questions/501886/...
Joe White
15

Istnieje wiele komentarzy na temat zalet i wad MVVM tutaj. Dla mnie zgadzam się z Nirem; to kwestia odpowiedniego użycia wzorca, a MVVM nie zawsze pasuje. Wydaje się, że ludzie byli gotowi poświęcić wszystkie najważniejsze zasady projektowania oprogramowania, aby dopasować go do MVVM.

To powiedziawszy ... myślę, że twoja sprawa może być dobrze dopasowana z odrobiną refaktoryzacji.

W większości przypadków, z którymi się spotkałem, WPF umożliwia obejście BEZ wielu Windows. Może mógłbyś spróbować użyć Frames i Pages zamiast Windowsa z DialogResults.

W twoim przypadku moja sugestia byłaby LoginFormViewModelobsługiwana, LoginCommanda jeśli login jest nieprawidłowy, ustaw właściwość na LoginFormViewModelodpowiednią wartość ( falselub pewną wartość wyliczeniową jak UserAuthenticationStates.FailedAuthentication). Zrobiłbyś to samo dla udanego logowania ( truelub innej wartości wyliczeniowej). Następnie użyjesz takiego, DataTriggerktóry reaguje na różne stany uwierzytelnienia użytkownika i możesz użyć prostej Setterzmiany Sourcewłaściwości Frame.

Po zwróceniu okna logowania DialogResultmyślę, że to miejsce, w którym się mylisz; że DialogResultjest naprawdę własnością Twojej ViewModel. W moim, co prawda ograniczonym doświadczeniu z WPF, gdy coś nie jest w porządku, zwykle dlatego, że myślę o tym, jak zrobiłbym to samo w WinForm.

Mam nadzieję, że to pomaga.

EightyOne Unite
źródło
10

Zakładając, że okno dialogowe logowania jest pierwszym oknem, które zostanie utworzone, wypróbuj to w klasie LoginViewModel:

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

            App.Current.MainWindow.Close();
            App.Current.MainWindow = window;
        }
        else
        {
            LoginError = true;
        }
    }
Jim Wallace
źródło
Mężczyźni to jest proste i działa świetnie. Obecnie używam tego podejścia.
Erre Efe,
Działa tylko w oknie MAIN. Więc nie używaj go do żadnych innych okien.
Oleksii
7

Jest to proste i czyste rozwiązanie - dodajesz zdarzenie do ViewModel i instruujesz okno, aby zamknęło się po uruchomieniu zdarzenia.

Aby uzyskać więcej informacji, zobacz mój post na blogu, Zamknij okno z 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);
}

Uwaga: w przykładzie użyto Pryzmatu DelegateCommand(patrz Pryzmat: Dowodzenie ), ale w tym przypadku ICommandmożna zastosować dowolną implementację.

Możesz używać zachowań z tego oficjalnego pakietu.

Shimmy Weitzhandler
źródło
2
+1, ale powinieneś podać więcej szczegółów w samej odpowiedzi, na przykład, że to rozwiązanie wymaga odniesienia do zestawu Interaktywność mieszania wyrażeń.
surfen
6

Sposób, w jaki sobie z tym poradzę, to dodanie modułu obsługi zdarzeń w moim ViewModel. Po pomyślnym zalogowaniu użytkownika uruchomiłbym zdarzenie. Moim zdaniem dołączam się do tego wydarzenia, a kiedy się uruchomi, zamknę okno.


źródło
2
Tak też zwykle robię. Mimo że wydaje się to trochę brudne, biorąc pod uwagę wszystkie te nowomodne polecenia dowodzenia wpf.
Botz3000,
4

Oto, co początkowo zrobiłem, co działa, ale wydaje się dość rozwlekłe i brzydkie (globalne statyczne cokolwiek nigdy nie jest dobre)

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

Później usunąłem cały ten kod i właśnie LoginFormViewModelwywołałem metodę Close w jego widoku. Okazało się, że jest o wiele ładniejszy i łatwiejszy do naśladowania. IMHO punkt wzorów jest dać ludziom łatwiejszy sposób, aby zrozumieć, co aplikacja robi, i w tym przypadku, MVVM robił to znacznie trudniejsze do zrozumienia niż gdybym nie go stosować, i był obecnie anty -pattern.

Orion Edwards
źródło
3

Do twojej wiadomości wpadłem na ten sam problem i myślę, że wymyśliłem rozwiązanie, które nie wymaga globalizacji ani statyki, chociaż może nie być najlepszą odpowiedzią. Pozwalam wam to sami zdecydować.

W moim przypadku ViewModel, który tworzy okno, które ma zostać wyświetlone (pozwala nazwać to ViewModelMain), również wie o LoginFormViewModel (wykorzystując powyższą sytuację jako przykład).

Więc stworzyłem właściwość na LoginFormViewModel, która była typu ICommand (nazwijmy ją CloseWindowCommand). Następnie, zanim wywołam funkcję .ShowDialog () w oknie, ustawiam właściwość CloseWindowCommand w LoginFormViewModel na metodę window.Close () instancji okna, którą utworzyłem. Następnie wewnątrz LoginFormViewModel wszystko, co muszę zrobić, to wywołać CloseWindowCommand.Execute (), aby zamknąć okno.

Przypuszczam, że jest to trochę obejście / hack, ale działa dobrze, nie naruszając wzorca MVVM.

Zapraszam do krytykowania tego procesu tak bardzo, jak chcesz, mogę to wziąć! :)


źródło
Nie jestem pewien, czy w pełni tego nie rozumiem, ale czy to nie oznacza, że ​​Twoje MainWindow musi zostać utworzone przed LoginWindow? Tego chciałbym uniknąć, jeśli to możliwe
Orion Edwards
3

Jest to prawdopodobnie bardzo późno, ale natknąłem się na ten sam problem i znalazłem rozwiązanie, które działa dla mnie.

Nie mogę wymyślić, jak stworzyć aplikację bez okien dialogowych (może to tylko blokada myśli). Byłem w impasie z MVVM i pokazałem dialog. Więc natknąłem się na ten artykuł CodeProject:

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

Który jest UserControl, który zasadniczo pozwala, aby okno znajdowało się w drzewie wizualnym innego okna (niedozwolone w Xaml). Ujawnia również wartość logiczną DependencyProperty o nazwie IsShowing.

Możesz ustawić styl, taki jak zwykle w słowniku zasobów, który zasadniczo wyświetla okno dialogowe za każdym razem, gdy właściwość Content formantu! = Null za pośrednictwem wyzwalaczy:

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

W widoku, w którym chcesz wyświetlić okno dialogowe, po prostu to:

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

A w ViewModel wszystko, co musisz zrobić, to ustawić właściwość na wartość (Uwaga: klasa ViewModel musi obsługiwać INotifyPropertyChanged, aby widok wiedział, że coś się stało).

tak:

DialogViewModel = new DisplayViewModel();

Aby dopasować ViewModel do widoku, powinieneś mieć coś takiego w słowniku zasobów:

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

Dzięki temu otrzymujesz jednoliniowy kod do wyświetlenia okna dialogowego. Problem pojawia się w tym, że nie można tak naprawdę zamknąć okna dialogowego za pomocą powyższego kodu. Dlatego właśnie musisz umieścić zdarzenie w klasie bazowej ViewModel, którą DisplayViewModel dziedziczy i zamiast powyższego kodu, napisz to

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

Następnie możesz obsłużyć wynik okna dialogowego poprzez oddzwonienie.

Może się to wydawać trochę skomplikowane, ale po ułożeniu podstaw jest to całkiem proste. Znowu to moja implementacja, jestem pewien, że są jeszcze inne :)

Mam nadzieję, że to pomaga, uratowało mnie.

Jose
źródło
3

Ok, więc to pytanie ma już prawie 6 lat i nadal nie mogę znaleźć tutaj tego, co uważam za właściwą odpowiedź, więc pozwólcie mi podzielić się moimi „2 centami” ...

Właściwie mam 2 sposoby na zrobienie tego, pierwszy jest prosty ... drugi na prawym, więc jeśli szukasz właściwego, po prostu pomiń # 1 i przejdź do # 2 :

1. Szybki i łatwy (ale niekompletny)

Jeśli mam tylko mały projekt, czasami po prostu tworzę CloseWindowAction w ViewModel:

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

I ktokolwiek tworzy skrzynkę Widoku lub w kodzie Widoku za, właśnie ustawiam Metodę, którą Wywoła Działanie:

(pamiętaj, że MVVM dotyczy separacji View i ViewModel ... kod View zachowuje się nadal w View i tak długo, jak istnieje właściwa separacja, nie naruszasz wzorca)

Jeśli jakiś ViewModel utworzy nowe okno:

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

Lub jeśli chcesz go w głównym oknie, po prostu umieść go pod konstruktorem widoku:

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

kiedy chcesz zamknąć okno, po prostu wywołaj akcję na swoim ViewModel.


2. Właściwa droga

Teraz właściwym sposobem na to jest użycie Pryzmatu (IMHO), a wszystko na ten temat można znaleźć tutaj .

Możesz złożyć wniosek o interakcję , zapełnić go dowolnymi danymi, które będą potrzebne w nowym oknie, zjeść je, zamknąć, a nawet otrzymać dane z powrotem . Wszystko to jest zamknięte i zatwierdzone przez MVVM. Otrzymujesz nawet status zamknięcia okna , na przykład jeśli użytkownik Canceledlub Accepted(przycisk OK) okno i dane z powrotem, jeśli są potrzebne . Jest to nieco bardziej skomplikowane i Odpowiedź nr 1, ale o wiele bardziej kompletna i zalecana przez Microsoft.

Link, który podałem, zawiera wszystkie fragmenty kodu i przykłady, więc nie zawracam sobie głowy umieszczeniem tutaj kodu, po prostu przeczytaj artykuł o pobieraniu Prism Quick Start i uruchom go, naprawdę bardzo łatwo jest nie docenić sprawiają, że działa, ale korzyści są większe niż tylko zamknięcie okna.

mFeinstein
źródło
Fajny sposób, ale rozdzielczość i przypisanie ViewModels nie zawsze mogą być takie proste. Co się stanie, jeśli ten sam model widoku to DataContext wielu systemów Windows?
Kylo Ren,
Potem myślę, że musielibyście zamknąć wszystkie okna naraz, pamiętajcie, że akcja może wywołać wielu delegatów naraz, wystarczy użyć +=dodać delegata i wywołać akcję, spowoduje to zwolnienie wszystkich z nich .... Albo będzie muszę stworzyć specjalną logikę na maszynie wirtualnej, aby wiedziała, które okno zamknąć (być może mieć kolekcję akcji zamykających) .... Ale myślę, że posiadanie wielu widoków powiązanych z jedną maszyną wirtualną nie jest najlepszą praktyką, jest to lepiej zarządzać posiadaniem jednego wystąpienia i jednego wystąpienia maszyny wirtualnej, które są ze sobą powiązane, i być może nadrzędna maszyna wirtualna zarządzająca wszystkimi podrzędnymi maszynami wirtualnymi powiązanymi ze wszystkimi widokami.
mFeinstein 24.04.16
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
źródło
2

Możliwe, że ViewModel ujawnia zdarzenie, w którym rejestruje się widok. Następnie, gdy ViewModel decyduje o czasie zamknięcia widoku, uruchamia to zdarzenie, które powoduje zamknięcie widoku. Jeśli chcesz, aby określona wartość wyniku była zwracana, to miałbyś do tego właściwość w ViewModel.

Abdulla Al-Qawasmeh
źródło
Zgadzam się z tym - prostota jest cenna. Muszę pomyśleć o tym, co się stanie, gdy kolejny młodszy programista zostanie zatrudniony do przejęcia tego projektu. Domyślam się, że będzie miał znacznie większą szansę, aby to dobrze opisać. Chyba że uważasz, że sam będziesz utrzymywać ten kod na zawsze? +1
Dziekan
2

Aby dodać do ogromnej liczby odpowiedzi, chcę dodać następujące. Zakładając, że masz ICommand na ViewModel i chcesz, aby to polecenie zamknęło jego okno (lub dowolne inne działanie w tym zakresie), możesz użyć czegoś takiego jak poniżej.

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

Nie jest idealny i może być trudny do przetestowania (ponieważ trudno jest wyśmiewać / wycierać ładunek elektrostatyczny), ale jest czystszy (IMHO) niż inne rozwiązania.

Erick

Erick T.
źródło
Byłem bardzo szczęśliwy, kiedy zobaczyłem twoją prostą odpowiedź! ale to też nie działa! Muszę otwierać i zamykać za pomocą Visual Basic. Czy znasz równoważność (Windows [i] .DataContext == this) w VB?
Ehsan
W końcu to mam! :) Dzięki. Jeśli Windows (i) .DataContext Is ja
Ehsan
Czy znasz ten sam prosty sposób na otwarcie okna? Muszę również wysyłać i odbierać niektóre dane w child viewmodel i odwrotnie.
Ehsan
1

Wdrożyłem rozwiązanie Joe White'a, ale wystąpiły problemy z okazjonalnymi błędamiDialogResult można ustawić tylko po utworzeniu okna i wyświetleniu go jako okna dialogowego ”.

Trzymałem ViewModel wokół po tym, jak widok został zamknięty, a od czasu do czasu otworzyłem nowy widok przy użyciu tej samej maszyny wirtualnej. Wygląda na to, że zamknięcie nowego widoku przed zrzuceniem starego widoku spowodowało, że DialogResultChanged próbował ustawić właściwość DialogResult w zamkniętym oknie, wywołując w ten sposób błąd.

Moim rozwiązaniem była zmiana DialogResultChanged, aby sprawdzić właściwość IsLoaded okna :

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

Po wprowadzeniu tej zmiany wszelkie załączniki do zamkniętych okien dialogowych są ignorowane.

Jim Hansen
źródło
Dziękuję Panu. Miałem ten sam problem
DJ Burb
1

Skończyłem mieszanie odpowiedzi Joe White'a i kodu z odpowiedzi Adama Millsa , ponieważ musiałem pokazać kontrolę użytkownika w programowo utworzonym oknie. Tak więc DialogCloser nie musi znajdować się w oknie, może znajdować się w samej kontroli użytkownika

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

DialogCloser znajdzie okno kontrolki użytkownika, jeśli nie zostało ono dołączone do samego okna.

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
źródło
1

Zachowanie jest tutaj najwygodniejszym sposobem.

  • Z jednej strony można go przypisać do danego modelu widoku (który może zasygnalizować „zamknij formularz!”)

  • Z drugiej strony ma dostęp do samego formularza, dzięki czemu może subskrybować niezbędne zdarzenia specyficzne dla formularza, wyświetlać okno dialogowe potwierdzenia lub cokolwiek innego.

Za pierwszym razem pisanie niezbędnych zachowań jest nudne. Jednak od teraz możesz go ponownie używać w każdym formularzu, którego potrzebujesz, dzięki jednopowłokowemu fragmentowi kodu XAML. W razie potrzeby możesz wyodrębnić go jako osobny zespół, aby można go było włączyć do każdego następnego projektu.

Yury Schkatula
źródło
0

Dlaczego nie przekazać okna jako parametru polecenia?

DO#:

 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}" />
Chrisrisars
źródło
Nie sądzę, że dobrym pomysłem jest ograniczenie maszyny wirtualnej do typu okna.
Shimmy Weitzhandler,
2
Nie sądzę, że dobrym pomysłem jest ograniczenie maszyny wirtualnej do Windowtypu, który nie jest „czystym” MVVM. Zobacz odpowiedź, w której maszyna wirtualna nie jest ograniczona do Windowobiektu.
Shimmy Weitzhandler,
w ten sposób zależność jest przypisywana do przycisku, który z pewnością nie zawsze jest sytuacją. Również przekazywanie typu interfejsu użytkownika do ViewModel jest złą praktyką.
Kylo Ren,
0

Innym rozwiązaniem jest utworzenie właściwości z INotifyPropertyChanged w widoku modelu, takim jak DialogResult, a następnie w Code Behind, napisz to:

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

Najważniejszym fragmentem jest _someViewModel_PropertyChanged. DialogResultPropertyNamemoże być publicznym ciągiem const SomeViewModel.

Używam tego rodzaju sztuczki, aby wprowadzić pewne zmiany w kontrolkach widoku w przypadku, gdy jest to trudne w ViewModel. OnPropertyChanged w ViewModel możesz robić cokolwiek chcesz w View. ViewModel jest nadal „testowalny jednostkowo”, a niektóre małe wiersze kodu w kodzie za nim nie mają znaczenia.

sliwinski.lukas
źródło
0

Wybrałbym tę drogę:

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
źródło
0

Przeczytałem wszystkie odpowiedzi, ale muszę powiedzieć, że większość z nich nie jest wystarczająco dobra lub nawet gorsza.

Możesz sobie z tym poradzić z klasą DialogService, której zadaniem jest pokazanie okna dialogowego i zwrócenie jego wyniku. Mam przykładowy projekt demonstrujący jego wdrożenie i wykorzystanie.

Oto najważniejsze części:

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

Czy to nie jest po prostu prostsze? bardziej przejrzysty, bardziej czytelny i ostatni, ale nie mniej ważny, do debugowania niż EventAggregator lub inne podobne rozwiązania?

jak widać, w moich modelach widoków zastosowałem pierwsze podejście ViewModel opisane w moim poście tutaj: Najlepsze praktyki dotyczące wywoływania View from ViewModel w WPF

Oczywiście w prawdziwym świecie DialogService.ShowDialogtrzeba mieć więcej opcji do skonfigurowania okna dialogowego, np. Przyciski i polecenia, które powinny wykonać. Można to zrobić na inny sposób, ale jest poza zakresem :)

Liero
źródło
0

Chociaż nie odpowiada to na pytanie, jak to zrobić za pomocą viewmodel, pokazuje to, jak to zrobić, używając tylko XAML + SDK mieszanki.

Zdecydowałem się pobrać i używać dwóch plików z Blend SDK, z których oba można pobrać jako pakiet od Microsoft za pośrednictwem NuGet. Pliki to:

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

Microsoft.Expression.Interactions.dll daje ładne możliwości, takie jak możliwość ustawiania właściwości lub wywoływania metody na viewmodel lub innym celu, a także zawiera inne widżety.

Niektóre 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>

Zauważ, że jeśli wybierasz się na proste zachowanie OK / Cancel, możesz uciec w / używając właściwości IsDefault i IsCancel, o ile okno jest pokazane w / Window.ShowDialog ().
Osobiście miałem problemy z przyciskiem, dla którego właściwość IsDefault była ustawiona na true, ale była ukryta podczas ładowania strony. Wydawało się, że nie chce ładnie grać po tym, jak zostało pokazane, więc właśnie ustawiam właściwość Window.DialogResult, jak pokazano powyżej, i działa dla mnie.

Wes
źródło
0

Oto proste rozwiązanie wolne od błędów (z kodem źródłowym), Działa dla mnie.

  1. Wyprowadź swój ViewModel z INotifyPropertyChanged

  2. Utwórz obserwowalną właściwość CloseDialog w 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. Dołącz uchwyt w widoku, aby zmienić tę właściwość

        _loginDialogViewModel = new LoginDialogViewModel();
        loginPanel.DataContext = _loginDialogViewModel;
        _loginDialogViewModel.PropertyChanged += OnPropertyChanged;
  4. Teraz jesteś prawie gotowy. W module obsługi zdarzenia makeDialogResult = true

    protected void OnPropertyChanged(object sender, PropertyChangedEventArgs args)
    {
        if (args.PropertyName == "CloseDialog")
        {
            DialogResult = true;
        }
    }
Anil8753
źródło
0

Utwórz Dependency Propertyw swoim View/ dowolnym UserControl(lub Windowchcesz zamknąć). Jak poniżej:

 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
        }

I powiąż go z właściwości ViewModel :

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

Nieruchomość w VeiwModel:

private bool closeWindow;

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

Teraz uruchom operację zamykania, zmieniając CloseWindowwartość w ViewModel. :)

Kylo Ren
źródło
-2

Tam, gdzie musisz zamknąć okno, po prostu umieść to w viewmodel:

ta-da

  foreach (Window window in Application.Current.Windows)
        {
            if (window.DataContext == this)
            {
                window.Close();
                return;
            }
        }
Cătălin Rădoi
źródło
ViewModel nie może zawierać UIElement w żaden sposób, ponieważ może to powodować błędy
WiiMaxx
Co się stanie, jeśli DataContext będzie dziedziczony przez kilka okien?
Kylo Ren,
ta-da, to nie jest całkowicie MVVM.
Alexandru Dicu
-10
Application.Current.MainWindow.Close() 

Wystarczy!

Alexey
źródło
3
-1 Prawda tylko wtedy, gdy okno, które chcesz zamknąć, jest głównym oknem ... Bardzo mało prawdopodobne założenie w oknie dialogowym logowania ...
surfuj