In che modo ViewModel dovrebbe chiudere il modulo?


247

Sto cercando di imparare WPF e il problema MVVM, ma ho colto di sorpresa. Questa domanda è simile ma non è la stessa di questa (gestione-dialoghi-in-wpf-con-mvvm) ...

Ho un modulo "Login" scritto usando il modello MVVM.

Questo modulo ha un ViewModel che contiene il nome utente e la password, che sono associati alla vista in XAML utilizzando normali associazioni di dati. Ha anche un comando "Login" associato al pulsante "Login" sul modulo, che può essere utilizzato come normale database.

Quando viene attivato il comando "Login", viene richiamata una funzione in ViewModel che si spegne e invia i dati sulla rete per accedere. Al termine di questa funzione, sono disponibili 2 azioni:

  1. L'accesso non è valido - mostriamo solo un MessageBox e tutto va bene

  2. L'accesso era valido, dobbiamo chiudere il modulo di accesso e farlo restituire vero come DialogResult...

Il problema è che ViewModel non sa nulla della vista effettiva, quindi come può chiudere la vista e dirgli di restituire un particolare DialogResult ?? Potrei attaccare un po 'di codice in CodeBehind e / o passare la vista attraverso ViewModel, ma sembra che questo avrebbe sconfitto del tutto il punto di MVVM ...


Aggiornare

Alla fine ho appena violato la "purezza" del modello MVVM e ho fatto pubblicare a View un Closedevento ed esponendo un Closemetodo. Il ViewModel avrebbe quindi solo chiamato view.Close. La vista è nota solo tramite un'interfaccia e cablata tramite un contenitore IOC, quindi non si perde alcuna testabilità o manutenibilità.

Sembra piuttosto sciocco che la risposta accettata sia di -5 voti! Mentre sono ben consapevole dei buoni sentimenti che si provano risolvendo un problema pur essendo "puri", sicuramente non sono l'unico che pensa che 200 linee di eventi, comandi e comportamenti solo per evitare un metodo a una linea in il nome di "modelli" e "purezza" è un po 'ridicolo ....


2
Non ho votato in negativo la risposta accettata, ma suppongo che il motivo del downvotes sia che non è utile in generale, anche se potrebbe funzionare in un caso. L'hai detto tu stesso in un altro commento: "Mentre il modulo di accesso è una finestra di dialogo" due campi ", ne ho molti altri che sono molto più complessi (e quindi giustificano MVVM), ma devono ancora essere chiusi ..."
Joe Bianco,

1
Vedo il tuo punto, ma personalmente penso che anche nel caso generale un Closemetodo semplice sia ancora la soluzione migliore. Tutto il resto nelle altre finestre di dialogo più complesse è MVVM e databound, ma sembrava sciocco implementare qui le enormi "soluzioni" invece di un semplice metodo ...
Orion Edwards,

2
È possibile controllare il seguente collegamento per il risultato della finestra di dialogo asimsajjad.blogspot.com/2010/10/… , che restituirà il dialogo della finestra di dialogo e chiuderà la vista dalla vista Modello
Asim Sajjad,

3
Modifica la risposta accettata a questa domanda. Esistono molte buone soluzioni che sono molto meglio di qualcuno che mette in dubbio l'uso di MVVM per questa funzionalità. Questa non è una risposta, è evitamento.
ScottCher,

2
@OrionEdwards Penso che tu abbia ragione a rompere il modello qui. Uno scopo principale di un modello di progettazione è accelerare i cicli di sviluppo, aumentare la manutenibilità e semplificare il codice facendo sì che l'intero team segua le stesse regole. Ciò non si ottiene aggiungendo dipendenze da librerie esterne e implementando centinaia di righe di codice per svolgere un compito, ignorando totalmente che esiste una soluzione molto più semplice, solo perché si è testardi nel sacrificare la "purezza" del modello. Basta assicurarsi di documentare ciò che your've fatto e BACIO il codice ( k PEE i t s Hort e s Imple).
M463,

Risposte:


324

Sono stato ispirato dalla risposta di Thejuan a scrivere una proprietà in allegato più semplice. Nessuno stile, nessun grilletto; invece, puoi semplicemente fare questo:

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

Questo è quasi pulito come se il team WPF avesse fatto bene e avesse fatto di DialogResult una proprietà di dipendenza in primo luogo. Basta inserire una bool? DialogResultproprietà su ViewModel e implementare INotifyPropertyChanged e voilà, ViewModel può chiudere la finestra (e impostare DialogResult) semplicemente impostando una proprietà. MVVM come dovrebbe essere.

Ecco il codice per 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);
        }
    }
}

Ho anche pubblicato questo sul mio blog .


3
Questa è la risposta che mi piace di più! Ottimo lavoro scrivendo quella proprietà allegata.
Jorge Vargas,

2
Bella opzione, ma c'è un bug sottile in questa soluzione. Se il modello di visualizzazione per la finestra di dialogo è un singleton, il valore DialogResult viene portato all'utilizzo successivo della finestra di dialogo. Ciò significa che verrà immediatamente annullato o accettato prima di mostrarsi, quindi la finestra di dialogo non verrà mostrata una seconda volta.
Codice finito

13
@HiTech Magic, sembra che il bug sia nell'usare un ViewModel singleton in primo luogo. (sogghigno) Seriamente, perché mai vorresti un ViewModel singleton? È una cattiva idea mantenere lo stato mutevole nelle variabili globali. Rende il test un incubo e il test è uno dei motivi per cui dovresti usare MVVM in primo luogo.
Joe White,

3
Il punto di MVVM non è quello di accoppiare strettamente la tua logica a un'interfaccia utente specifica? In questo caso, bool? non è certamente utilizzabile da un'altra interfaccia utente come WinForm e DialogCloser è specifico di WPF. Quindi come si inserisce bene in una soluzione? Inoltre, perché scrivere un codice 2x-10x solo per chiudere una finestra tramite un binding?
David Anderson,

2
@DavidAnderson, non proverei comunque MVVM con WinForms; il suo supporto di database è troppo debole e MVVM si affida a un sistema di rilegatura ben congegnato. E non è vicino al codice 2x-10x. Scrivi quel codice una volta , non una volta per ogni finestra. Dopodiché si tratta di un'associazione di una riga più una proprietà di notifica, utilizzando lo stesso meccanismo che stai già utilizzando per tutto il resto nella tua vista (quindi, ad esempio, non è necessario iniettare un'interfaccia di visualizzazione extra solo per gestire la chiusura del finestra). Sei il benvenuto a fare altri compromessi, ma a me sembra generalmente un buon affare.
Joe White,

64

Dal mio punto di vista la domanda è piuttosto buona in quanto lo stesso approccio sarebbe usato non solo per la finestra "Login", ma per qualsiasi tipo di finestra. Ho esaminato molti suggerimenti e nessuno mi va bene. Esamina il mio suggerimento tratto dall'articolo sul modello di progettazione MVVM .

Ogni classe ViewModel deve ereditare da WorkspaceViewModelciò che ha l' RequestCloseevento e la CloseCommandproprietà del ICommandtipo. L'implementazione predefinita della CloseCommandproprietà genererà l' RequestCloseevento.

Per chiudere la finestra, il OnLoadedmetodo della finestra deve essere ignorato:

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

o OnStartupmetodo della tua 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();
    }

Immagino che l' implementazione di RequestCloseeventi e CloseCommandproprietà WorkspaceViewModelsia abbastanza chiara, ma mostrerò che sono coerenti:

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

E il codice sorgente di 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 Non trattarmi male per quelle fonti! Se le avessi fatte ieri mi avrebbe risparmiato qualche ora ...

PPS Eventuali commenti o suggerimenti sono benvenuti.


2
Umm, il fatto che tu abbia attaccato al gestore di eventi customer.RequestClosenel codice dietro il tuo file XAML non viola il modello MVVM? Potresti anche legarti al Clickgestore dell'evento sul tuo pulsante di chiusura, in primo luogo vedendo che hai toccato il codice dietro comunque e fatto un this.Close()! Destra?
GONeale,

1
Non ho troppi problemi con l'approccio all'evento ma non mi piace la parola RequestClose perché per me implica ancora molta conoscenza sull'implementazione della vista. Preferisco esporre proprietà come IsCancelled che tendono ad essere più significative dato il contesto e implicano meno su ciò che la vista dovrebbe fare in risposta.
jpierson,

18

Ho usato comportamenti associati per chiudere la finestra. Associa una proprietà "signal" su ViewModel al comportamento associato (in realtà utilizzo un trigger) Quando è impostato su true, il comportamento chiude la finestra.

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


Questa è l'unica risposta finora che non richiede alcun codebehind nella finestra (e in realtà chiude una finestra modale, invece di suggerire un altro approccio). Peccato che richieda tanta complessità, con lo stile e il grilletto e tutto ciò che fa schifo - sembra che questo dovrebbe davvero essere fattibile con un comportamento attaccato su una linea.
Joe White,

4
Ora è fattibile con un comportamento associato di una riga. Vedi la mia risposta: stackoverflow.com/questions/501886/…
Joe White il

15

Ci sono molti commenti che sostengono i pro ei contro di MVVM qui. Per me, sono d'accordo con Nir; si tratta di utilizzare il modello in modo appropriato e MVVM non sempre si adatta. Le persone sembrano essere disposte a sacrificare tutti i principi più importanti della progettazione del software SOLO per adattarlo a MVVM.

Detto questo, .. penso che il tuo caso potrebbe adattarsi bene con un po 'di refactoring.

Nella maggior parte dei casi in cui mi sono imbattuto, WPF ti consente di cavartela SENZA più Windows. Forse potresti provare a usare Frames e Pages invece di Windows con DialogResults.

Nel tuo caso il mio suggerimento sarebbe di LoginFormViewModelgestire il LoginCommande se il login non è valido, impostare una proprietà su LoginFormViewModelun valore appropriato ( falseo un valore enum come UserAuthenticationStates.FailedAuthentication). Faresti lo stesso per un login riuscito ( trueo qualche altro valore enum). Utilizzeresti quindi un DataTriggerche risponde ai vari stati di autenticazione dell'utente e potresti usare un semplice Setterper cambiare la Sourceproprietà di Frame.

Avere la tua finestra di accesso restituire un DialogResultpenso che sia dove ti stai confondendo; che DialogResultè davvero una proprietà del ViewModel. Nella mia esperienza, certamente limitata, con WPF, quando qualcosa non va bene di solito perché sto pensando in termini di come avrei fatto la stessa cosa in WinForms.

Spero che aiuti.


10

Supponendo che la finestra di dialogo di accesso sia la prima finestra che viene creata, provala nella classe 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;
        }
    }

Uomini questo è semplice e funziona alla grande. Attualmente sto usando questo approccio.
Erre Efe,

Funziona solo per la finestra MAIN. Quindi non usarlo per altre finestre.
Oleksii,

7

Questa è una soluzione semplice e pulita: aggiungi un evento a ViewModel e chiedi alla finestra di chiudersi automaticamente quando l'evento viene generato.

Per maggiori dettagli vedi il mio post sul blog, Chiudi finestra da 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);
}

Nota: l'esempio usa Prism DelegateCommand(vedi Prism: Commanding ), ma qualsiasi ICommandimplementazione può essere usata per quella materia.

Puoi usare i comportamenti di questo pacchetto ufficiale.


2
+1 ma è necessario fornire ulteriori dettagli nella risposta stessa, ad esempio che questa soluzione richiede riferimento all'assemblaggio di interattività di fusione di espressioni.
surfato il

6

Il modo in cui lo gestisco è quello di aggiungere un gestore eventi nel mio ViewModel. Quando l'utente ha effettuato correttamente l'accesso, accenderei l'evento. Nella mia vista, mi allego a questo evento e quando viene attivato chiudo la finestra.


2
È quello che faccio di solito anche io. Anche se questo sembra un po 'sporco, considerando tutte quelle cose nuove che comandano wpf.
Botz3000

4

Ecco cosa ho fatto inizialmente, che funziona, tuttavia sembra piuttosto prolisso e brutto (tutto ciò che è statico globale non è mai buono)

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

Successivamente ho rimosso tutto questo codice e ho appena ricevuto la LoginFormViewModelchiamata del metodo Close nella sua vista. Alla fine è stato molto più bello e più facile da seguire. IMHO il punto dei motivi è quello di offrire alle persone un modo più semplice per capire cosa sta facendo la tua app e, in questo caso, MVVM rendeva molto più difficile capire che se non l'avessi usato, e ora era un anti- modello.


3

Cordiali saluti, mi sono imbattuto in questo stesso problema e penso di aver capito un lavoro che non richiede globali o statica, anche se potrebbe non essere la risposta migliore. Ho lasciato che decidiate voi stessi per voi.

Nel mio caso, ViewModel che crea un'istanza della finestra da visualizzare (chiamiamolo ViewModelMain) conosce anche LoginFormViewModel (usando la situazione sopra come esempio).

Quindi quello che ho fatto è stato creare una proprietà su LoginFormViewModel di tipo ICommand (chiamiamola CloseWindowCommand). Quindi, prima di chiamare .ShowDialog () sulla finestra, ho impostato la proprietà CloseWindowCommand su LoginFormViewModel sul metodo window.Close () della finestra che ho istanziato. Quindi all'interno di LoginFormViewModel tutto ciò che devo fare è chiamare CloseWindowCommand.Execute () per chiudere la finestra.

Suppongo che sia un po 'una soluzione alternativa / hack, ma funziona bene senza interrompere il modello MVVM.

Sentiti libero di criticare questo processo quanto vuoi, posso prenderlo! :)


Non sono sicuro di averlo sviluppato completamente, ma ciò non significa che la tua MainWindow debba essere istanziata prima della LoginWindow? È qualcosa che vorrei evitare se possibile
Orion Edwards,

3

Probabilmente è molto tardi, ma ho riscontrato lo stesso problema e ho trovato una soluzione che funziona per me.

Non riesco a capire come creare un'app senza dialoghi (forse è solo un blocco mentale). Quindi ero in un vicolo cieco con MVVM e mostrando una finestra di dialogo. Quindi mi sono imbattuto in questo articolo di CodeProject:

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

Che è un UserControl che sostanzialmente consente a una finestra di essere all'interno dell'albero visivo di un'altra finestra (non consentito in xaml). Espone anche una DependencyProperty booleana chiamata IsShowing.

Puoi impostare uno stile come, in genere in una risorsa di risorse, che sostanzialmente visualizza la finestra di dialogo ogni volta che la proprietà Content del controllo! = Null tramite trigger:

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

Nella vista in cui si desidera visualizzare la finestra di dialogo è sufficiente:

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

E nel tuo ViewModel tutto ciò che devi fare è impostare la proprietà su un valore (Nota: la classe ViewModel deve supportare INotifyPropertyChanged affinché la vista sappia che è successo).

così:

DialogViewModel = new DisplayViewModel();

Per abbinare ViewModel a View dovresti avere qualcosa del genere in un risorse risorse:

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

Con tutto ciò ottieni un codice di una riga per mostrare la finestra di dialogo. Il problema che si presenta è che non puoi davvero chiudere la finestra di dialogo solo con il codice sopra. Ecco perché devi inserire un evento in una classe di base ViewModel che DisplayViewModel eredita e al posto del codice sopra, scrivi questo

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

Quindi è possibile gestire il risultato della finestra di dialogo tramite il callback.

Questo può sembrare un po 'complesso, ma una volta posate le basi, è abbastanza semplice. Anche questa è la mia implementazione, sono sicuro che ce ne sono altri :)

Spero che questo aiuti, mi ha salvato.


3

Ok, quindi questa domanda ha quasi 6 anni e non riesco ancora a trovare qui quale sia la risposta corretta, quindi permettimi di condividere i miei "2 centesimi" ...

In realtà ho 2 modi per farlo, il primo è quello semplice ... il secondo a destra, quindi se stai cercando quello giusto, salta il numero 1 e passa al numero 2 :

1. Facile e veloce (ma non completo)

Se ho solo un piccolo progetto, a volte creo un CloseWindowAction nel ViewModel:

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

E chiunque crei la vista o nel codice della vista dietro ho appena impostato il metodo che l'azione chiamerà:

(ricorda che MVVM riguarda la separazione tra View e ViewModel ... il codice della View diventa ancora View e finché c'è una separazione adeguata non stai violando il pattern)

Se alcuni ViewModel creano una nuova finestra:

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

O se lo vuoi nella tua finestra principale, posizionalo sotto il costruttore della tua vista:

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

quando vuoi chiudere la finestra, chiama semplicemente l'azione sul tuo ViewModel.


2. Il modo giusto

Ora il modo corretto di farlo è usare Prism (IMHO), e tutto ciò può essere trovato qui .

È possibile effettuare una richiesta di interazione , popolarla con tutti i dati necessari nella nuova finestra, pranzarla, chiuderla e persino ricevere nuovamente i dati . Tutto questo incapsulato e approvato MVVM. Puoi anche ottenere uno stato di come la finestra è stata chiusa , come se l'utente Canceledo Accepted(pulsante OK) la finestra e i dati indietro se ne hai bisogno . È un po 'più complicato e risposta n. 1, ma è molto più completo e un modello consigliato da Microsoft.

Il link che ho fornito ha tutti gli snippet di codice e gli esempi, quindi non mi preoccuperò di inserire alcun codice qui, basta leggere l'articolo di download di Prism Quick Start ed eseguirlo, è davvero semplice sottolineare solo un po 'più dettagliato farlo funzionare, ma i vantaggi sono maggiori della semplice chiusura di una finestra.


Bel modo, ma la risoluzione e l'assegnazione di ViewModels non possono essere sempre così semplici. Cosa succede se lo stesso modello di visualizzazione è DataContext di molte finestre?
Kylo Ren,

Quindi suppongo che dovresti chiudere tutte le finestre contemporaneamente, ricordare che un'azione può attivare molti delegati contemporaneamente, basta usare +=per aggiungere un delegato e chiamare l'azione, farà scattare tutti loro .... O lo farai devi creare una logica speciale sulla tua VM in modo che sia consapevole di quale finestra chiudere (forse avere una raccolta di azioni ravvicinate) .... Ma penso che avere più viste associate a una VM non sia una buona pratica, è meglio gestire per avere una vista e un'istanza di macchina virtuale, associate l'una all'altra e forse una macchina virtuale principale che gestisce tutte le macchine virtuali figlio associate a tutte le viste.
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();
  }
}

2

È possibile che ViewModel esponga un evento a cui si registra View. Quindi, quando ViewModel decide l'ora di chiudere la vista, genera l'evento che provoca la chiusura della vista. Se si desidera restituire un valore di risultato specifico, è necessario disporre di una proprietà in ViewModel.


Sono d'accordo con questo: la semplicità è preziosa. Devo pensare a cosa succede quando il prossimo sviluppatore junior viene assunto per assumere questo progetto. Suppongo che avrà una possibilità molto migliore di farlo bene come descrivi. A meno che non pensi di mantenere questo codice per sempre da solo? +1
Decano del

2

Solo per aggiungere all'enorme numero di risposte, voglio aggiungere quanto segue. Supponendo che tu abbia un ICommand sul tuo ViewModel e desideri che quel comando chiuda la sua finestra (o qualsiasi altra azione per quella materia), puoi usare qualcosa come il seguente.

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

Non è perfetto e potrebbe essere difficile da testare (poiché è difficile deridere / stubare una statica) ma è più pulito (IMHO) rispetto alle altre soluzioni.

Erick


Sono diventato molto felice quando ho visto la tua semplice risposta! ma non funziona neanche! Devo aprire e chiudere con Visual Basic. Conosci l'equivalenza di (windows [i] .DataContext == this) in VB?
Ehsan,

L'ho capito finalmente! :) Grazie. Se windows (i) .DataContext Sono io
Ehsan

Conosci lo stesso modo semplice anche per aprire una finestra? Devo inviare e ricevere anche alcuni dati in viewmodel figlio e viceversa.
Ehsan,

1

Ho implementato la soluzione di Joe White, ma ho riscontrato problemi con occasionali errori " DialogResult può essere impostato solo dopo che Window è stato creato e mostrato come finestra di dialogo ".

Stavo tenendo il ViewModel in giro dopo che la View era chiusa e occasionalmente ho aperto una nuova View usando la stessa VM. Sembra che la chiusura della nuova vista prima che la vecchia vista fosse stata spazzata via causasse DialogResultChanged nel tentativo di impostare DialogResult proprietà sulla finestra chiusa, provocando così l'errore.

La mia soluzione era quella di cambiare DialogResultChanged per controllare la proprietà IsLoaded della finestra :

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

Dopo aver apportato questa modifica, gli eventuali allegati alle finestre di dialogo chiuse vengono ignorati.


Grazie Signore. Ho avuto lo stesso problema
DJ Burb,

1

Ho finito per fondere la risposta di Joe White e un po 'di codice dalla risposta di Adam Mills , poiché avevo bisogno di mostrare un controllo utente in una finestra creata a livello di programmazione. Quindi DialogCloser non ha bisogno di essere sulla finestra, può essere sul controllo utente stesso

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

E DialogCloser troverà la finestra del controllo utente se non fosse collegata alla finestra stessa.

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

1

Il comportamento è il modo più conveniente qui.

  • Da una mano, può essere associato al modello di visualizzazione specificato (che può segnalare "chiudi il modulo!")

  • Da un'altra parte, ha accesso al modulo stesso, quindi può iscriversi agli eventi specifici del modulo necessari o mostrare la finestra di dialogo di conferma o qualsiasi altra cosa.

La scrittura del comportamento necessario può essere vista noiosa la prima volta. Tuttavia, da ora in poi, puoi riutilizzarlo su ogni singolo modulo di cui hai bisogno tramite lo snippet XAML con una riga esatta. E se necessario, è possibile estrarlo come assembly separato in modo che possa essere incluso in qualsiasi progetto successivo desiderato.


0

Perché non passare semplicemente la finestra come parametro di comando?

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}" />

Non credo sia una buona idea limitare la macchina virtuale a un tipo di finestra.
Shimmy Weitzhandler,

2
Non credo sia una buona idea limitare la VM a un Windowtipo che in qualche modo non è "puro" MVVM. Vedi questa risposta, dove la VM non è limitata a un Windowoggetto.
Shimmy Weitzhandler,

in questo modo la dipendenza viene messa su un pulsante che sicuramente non può essere sempre la situazione. Anche passare il tipo di interfaccia utente a ViewModel è una cattiva pratica.
Kylo Ren,

0

Un'altra soluzione è quella di creare proprietà con INotifyPropertyChanged in Visualizza modello come DialogResult, quindi in Codice dietro scrivere questo:

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

Il frammento più importante è _someViewModel_PropertyChanged. DialogResultPropertyNamepuò essere una stringa const pubblica inSomeViewModel .

Uso questo tipo di trucco per apportare alcune modifiche ai controlli di visualizzazione nel caso in cui ciò sia difficile da eseguire in ViewModel. OnPropertyChanged in ViewModel puoi fare tutto quello che vuoi in View. ViewModel è ancora "unit testable" e alcune piccole righe di codice nel codice dietro non fanno differenza.


0

Vorrei andare così:

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

0

Ho letto tutte le risposte ma devo dire che la maggior parte di loro non è abbastanza buona o anche peggio.

È possibile gestirlo in modo bello con la classe DialogService la cui responsabilità è mostrare la finestra di dialogo e restituire il risultato della finestra di dialogo. Ho creato un progetto di esempio dimostra la sua implementazione e utilizzo.

ecco le parti più importanti:

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

Non è solo più semplice? più stretto, più leggibile e ultimo ma non meno facile da eseguire il debug di EventAggregator o altre soluzioni simili?

come puoi vedere, nei miei modelli di visualizzazione ho usato il primo approccio di ViewModel descritto nel mio post qui: Best practice per chiamare View from ViewModel in WPF

Naturalmente, nel mondo reale, è DialogService.ShowDialognecessario disporre di più opzioni per configurare la finestra di dialogo, ad esempio pulsanti e comandi che devono eseguire. Esistono diversi modi per farlo, ma è fuori portata :)


0

Anche se questo non risponde alla domanda su come farlo tramite il modello di visualizzazione, questo mostra come farlo usando solo XAML + l'SDK di fusione.

Ho scelto di scaricare e utilizzare due file da Blend SDK, entrambi i quali è possibile come pacchetto da Microsoft tramite NuGet. I file sono:

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

Microsoft.Expression.Interactions.dll ti offre buone capacità come la possibilità di impostare proprietà o invocare un metodo sul tuo viewmodel o altro target e ha anche altri widget all'interno.

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

Nota che se stai solo andando per un semplice comportamento OK / Annulla, puoi scappare usando le proprietà IsDefault e IsCancel fintanto che la finestra è mostrata con Window.ShowDialog ().
Personalmente ho avuto problemi con un pulsante che aveva la proprietà IsDefault impostata su true, ma era nascosta quando la pagina veniva caricata. Sembrava che non volesse giocare bene dopo che è stato mostrato, quindi sto solo impostando la proprietà Window.DialogResult come mostrato sopra e funziona per me.


0

Ecco la semplice soluzione senza bug (con codice sorgente), Funziona per me.

  1. Deriva il tuo ViewModel da INotifyPropertyChanged

  2. Crea una proprietà osservabile 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. Collega un gestore in vista per questa modifica di proprietà

        _loginDialogViewModel = new LoginDialogViewModel();
        loginPanel.DataContext = _loginDialogViewModel;
        _loginDialogViewModel.PropertyChanged += OnPropertyChanged;
  4. Adesso hai quasi finito. Nel caso in cui il gestore facciaDialogResult = true

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

0

Crea un Dependency Propertynel tuo View/ qualsiasi UserControl(o Windowvuoi chiudere). Come sotto:

 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
        }

E collegalo dalla proprietà del tuo 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}"

Proprietà in VeiwModel:

private bool closeWindow;

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

Ora attiva l'operazione di chiusura modificando il CloseWindowvalore in ViewModel. :)


-2

Dove è necessario chiudere la finestra, è sufficiente inserirlo nella viewmodel:

ta-da

  foreach (Window window in Application.Current.Windows)
        {
            if (window.DataContext == this)
            {
                window.Close();
                return;
            }
        }

Un ViewModel non deve contenere un UIElement in alcun modo, perché questo può creare bug
WiiMaxx

Cosa succede se DataContext viene ereditato da più finestre?
Kylo Ren,

ta-da, questo non è assolutamente MVVM.
Alexandru Dicu,

-10
Application.Current.MainWindow.Close() 

È abbastanza!


3
-1 Vero solo se la finestra che si desidera chiudere è la finestra principale ... Presupposto molto improbabile per la finestra di dialogo di accesso ...
surfen,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.