Як ViewModel повинен закрити форму?


247

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

У мене є форма "Вхід", написана за схемою MVVM.

Ця форма має ViewModel, який містить ім'я користувача та пароль, які пов'язані з представленням у XAML, використовуючи звичайні прив'язки даних. Він також має команду "Вхід", яка прив'язана до кнопки "Вхід" у формі, починаючи з звичайного прив'язки даних.

Коли команда "Вхід" спрацьовує, вона викликає функцію в ViewModel, яка вимикається та надсилає дані по мережі для входу в систему. Коли ця функція завершена, виконуються дві дії:

  1. Логін недійсний - ми просто показуємо MessageBox і все добре

  2. Логін був дійсним, нам потрібно закрити форму для входу, і вона поверне справжню як свою DialogResult...

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


Оновлення

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

Здається, досить нерозумно, що прийнята відповідь на 5 голосів! Хоча я добре усвідомлюю хороші почуття, які виникає, вирішуючи проблему, будучи «чистим», я, безумовно, не єдиний, хто думає, що 200 рядків подій, команд та поведінки просто уникнути однорядного методу в назва "візерунків" та "чистоти" трохи смішна ....


2
Я не спростував прийнятої відповіді, але я здогадуюсь, що причиною цього є те, що вона взагалі не корисна, навіть якщо це може спрацювати в одному випадку. Ви сказали це самі в іншому коментарі: "Хоча форма для входу - це діалог" два поля ", у мене є багато інших, які набагато складніші (і, отже, ордер MVVM), але їх все одно потрібно закрити ..."
Джо Білий

1
Я бачу вашу думку, але я особисто вважаю, що навіть для загального випадку простий Closeметод все ще є найкращим рішенням. Все інше в інших більш складних діалогах - це MVVM і обмінювання даних, але просто здалося нерозумним реалізувати тут величезні "рішення" замість простого методу ...
Orion Edwards

2
Ви можете перевірити наступне посилання на результат діалогу asimsajjad.blogspot.com/2010/10/… , який поверне діалогове resutl і закриє вигляд з viewModel
Асім Саджад

3
Будь ласка, змініть прийняту відповідь на це запитання. Є багато хороших рішень, які набагато кращі, ніж хтось ставить під сумнів використання MVVM для цієї функції. Це не відповідь, це уникнення.
ScottCher

2
@OrionEdwards Я думаю, ви мали рацію порушити схему. Основна мета дизайну - пришвидшити цикли розробки, збільшити ремонтопридатність та спростити свій код, змусивши всю команду дотримуватися одних і тих же правил. Цього не досягається додаванням залежностей від зовнішніх бібліотек та реалізацією сотень рядків коду для виконання завдання, повністю ігноруючи, що існує набагато простіше рішення, лише тому, що потрібно вперто пожертвувати «чистотою» шаблону. Просто переконайтесь, що задокументуйте, що ви зробили, і KISS- код ( k eep i t s hort and s Imple).
M463

Відповіді:


324

Мене надихнула відповідь Теджуана написати простіший доданий майно. Ні стилів, ні тригерів; натомість ви можете просто зробити це:

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

Це майже так само чисто, як якщо б команда WPF правильно поставила його і зробила DialogResult властивістю залежності в першу чергу. Просто поставтеbool? DialogResult властивість у свій ViewModel та реалізуйте INotifyPropertyChanged та voilà, ваш 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);
        }
    }
}

Я також розмістив це у своєму блозі .


3
Ця відповідь мені подобається найбільше! Хороша робота з написанням цього доданого майна.
Хорхе Варгас

2
Хороший варіант, але в цьому рішенні є тонка помилка. Якщо модель перегляду діалогового вікна є однотонною, значення DialogResult переноситься до наступного використання діалогового вікна. Це означає, що він миттєво скасує або прийме, перш ніж показувати себе, тому діалогове вікно не з’явиться вдруге.
Пройшов кодування

13
@HiTech Magic, схоже, що помилка полягає у використанні одинарного ViewModel в першу чергу. (усмішка) Серйозно, чому на землі ви хочете одноосібного ViewModel? Дуже погано зберігати змінний стан у глобальних змінних. Здійснює тестування кошмару, і тестування є однією з причин, коли ви використовуєте MVVM в першу чергу.
Джо Вайт

3
Чи не сенс MVVM не чітко поєднувати вашу логіку з будь-яким конкретним інтерфейсом? У цьому випадку бул? це, звичайно, не використовується іншим інтерфейсом, таким як WinForm, і DialogCloser є специфічним для WPF. То як це добре входить у рішення? Крім того, навіщо писати 2x-10x код просто для закриття вікна через прив'язку?
Девід Андерсон

2
@DavidAnderson, я б ні в якому разі не пробував MVVM з WinForms; його підтримка передачі даних є занадто слабкою, і MVVM покладається на продуману систему зв'язування. І це ніде поблизу 2x-10x коду. Ви пишете цей код один раз , а не один раз для кожного вікна. Після цього це однорядкове прив'язування плюс властивість сповіщення, використовуючи той самий механізм, який ви вже використовуєте для всього іншого у вашому огляді (так, наприклад, вам не потрібно вводити додатковий інтерфейс перегляду просто для обробки закриття вікно). Ви можете зробити інші компроміси, але мені це здається загалом непоганим.
Джо Вайт

64

З моєї точки зору, питання досить добре, оскільки той самий підхід застосовуватиметься не лише для вікна "Вхід", але і для будь-якого вікна. Я переглянув багато пропозицій, і жодна для мене не в порядку. Будь ласка, перегляньте мою пропозицію, взяту із статті дизайну MVVM .

Кожен клас 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
}

PS Не ставтесь до мене погано за ці джерела! Якби я був учора, це врятувало б мене кілька годин ...

PPS Будь-які коментарі чи пропозиції вітаються.


2
Гм, те, що ви підключили до обробника подій customer.RequestCloseу коді за вашим файлом XAML, чи не порушує він шаблон MVVM? Ви могли б так само прив’язатись до Clickобробника подій на вашій кнопці закриття, в першу чергу, побачивши, що ви торкнулися коду позаду і все-таки зробили this.Close()! Правильно?
GONeale

1
У мене немає занадто багато проблем з підходом до події, але мені не подобається слово RequestClose, тому що для мене це все ще означає багато знань щодо реалізації Погляду. Я вважаю за краще викривати такі властивості, як IsCancelated, які, як правило, мають більш значущий зміст з огляду на контекст і менше передбачають те, що думка повинна робити у відповідь.
jpierson

18

Я використовував прикріплені форми поведінки, щоб закрити вікно. Прив’яжіть властивість "сигнал" у вашому ViewModel до прикладеної поведінки (я фактично використовую тригер) Коли його встановлено на істинне, поведінка закриває вікно.

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


Це єдина відповідь на даний момент, що не вимагає ніякого коду позаду у вікні (і фактично закриває модальне вікно, замість того, щоб запропонувати інший підхід). Шкода, що це вимагає такої великої складності, що стосується стилю та тригера та всього цього манку - здається, що це дійсно повинно бути виконано з однолінійною поведінкою.
Джо Уайт

4
Тепер це можна зробити за допомогою однолінійної поведінки. Дивіться мою відповідь: stackoverflow.com/questions/501886/…
Джо Уайт,

15

Тут є багато коментарів, які сперечаються плюси та мінуси MVVM. Для мене я згоден з Ніром; справа належним чином використовувати шаблон, і MVVM не завжди підходить. Люди, схоже, стали готові пожертвувати усіма найважливішими принципами розробки програмного забезпечення ПОСЛІДНО, щоб пристосувати його до MVVM.

Це сказало: .. Я думаю, що ваш випадок міг би добре підійти з невеликим рефакторингом.

У більшості випадків я стикаюся, WPF дозволяє вам отримати БЕЗ декількох Windows. Можливо, ви можете спробувати використовувати Frames та Pages замість Windows із DialogResults.

У вашому випадку моєю пропозицією було б LoginFormViewModelобрати LoginCommandі, якщо логін недійсний, встановіть властивість на LoginFormViewModelвідповідне значення ( falseабо якесь значення перерахунку, як UserAuthenticationStates.FailedAuthentication). Ви зробите те ж саме для успішного входу ( trueабо якогось іншого значення перерахунку). Потім ви будете використовувати a, DataTriggerякий відповідає на різні стани автентифікації користувача, і ви можете використовувати просту Setterдля зміни SourceвластивостіFrame .

Після повернення вікна для входу, DialogResultя думаю, це те, де ви плутаєтесь; щоDialogResult насправді це властивість вашого ViewModel. У моєму, загальновизнаному, обмеженому досвіді роботи з WPF, коли щось не здається нормальним, зазвичай, тому що я думаю про те, як би я зробив те саме в WinForms.

Сподіваюся, що це допомагає.


10

Якщо припустимо, що діалог для входу є першим створеним вікном, спробуйте це всередині вашого класу 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;
        }
    }

Чоловіки це просто і чудово працює. В даний час я використовую такий підхід.
Ерре Ефе

Він працює лише для ОСНОВНОГО вікна. Тому не використовуйте його для будь-яких інших вікон.
Олексій

7

Це просте і чітке рішення - Ви додаєте подію до ViewModel і доручаєте Вікну закриватися, коли ця подія буде запущена.

Детальніше дивіться у мій допис у блозі, Закрийте вікно від 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);
}

Примітка. У прикладі використовується призма DelegateCommand(див. Призма: Командування ), але ICommandдля цього може бути використана будь-яка реалізація.

Ви можете використовувати поведінку з цього офіційного пакету.


2
+1, але ви повинні надати більше деталей у самій відповіді, наприклад, що це рішення вимагає посилання на збірку виразних сумішей Interactive.
surfen

6

Як я б впорався з цим, це додати обробник подій у свій ViewModel. Після успішного входу користувача я б запустив подію. У своєму перегляді я би приєднався до цієї події, і коли він запустився, я закрив би вікно.


2
Ось що я теж роблю. Незважаючи на те, що це здається трохи брудним, враховуючи всі ці новомодні речі, що командують wpf.
Botz3000

4

Ось що я спочатку робив, що робить, але це здається досить затятим і потворним (глобальна статична річ ніколи не буває доброю)

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зателефонував метод закриття на його перегляд. Зрештою, це було набагато приємніше і легше наслідувати. ИМХО точка моделей, щоб дати людям простий спосіб зрозуміти , що робить ваш додаток, і в цьому випадку, MVVM робив це набагато важче зрозуміти , ніж якби я не використовував його, і був тепер анти -pattern.


3

FYI, я зіткнувся з цією самою проблемою, і, думаю, я вирішив вирішити проблему, яка не потребує глобальних або статичних даних, хоча це може бути не найкращою відповіддю. Я дозволяю вам, хлопці, вирішити це для себе.

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

Тому я створив властивість у LoginFormViewModel типу ICommand (Дозвольте назвати це CloseWindowCommand). Потім, перш ніж викликати .ShowDialog () у вікні, я встановлюю властивість CloseWindowCommand у LoginFormViewModel методом window.Close () вікна, який я створив. Потім всередині LoginFormViewModel все, що мені потрібно зробити, це зателефонувати CloseWindowCommand.Execute (), щоб закрити вікно.

Я думаю, це трохи обхід / взлом, але він працює добре, не порушуючи схему MVVM.

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


Я не впевнений, що я цілком вирішую це, але чи це не означає, що ваше головне вікно повинно бути створено перед вашим входом для входу? Це те, чого я хотів би уникнути, якщо можливо
Orion Edwards

3

Напевно, це дуже пізно, але я зіткнувся з тією ж проблемою і знайшов рішення, яке працює для мене.

Я не можу зрозуміти, як створити додаток без діалогів (можливо, це просто блок розуму). Тож я опинився в тупіку з MVVM і показав діалог. Тому я натрапив на цю статтю CodeProject:

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

Що таке UserControl, який в основному дозволяє вікну знаходитись у візуальному дереві іншого вікна (не допускається у xaml). Він також відкриває булеву власність DependencyPorperty під назвою IsShowing.

Ви можете встановити стиль на зразок, як правило, у ресурсі, що використовує ресурси, який в основному відображає діалогове вікно, коли властивість вмісту елемента керування! = Null через тригери:

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

У вікні перегляду діалогового вікна просто виберіть таке:

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

І у вашому ViewModel все, що вам потрібно зробити, це встановити властивість у значення (Примітка. Клас ViewModel повинен підтримувати INotifyPropertyChanged, щоб представлення знало, що щось сталося).

так:

DialogViewModel = new DisplayViewModel();

Для відповідності ViewModel та View у вас має бути щось подібне у ресурсному словнику:

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

З усім цим ви отримуєте однолінійний код для відображення діалогового вікна. Проблема, яку ви отримаєте, це те, що ви не можете дійсно закрити діалогове вікно лише з наведеним вище кодом. Отож, тому вам потрібно ввести подію в базовий клас ViewModel, який DisplayViewModel успадковує і замість коду вище, напишіть це

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

Потім ви можете обробити результат діалогу за допомогою зворотного дзвінка.

Це може здатися трохи складним, але як тільки основи будуть закладені, це досить просто. Знову це моя реалізація, я впевнений, що є й інші :)

Сподіваюсь, це допомагає, і мене врятувало.


3

Гаразд, тому це питання майже 6 років, і я досі не можу знайти тут те, що, на мою думку, це правильна відповідь, тому дозвольте мені поділитися своїми "2 копійками" ...

Насправді у мене є два способи зробити це: перший - простий ... другий - правильний, тож якщо ви шукаєте потрібний, просто пропустіть №1 та перейдіть до №2 :

1. Швидкий і простий (але не повний)

Якщо у мене є лише невеликий проект, я іноді просто створюю CloseWindowAction у ViewModel:

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

І той, хто створює ящик Перегляду, або в коді Погляду позаду, я просто встановив Метод, який дія закликає:

(пам’ятайте, що 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
                           };
}

коли ви хочете закрити вікно, просто зателефонуйте Дія у своєму ViewModel.


2. Правильний шлях

Тепер правильний спосіб зробити це за допомогою Prism (IMHO), і все про це можна знайти тут .

Ви можете зробити запит на взаємодію , заповнити його будь-якими даними, які знадобляться у вашому новому Вікні, обід, закрити його і навіть отримати дані назад . Все це було інкапсульовано та затверджено MVVM. Ви навіть отримуєте статус закриття Вікна , наприклад, якщо користувач Canceledабо Accepted(кнопку ОК) Вікно та дані повертаються, якщо вам це потрібно . Це трохи складніше і відповідь №1, але це набагато повніше і рекомендований шаблон від Microsoft.

Посилання, яке я дав, містить усі фрагменти коду та приклади, тому я не буду намагатися розміщувати тут який-небудь код, просто прочитайте статтю про завантаження швидкого запуску Prism та запустіть її, це дуже просто, щоб зрозуміти лише трохи більше деталізованого змусити його працювати, але переваги більше, ніж просто закрити вікно.


Хороший спосіб, але роздільна здатність та призначення ViewModels не завжди можуть бути такими прямо. Що робити, якщо той же viewmodel є DataContext для багатьох Windows?
Kylo Ren

Тоді я думаю, що вам доведеться закрити всі вікна відразу, пам’ятайте, що дія може викликати багато делегатів одночасно, просто використовуйте, +=щоб додати делегата, і зателефонуйте до Action, це звільнить їх усіх… Або ви Ви повинні зробити спеціальну логіку на своєму ВМ, щоб було відомо про те, яке вікно закрити (можливо, мати колекцію дій із закриттям) .... Але я думаю, що наявність декількох поглядів, прив’язаних до одного віртуального комп'ютера, не найкраща практика, це краще керувати тим, щоб мати один примірник View та один VM, прив’язаний один до одного, і, можливо, батьківський VM, який управляє всіма дочірніми VM, які пов'язані з усіма переглядами.
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

Ви можете змусити ViewModel викрити подію, на яку View реєструється. Потім, коли ViewModel вирішує час закрити подання, він запускає ту подію, яка спричиняє перегляд. Якщо ви хочете, щоб певне значення результату було передано назад, ви матимете для цього властивість у ViewModel.


Я згоден з цим - простота цінна. Я повинен думати про те, що станеться, коли наступного молодшого розробника наймуть взяти на себе цей проект. Я гадаю, що у нього буде набагато більше шансів отримати це право, як ви описуєте. Якщо ви не думаєте, що збираєтесь постійно підтримувати цей код? +1
Дін

2

Просто для додання величезної кількості відповідей, я хочу додати наступне. Якщо припустити, що у вашому ViewModel є ICommand, і ви хочете, щоб ця команда закрила його вікно (або будь-які інші дії з цього приводу), ви можете використовувати щось на зразок наступного.

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

Це не ідеально, і це може бути важко перевірити (так як важко знущатися / заглушувати статику), але він чистіший (IMHO), ніж інші рішення.

Ерік


Я дуже зрадів, коли побачив вашу просту відповідь! але це також не працює! Мені потрібно відкрити і закрити візуальну базу. Чи знаєте ви еквівалентність (windows [i] .DataContext == this) у VB?
Ехсан

Я отримав це нарешті! :) Дякую. Якщо windows (i) .DataContext - це я
Ехсан

Чи знаєте ви такий же простий спосіб відкриття вікна? Мені потрібно надсилати та отримувати деякі дані також у дитячому перегляді моделей та навпаки.
Ехсан

1

Я реалізував рішення Джо Уайта, але зіткнувся з проблемами з випадковими помилками " DialogResult можна встановити лише після створення вікна та відображення у вигляді діалогу ".

Я тримав ViewModel навколо, після того, як Перегляд був закритий, а іноді я пізніше відкривав новий Перегляд, використовуючи ту ж ВМ. Здається, що закриття нового перегляду до того, як старий вигляд був зібраний сміттям, призвів до того, що DialogResultChanged намагався встановити властивість DialogResult на закритому вікні, тим самим спровокувавши помилку.

Моє рішення було змінити DialogResultChanged, щоб перевірити властивість IsLoaded вікна :

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

Після внесення цієї зміни будь-які додатки до закритих діалогових вікон ігноруються.


Дякую вам сер. У мене була така ж проблема
DJ Burb

1

У кінцевому підсумку я змішував відповідь Джо Уайта і деякий код з відповіді Адама Міллса , оскільки мені потрібно було показати управління користувачем у вікні, створеному програмно. Тож DialogCloser не повинен знаходитись у вікні, це може бути на самому керуванні користувача

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

І DialogCloser знайде вікно керування користувачем, якщо воно не було прикріплено до самого вікна.

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

Поведінка - це найзручніший спосіб тут.

  • З одного боку, його можна прив’язати до заданої моделі перегляду (це може сигналізувати "закрити форму!")

  • З іншого боку, він має доступ до самої форми, тому може підписатись на необхідні конкретні форми подій, або показати діалог підтвердження чи будь-що інше.

Написання необхідної поведінки можна побачити нудно вперше. Однак відтепер ви можете повторно використовувати його в кожній необхідній формі точним фрагментом XAML. А якщо потрібно, ви можете витягнути її як окрему збірку, щоб вона могла бути включена у будь-який наступний проект, який ви хочете.


0

Чому б просто не передати вікно як параметр команди?

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

Я не думаю, що вдало обмежувати VM типом Window.
Шиммі Вайцхандлер

2
Я не думаю, що це гарна ідея обмежувати VM Windowтипом, який дещо не є "чистим" MVVM. Дивіться цю відповідь, де VM не обмежується Windowоб'єктом.
Шиммі Вайцхандлер

таким чином залежність ставиться на кнопку, яка, безумовно, не завжди може бути такою ситуацією. Також погана практика передачі типу інтерфейсу користувача ViewModel.
Kylo Ren

0

Ще одне рішення - створити властивість за допомогою INotifyPropertyChanged у моделі перегляду, як DialogResult, а потім у Code Behind напишіть це:

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

Найважливіший фрагмент - це _someViewModel_PropertyChanged. DialogResultPropertyNameможе бути деяким публічним рядком const в SomeViewModel.

Я використовую подібний трюк, щоб внести деякі зміни у елементи керування переглядачами у випадку, коли це важко зробити у ViewModel. OnPropertyChanged в ViewModel ви можете робити все, що завгодно, у View. ViewModel все ще є "тестовим одиницею", і деякі невеликі рядки коду позаду не мають ніякої різниці.


0

Я б пішов так:

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

Я прочитав усі відповіді, але мушу сказати, що більшість із них просто недостатньо хороші або навіть гірші.

Ви можете з цим красиво поводитися з класом DialogService, відповідальність за те, щоб показати діалогове вікно та повернути результат діалогу. Я створив зразок проекту, який демонструє його реалізацію та використання.

ось найважливіші частини:

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

Це не просто простіше? більш прямим, більш читабельним і не в останню чергу простіше налагодження, ніж EventAggregator чи інші подібні рішення?

як ви бачите, на моїх моделях перегляду я використав перший підхід ViewModel, описаний у моєму дописі тут: Найкраща практика для виклику View з ViewModel у WPF

Звичайно, в реальному світі DialogService.ShowDialogдля налаштування діалогового вікна повинно бути більше опцій, наприклад, кнопки та команди, які вони повинні виконувати. Існують різні способи, але це поза сферою :)


0

Хоча це не дає відповіді на питання, як це зробити через viewmodel, це показує, як це зробити за допомогою лише XAML + SDK суміші.

Я вирішив завантажити та використовувати два файли з SDK Blend, обидва ви можете отримати як пакет від Microsoft через NuGet. Файли:

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

Microsoft.Expression.Interactions.dll дає вам приємні можливості, такі як можливість встановити властивість або викликати метод у вашому переглядовому моделі чи іншій цілі, а також у ньому є інші віджети.

Деякі 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>

Зауважте, що якщо ви просто збираєтеся просту ОК / Скасувати поведінку, ви можете піти з / за допомогою властивостей IsDefault та IsCancel до тих пір, поки не буде показано вікно w / Window.ShowDialog ().
У мене особисто були проблеми з кнопкою, у якої властивість IsDefault було встановлено на істинне, але воно було приховано при завантаженні сторінки. Здавалося, вона не захоче грати добре після її показу, тому я просто встановлюю властивість Window.DialogResult, як показано вище, і це працює для мене.


0

Ось просте рішення про помилку (із вихідним кодом), воно працює для мене.

  1. Отримайте ваш ViewModel від INotifyPropertyChanged

  2. Створіть у ViewModel властивість, що спостерігається, CloseDialog

    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. Додайте обробник до перегляду для цієї зміни властивості

        _loginDialogViewModel = new LoginDialogViewModel();
        loginPanel.DataContext = _loginDialogViewModel;
        _loginDialogViewModel.PropertyChanged += OnPropertyChanged;
  4. Тепер ви майже готові. У випадку, якщо обробник робитьDialogResult = true

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

0

Створіть Dependency Propertyсвій View/ будь-який UserControl(або Windowви хочете закрити). Як і нижче:

 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
        }

І прив’яжіть його до власності вашого 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}"

Власність VeiwModel:

private bool closeWindow;

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

Тепер запустіть операцію закриття, змінивши CloseWindowзначення в ViewModel. :)


-2

Там, де вам потрібно закрити вікно, просто поставте це у перегляд:

та-да

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

ViewModel ні в якому разі не повинен містити UIElement , оскільки це може створити помилки
WiiMaxx

Що робити, якщо DataContext успадковується декількома вікнами?
Kylo Ren

та-да, це зовсім не MVVM.
Олександру Діку

-10
Application.Current.MainWindow.Close() 

Цього достатньо!


3
-1 Вірно лише, якщо вікно, яке ви хочете закрити, є головним вікном ... Дуже
ймовірне
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.