Wie soll das ViewModel das Formular schließen?

247

Ich versuche, WPF und das MVVM-Problem zu lernen, habe aber einen Haken bekommen. Diese Frage ist ähnlich, aber nicht ganz dieselbe wie diese (Umgang mit Dialogen in wpf-mit-mvvm) ...

Ich habe ein "Login" -Formular, das mit dem MVVM-Muster geschrieben wurde.

Dieses Formular verfügt über ein ViewModel, das den Benutzernamen und das Kennwort enthält, die mithilfe normaler Datenbindungen an die Ansicht in der XAML gebunden sind. Es hat auch einen "Login" -Befehl, der an die "Login" -Schaltfläche im Formular gebunden ist, auch unter Verwendung der normalen Datenbindung.

Wenn der Befehl "Anmelden" ausgelöst wird, ruft er eine Funktion im ViewModel auf, die ausgelöst wird und Daten über das Netzwerk sendet, um sich anzumelden. Wenn diese Funktion abgeschlossen ist, gibt es zwei Aktionen:

  1. Das Login war ungültig - wir zeigen nur eine MessageBox und alles ist in Ordnung

  2. Der Login war gültig, wir müssen das Login-Formular schließen und es als DialogResult...

Das Problem ist, dass das ViewModel nichts über die tatsächliche Ansicht weiß. Wie kann es also die Ansicht schließen und anweisen, ein bestimmtes DialogResult zurückzugeben? Ich könnte etwas Code in das CodeBehind stecken und / oder die Ansicht an das ViewModel weiterleiten, aber das scheint den ganzen Sinn von MVVM völlig zu zerstören ...


Aktualisieren

Am Ende habe ich nur die "Reinheit" des MVVM-Musters verletzt und die Ansicht ein ClosedEreignis veröffentlichen lassen und eine CloseMethode verfügbar machen lassen . Das ViewModel würde dann einfach aufrufen view.Close. Die Ansicht ist nur über eine Schnittstelle bekannt und über einen IOC-Container verkabelt, sodass keine Testbarkeit oder Wartbarkeit verloren geht.

Es scheint ziemlich dumm, dass die akzeptierte Antwort bei -5 Stimmen liegt! Während ich mir der guten Gefühle bewusst bin, die man bekommt, wenn man ein Problem löst, während man "rein" ist, bin ich sicherlich nicht der einzige, der glaubt, dass 200 Zeilen mit Ereignissen, Befehlen und Verhaltensweisen nur eine einzeilige Methode vermeiden Der Name "Muster" und "Reinheit" ist ein bisschen lächerlich ....

Orion Edwards
quelle
2
Ich habe die akzeptierte Antwort nicht abgelehnt, aber ich vermute, der Grund für die Ablehnung ist, dass sie im Allgemeinen nicht hilfreich ist, auch wenn sie in einem Fall funktionieren könnte. Sie haben es selbst in einem anderen Kommentar gesagt: "Während das Anmeldeformular ein Dialogfeld mit zwei Feldern ist, habe ich viele andere, die viel komplexer sind (und daher MVVM rechtfertigen), aber dennoch geschlossen werden müssen ..."
Joe Weiß
1
Ich verstehe Ihren Standpunkt, aber ich persönlich denke, dass selbst für den allgemeinen Fall eine einfache CloseMethode immer noch die beste Lösung ist. Alles andere in den anderen komplexeren Dialogen ist MVVM und datengebunden, aber es schien nur albern, die riesigen "Lösungen" hier anstelle einer einfachen Methode zu implementieren ...
Orion Edwards
2
Sie können den folgenden Link für das Dialogergebnis asimsajjad.blogspot.com/2010/10/… überprüfen , das die Dialog-Resutl zurückgibt und die Ansicht aus dem viewModel
Asim Sajjad
3
Bitte ändern Sie die akzeptierte Antwort auf diese Frage. Es gibt viele gute Lösungen, die weitaus besser sind als jemand, der die Verwendung von MVVM für diese Funktionalität in Frage stellt. Das ist keine Antwort, das ist Vermeidung.
ScottCher
2
@OrionEdwards Ich denke, Sie hatten Recht, das Muster hier zu brechen. Ein Hauptzweck eines Entwurfsmusters besteht darin, die Entwicklungszyklen zu beschleunigen, die Wartbarkeit zu verbessern und den Code zu vereinfachen, indem das gesamte Team denselben Regeln folgt. Dies wird nicht erreicht, indem Abhängigkeiten von externen Bibliotheken hinzugefügt und Hunderte von Codezeilen implementiert werden, um eine Aufgabe zu erfüllen. Dabei wird völlig ignoriert, dass es eine viel einfachere Lösung gibt, nur weil man zu hartnäckig ist, um die "Reinheit" des Musters zu opfern. Einfach sicher zu dokumentieren , was getan your've und KISS Code ( k eep i t s hort und s Umset).
M463

Antworten:

324

Ich war inspiriert von Thejuans Antwort , eine einfachere angehängte Eigenschaft zu schreiben. Keine Stile, keine Auslöser; Stattdessen können Sie dies einfach tun:

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

Dies ist fast so sauber, als hätte das WPF-Team alles richtig gemacht und DialogResult überhaupt zu einer Abhängigkeitseigenschaft gemacht. bool? DialogResultFügen Sie einfach eine Eigenschaft in Ihr ViewModel ein und implementieren Sie INotifyPropertyChanged. Voilà, Ihr ViewModel kann das Fenster schließen (und sein DialogResult festlegen), indem Sie einfach eine Eigenschaft festlegen. MVVM wie es sein sollte.

Hier ist der Code für 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);
        }
    }
}

Ich habe dies auch in meinem Blog gepostet .

Joe White
quelle
3
Dies ist die Antwort, die mir am besten gefällt! Gute Arbeit beim Schreiben des angehängten Eigentums.
Jorge Vargas
2
Gute Option, aber es gibt einen subtilen Fehler in dieser Lösung. Wenn das Ansichtsmodell für den Dialog ein Singleton ist, wird der DialogResult-Wert auf die nächste Verwendung des Dialogs übertragen. Das heißt, es wird sofort abgebrochen oder akzeptiert, bevor es angezeigt wird, sodass der Dialog nicht ein zweites Mal angezeigt wird.
Gone Coding
13
@HiTech Magic, hört sich so an, als ob der Fehler darin besteht, überhaupt ein einzelnes ViewModel zu verwenden. (grinst) Im Ernst, warum um alles in der Welt möchten Sie ein einzelnes ViewModel? Es ist eine schlechte Idee, den veränderlichen Zustand in globalen Variablen beizubehalten. Das Testen ist ein Albtraum, und das Testen ist einer der Gründe, warum Sie MVVM überhaupt verwenden würden.
Joe White
3
Ist es nicht der Sinn von MVVM, Ihre Logik nicht eng an eine bestimmte Benutzeroberfläche zu koppeln? In diesem Fall bool? wird mit Sicherheit nicht von einer anderen Benutzeroberfläche wie WinForm verwendet, und DialogCloser ist WPF-spezifisch. Wie passt das gut zu einer Lösung? Warum sollte man auch 2x-10x-Code schreiben, um ein Fenster über eine Bindung zu schließen?
David Anderson
2
@ DavidAnderson, ich würde MVVM auf keinen Fall mit WinForms ausprobieren. Die Unterstützung für die Datenbindung ist zu schwach, und MVVM stützt sich auf ein durchdachtes Bindungssystem. Und es ist nicht annähernd 2x-10x Code. Sie schreiben diesen Code einmal , nicht einmal für jedes Fenster. Danach handelt es sich um eine einzeilige Bindung plus eine Benachrichtigungseigenschaft, wobei derselbe Mechanismus verwendet wird, den Sie bereits für alles andere in Ihrer Ansicht verwenden (Sie müssen also beispielsweise keine zusätzliche Ansichtsoberfläche einfügen, um das Schließen der zu übernehmen Fenster). Sie können gerne andere Kompromisse eingehen, aber es scheint mir im Allgemeinen ein gutes Geschäft zu sein.
Joe White
64

Aus meiner Sicht ist die Frage ziemlich gut, da der gleiche Ansatz nicht nur für das "Anmelde" -Fenster verwendet wird, sondern für jede Art von Fenster. Ich habe viele Vorschläge geprüft und keine ist für mich in Ordnung. Bitte überprüfen Sie meinen Vorschlag, der dem MVVM-Entwurfsmusterartikel entnommen wurde .

Jede ViewModel-Klasse sollte davon erben, WorkspaceViewModeldass sie das RequestCloseEreignis und die CloseCommandEigenschaft des ICommandTyps hat. Die Standardimplementierung der CloseCommandEigenschaft löst das RequestCloseEreignis aus.

Um das Fenster zu schließen, sollte die OnLoadedMethode Ihres Fensters überschrieben werden:

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

oder OnStartupMethode Ihrer App:

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

Ich denke, dass die Implementierung von RequestCloseEreignissen und CloseCommandEigenschaften in der WorkspaceViewModelziemlich klar ist, aber ich werde zeigen, dass sie konsistent sind:

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

Und der Quellcode des 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 Behandle mich nicht schlecht für diese Quellen! Wenn ich sie gestern gehabt hätte, hätte mir das ein paar Stunden gespart ...

PPS Kommentare oder Vorschläge sind willkommen.

Budda
quelle
2
Ähm, die Tatsache, dass Sie sich in den Ereignishandler von customer.RequestCloseim Code hinter Ihrer XAML-Datei eingebunden haben, verstößt nicht gegen das MVVM-Muster? Sie hätten sich genauso gut an den ClickEreignishandler auf Ihrer Schaltfläche zum Schließen binden können, wenn Sie den Code dahinter ohnehin berührt und eine this.Close()! Richtig?
GONeale
1
Ich habe nicht zu viele Probleme mit dem Ereignisansatz, aber ich mag das Wort RequestClose nicht, weil es für mich immer noch zu viel Wissen über die Implementierung der Ansicht impliziert. Ich ziehe es vor, Eigenschaften wie IsCancelled verfügbar zu machen, die angesichts des Kontexts tendenziell aussagekräftiger sind und weniger implizieren, was die Ansicht als Antwort tun soll.
Jpierson
18

Ich habe angehängte Verhaltensweisen verwendet, um das Fenster zu schließen. Binden Sie eine "Signal" -Eigenschaft in Ihrem ViewModel an das angehängte Verhalten (ich verwende tatsächlich einen Trigger). Wenn es auf "true" gesetzt ist, schließt das Verhalten das Fenster.

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

Adam Mills
quelle
Dies ist die einzige Antwort, die bisher kein Codebehind im Fenster erfordert (und tatsächlich ein modales Fenster schließt, anstatt einen anderen Ansatz vorzuschlagen). Schade, dass es so viel Komplexität erfordert, mit dem Stil und dem Auslöser und all dem Mist - es scheint, dass dies wirklich mit einem einzeiligen Verhalten machbar sein sollte.
Joe White
4
Jetzt ist es mit einem einzeiligen Verhalten machbar. Siehe meine Antwort: stackoverflow.com/questions/501886/…
Joe White
15

Es gibt viele Kommentare, die die Vor- und Nachteile von MVVM hier diskutieren. Für mich stimme ich Nir zu; Es geht darum, das Muster richtig zu verwenden, und MVVM passt nicht immer. Die Leute scheinen bereit zu sein, alle wichtigen Prinzipien des Software-Designs NUR zu opfern, um es an MVVM anzupassen.

Das heißt, ich denke, Ihr Fall könnte gut zu ein bisschen Refactoring passen.

In den meisten Fällen, auf die ich gestoßen bin, können Sie mit WPF OHNE mehrere Windows auskommen . Vielleicht könnten Sie versuchen, Frames und Pages anstelle von Windows mit DialogResults zu verwenden.

In Ihrem Fall wäre mein Vorschlag, das zu LoginFormViewModelbehandeln LoginCommandund wenn die Anmeldung ungültig ist, setzen Sie eine Eigenschaft auf LoginFormViewModeleinen geeigneten Wert ( falseoder einen Aufzählungswert wie UserAuthenticationStates.FailedAuthentication). Sie würden dasselbe für eine erfolgreiche Anmeldung ( trueoder einen anderen Aufzählungswert) tun . Sie würden dann ein verwenden, DataTriggerdas auf die verschiedenen Benutzerauthentifizierungszustände reagiert und ein einfaches verwenden könnte Setter, um die SourceEigenschaft des zu ändern Frame.

Wenn Sie Ihr Anmeldefenster zurückgeben, werden Sie meiner DialogResultMeinung nach verwirrt. Das DialogResultist wirklich eine Eigenschaft Ihres ViewModel. In meiner zugegebenermaßen begrenzten Erfahrung mit WPF, wenn sich etwas normalerweise nicht richtig anfühlt, weil ich darüber nachdenke, wie ich dasselbe in WinForms gemacht hätte.

Hoffentlich hilft das.

EightyOne Unite
quelle
10

Angenommen, Ihr Anmeldedialog ist das erste Fenster, das erstellt wird, versuchen Sie dies in Ihrer LoginViewModel-Klasse:

    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
quelle
Männer das ist einfach und funktioniert super. Derzeit verwende ich diesen Ansatz.
Erre Efe
Es funktioniert nur für das Hauptfenster. Verwenden Sie es also nicht für andere Fenster.
Oleksii
7

Dies ist eine einfache und saubere Lösung. Sie fügen dem ViewModel ein Ereignis hinzu und weisen das Fenster an, sich selbst zu schließen, wenn dieses Ereignis ausgelöst wird.

Weitere Informationen finden Sie in meinem Blogbeitrag " Fenster schließen" von 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);
}

Hinweis: In diesem Beispiel werden Prismas verwendet DelegateCommand(siehe Prisma: Befehle ), es kann jedoch jede ICommandImplementierung verwendet werden.

Sie können Verhaltensweisen aus diesem offiziellen Paket verwenden.

Shimmy Weitzhandler
quelle
2
+1, aber Sie sollten in der Antwort selbst weitere Details angeben, z. B. dass für diese Lösung ein Verweis auf die Assembly Interaction "Expression Blend" erforderlich ist.
Surfen
6

Ich würde damit umgehen, indem ich meinem ViewModel einen Ereignishandler hinzufüge. Wenn der Benutzer erfolgreich angemeldet war, würde ich das Ereignis auslösen. In meiner Ansicht würde ich an dieses Ereignis anhängen und wenn es ausgelöst wird, würde ich das Fenster schließen.


quelle
2
Das mache ich normalerweise auch. Auch wenn das angesichts all der neuen wpf-befehlenden Dinge ein bisschen schmutzig erscheint.
Botz3000
4

Hier ist, was ich anfangs getan habe, was funktioniert, aber es scheint ziemlich langatmig und hässlich zu sein (globale Statik, alles ist nie gut)

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

Ich habe später dann den ganzen Code entfernt und hatte gerade den LoginFormViewModelAufruf der Close-Methode in seiner Ansicht. Es war viel schöner und leichter zu verfolgen. IMHO geht es bei Mustern darum, den Leuten einen einfacheren Weg zu geben, um zu verstehen, was Ihre App tut. In diesem Fall machte MVVM das Verständnis weitaus schwieriger, als wenn ich es nicht verwendet hätte, und war jetzt ein Anti- Muster.

Orion Edwards
quelle
3

Zu Ihrer Information, ich bin auf dasselbe Problem gestoßen und ich denke, ich habe eine Lösung gefunden, die keine globalen oder statischen Elemente erfordert, obwohl dies möglicherweise nicht die beste Antwort ist. Ich überlasse es euch, das selbst zu entscheiden.

In meinem Fall kennt das ViewModel, das das anzuzeigende Fenster instanziiert (nennen wir es ViewModelMain), auch das LoginFormViewModel (am Beispiel der obigen Situation).

Also habe ich eine Eigenschaft im LoginFormViewModel vom Typ ICommand erstellt (nennen wir es CloseWindowCommand). Bevor ich dann .ShowDialog () im Fenster aufrufe, setze ich die CloseWindowCommand-Eigenschaft im LoginFormViewModel auf die window.Close () -Methode des von mir instanziierten Fensters. Dann muss ich im LoginFormViewModel nur noch CloseWindowCommand.Execute () aufrufen, um das Fenster zu schließen.

Ich nehme an, es ist eine Art Workaround / Hack, aber es funktioniert gut, ohne das MVVM-Muster wirklich zu brechen.

Fühlen Sie sich frei, diesen Prozess so oft zu kritisieren, wie Sie möchten, ich kann es nehmen! :) :)


quelle
Ich bin mir nicht sicher, ob ich es vollständig verstanden habe, aber bedeutet dies nicht, dass Ihr MainWindow vor Ihrem LoginWindow instanziiert werden muss? Das möchte ich nach Möglichkeit vermeiden
Orion Edwards
3

Dies ist wahrscheinlich sehr spät, aber ich bin auf dasselbe Problem gestoßen und habe eine Lösung gefunden, die für mich funktioniert.

Ich kann nicht herausfinden, wie man eine App ohne Dialoge erstellt (vielleicht ist es nur ein Mind Block). Also war ich mit MVVM in einer Sackgasse und zeigte einen Dialog. Also bin ich auf diesen CodeProject-Artikel gestoßen:

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

Dies ist ein UserControl, mit dem sich ein Fenster im visuellen Baum eines anderen Fensters befinden kann (in xaml nicht zulässig). Außerdem wird eine boolesche DependencyProperty namens IsShowing verfügbar gemacht.

Sie können einen Stil wie in einem Ressourcenwörterbuch festlegen, der den Dialog grundsätzlich immer dann anzeigt, wenn die Content-Eigenschaft des Steuerelements! = Null über Trigger:

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

In der Ansicht, in der Sie den Dialog anzeigen möchten, haben Sie einfach Folgendes:

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

In Ihrem ViewModel müssen Sie lediglich die Eigenschaft auf einen Wert setzen (Hinweis: Die ViewModel-Klasse muss INotifyPropertyChanged unterstützen, damit die Ansicht weiß, dass etwas passiert ist).

wie so:

DialogViewModel = new DisplayViewModel();

Um das ViewModel mit der Ansicht abzugleichen, sollten Sie Folgendes in einem Ressourcenwörterbuch haben:

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

Mit all dem erhalten Sie einen Einzeiler-Code, um den Dialog anzuzeigen. Das Problem ist, dass Sie den Dialog nicht wirklich nur mit dem obigen Code schließen können. Aus diesem Grund müssen Sie ein Ereignis in eine ViewModel-Basisklasse einfügen, von der DisplayViewModel erbt, und anstelle des obigen Codes dieses schreiben

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

Anschließend können Sie das Ergebnis des Dialogs über den Rückruf bearbeiten.

Dies mag etwas komplex erscheinen, aber sobald die Grundlagen geschaffen sind, ist es ziemlich einfach. Auch dies ist meine Implementierung, ich bin mir sicher, dass es noch andere gibt :)

Hoffe das hilft, es hat mich gerettet.

Jose
quelle
3

Ok, diese Frage ist fast 6 Jahre alt und ich kann hier immer noch nicht finden, was ich für die richtige Antwort halte. Erlauben Sie mir also, meine "2 Cent" zu teilen ...

Ich habe tatsächlich zwei Möglichkeiten, die erste ist die einfache ... die zweite auf der rechten. Wenn Sie also nach der richtigen suchen, überspringen Sie einfach # 1 und springen Sie zu # 2 :

1. Schnell und einfach (aber nicht vollständig)

Wenn ich nur ein kleines Projekt habe, erstelle ich manchmal einfach eine CloseWindowAction im ViewModel:

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

Und wer auch immer die Ansicht erstellt oder im Code der Ansicht dahinter, ich habe nur die Methode festgelegt, die die Aktion aufruft:

(Denken Sie daran, bei MVVM geht es um die Trennung von Ansicht und ViewModel. Der Code der Ansicht ist immer noch die Ansicht. Solange eine ordnungsgemäße Trennung vorliegt, verletzen Sie das Muster nicht.)

Wenn ein ViewModel ein neues Fenster erstellt:

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

Oder wenn Sie es in Ihrem Hauptfenster haben möchten, platzieren Sie es einfach unter dem Konstruktor Ihrer Ansicht:

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

Wenn Sie das Fenster schließen möchten, rufen Sie einfach die Aktion in Ihrem ViewModel auf.


2. Der richtige Weg

Die richtige Vorgehensweise ist nun die Verwendung von Prisma (IMHO). Alles darüber finden Sie hier .

Sie können eine Interaktionsanforderung stellen , sie mit den Daten füllen , die Sie in Ihrem neuen Fenster benötigen, sie zu Mittag essen, schließen und sogar Daten zurückerhalten . All dies gekapselt und MVVM genehmigt. Sie erhalten sogar einen Status darüber, wie das Fenster geschlossen wurde , z. B. ob der Benutzer Canceledoder Accepted(OK) das Fenster und Daten zurück, wenn Sie sie benötigen . Es ist etwas komplizierter und Antwort Nr. 1, aber es ist viel vollständiger und ein empfohlenes Muster von Microsoft.

Der Link, den ich gegeben habe, enthält alle Codefragmente und Beispiele, sodass ich keinen Code hier einfügen muss. Lesen Sie einfach den Artikel zum Herunterladen des Prisma-Schnellstarts und führen Sie ihn aus. Es ist wirklich einfach, etwas ausführlicher zu verstehen Damit es funktioniert, sind die Vorteile größer als nur das Schließen eines Fensters.

mFeinstein
quelle
Netter Weg, aber die Auflösung und Zuweisung von ViewModels kann nicht immer so einfach sein. Was ist, wenn dasselbe Ansichtsmodell DataContext vieler Windows ist?
Kylo Ren
Dann müssten Sie wohl alle Fenster auf einmal schließen. Denken Sie daran, dass eine Aktion viele Delegaten gleichzeitig auslösen kann. Verwenden Sie +=diese Option, um einen Delegaten hinzuzufügen, und rufen Sie die Aktion auf. Sie wird alle auslösen . Oder Sie werden es tun Sie müssen eine spezielle Logik für Ihre VM erstellen, damit sie weiß, welches Fenster geschlossen werden muss (möglicherweise eine Sammlung von Abschlussaktionen). Ich denke jedoch, dass es nicht empfehlenswert ist, mehrere Ansichten an eine VM zu binden Es ist besser, eine Ansicht und eine VM-Instanz miteinander zu verknüpfen und möglicherweise eine übergeordnete VM, die alle untergeordneten VMs verwaltet, die an alle Ansichten gebunden sind.
mFeinstein
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
quelle
2

Das ViewModel kann ein Ereignis verfügbar machen, für das sich die Ansicht registriert. Wenn das ViewModel dann entscheidet, wann die Ansicht geschlossen werden soll, wird das Ereignis ausgelöst, durch das die Ansicht geschlossen wird. Wenn Sie möchten, dass ein bestimmter Ergebniswert zurückgegeben wird, haben Sie dafür eine Eigenschaft im ViewModel.

Abdulla Al-Qawasmeh
quelle
Ich stimme dem zu - Einfachheit ist wertvoll. Ich muss darüber nachdenken, was passiert, wenn der nächste Nachwuchsentwickler eingestellt wird, um dieses Projekt zu übernehmen. Ich vermute, er hat eine viel bessere Chance, dies richtig zu machen, wie Sie beschreiben. Es sei denn, Sie glauben, dass Sie diesen Code für immer selbst pflegen werden? +1
Dean
2

Um die enorme Anzahl von Antworten zu ergänzen, möchte ich Folgendes hinzufügen. Angenommen, Sie haben einen ICommand in Ihrem ViewModel und möchten, dass dieser Befehl sein Fenster (oder eine andere Aktion) schließt, können Sie Folgendes verwenden.

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

Es ist nicht perfekt und möglicherweise schwierig zu testen (da es schwierig ist, eine statische Aufladung zu verspotten / zu stoppen), aber es ist sauberer (IMHO) als die anderen Lösungen.

Erick

Erick T.
quelle
Ich wurde sehr glücklich, als ich Ihre einfache Antwort sah! aber es funktioniert auch nicht! Ich muss mit Visual Basic öffnen und schließen. Kennen Sie die Äquivalenz von (windows [i] .DataContext == this) in VB?
Ehsan
Ich habe es endlich verstanden! :) Vielen Dank. Wenn Windows (i) .DataContext Is me
Ehsan
Kennen Sie den gleichen einfachen Weg, um auch ein Fenster zu öffnen? Ich muss einige Daten auch im untergeordneten Ansichtsmodell senden und empfangen und umgekehrt.
Ehsan
1

Ich habe die Lösung von Joe White implementiert, bin jedoch auf Probleme mit gelegentlichen Fehlern " DialogResult kann erst festgelegt werden, nachdem das Fenster erstellt und als Dialog angezeigt wird " gestoßen .

Ich habe das ViewModel beibehalten, nachdem die Ansicht geschlossen wurde, und gelegentlich habe ich später eine neue Ansicht mit derselben VM geöffnet. Es scheint, dass das Schließen der neuen Ansicht vor der Speicherbereinigung der alten Ansicht dazu führte, dass DialogResultChanged versuchte, die DialogResult- Eigenschaft im geschlossenen Fenster festzulegen , wodurch der Fehler ausgelöst wurde .

Meine Lösung bestand darin, DialogResultChanged zu ändern , um die IsLoaded- Eigenschaft des Fensters zu überprüfen :

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

Nach dieser Änderung werden alle Anhänge zu geschlossenen Dialogen ignoriert.

Jim Hansen
quelle
Danke mein Herr. Ich hatte das gleiche Problem
DJ Burb
1

Am Ende habe ich die Antwort von Joe White und einen Code aus der Antwort von Adam Mills gemischt , da ich ein Benutzersteuerelement in einem programmgesteuert erstellten Fenster anzeigen musste. Der DialogCloser muss sich also nicht im Fenster befinden, sondern kann sich auf dem Benutzersteuerelement selbst befinden

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

Und der DialogCloser findet das Fenster des Benutzersteuerelements, wenn es nicht an das Fenster selbst angehängt wurde.

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

Verhalten ist hier der bequemste Weg.

  • Einerseits kann es an das angegebene Ansichtsmodell gebunden werden (dies kann signalisieren, dass das Formular geschlossen wird!)

  • Von einer anderen Seite hat es Zugriff auf das Formular selbst, sodass er die erforderlichen formularspezifischen Ereignisse abonnieren oder einen Bestätigungsdialog oder etwas anderes anzeigen kann.

Das Schreiben des notwendigen Verhaltens kann beim ersten Mal als langweilig angesehen werden. Von nun an können Sie es jedoch für jedes einzelne Formular, das Sie benötigen, mit einem exakten einzeiligen XAML-Snippet wiederverwenden. Bei Bedarf können Sie es als separate Baugruppe extrahieren, damit es in jedes gewünschte nächste Projekt aufgenommen werden kann.

Yury Schkatula
quelle
0

Warum nicht einfach das Fenster als Befehlsparameter übergeben?

C #:

 private void Cancel( Window window )
  {
     window.Close();
  }

  private ICommand _cancelCommand;
  public ICommand CancelCommand
  {
     get
     {
        return _cancelCommand ?? ( _cancelCommand = new Command.RelayCommand<Window>(
                                                      ( window ) => Cancel( window ),
                                                      ( window ) => ( true ) ) );
     }
  }

XAML:

<Window x:Class="WPFRunApp.MainWindow"
        x:Name="_runWindow"
...
   <Button Content="Cancel"
           Command="{Binding Path=CancelCommand}"
           CommandParameter="{Binding ElementName=_runWindow}" />
chrislarson
quelle
Ich halte es nicht für eine gute Idee, die VM auf einen Fenstertyp zu beschränken.
Shimmy Weitzhandler
2
Ich denke nicht, dass es eine gute Idee ist, die VM auf einen WindowTyp zu beschränken , der etwas nicht "reine" MVVM ist. In dieser Antwort wird die VM nicht auf ein WindowObjekt beschränkt.
Shimmy Weitzhandler
Auf diese Weise wird die Abhängigkeit von einem Button festgelegt, was sicherlich nicht immer der Fall sein kann. Auch das Übergeben des UI-Typs an ViewModel ist eine schlechte Praxis.
Kylo Ren
0

Eine andere Lösung besteht darin, eine Eigenschaft mit INotifyPropertyChanged im Ansichtsmodell wie DialogResult zu erstellen und dann in Code Behind Folgendes zu schreiben:

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

Das wichtigste Fragment ist _someViewModel_PropertyChanged. DialogResultPropertyNamekann eine öffentliche const-Zeichenfolge sein SomeViewModel.

Ich verwende diese Art von Trick, um einige Änderungen in den Ansichtssteuerelementen vorzunehmen, falls dies in ViewModel schwierig ist. OnPropertyChanged in ViewModel können Sie in View alles tun, was Sie wollen. ViewModel ist immer noch "Unit-testbar" und einige kleine Codezeilen im Code dahinter machen keinen Unterschied.

sliwinski.lukas
quelle
0

Ich würde diesen Weg gehen:

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

Ich habe alle Antworten gelesen, aber ich muss sagen, die meisten sind einfach nicht gut genug oder noch schlimmer.

Sie können dies mit der DialogService- Klasse wunderbar handhaben , deren Aufgabe es ist, das Dialogfenster anzuzeigen und das Dialogergebnis zurückzugeben. Ich habe ein Beispielprojekt erstellt , das die Implementierung und Verwendung demonstriert.

Hier sind die wichtigsten Teile:

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

Ist das nicht einfach einfacher? direkter, lesbarer und nicht zuletzt einfacher zu debuggen als EventAggregator oder andere ähnliche Lösungen?

Wie Sie sehen können, habe ich in meinen Ansichtsmodellen den ersten in meinem Beitrag hier beschriebenen ViewModel-Ansatz verwendet: Best Practice für den Aufruf von View aus ViewModel in WPF

In der realen Welt DialogService.ShowDialogmüssen natürlich mehr Optionen zum Konfigurieren des Dialogfelds vorhanden sein, z. B. Schaltflächen und Befehle, die ausgeführt werden sollen. Es gibt verschiedene Möglichkeiten, dies zu tun, aber es liegt außerhalb des Anwendungsbereichs :)

Liero
quelle
0

Dies beantwortet zwar nicht die Frage, wie dies über das Ansichtsmodell zu tun ist, zeigt jedoch, wie dies nur mit XAML + dem Blend-SDK erfolgt.

Ich habe mich entschieden, zwei Dateien aus dem Blend SDK herunterzuladen und zu verwenden, die Sie beide als Paket von Microsoft über NuGet verwenden können. Die Dateien sind:

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

Microsoft.Expression.Interactions.dll bietet Ihnen nützliche Funktionen wie die Möglichkeit, Eigenschaften festzulegen oder eine Methode für Ihr Ansichtsmodell oder ein anderes Ziel aufzurufen, und enthält auch andere Widgets.

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

Beachten Sie, dass Sie, wenn Sie nur ein einfaches OK / Abbrechen-Verhalten anstreben, mit den Eigenschaften IsDefault und IsCancel davonkommen können, solange das Fenster mit Window.ShowDialog () angezeigt wird.
Ich persönlich hatte Probleme mit einer Schaltfläche, bei der die IsDefault-Eigenschaft auf true gesetzt war, die jedoch beim Laden der Seite ausgeblendet wurde. Es schien nicht gut spielen zu wollen, nachdem es gezeigt wurde, also setze ich stattdessen nur die Window.DialogResult-Eigenschaft wie oben gezeigt und es funktioniert für mich.

Wir s
quelle
0

Hier ist die einfache fehlerfreie Lösung (mit Quellcode). Sie funktioniert für mich.

  1. Leiten Sie Ihr ViewModel von ab INotifyPropertyChanged

  2. Erstellen Sie eine beobachtbare Eigenschaft CloseDialog in 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. Fügen Sie in Hand einen Handler für diese Eigenschaftsänderung hinzu

        _loginDialogViewModel = new LoginDialogViewModel();
        loginPanel.DataContext = _loginDialogViewModel;
        _loginDialogViewModel.PropertyChanged += OnPropertyChanged;
  4. Jetzt bist du fast fertig. Im Event-Handler machenDialogResult = true

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

Erstellen Sie ein Dependency Propertyin Ihrem View/ any UserControl(oder WindowSie möchten schließen). Wie unten:

 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
        }

Und binden Sie es aus der Eigenschaft Ihres 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}"

Eigentum in VeiwModel:

private bool closeWindow;

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

Lösen Sie nun den Schließvorgang aus, indem Sie den CloseWindowWert in ViewModel ändern . :) :)

Kylo Ren
quelle
-2

Wenn Sie das Fenster schließen müssen, fügen Sie dies einfach in das Ansichtsmodell ein:

ta-da

  foreach (Window window in Application.Current.Windows)
        {
            if (window.DataContext == this)
            {
                window.Close();
                return;
            }
        }
Cătălin Rădoi
quelle
Ein ViewModel darf in keiner Weise ein UIElement enthalten , da dies zu Fehlern führen kann
WiiMaxx
Was ist, wenn DataContext über mehrere Fenster vererbt wird?
Kylo Ren
ta-da, das ist absolut nicht MVVM.
Alexandru Dicu
-10
Application.Current.MainWindow.Close() 

Das ist genug!

Alexey
quelle
3
-1 Nur wahr, wenn das Fenster, das Sie schließen möchten, das Hauptfenster ist ... Sehr unwahrscheinliche Annahme für den
Anmeldedialog