Как ViewModel должен закрыть форму? - PullRequest
235 голосов
/ 02 февраля 2009

Я пытаюсь изучить WPF и проблему MVVM, но столкнулся с проблемой. Этот вопрос похож, но не совсем такой , как этот (обработка-диалогов-в-wpf-с-mvvm) ...

У меня есть форма "Логин", написанная с использованием шаблона MVVM.

Эта форма имеет ViewModel, которая содержит имя пользователя и пароль, которые связаны с представлением в XAML с использованием обычных привязок данных. Он также имеет команду «Логин», которая связана с кнопкой «Логин» в форме, кроме того, используется обычная привязка данных.

Когда запускается команда «Войти», она вызывает функцию в ViewModel, которая отключается и отправляет данные по сети для входа в систему. Когда эта функция завершается, есть 2 действия:

  1. Логин был неверен - мы просто показываем MessageBox и все в порядке

  2. Логин был действителен, нам нужно закрыть форму входа в систему и сделать так, чтобы она возвращала значение true как DialogResult ...

Проблема в том, что ViewModel ничего не знает о реальном представлении, так как он может закрыть представление и сказать ему, чтобы он возвращал определенный DialogResult? Я мог бы вставить некоторый код в CodeBehind и / или передать View через ViewModel, но похоже, что он полностью победил бы весь смысл MVVM ...


Обновление

В конце я просто нарушил «чистоту» шаблона MVVM, и View опубликовал событие Closed и выставил метод Close. Тогда ViewModel просто вызовет view.Close. Представление известно только через интерфейс и подключено через контейнер IOC, поэтому тестирование и ремонтопригодность не теряются.

Кажется довольно глупым, что принятый ответ при -5 голосов! В то время как я хорошо осведомлен о хороших чувствах, которые можно получить, решая проблему, будучи «чистым», Конечно, я не единственный, кто думает, что 200 строк событий, команд и поведений просто для того, чтобы избежать однострочного метода в название «узоры» и «чистота» немного смешно ....

Ответы [ 26 ]

310 голосов
/ 25 июля 2010

Меня вдохновил ответ Теджуана , чтобы написать более простое присоединенное свойство. Нет стилей, нет триггеров; вместо этого вы можете просто сделать это:

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

Это почти так же чисто, как если бы команда WPF все правильно поняла и сделала DialogResult свойством зависимости в первую очередь. Просто поместите свойство bool? DialogResult в вашу ViewModel и реализуйте INotifyPropertyChanged, и вуаля, ваша ViewModel может закрыть окно (и установить его DialogResult), просто установив свойство. MVVM в порядке.

Вот код для 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);
        }
    }
}

Я также разместил это в своем блоге .

64 голосов
/ 20 января 2010

С моей точки зрения, вопрос довольно хороший, так как тот же подход будет использоваться не только для окна «Вход», но и для любого вида окна. Я рассмотрел много предложений, и ни один не подходит для меня. Пожалуйста, ознакомьтесь с моим предложением, которое было взято из статьи MVPM .

Каждый класс ViewModel должен наследовать от WorkspaceViewModel, который имеет событие RequestClose и свойство CloseCommand типа ICommand. Реализация свойства CloseCommand по умолчанию вызовет событие RequestClose.

Чтобы закрыть окно, метод OnLoaded вашего окна должен быть переопределен:

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

или OnStartup метод вашего приложения:

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

Полагаю, что реализация события RequestClose и свойства CloseCommand в WorkspaceViewModel довольно понятна, но я покажу, что они согласованы:

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

И исходный код 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
}

P.S. Не обращайся со мной плохо из-за этих источников! Если бы они были у меня вчера, это спасло бы меня на несколько часов ...

P.P.S. Любые комментарии или предложения приветствуются.

18 голосов
/ 20 июля 2009

Я использовал прикрепленное поведение, чтобы закрыть окно. Свяжите свойство «signal» в вашей ViewModel с прикрепленным поведением (на самом деле я использую триггер) Когда установлено значение true, поведение закрывает окно.

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

15 голосов
/ 24 марта 2009

Здесь много комментариев, в которых приводятся аргументы за и против MVVM. Для меня я согласен с Нир; это вопрос правильного использования шаблона, а MVVM не всегда подходит. Кажется, что люди готовы пожертвовать всеми наиболее важными принципами разработки программного обеспечения, просто чтобы привести его в соответствие с MVVM.

Тем не менее, ... я думаю, что ваше дело могло бы быть хорошо с небольшим рефакторингом.

В большинстве случаев, с которыми я сталкивался, WPF позволяет вам получить БЕЗ нескольких Window с. Возможно, вы могли бы попробовать использовать Frame s и Page s вместо Windows с DialogResult s.

В вашем случае мое предложение будет иметь LoginFormViewModel для обработки LoginCommand, и если логин неверен, присвойте свойству LoginFormViewModel подходящее значение (false или какое-нибудь перечисленное значение, например UserAuthenticationStates.FailedAuthentication) , Вы сделали бы то же самое для успешного входа в систему (true или другое значение enum). Затем вы используете DataTrigger, который отвечает на различные состояния аутентификации пользователя и может использовать простой Setter для изменения свойства Source Frame.

Возвращение вашего окна входа в систему DialogResult Я думаю, это то, где вы запутались; что DialogResult действительно является свойством вашей ViewModel. В моем, по общему признанию, ограниченном опыте работы с WPF, когда что-то не так, как обычно, потому что я думаю о том, как бы я сделал то же самое в WinForms.

Надеюсь, это поможет.

9 голосов
/ 28 декабря 2009

Предполагая, что ваш диалог входа в систему является первым окном, которое создается, попробуйте это внутри вашего класса 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;
        }
    }
6 голосов
/ 29 апреля 2009

Я бы справился с этим, добавив обработчик событий в мою ViewModel. Когда пользователь успешно вошел в систему, я бы запустил событие. В моем представлении я прикрепляюсь к этому событию, а когда оно срабатывает, я закрываю окно.

5 голосов
/ 06 октября 2011

Это простое и чистое решение - вы добавляете событие в ViewModel и инструктируете окно закрываться, когда это событие запускается.

Подробнее см. В моем блоге Закрыть окно из ViewModel .

4 голосов
/ 02 февраля 2009

Вот то, что я изначально сделал, и это работает, однако это выглядит довольно скучно и некрасиво (глобальная статика ничего не дает)

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

Позже я удалил весь этот код и просто LoginFormViewModel вызвал метод Close в своем представлении. Это оказалось намного приятнее и легче следовать. ИМХО смысл шаблонов в том, чтобы дать людям более простой способ понять, что делает ваше приложение, и в этом случае MVVM усложнял понимание, чем если бы я не использовал его, а теперь был анти -pattern.

3 голосов
/ 29 апреля 2009

К вашему сведению, я столкнулся с той же проблемой, и мне кажется, что я нашел способ обойтись без глобальных или статических вычислений, хотя, возможно, это не самый лучший ответ. Я позволю вам, ребята, решить это для себя.

В моем случае ViewModel, который создает экземпляр окна для отображения (давайте назовем его ViewModelMain), также знает о LoginFormViewModel (используя приведенную выше ситуацию в качестве примера).

Итак, я создал свойство LoginFormViewModel, имеющее тип ICommand (назовем его CloseWindowCommand). Затем, прежде чем я вызову .ShowDialog () в Window, я установил свойство CloseWindowCommand в LoginFormViewModel в метод window.Close () окна, который я создал. Затем внутри LoginFormViewModel все, что мне нужно сделать, это вызвать CloseWindowCommand.Execute (), чтобы закрыть окно.

Я полагаю, это немного обходной путь / хак, но он работает хорошо, не нарушая паттерн MVVM.

Не стесняйтесь критиковать этот процесс столько, сколько хотите, я могу принять это! :)

3 голосов
/ 14 июня 2015

Хорошо, так что этому вопросу уже почти 6 лет, и я до сих пор не могу найти здесь, что я думаю, что это правильный ответ, поэтому позвольте мне поделиться своими "2 центами" ...

На самом деле у меня есть 2 способа сделать это, первый простой ... второй справа, поэтому , если вы ищете правильный, просто пропустите # 1 и перейдите к # 2

1. Быстро и легко (но не полностью)

Если у меня небольшой проект, я иногда просто создаю CloseWindowAction в ViewModel:

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

И кто бы ни создал View, или в коде View, я просто установил метод, который будет вызывать действие:

(помните, что MVVM относится к разделению View и ViewModel ... Код представления здесь все еще является View и до тех пор, пока существует правильное разделение, вы не нарушаете шаблон)

Если какой-то ViewModel создает новое окно:

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

Или, если вы хотите, чтобы это было в вашем главном окне, просто поместите его под конструктор вашего представления:

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

когда вы хотите закрыть окно, просто вызовите Action на вашей ViewModel.


2. Правильный путь

Теперь правильный способ сделать это - использовать Prism (ИМХО), и все об этом можно найти здесь .

Вы можете сделать Запрос на взаимодействие , заполнить его любыми данными, которые вам понадобятся в новом окне, пообедать, закрыть и даже получить данные обратно . Все это инкапсулировано и одобрено MVVM. Вы даже получаете статус того, как было закрыто Окно , например, если Пользователь Canceled или Accepted (кнопка ОК), Окно и данные возвращаются, если вам это нужно . Это немного сложнее, ответ № 1, но он намного более полный и рекомендуемый шаблон от Microsoft.

Ссылка, которую я дал, содержит все фрагменты кода и примеры, так что я не стану размещать здесь какой-либо код, просто прочитайте статью о загрузке Prism Quick Start и запустите ее, ее очень просто понять немного более подробный, чтобы заставить его работать, но преимущества больше, чем просто закрытие окна.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...