Como o ViewModel deve fechar o formulário?

247

Estou tentando aprender o WPF e o problema do MVVM, mas tive um problema. Esta questão é semelhante, mas não é a mesma que esta (manipulação de diálogos no wpf-with-mvvm) ...

Eu tenho um formulário "Login" escrito usando o padrão MVVM.

Este formulário possui um ViewModel que contém o nome de usuário e a senha, que são vinculados à exibição no XAML usando ligações de dados normais. Ele também possui um comando "Login", que é vinculado ao botão "Login" no formulário, usando uma ligação de dados normal.

Quando o comando "Login" é acionado, ele invoca uma função no ViewModel que dispara e envia dados pela rede para efetuar login. Quando essa função é concluída, existem 2 ações:

  1. O login foi inválido - apenas mostramos uma MessageBox e está tudo bem

  2. O login foi válido, precisamos fechar o formulário de Login e retornar como verdadeiro DialogResult...

O problema é que o ViewModel não sabe nada sobre a exibição real, então como ele pode fechar a exibição e dizer-lhe para retornar um determinado DialogResult? Eu poderia colocar algum código no CodeBehind e / ou passar o View até o ViewModel, mas parece que isso iria anular completamente todo o ponto do MVVM ...


Atualizar

No final, acabei de violar a "pureza" do padrão MVVM e o View publicou um Closedevento e expôs um Closemétodo. O ViewModel então chamaria view.Close. A visualização é conhecida apenas por meio de uma interface e conectada por meio de um contêiner IOC, de modo que nenhuma testabilidade ou manutenção são perdidas.

Parece bastante tolo que a resposta aceita seja de -5 votos! Embora eu esteja ciente dos bons sentimentos que alguém obtém ao resolver um problema enquanto é "puro", certamente não sou o único que pensa que 200 linhas de eventos, comandos e comportamentos apenas para evitar um método de uma linha. o nome de "padrões" e "pureza" é um pouco ridículo ...

Orion Edwards
fonte
2
Não neguei a resposta aceita, mas acho que a razão para as votações negativas é que não é útil em geral, mesmo que funcione em um caso. Você mesmo disse em outro comentário: "Embora o formulário de login seja uma caixa de diálogo 'dois campos', eu tenho muitos outros que são muito mais complexos (e, portanto, garantem MVVM), mas ainda precisam ser fechados ..."
Joe Branco
1
Entendo o seu argumento, mas pessoalmente acho que, mesmo para o caso geral, um Closemétodo simples ainda é a melhor solução. Tudo o resto em outros diálogos mais complexos é MVVM e ligação de dados, mas ele só parecia bobo para implementar as enormes "soluções" aqui em vez de apenas um método simples ...
Orion Edwards
2
Você pode verificar o seguinte link para obter o resultado da caixa de diálogo asimsajjad.blogspot.com/2010/10/… , que retornará o resultado da caixa de diálogo e fechará a exibição do viewModel
Asim Sajjad
3
Altere a resposta aceita para esta pergunta. Existem muitas boas soluções que são muito melhores do que alguém questionando o uso do MVVM para essa funcionalidade. Isso não é uma resposta, é evitar.
ScottCher
2
@OrionEdwards Acho que você estava certo em quebrar o padrão aqui. O principal objetivo de um padrão de design é acelerar os ciclos de desenvolvimento, aumentar a manutenção e simplificar seu código, fazendo com que toda a equipe siga as mesmas regras. Isso não é alcançado adicionando dependências em bibliotecas externas e implementando centenas de linhas de código para realizar uma tarefa, ignorando totalmente que existe uma solução muito mais simples, apenas porque alguém é teimoso para sacrificar a "pureza" do padrão. Basta garantir para documentar o que your've feito e BEIJAR seu código ( k eep i t s hort e s imple).
M463 16/06/2015

Respostas:

324

Fui inspirado pela resposta de Thejuan para escrever uma propriedade anexa mais simples. Sem estilos, sem gatilhos; em vez disso, você pode fazer isso:

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

Isso é quase tão limpo como se a equipe do WPF tivesse acertado e tornado o DialogResult uma propriedade de dependência em primeiro lugar. Basta colocar umbool? DialogResult propriedade no seu ViewModel e implementar INotifyPropertyChanged, e pronto, seu ViewModel pode fechar a janela (e definir seu DialogResult) apenas definindo uma propriedade. MVVM como deveria ser.

Aqui está o código para 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);
        }
    }
}

Eu também postei isso no meu blog .

Joe White
fonte
3
Essa é a resposta que eu mais gosto! Bom trabalho escrevendo essa propriedade anexada.
Jorge Vargas
2
Boa opção, mas há um bug sutil nesta solução. Se o modelo de exibição da caixa de diálogo for um singleton, o valor DialogResult será transportado para o próximo uso da caixa de diálogo. Isso significa que ele será cancelado ou aceito instantaneamente antes de aparecer, para que o diálogo não seja exibido uma segunda vez.
Gone Coding
13
@ Hi Tech Tech, parece que o bug está em usar um ViewModel singleton em primeiro lugar. (sorrindo) Sério, por que diabos você quer um ViewModel único? É uma má idéia manter um estado mutável nas variáveis ​​globais. Torna o teste um pesadelo e o teste é um dos motivos pelos quais você usaria o MVVM em primeiro lugar.
Joe White
3
O objetivo do MVVM não é acoplar sua lógica a nenhuma interface específica? Nesse caso, bool? certamente não é utilizável por outra interface do usuário, como um WinForm, e o DialogCloser é específico ao WPF. Então, como isso se encaixa bem como uma solução? Além disso, por que escrever código 2x-10x apenas para fechar uma janela por meio de uma encadernação?
David Anderson
2
@ David David, eu não tentaria MVVM com WinForms em qualquer caso; seu suporte à ligação de dados é muito fraco e o MVVM depende de um sistema de ligação bem pensado. E não chega nem perto do código 2x-10x. Você escreve esse código uma vez , não uma vez para cada janela. Depois disso, é uma ligação de uma linha mais uma propriedade de notificação, usando o mesmo mecanismo que você já está usando para todo o resto em sua visualização (por exemplo, você não precisa injetar uma interface de visualização extra apenas para controlar o fechamento do janela). Você pode fazer outras trocas, mas parece-me um bom negócio.
Joe White
64

Na minha perspectiva, a pergunta é muito boa, pois a mesma abordagem seria usada não apenas para a janela "Login", mas para qualquer tipo de janela. Analisei muitas sugestões e nenhuma está correta para mim. Revise minha sugestão que foi retirada do artigo do padrão de design do MVVM .

Cada classe ViewModel deve herdar da WorkspaceViewModelque possui o RequestCloseevento e a CloseCommandpropriedade do ICommandtipo. A implementação padrão da CloseCommandpropriedade aumentará o RequestCloseevento.

Para fechar a janela, o OnLoadedmétodo da sua janela deve ser substituído:

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

ou OnStartupmétodo do seu aplicativo:

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

Eu acho que a implementação de RequestCloseeventos e CloseCommandpropriedades na WorkspaceViewModelsão bastante claras, mas mostrarei que elas são consistentes:

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 o código fonte do 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: Não me trate mal por essas fontes! Se eu os tivesse ontem, isso me salvaria algumas horas ...

PPS Quaisquer comentários ou sugestões são bem-vindos.

Budda
fonte
2
Hum, o fato de você ter se conectado ao manipulador de eventos customer.RequestCloseno código por trás do seu arquivo XAML não viola o padrão MVVM? Você também poderia vincular o Clickmanipulador de eventos no botão "Fechar", pois, ao mesmo tempo, tocou o código e fez um this.Close()! Certo?
GONeale
1
Não tenho muitos problemas com a abordagem do evento, mas não gosto da palavra RequestClose porque, para mim, ela ainda implica muito conhecimento sobre a implementação da Visualização. Prefiro expor propriedades como IsCancelled, que tendem a ser mais significativas, dado o contexto e implicam menos sobre o que a exibição deve fazer em resposta.
precisa saber é
18

Eu usei comportamentos anexados para fechar a janela. Vincule uma propriedade "signal" no seu ViewModel ao comportamento anexado (na verdade, uso um gatilho) Quando definido como true, o comportamento fecha a janela.

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

Adam Mills
fonte
Até agora, esta é a única resposta que não requer código na janela (e na verdade fecha uma janela modal, em vez de sugerir outra abordagem). Pena que requer tanta complexidade, com o Style and Trigger e toda essa sujeira - parece que isso realmente deve ser possível com um comportamento anexado de uma linha.
Joe White
4
Agora é possível com um comportamento anexado de uma linha. Veja minha resposta: stackoverflow.com/questions/501886/…
Joe White
15

Há muitos comentários discutindo os prós e contras da MVVM aqui. Para mim, eu concordo com Nir; é uma questão de usar o padrão adequadamente e o MVVM nem sempre se encaixa. As pessoas parecem estar dispostas a sacrificar todos os princípios mais importantes do design de software, APENAS para ajustá-lo ao MVVM.

Dito isto, acho que o seu caso poderia se encaixar bem com um pouco de refatoração.

Na maioria dos casos, o WPF permite que você aguarde SEM múltiplos Windows. Talvez você possa tentar usar Frames e Pages em vez do Windows com DialogResults.

No seu caso, minha sugestão seria LoginFormViewModelmanipular oe, LoginCommandse o login for inválido, defina uma propriedade LoginFormViewModelcom um valor apropriado ( falseou algum valor de enumeração como UserAuthenticationStates.FailedAuthentication). Você faria o mesmo para um login bem-sucedido ( trueou algum outro valor de enumeração). Você usaria um DataTriggerque responde aos vários estados de autenticação do usuário e poderia usar um simples Setterpara alterar a Sourcepropriedade do Frame.

Tendo sua janela de login retornando, DialogResultacho que é onde você está ficando confuso; isso DialogResulté realmente uma propriedade do seu ViewModel. Na minha experiência reconhecidamente limitada com o WPF, quando algo não parece certo, normalmente porque estou pensando em termos de como eu teria feito a mesma coisa no WinForms.

Espero que ajude.

EightyOne Unite
fonte
10

Supondo que sua caixa de diálogo de login seja a primeira janela criada, tente isso dentro da 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;
        }
    }
Jim Wallace
fonte
Homens isso é simples e funciona muito bem. Atualmente, estou usando essa abordagem.
precisa saber é o seguinte
Funciona apenas para a janela PRINCIPAL. Portanto, não o use para outras janelas.
Oleksii
7

Esta é uma solução simples e limpa - Você adiciona um evento ao ViewModel e instrui a Janela a fechar-se quando esse evento é acionado.

Para mais detalhes, consulte minha postagem no blog, Fechar janela no 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: O exemplo usa o Prism DelegateCommand(consulte Prism: Commanding ), mas qualquer ICommandimplementação pode ser usada para esse assunto.

Você pode usar comportamentos deste pacote oficial.

Shimmy Weitzhandler
fonte
2
+1, mas você deve fornecer mais detalhes na própria resposta, por exemplo, que esta solução requer referência ao assembly de Interatividade da Mistura de Expressão.
surfen
6

A maneira como eu lidaria com isso é adicionar um manipulador de eventos no meu ViewModel. Quando o usuário efetuava login com êxito, eu acionava o evento. Na minha opinião, eu me apegaria a esse evento e, quando disparasse, fecharia a janela.


fonte
2
É o que eu costumo fazer também. Mesmo que isso pareça um pouco sujo, considerando todas essas coisas novas e controladas pelo wpf.
Botz3000
4

Aqui está o que eu fiz inicialmente, o que funciona, no entanto, parece bastante demorado e feio (estática global, nada é bom)

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

Mais tarde, em seguida, removi todo esse código e apenas LoginFormViewModelchamei o método Close na exibição. Acabou sendo muito melhor e mais fácil de seguir. IMHO, o objetivo dos padrões é oferecer às pessoas uma maneira mais fácil de entender o que seu aplicativo está fazendo e, neste caso, o MVVM estava tornando muito mais difícil de entender do que se eu não o tivesse usado, e agora era um anti- padrão.

Orion Edwards
fonte
3

Para sua informação, encontrei o mesmo problema e acho que descobri uma solução alternativa que não requer dados globais nem estáticos, embora possa não ser a melhor resposta. Eu deixo vocês decidirem por si mesmos.

No meu caso, o ViewModel que instancia a janela a ser exibida (vamos chamá-la de ViewModelMain) também conhece o LoginFormViewModel (usando a situação acima como exemplo).

Então, o que fiz foi criar uma propriedade no LoginFormViewModel que era do tipo ICommand (vamos chamá-lo de CloseWindowCommand). Em seguida, antes de chamar .ShowDialog () na janela, defino a propriedade CloseWindowCommand no LoginFormViewModel para o método window.Close () da janela que instanciei. Então, dentro do LoginFormViewModel, tudo o que preciso fazer é chamar CloseWindowCommand.Execute () para fechar a janela.

É um pouco de uma solução alternativa / hack, suponho, mas funciona bem sem realmente quebrar o padrão MVVM.

Sinta-se livre para criticar esse processo o quanto quiser, eu posso aceitar! :)


fonte
Não tenho certeza se o entro totalmente, mas isso não significa que sua MainWindow precise ser instanciada antes da sua LoginWindow? Isso é algo que eu gostaria de evitar, se possível
Orion Edwards
3

Provavelmente é muito tarde, mas me deparei com o mesmo problema e encontrei uma solução que funciona para mim.

Não consigo descobrir como criar um aplicativo sem caixas de diálogo (talvez seja apenas um bloqueio mental). Então, eu estava em um impasse com o MVVM e mostrando um diálogo. Então me deparei com este artigo do CodeProject:

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

Que é um UserControl que basicamente permite que uma janela esteja dentro da árvore visual de outra janela (não permitida no xaml). Também expõe um DependencyProperty booleano chamado IsShowing.

Você pode definir um estilo como, normalmente em um dicionário com recursos, que basicamente exibe a caixa de diálogo sempre que a propriedade Content do controle! = Null via triggers:

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

Na visão em que você deseja exibir a caixa de diálogo, basta ter este:

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

E no seu ViewModel, tudo o que você precisa fazer é definir a propriedade como um valor (Nota: a classe ViewModel deve suportar INotifyPropertyChanged para que o modo de exibição saiba que algo aconteceu).

igual a:

DialogViewModel = new DisplayViewModel();

Para combinar o ViewModel com o View, você deve ter algo parecido com isto em um dicionário de recursos:

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

Com tudo isso, você obtém um código de uma linha para mostrar o diálogo. O problema é que você não pode realmente fechar a caixa de diálogo apenas com o código acima. É por isso que você deve inserir um evento em uma classe base do ViewModel que o DisplayViewModel herda e, em vez do código acima, escreva

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

Em seguida, você pode lidar com o resultado da caixa de diálogo através do retorno de chamada.

Isso pode parecer um pouco complexo, mas uma vez estabelecidas as bases, é bem direto. Novamente, esta é a minha implementação, tenho certeza de que existem outras :)

Espero que isso ajude, isso me salvou.

Jose
fonte
3

Ok, então esta pergunta tem quase 6 anos e ainda não consigo encontrar aqui o que eu acho que é a resposta adequada, então permita-me compartilhar meus "2 centavos" ...

Na verdade, eu tenho duas maneiras de fazer isso, a primeira é a mais simples ... a segunda à direita, então, se você está procurando a correta, pule a primeira e pule para a segunda :

1. Rápido e fácil (mas não completo)

Se eu tenho apenas um projeto pequeno, às vezes apenas crio uma CloseWindowAction no ViewModel:

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

E quem criar o View, ou no código do View atrás, apenas defino o método que a ação chamará:

(lembre-se de que o MVVM trata da separação da View e do ViewModel ... o código da View ainda é a View e enquanto houver uma separação adequada, você não estará violando o padrão)

Se algum ViewModel criar uma nova janela:

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

Ou, se você quiser na sua janela principal, basta colocá-lo sob o construtor do seu View:

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

quando quiser fechar a janela, basta chamar a Ação no seu ViewModel.


2. O caminho certo

Agora, a maneira correta de fazer isso é usar o Prism (IMHO), e tudo sobre isso pode ser encontrado aqui .

Você pode fazer uma Solicitação de Interação , preenchê-la com quaisquer dados necessários em sua nova Janela, almoçar, fechá-la e até receber dados novamente . Tudo isso encapsulado e aprovado pela MVVM. Você ainda obtém um status de como a Janela foi fechada , como se o Usuário Canceledou Accepted(botão OK) da Janela e os dados retornassem, se necessário . É um pouco mais complicado e a resposta nº 1, mas é muito mais completa e um padrão recomendado pela Microsoft.

O link que eu dei tem todos os trechos de código e exemplos, então não vou me incomodar em colocar nenhum código aqui, basta ler o artigo de download do Prism Quick Start e executá-lo, é muito simples entender um pouco mais detalhadamente fazê-lo funcionar, mas os benefícios são maiores do que apenas fechar uma janela.

mFeinstein
fonte
Boa maneira, mas a resolução e a atribuição de ViewModels nem sempre podem ser tão diretas. E se o mesmo viewmodel for o DataContext de muitos Windows?
Kylo Ren
Então eu acho que você teria que fechar todas as janelas de uma só vez, lembre-se de que uma ação pode acionar muitos delegados de uma só vez, basta usar +=para adicionar um delegado e chamar a ação, ele acionará todos eles .... Ou você precisa criar uma lógica especial em sua VM para que ele saiba qual janela fechar (talvez tenha uma coleção de ações próximas) .... Mas acho que ter várias visualizações vinculadas a uma VM não é uma prática recomendada. é melhor gerenciar ter uma instância de View e uma VM conectadas entre si e talvez uma VM pai que gerencia todas as VMs filhas associadas a todas as Views.
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
fonte
2

Você pode fazer com que o ViewModel exponha um evento ao qual a View se registra. Então, quando o ViewModel decide sua hora de fechar a exibição, ele dispara o evento que faz com que a exibição seja fechada. Se você deseja que um valor de resultado específico seja devolvido, você terá uma propriedade no ViewModel para isso.

Abdulla Al-Qawasmeh
fonte
Eu concordo com isso - a simplicidade é valiosa. Eu tenho que pensar no que acontece quando o próximo desenvolvedor júnior é contratado para assumir o projeto. Meu palpite é que ele terá uma chance muito melhor de acertar como você descreve. A menos que você pense que manterá esse código para sempre? 1
Dean
2

Apenas para adicionar ao grande número de respostas, quero adicionar o seguinte. Supondo que você tenha um ICommand no seu ViewModel e deseje que esse comando feche sua janela (ou qualquer outra ação), é possível usar algo como o seguinte.

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

Não é perfeito e pode ser difícil de testar (como é difícil zombar / stub de uma estática), mas é mais limpo (IMHO) do que as outras soluções.

Erick

Erick T
fonte
Fiquei muito feliz quando vi sua resposta simples! mas também não funciona! Eu preciso abrir e fechar com o visual basic. Você conhece a equivalência de (windows [i] .DataContext == this) no VB?
Ehsan 29/01
Eu entendi finalmente! :) Obrigado. Se o Windows (i) .DataContext sou eu
Ehsan
Você conhece a mesma maneira simples de abrir uma janela também? Preciso enviar e receber alguns dados também no child viewmodel e vice-versa.
Ehsan 29/01
1

Eu implementei a solução de Joe White, mas tive problemas com ocasionais " DialogResult pode ser definido somente depois que o Windows é criado e mostrado como caixa de diálogo erros ".

Eu mantive o ViewModel por perto depois que o View foi fechado e, ocasionalmente, abri um novo View usando a mesma VM. Parece que o fechamento da nova visualização antes da coleta antiga de lixo resultou em DialogResultChanged tentando definir o DialogResult propriedade na janela fechada, provocando o erro.

Minha solução foi alterar DialogResultChanged para verificar a propriedade IsLoaded da janela :

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

Após fazer essa alteração, todos os anexos às caixas de diálogo fechadas são ignorados.

Jim Hansen
fonte
Obrigado senhor. Eu tive o mesmo problema
DJ Burb
1

Acabei misturando a resposta de Joe White e algum código da resposta de Adam Mills , pois precisava mostrar um controle de usuário em uma janela criada programaticamente. Portanto, o DialogCloser não precisa estar na janela, pode estar no próprio controle do usuário

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

E o DialogCloser encontrará a janela do controle do usuário se não estiver anexado à própria janela.

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

O comportamento é a maneira mais conveniente aqui.

  • Por um lado, ele pode ser vinculado ao modelo de exibição fornecido (que pode sinalizar "fechar o formulário!")

  • Por outro lado, ele tem acesso ao próprio formulário para que possa se inscrever nos eventos específicos do formulário necessários ou mostrar a caixa de diálogo de confirmação ou qualquer outra coisa.

Escrever o comportamento necessário pode ser visto entediante desde a primeira vez. No entanto, a partir de agora, você poderá reutilizá-lo em todos os formulários necessários, usando o snippet XAML de uma linha exata. E, se necessário, você pode extraí-lo como uma montagem separada para que possa ser incluído em qualquer próximo projeto que desejar.

Yury Schkatula
fonte
0

Por que não passar a janela como um parâmetro de 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}" />
chrislarson
fonte
Não acho uma boa ideia restringir a VM a um tipo de janela.
Shimmy Weitzhandler
2
Eu não acho que seja uma boa ideia restringir a VM a um Windowtipo que não seja MVVM "puro". Veja esta resposta, onde a VM não está restrita a um Windowobjeto.
Shimmy Weitzhandler
Dessa forma, a dependência está sendo colocada em um botão que certamente não pode ser a situação sempre. Também passar o tipo de interface do usuário para o ViewModel é uma prática ruim.
Kylo Ren
0

Outra solução é criar propriedade com INotifyPropertyChanged no modelo de exibição como DialogResult e, em seguida, no código atrás, escreva isto:

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

O fragmento mais importante é _someViewModel_PropertyChanged. DialogResultPropertyNamepode ser uma string pública pública em SomeViewModel.

Eu uso esse tipo de truque para fazer algumas alterações no View Controls, caso isso seja difícil de fazer no ViewModel. OnPropertyChanged no ViewModel, você pode fazer o que quiser no ViewModel. O ViewModel ainda é 'testável por unidade' e algumas pequenas linhas de código no código por trás não fazem diferença.

sliwinski.lukas
fonte
0

Eu iria desta maneira:

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

Eu li todas as respostas, mas devo dizer que a maioria delas não é boa o suficiente ou pior.

Você poderia lidar com isso com a classe DialogService, cuja responsabilidade é mostrar a janela e retornar o resultado da mesma. Eu tenho criar projeto de exemplo que demonstra a sua implementação e uso.

aqui estão as partes mais importantes:

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

Isso não é apenas mais simples? mais simples, mais legível e por último mas não menos fácil de depurar do que o EventAggregator ou outras soluções semelhantes?

Como você pode ver, nos meus modelos de exibição, usei a primeira abordagem do ViewModel descrita no meu post aqui: Práticas recomendadas para chamar o View do ViewModel no WPF

Obviamente, no mundo real, é DialogService.ShowDialogpreciso ter mais opções para configurar a caixa de diálogo, por exemplo, botões e comandos que devem ser executados. Há uma maneira diferente de fazer isso, mas está fora do escopo :)

Liero
fonte
0

Embora isso não responda à pergunta de como fazer isso por meio do viewmodel, isso mostra como fazer isso usando apenas XAML + o SDK de combinação.

Eu escolhi fazer o download e usar dois arquivos do SDK do Blend, os quais você pode usar como pacote da Microsoft através do NuGet. Os arquivos são:

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

O Microsoft.Expression.Interactions.dll oferece ótimos recursos, como a capacidade de definir propriedades ou chamar um método no seu modelo de exibição ou outro destino, além de ter outros widgets internos.

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

Observe que, se você está apenas adotando um comportamento simples de OK / Cancel, pode se safar usando as propriedades IsDefault e IsCancel, desde que a janela seja mostrada com Window.ShowDialog ().
Pessoalmente, tive problemas com um botão que tinha a propriedade IsDefault definida como true, mas estava oculta quando a página é carregada. Não parecia querer jogar muito bem depois que foi mostrado, então estou definindo a propriedade Window.DialogResult, como mostrado acima, e funciona para mim.

Wes
fonte
0

Aqui está a solução simples e sem erros (com código fonte), está funcionando para mim.

  1. Derive seu ViewModel de INotifyPropertyChanged

  2. Crie uma propriedade observável CloseDialog no 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. Anexar um manipulador no modo de exibição para esta alteração de propriedade

        _loginDialogViewModel = new LoginDialogViewModel();
        loginPanel.DataContext = _loginDialogViewModel;
        _loginDialogViewModel.PropertyChanged += OnPropertyChanged;
  4. Agora você está quase pronto. No manipulador de eventos, façaDialogResult = true

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

Crie um Dependency Propertyem seu View/ qualquer UserControl(ou Windowvocê deseja fechar). Como abaixo:

 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 vincule-o a partir da propriedade do seu 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}"

Propriedade em VeiwModel:

private bool closeWindow;

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

Agora, inicie a operação de fechamento alterando o CloseWindowvalor no ViewModel. :)

Kylo Ren
fonte
-2

Onde você precisa fechar a janela, basta colocar isso no viewmodel:

ta-da

  foreach (Window window in Application.Current.Windows)
        {
            if (window.DataContext == this)
            {
                window.Close();
                return;
            }
        }
Cătălin Rădoi
fonte
Um ViewModel não deve conter um UIElement de forma alguma, porque isso pode criar bugs
WiiMaxx
E se o DataContext estiver sendo herdado por várias janelas?
Kylo Ren
ta-da, isso não é totalmente MVVM.
Alexandru Dicu 11/02
-10
Application.Current.MainWindow.Close() 

É o bastante!

Alexey
fonte
3
-1 Verdadeiro apenas se a janela que você deseja fechar for a janela principal ... Suposição muito improvável para a caixa de diálogo Login ...
surfen