Guide/Tutorial по WPF/MVVM
От: Fortnum  
Дата: 07.06.11 07:57
Оценка:
Подсказажите известный компилируемый гайд/тьюториал по созданию более-менее сложного и работающего приложения для демонстрации WPF/MVVM?
Re: Guide/Tutorial по WPF/MVVM
От: BluntBlind  
Дата: 07.06.11 08:43
Оценка:
Здравствуйте, Fortnum, Вы писали:

F>Подсказажите известный компилируемый гайд/тьюториал по созданию более-менее сложного и работающего приложения для демонстрации WPF/MVVM?


http://msdn.microsoft.com/ru-ru/magazine/dd419663.aspx
Re[2]: Guide/Tutorial по WPF/MVVM
От: Fortnum  
Дата: 07.06.11 09:40
Оценка:
Здравствуйте, BluntBlind, Вы писали:

F>>Подсказажите известный компилируемый гайд/тьюториал по созданию более-менее сложного и работающего приложения для демонстрации WPF/MVVM?

BB>http://msdn.microsoft.com/ru-ru/magazine/dd419663.aspx

Лучший пример непонятности MVVM как фреймворка (системы) для написания своих приложений Неплохо поясняет суть WPF, но глубину MVVM практически не раскрывает.

Попробую покритиковать предметно. Создание главного окна и его модели происходит в методе App.OnStartup, там же происходит их вязка друг с другом. Вопрос. А с какого именно там? Что это за App.OnStartup такое вообще? С системной точки зрения в App.OnStartup должно происходить либо создание первого вида, либо создание первой модели вида. Все. Дальше работа должна вестись на этих уровнях и в их взаимодействии. Поэтому для начала надо весь код по созданию модели вида и ее вязки перенести в code-behind класса MainWindow.

Вторая проблема, которая сразу бросается в глаза в этом примере, если говорить о MVVM как о системном подходе — при закрытии приложения через CloseCommand, сигнал о необходимости закрытия приложения проходит с уровня View на уровень ViewModel, и возвращается обратно через событие RequestClose. Однако. При закрытии приложения через крестик на самом окне, слой ViewModel ничего о происходящем не знает. А почему? Если у модели вида главного окна имеется пара CloseCommand/RequestClose, логично было бы ожидать, что все закрытия приложения проходят через них, как шнурки в ботинке проходят через все дырочки.

Я понимаю, что в данном приложении для демонстрации WPF/MVVM это годно. Но при попытке сделать на том же MVVM что-нибудь серьезное, рано или поздно человек сталкивается с такой проблемой, когда непонятны системные принципы. Это лишь верхушка глобальной проблемы, когда приходится выбирать и самому изобретать эти принципы, что в итоге выливается в то, что каждый изобретает под себя и мы имеем в этой области то, что имеем — помойку. Кто-то обходит, кто-то пишет макаронный код в уровне View, чтобы обвязать ViewModel. Но нафига? Теряется ведь весь смысл MVVM. В общем, тема MVVM в данной статье и в данном примере совершенно не раскрыта, как и в его книге Advanced MVVM, которую круто откритиковал в том же духе Ward Bell. Отрывок оттуда на тему того же отсутствия системности:

What Belongs In Code-Behind?

Do we allow or prohibit code-behind? That’s a fist-fight topic. Josh looks for a Goldilocks solution, suggesting the amount of code-behind is just right if confined to “logic scoped to the view”.

Such guidance is far too loose in my opinion. It’s not effective guidance at all.

We choose presentation separation patterns in part because we want to minimize testing of the view. We should test any custom logic we write – no matter how it is scoped. The poverty of our testing tools means that, in practice, we will not test the code in the view. Therefore, any code in the code-behind is suspect.

I am open to some code in the code-behind; the “InitializeComponent” method is inescapable. I’ll accept a minimum of “switch board” code in the code behind; direct wiring to a ViewModel member is ok in small doses.

I draw the line at decision logic. I smell a rat when I see a conditional statement of any kind. That’s where bugs breed. Conditional logic is code we should be testing.

I much prefer this rule: “No conditional logic in the code-behind”.

That’s unambiguous. We don’t have to wonder what “scope of the view” means. If there’s an “if” or a “switch” statement, something is wrong. You can detect a violation with automation or visual inspection. It’s a rule that’s easy to follow and pretty easy to abide by … when you know how.

Re[3]: Guide/Tutorial по WPF/MVVM
От: Qbit86 Кипр
Дата: 07.06.11 10:07
Оценка:
Здравствуйте, Fortnum, Вы писали:

F>Поэтому для начала надо весь код по созданию модели вида и ее вязки перенести в code-behind класса MainWindow.


Лолшто? Зависимость вида от вью-модела должна внедряться, будь то вручную, или с помощью IoC-контейнера; вид не должен создавать вью-модел. Вязка вообще должна задаваться декларативно в замле.
Глаза у меня добрые, но рубашка — смирительная!
Re[4]: Guide/Tutorial по WPF/MVVM
От: Fortnum  
Дата: 07.06.11 10:18
Оценка:
Здравствуйте, Qbit86, Вы писали:

F>>Поэтому для начала надо весь код по созданию модели вида и ее вязки перенести в code-behind класса MainWindow.

Q>Лолшто? Зависимость вида от вью-модела должна внедряться, будь то вручную, или с помощью IoC-контейнера; вид не должен создавать вью-модел. Вязка вообще должна задаваться декларативно в замле.

Да пофиг, декларативно в хамле, в code-behind или еще каким способом — это детали. Главное, что создаваться ViewModel должен именно в уровне View. Или еще в каком-то третьем уровне, но тогда это уже будет не MVVM (есть такие способы создания и вязки, когда используется третий уровень типа Configuration или UseCase).

Главное еще, что я не понял вашего высказывания. Смотрите, вы говорите:
1. "вид не должен создавать вью-модел"
2. "Вязка вообще должна задаваться декларативно в замле"

Но хамл — это и есть View! Когда вы создаете ViewModel через хамл — это всего лишь иной способ создания, более или менее удобный или привычный. Пусть под хамлом там сидит какой-то сервис типа VMLocator, но в абсолютно любом случае при View-First вы сперва создаете вид, а затем этот вид создает свою модель. Через посредника или напрямую в code-behind — это никакой роли не играет.
Re[5]: Внедрение зависимостей
От: Qbit86 Кипр
Дата: 07.06.11 10:34
Оценка:
Здравствуйте, Fortnum, Вы писали:

F>Но хамл — это и есть View! Когда вы создаете ViewModel через хамл — это всего лишь иной способ создания, более или менее удобный или привычный.


В замл я не создаю экземпляр, а задаю привязки к контексту данных. Я могу это сделать ещё до написания собственно класса модели-представления.

F>...но в абсолютно любом случае при View-First вы сперва создаете вид, а затем этот вид создает свою модель.


Ни в коем случае, это противоречит принципу внедрения зависимостей.

F>Главное еще, что я не понял вашего высказывания.


Если обходиться без IoC-контейнеров, то создание (условно) может выглядеть примерно так:
private void Application_Startup(object sender, StartupEventArgs e)
{
    // Откуда-нибудь резолвим модель:
    ModelBase model = new Model(...);
    // Внедряем модель во вью-модел.
    ViewModelBase viewModel = new MainViewModel(model);
    // Внедряем вью-модел в представление.
    Window view = new MainWindow(viewModel); // В кторе выполняем this.DataContext = viewModel.
}
Глаза у меня добрые, но рубашка — смирительная!
Re[6]: Внедрение зависимостей
От: Fortnum  
Дата: 07.06.11 10:47
Оценка:
Здравствуйте, Qbit86, Вы писали:

Q>В замл я не создаю экземпляр, а задаю привязки к контексту данных. Я могу это сделать ещё до написания собственно класса модели-представления.


Можно поподробнее про "привязки к контексту данных". Не совсем понятно, что вы имеете в виду под контекстом данных.

Q>Ни в коем случае, это противоречит принципу внедрения зависимостей.


ОК. То есть вы используете какой-то IoC-фреймворк и в виде ссылаетесь не на класс модели вида, а на интерфейс модели вида, может быть еще на имя модели вида. Подбор конкретного класса по этим параметрам осуществляет IoC-фреймворк, поэтому вы можете скомпилировать свой вид не имея в распоряжении конкретной реализации интерфейса модели вида. Ну и что? Все равно, пусть даже через тот же IoC-фреймворк и посредством интерфейса модели вида, все равно модель вида создается видом. Я не вижу или не понимаю кардинальной разницы.

Q>Если обходиться без IoC-контейнеров, то создание (условно) может выглядеть примерно так:

Q>
Q>private void Application_Startup(object sender, StartupEventArgs e)
Q>{
Q>    // Откуда-нибудь резолвим модель:
Q>    ModelBase model = new Model(...);
Q>    // Внедряем модель во вью-модел.
Q>    ViewModelBase viewModel = new MainViewModel(model);
Q>    // Внедряем вью-модел в представление.
Q>    Window view = new MainWindow(viewModel); // В кторе выполняем this.DataContext = viewModel.
Q>}
Q>


Ну так это тоже самое, что у Josh Smith'а. Что вы показываете этим примером? Что во View-First приложении вы создаете модель вида в непонятно каком архитектурном слое? Как вы из MainWindow будете открывать ChildWindow, приведите, пожалуйста, набросок.
Re[7]: Внедрение зависимостей
От: Qbit86 Кипр
Дата: 07.06.11 11:17
Оценка:
Здравствуйте, Fortnum, Вы писали:

Q>>В замл я не создаю экземпляр, а задаю привязки к контексту данных. Я могу это сделать ещё до написания собственно класса модели-представления.

F>Можно поподробнее про "привязки к контексту данных".

<UserControl ...>
    <StackPanel>
        <TextBlock Text="{Binding Path=Message, Mode=OneWay}" />
    </StackPanel>
</UserControl>


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

F>Не совсем понятно, что вы имеете в виду под контекстом данных.


DataContext
Data Binding Overview

Q>>Ни в коем случае, это противоречит принципу внедрения зависимостей.

F>Ну и что? Все равно, пусть даже через тот же IoC-фреймворк и посредством интерфейса модели вида, все равно модель вида создается видом.

Да где же видом? Экземпляры триады создаются в «точке входа» приложения — в обработчике Application_Startup. Вид (здесь это класс MainWindow) создаётся после создания своей модели-представления, последняя передаётся виду в конструктор:
private void Application_Startup(object sender, StartupEventArgs e)
{
    ...
    Window view = new MainWindow(viewModel);
    view.Show();
}


Если модель-представления будет создаваться видом, то как же мы их сможем независимо тестировать или подменять?

F>Ну так это тоже самое, что у Josh Smith'а.


Ну, уязвимость его книги для критики не говорит о том, что всё им сказанное — ересь и должно быть предано анафеме. Он таки один из «отцов-основателей» как-никак.

F>Что вы показываете этим примером? Что во View-First приложении вы создаете модель вида в непонятно каком архитектурном слое?


Не в «непонятно каком архитектурном слое», а в точке настройки.

Есть ещё книжка «Building Enterprise Applications with Windows Presentation Foundation and the Model View ViewModel Pattern» by Raffaele Garofalo, можно её попробовать полистать.
Глаза у меня добрые, но рубашка — смирительная!
Re[8]: Внедрение зависимостей
От: Fortnum  
Дата: 07.06.11 11:34
Оценка:
Здравствуйте, Qbit86, Вы писали:

F>>Ну и что? Все равно, пусть даже через тот же IoC-фреймворк и посредством интерфейса модели вида, все равно модель вида создается видом.

Q>Да где же видом? Экземпляры триады создаются в «точке входа» приложения — в обработчике Application_Startup. Вид (здесь это класс MainWindow) создаётся после создания своей модели-представления, последняя передаётся виду в конструктор:
Q>
Q>private void Application_Startup(object sender, StartupEventArgs e)
Q>{
Q>    ...
Q>    Window view = new MainWindow(viewModel);
Q>    view.Show();
Q>}
Q>


Q>Если модель-представления будет создаваться видом, то как же мы их сможем независимо тестировать или подменять?


Что такое "точка настройки" ("точка входа")? Каким боком она влазит в MVVM? И в связи с этим, вы обошли мой ключевой вопрос: "Как вы из MainWindow будете открывать ChildWindow, приведите, пожалуйста, набросок". Или вы создаете все возможные модели вида и виды в точке входа и все?

Q>Ну, уязвимость его книги для критики не говорит о том, что всё им сказанное — ересь и должно быть предано анафеме. Он таки один из «отцов-основателей» как-никак.


Я не говорю, что все. Я говорю лишь то, что он не касается, старается не касаться, острых вопросов/проблем, которые в обязательном порядке возникнут при попытке следовать его методологии в чистом виде для разработки более-менее крупного приложения. Самый простой вопрос, который затронул Ward Bell — что же все-таки должно находиться в code-behind, им так и не отвечено. Точнее отвечено, но общей формулировкой "logic scoped to the view". А то, что модель вида — управляет видом, ничего? Вот из-за таких размытых формулировок помойка и творится. А так, Josh Smith несомненно крут.

PS. Сейчас я полотно вброшу, как я модифицировал его пример в соответствии со своими предпочтениями и пониманием, что такое MVVM.
Re[9]: Внедрение зависимостей
От: Qbit86 Кипр
Дата: 07.06.11 11:46
Оценка:
Здравствуйте, Fortnum, Вы писали:

F>Как вы из MainWindow будете открывать ChildWindow, приведите, пожалуйста, набросок.


Например, в команде, используя внедрённую стратегию создания, инстанцирую вью-модел дочернего окна и передаю его в представление дочернего окна.

F>Или вы создаете все возможные модели вида и виды в точке входа и все?


Не создаю, но задаю стратегии их создания. Сами экземпляры создаются по необходимости.

F>Ну так это тоже самое, что у Josh Smith'а


Книжку Джоша Смита не читал, но по приведённой тобой ссылке на критику сказано, что у Джоша Смита в примерах в книге как раз наоборот. Экземпляры у него инстанцируются прямо в замл-разметке (<DataContext><myNamespace:ViewModel /></DataContext>), т.е. из вью. Разве я это предлагаю?

F>А то, что модель вида — управляет видом, ничего?


Управляет косвенно, через механизм привязок. Как бы, она для того и придумана.
Глаза у меня добрые, но рубашка — смирительная!
Re: Guide/Tutorial по WPF/MVVM
От: Fortnum  
Дата: 07.06.11 17:46
Оценка:
Здравствуйте, BluntBlind, Вы писали:

F>>Подсказажите известный компилируемый гайд/тьюториал по созданию более-менее сложного и работающего приложения для демонстрации WPF/MVVM?

BB>http://msdn.microsoft.com/ru-ru/magazine/dd419663.aspx

Попробую описать, как я бы модифицировал этот пример. Посмотрим, упрусь ли я во что-нибудь нерешаемое или нелогичное.

1. Пусть мы пойдем по пути View-First. Очищаю App.OnStartup от логики создания модели вида и вязки ее с первым (MainWindow) видом. Переношу эту логику в конструктор MainWindow сразу после InitializeComponent. Теперь App.OnStartup у меня выглядит так:

protected override void OnStartup(StartupEventArgs e)
{
    base.OnStartup(e);

    var window = new MainWindow();

    window.Show();
}


А конструктор Main Window так:

public MainWindow()
{
    InitializeComponent();

    string path = "Data/customers.xml";
    var viewModel = new MainWindowViewModel(path);

    EventHandler handler = null;
    handler = delegate
    {
        viewModel.RequestClose -= handler;
        Close();
    };
    viewModel.RequestClose += handler;

    DataContext = viewModel;
}


2. Если модель MainWindow реализует пару CloseCommand/RequestClose, то надо, чтобы все закрытия приложения проходили через уровень ViewModel. Ну хотя бы для того, чтобы во время создания нового клиента ViewModel мог прервать процесс закрытия приложения, чтобы сохранить введенные данные. Самое простейшее, конечно, написать у MainWindow в code-behind обработчик события. Создал сперва такой обработчик Window_Closing:

void Window_Closing(object sender, CancelEventArgs e)
{
    e.Cancel = true;

    Dispatcher.BeginInvoke((Action)(() =>
    {
        var viewModel = (MainWindowViewModel)DataContext;

        viewModel.CloseCommand.Execute(null);
    }));
}


Но здесь проблема в том, что теперь окно вообще не закрывается, ни по крестику в углу, ни по команде из меню File->Exit. Немного модифицировал обработчик события RequestClose от MainWindowViewModel, ввел внутренний для MainWindow флаг _requestCloseFromViewModel, теперь все работает как надо:

public MainWindow()
{
    InitializeComponent();

    string path = "Data/customers.xml";
    var viewModel = new MainWindowViewModel(path);

    EventHandler handler = null;
    handler = delegate
    {
        _requestCloseFromViewModel = true;
        viewModel.RequestClose -= handler;
        Close();
    };
    viewModel.RequestClose += handler;

    DataContext = viewModel;
}

bool _requestCloseFromViewModel;

void Window_Closing(object sender, CancelEventArgs e)
{
    e.Cancel = !_requestCloseFromViewModel;

    if(!_requestCloseFromViewModel)
    {
        Dispatcher.BeginInvoke((Action)(() =>
        {
            var viewModel = (MainWindowViewModel)DataContext;

            viewModel.CloseCommand.Execute(null);
        }));
    }
}


Но это разве, друзья, не изврат? Можно, конечно, запаковать это поведение в AttachedBehavior, тогда все будет работать одной строкой в хамле. Но, по-моему, хрен редьки не слаще — получится в итоге тот же изврат только в профиль.

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

3.1. Во-первых, наследуем от базового класса System.Windows.Window, создаем свой класс WindowView. Во-вторых, заводим в этом WindowView свое DependencyProperty под названием CanClose, которое бы было по умолчанию true. Ну, и в-третьих, в хамле просто биндимся при необходимости к соответствующему свойству модели вида. Итак, вот мой WindowView с CanCloseProperty:

public class WindowView : Window
{
    public WindowView()
    {
    }

    public bool CanClose
    {
        get { return (bool)GetValue(CanCloseProperty); }
        set { SetValue(CanCloseProperty, value); }
    }
        
    public static readonly DependencyProperty CanCloseProperty =
        DependencyProperty.Register("CanClose", typeof(bool), typeof(WindowView),
        new FrameworkPropertyMetadata(true, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
}


Теперь можно MainWindow унаследовать от WindowView, а в хамле прибиндить CanClose к соответствующему свойству модели вида MainWindowViewModel.

Примечание: класс WindowView я вынес в отдельную библиотеку ServEn.MVVm в пространство имен ServEn.MVVm.Views.

Вот новый MainWindow:

<mvvmViews:WindowView
    x:Class="DemoApp.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:vm="clr-namespace:DemoApp.ViewModel"
    xmlns:mvvmViews="clr-namespace:ServEn.MVVm.Views;assembly=ServEn.MVVm"
    CanClose="{Binding CanClose}">
    FontSize="13" 
...
</mvvmViews:WindowView>


public partial class MainWindow : WindowView
{
    public MainWindow()
    {
        InitializeComponent();

        string path = "Data/customers.xml";
        var viewModel = new MainWindowViewModel(path);

        EventHandler handler = null;
        handler = delegate
        {
            viewModel.RequestClose -= handler;
            Close();
        };
        viewModel.RequestClose += handler;

        DataContext = viewModel;
    }
}


Обработчик события Closing пропал. Вместо него появился биндинг к модели вида CanClose, а code-behind вернулся к изначальному состоянию.

3.2. Теперь надо модель вида MainWindowViewModel привести к соответствию с протоколом, который мы установили между видом и его моделью — надо во ViewModel определить публичное свойство CanClose, которое бы синхронно с остальным касающимся закрытия поведением модели вида определяло бы, можно закрывать вид или нельзя.

Так как MainWindowViewModel получает поведение касающееся закрытия от базового WorkspaceViewModel, я решил это публичное свойство вводить именно там:

public abstract class WorkspaceViewModel : ViewModelBase
{
    ...

    bool _canClose;

    public bool CanClose
    {
        get
        {
            return _canClose;
        }
        set
        {
            if (_canClose != value)
            {
                _canClose = value;

                OnPropertyChanged("CanClose");
            }
        }
    }
}


Так как CanClose имеет по-умолчанию значение false, то на данном этапе окно нельзя закрыть, ни через крестик, ни через меню File->Exit. Синхронизировав его с обработчиком CloseCommand.Execute, заставим уважающий свою модель вид в любом случае закрывать себя через команду:

public abstract class WorkspaceViewModel : ViewModelBase
{
    ...

    public ICommand CloseCommand
    {
        get
        {
            if (_closeCommand == null)
                _closeCommand = new RelayCommand(param => 
                    {
                        if (!CanClose)
                        {
                            CanClose = true;
                            this.OnRequestClose();
                        }
                    });

            return _closeCommand;
        }
    }

    ...
}


Может показаться, что я выстрелил себе в ногу, потому что теперь для закрытия окна через крестик — сейчас через крестик оно банально не закрывается — мне надо снова вводить обработчик события Closing. Но это не так. Обработчик я, конечно же, введу, но это временно:

void WindowView_Closing(object sender, CancelEventArgs e)
{
    Dispatcher.BeginInvoke((Action)(() =>
    {
        var viewModel = (MainWindowViewModel)DataContext;

        viewModel.CloseCommand.Execute(null);
    }));
}


По сути, я просто передаю (проталкиваю) обработку WM_CLOSE на уровень ниже — во ViewModel.

3.3. Настало время избавиться от code-behind полностью. Для этого я использую триггеры. Только не те триггеры, что в шаблонах WPF, а другие. Идея не моя, но готового необходимого мне решения, я не нашел. Поэтому я решил просто переписать, что есть, с нуля под себя. Идея этих триггеров — "проталкивание" сущностей между уровнями View и ViewModel — как в одну сторону, так и в другую. Без кода. Через определения в хамле. "Украл" я эту идею у библиотеки Silverlight.FX, но реализована она во многих других библиотеках, в том числе и в Microsoft'овском Expression Blend SDK. О том, чего и там и там мне не хватило, скажу ниже.

Само "проталкивание" заключается в паре: "что-то происходит" — "что-то делается", т.е. в паре Trigger/TriggerAction.

В основе этих триггеров лежит замечательное свойство Freezable-объектов наследовать WPF-контекст элемента WPF-дерева. В результате, если Freezable-объект прикрепить в качестве свойства к элементу WPF-дерева, любое из DependencyProperty этого Freezable-объекта можно прибиндить к живому DataContext'у этого элемента. Если в качестве AttachedProperty прикрепить к элементу WPF-дерева любой другой объект (не-Freezable), то в его DependencyProperty-ях использовать Binding уже нельзя.

Дело будет развиваться в пространстве имен, по традиции для таких триггеров, ServEn.MVVm.Interactivity.

3.3.1. Например, мы хотим написать триггер (элемент хамл-разметки), который, когда меняется любое свойство в модели вида, выдавал бы нам MsgBox со значением этого свойства. Это триггер, реагирует на изменение свойства, поэтому назовем его PropertyTrigger:

public class PropertyTrigger : Freezable
{
    public PropertyTrigger()
    {
    }

    protected override Freezable CreateInstanceCore()
    {
        return new PropertyTrigger();
    }

    #region BindingProperty

    public static readonly DependencyProperty BindingProperty =
        DependencyProperty.RegisterAttached("Binding", typeof(object), typeof(PropertyTrigger),
        new FrameworkPropertyMetadata(OnBindingChanged));

    public static object GetBinding(DependencyObject d)
    {
        return d.GetValue(BindingProperty);
    }
    public static void SetBinding(DependencyObject d, object value)
    {
        d.SetValue(BindingProperty, value);
    }

    static void OnBindingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        MessageBox.Show("Binding Value = " + e.NewValue);
    }

    #endregion
}


Привяжем его к нашему WorkspaceViewModel.CanClose:

<mvvmViews:WindowView
    xmlns:mvvmInteractivity="clr-namespace:ServEn.MVVm.Interactivity;assembly=ServEn.MVVm"
    mvvmInteractivity:PropertyTrigger.Binding="{Binding CanClose}" ...


3.3.2. Аналогичным образом мы можем создать триггер, который бы реагировал на какое-нибудь указанное в хамл событие от модели вида. Например, на тот же RequestClose. Для этого создадим EventTrigger:

public class EventTrigger : Freezable
{
    public EventTrigger()
    {
    }

    protected override Freezable CreateInstanceCore()
    {
        return new EventTrigger();
    }

    #region EventNameProperty

    public static readonly DependencyProperty EventNameProperty =
        DependencyProperty.RegisterAttached("EventName", typeof(string), typeof(EventTrigger),
        new FrameworkPropertyMetadata(null, OnEventNameChanged));

    public static string GetEventName(DependencyObject d)
    {
        return (string)d.GetValue(EventNameProperty);
    }
    public static void SetEventName(DependencyObject d, string value)
    {
        d.SetValue(EventNameProperty, value);
    }

    static void OnEventNameChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        TryUnsubscribe(d, GetEventSource(d), e.OldValue as string);
        TrySubscribe(d, GetEventSource(d), e.NewValue as string);
    }

    #endregion

    #region EventSourceProperty

    public static readonly DependencyProperty EventSourceProperty =
        DependencyProperty.RegisterAttached("EventSource", typeof(object), typeof(EventTrigger),
        new FrameworkPropertyMetadata(null, OnEventSourceChanged));

    public static object GetEventSource(DependencyObject d)
    {
        return d.GetValue(EventSourceProperty);
    }
    public static void SetEventSource(DependencyObject d, object value)
    {
        d.SetValue(EventSourceProperty, value);
    }

    static void OnEventSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        TryUnsubscribe(d, e.OldValue, GetEventName(d));
        TrySubscribe(d, e.NewValue, GetEventName(d));
    }
                
    #endregion

    #region EventHandlerProperty

    static readonly DependencyPropertyKey EventHandlerPropertyKey =
        DependencyProperty.RegisterAttachedReadOnly("EventHandler", typeof(Delegate), typeof(EventTrigger),
        new UIPropertyMetadata(null));

    static Delegate GetEventHandler(DependencyObject d)
    {
        return (Delegate)d.GetValue(EventHandlerPropertyKey.DependencyProperty);
    }
    static void SetEventHandler(DependencyObject d, Delegate value)
    {
        d.SetValue(EventHandlerPropertyKey, value);
    }
        
    #endregion

    static void TrySubscribe(DependencyObject d, object source, string eventName)
    {
        if (source != null && !string.IsNullOrEmpty(eventName))
        {
            var sourceType = source.GetType();

            var eventInfo = sourceType.GetEvent(eventName,
                BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy);

            if (eventInfo != null)
            {
                var eventHandlerMethod = typeof(EventTrigger).GetMethod("OnEvent",
                    BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.FlattenHierarchy);

                var eventHandler = Delegate.CreateDelegate(eventInfo.EventHandlerType, eventHandlerMethod, false);

                if (eventHandler != null)
                {
                    eventInfo.AddEventHandler(source, eventHandler);

                    SetEventHandler(d, eventHandler);
                }
            }
        }
    }

    static void TryUnsubscribe(DependencyObject d, object source, string eventName)
    {
        var eventHandler = GetEventHandler(d);

        if (eventHandler != null && source != null && !string.IsNullOrEmpty(eventName))
        {
            var sourceType = source.GetType();

            var eventInfo = sourceType.GetEvent(eventName,
                BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy);

            eventInfo.RemoveEventHandler(source, eventHandler);

            SetEventHandler(d, null);
        }
    }
        
    static void OnEvent(object sender, EventArgs e)
    {
        MessageBox.Show("Event happened!");
    }
}


<mvvmViews:WindowView
    xmlns:mvvmInteractivity="clr-namespace:ServEn.MVVm.Interactivity;assembly=ServEn.MVVm"
    mvvmInteractivity:EventTrigger.EventSource="{Binding}"
    mvvmInteractivity:EventTrigger.EventName="RequestClose" ...


3.3.3. Небольшие изменения в хамле MainWindow и мы реагируем на событие Closed:

<mvvmViews:WindowView
    x:Name="_window" x:FieldModifier="private"
    xmlns:mvvmInteractivity="clr-namespace:ServEn.MVVm.Interactivity;assembly=ServEn.MVVm"
    mvvmInteractivity:EventTrigger.EventSource="{Binding ElementName=_window}"
    mvvmInteractivity:EventTrigger.EventName="Closed" ...


3.3.4. Чтобы убрать имеющийся изврат в code-behind MainWindow, надо сделать две вещи:
1. в ответ на событие Window.Closing, сообщать модели вида, что мы хотим закрыться (как вариант, дергать команду)
2. следить за событием MainWindowViewModel.RequestClose и в случае данного события вызывать метод MainWindow.Close

Реагировать на события мы научились достаточно гибко (Trigger), теперь надо перейти к тому, чтобы научиться достаточно гибко задавать, что в качестве реакции, собственно, делать (TriggerAction). А делать в ответ на триггер нам надо две вещи: дергать команду и вызывать метод. Чтобы не дергать команду, создадим в WorkspaceViewModel публичный метод Close, и будем его вызывать из CloseCommand.Execute:

public ICommand CloseCommand
{
    get
    {
        if (_closeCommand == null)
            _closeCommand = new RelayCommand(param => { Close(); });

        return _closeCommand;
    }
}

public void Close()
{
    if (!CanClose)
    {
        CanClose = true;
        this.OnRequestClose();
    }
}


Теперь нам надо научиться в качестве реакции на триггер делать одну вещь: вызывать публичный метод.

Итак, знакомьтесь, InvokeMethodAction:



3.3.5. Теперь небольшие изменения в EventTrigger, чтобы можно было в хамле сопоставить ему экземпляр InvokeMethodAction. Изменения небольшие, но еще не окончательные (проблемное место помечено комментарием "???"):

public class EventTrigger : Freezable
{
    ...
        
    #region InvokeMethodActionProperty

    public static readonly DependencyProperty InvokeMethodActionProperty =
        DependencyProperty.RegisterAttached("InvokeMethodAction", typeof(InvokeMethodAction), typeof(EventTrigger),
        new UIPropertyMetadata(null));

    public static InvokeMethodAction GetInvokeMethodAction(DependencyObject d)
    {
        return (InvokeMethodAction)d.GetValue(InvokeMethodActionProperty);
    }
    public static void SetInvokeMethodAction(DependencyObject d, InvokeMethodAction value)
    {
        d.SetValue(InvokeMethodActionProperty, value);
    }

    #endregion

    static void OnEvent(object sender, EventArgs e)
    {
        MessageBox.Show("Event happened!"); // ???
    }
}


Теперь можно в хамле создать экземпляр InvokeMethodAction и присвоить его Attached-свойству EventTrigger.InvokeMethodAction:

<mvvmViews:WindowView
    ...
    xmlns:mvvmInteractivity="clr-namespace:ServEn.MVVm.Interactivity;assembly=ServEn.MVVm"
    mvvmInteractivity:EventTrigger.EventName="RequestClose"
    mvvmInteractivity:EventTrigger.EventSource="{Binding}">

    <mvvmInteractivity:EventTrigger.InvokeMethodAction>
        <mvvmInteractivity:InvokeMethodAction Target="{Binding ElementName=_window}" MethodName="Close"/>
    </mvvmInteractivity:EventTrigger.InvokeMethodAction>

    ...

</mvvmViews:WindowView>


Теперь, при событии RequestClose в модели вида, будет вызван метод Close вида. Однако, оно сейчас не работает — вернемся к проблемному месту, помеченному выше комментарием "???". Дело в том, что статический метод OnEvent получит в свое распоряжение ссылку на модель вида и аргументы события RequestClose (EventArgs). Из тела этого статического метода мы никак не сможем добраться до значения Attached-свойства EventTrigger.InvokeMethodAction, определенного на элементе WindowView. Решить эту проблему очень просто — до этого мы хранили в скрытом Attached-свойстве EventHandlerProperty делегат для вызова при событии — теперь надо хранить там не делегат, а снаряженный всей необходимой информацией объект, а вызывать метод этого объекта.

Для этого, во-первых, определим вложенные в EventTrigger класс этого объекта, который мы собираемся снаряжать:

public class EventTrigger : Freezable
{
    ...
        
    class InnerEventTarget
    {
        public Delegate EventHandler;

        public void OnEvent(object sender, EventArgs e)
        {
            MessageBox.Show("Event happened!");
        }

        public DependencyObject AssociatedObject;
    }
}


Во-вторых, изменим тип и наименование скрытого Attached-свойства EventHandlerProperty на InnerEventTargetProperty:

public class EventTrigger : Freezable
{
    ...
        
    #region InnerEventTargetProperty

    static readonly DependencyPropertyKey InnerEventTargetPropertyKey =
        DependencyProperty.RegisterAttachedReadOnly("InnerEventTarget", typeof(InnerEventTarget), typeof(EventTrigger),
        new UIPropertyMetadata(null));

    static InnerEventTarget GetInnerEventTargetProperty(DependencyObject d)
    {
        return (InnerEventTarget)d.GetValue(InnerEventTargetPropertyKey.DependencyProperty);
    }
    static void SetInnerEventTargetProperty(DependencyObject d, InnerEventTarget value)
    {
        d.SetValue(InnerEventTargetPropertyKey, value);
    }
        
    #endregion

    ...
}


В-третьих, заменим в методах TrySubscribe/TryUnsubscribe операции с делегатом на операции со снаряжаемым объектом:

public class EventTrigger : Freezable
{
    ...
        
    static void TrySubscribe(DependencyObject d, object source, string eventName)
    {
        if (source != null && !string.IsNullOrEmpty(eventName))
        {
            var sourceType = source.GetType();

            var eventInfo = sourceType.GetEvent(eventName,
                BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy);

            if (eventInfo != null)
            {
                var innerEventTarget = new InnerEventTarget
                {
                    AssociatedObject = d
                };

                var eventHandlerMethod = typeof(InnerEventTarget).GetMethod("OnEvent",
                    BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy);

                var eventHandler = Delegate.CreateDelegate(eventInfo.EventHandlerType, innerEventTarget, eventHandlerMethod, false);

                if (eventHandler != null)
                {
                    innerEventTarget.EventHandler = eventHandler;

                    eventInfo.AddEventHandler(source, eventHandler);

                    SetInnerEventTarget(d, innerEventTarget);
                }
            }
        }
    }

    static void TryUnsubscribe(DependencyObject d, object source, string eventName)
    {
        var innerEventTarget = GetInnerEventTarget(d);

        if (innerEventTarget != null && source != null && !string.IsNullOrEmpty(eventName))
        {
            var sourceType = source.GetType();

            var eventInfo = sourceType.GetEvent(eventName,
                BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy);

            eventInfo.RemoveEventHandler(source, innerEventTarget.EventHandler);

            SetInnerEventTarget(d, null);
        }
    }

    ...
}


Все. Теперь из code-behind MainWindow можно убрать операцию подписки на RequestClose. MainWindow теперь выглядит так:

public partial class MainWindow : WindowView
{
    public MainWindow()
    {
        InitializeComponent();

        string path = "Data/customers.xml";
        var viewModel = new MainWindowViewModel(path);

        DataContext = viewModel;
    }

    void WindowView_Closing(object sender, CancelEventArgs e)
    {
        Dispatcher.BeginInvoke((Action)(() =>
        {
            var viewModel = (MainWindowViewModel)DataContext;

            viewModel.CloseCommand.Execute(null);    
        }));
    }
}


При этом закрытие окна через пункт меню File->Exit прекрасно работает! Клик по данному пункту меню дергает команду CloseCommand. Команда в модели вида присваивает CanClose = true и дергает событие RequestClose, остальное делает пара Trigger/TriggerAction — у вида (у MainWindow) в ответ на событие модели RequestClose дергается триггер EventTrigger, который в свою очередь дергает метод Close самого этого вида. Все это прописывается и настраивается в хамл.

3.3.6. Теперь избавимся от обработчика события Closing в code-behind у MainWindow. Для этого нам нужно определить на MainWindow еще один триггер, который будет слушать событие Window.Closing и дергать метод MainWindowViewModel.Close. Но тут проблема. Пока мы не можем определить два и более триггеров на одном элементе WPF-дерева — нельзя на одном элементе определить два одинаковых Attached-свойства с разными значениями. Для этого придется создать у триггера еще одно Attached-свойство, которое будет представлять собой коллекцию триггеров. Данную операцию будем производить на необходимом нам EventTrigger'е. В игру вступает FreezableCollection<T> и его наследник TriggerCollection:

public class TriggerCollection : FreezableCollection<EventTrigger>
{
    public TriggerCollection()
    {
    }

    protected override Freezable CreateInstanceCore()
    {
        return new TriggerCollection();
    }
}


У EventTrigger определяем новое Attached-свойство — коллекцию триггеров:

public class EventTrigger : Freezable
{
    ...
        
    #region Triggers

    public static readonly DependencyProperty TriggersProperty =
        DependencyProperty.RegisterAttached("Triggers", typeof(TriggerCollection), typeof(EventTrigger),
        new UIPropertyMetadata(null));

    public static TriggerCollection GetTriggers(DependencyObject d)
    {
        var triggers = (TriggerCollection)d.GetValue(TriggersProperty);

        if (triggers == null)
        {
            triggers = new TriggerCollection();

            SetTriggers(d, triggers);
        }

        return triggers;
    }

    public static void SetTriggers(DependencyObject d, TriggerCollection triggers)
    {
        d.SetValue(TriggersProperty, triggers);
    }

    #endregion
}


И, наконец, переписываем хамл MainWindow:

<mvvmViews:WindowView
    ...
    x:Name="_window" x:FieldModifier="private"
    xmlns:mvvmInteractivity="clr-namespace:ServEn.MVVm.Interactivity;assembly=ServEn.MVVm">
    
    <mvvmInteractivity:EventTrigger.Triggers>
        <mvvmInteractivity:TriggerCollection>
            <mvvmInteractivity:EventTrigger EventName="RequestClose" EventSource="{Binding}">
                <mvvmInteractivity:EventTrigger.InvokeMethodAction>
                    <mvvmInteractivity:InvokeMethodAction Target="{Binding ElementName=_window}" MethodName="Close"/>
                </mvvmInteractivity:EventTrigger.InvokeMethodAction>
            </mvvmInteractivity:EventTrigger>
        </mvvmInteractivity:TriggerCollection>
    </mvvmInteractivity:EventTrigger.Triggers>

    ...

</mvvmViews:WindowView>


Все работает, также как и до введения TriggerCollection & EventTrigger.TriggersProperty.

Примечание: Нагромождение тэгов можно убрать в какой-нибудь скриптовый язык по примеру того, как это сделано в Caliburn.

Теперь для дерганья метода MainWindowViewModel.Close в ответ на событие MainWindow.Closing надо добавить в MainWindow второй EventTrigger и второй TriggerAction:

<mvvmViews:WindowView
    ...
    x:Name="_window" x:FieldModifier="private"
    xmlns:mvvmInteractivity="clr-namespace:ServEn.MVVm.Interactivity;assembly=ServEn.MVVm">
    
    <mvvmInteractivity:EventTrigger.Triggers>
        <mvvmInteractivity:TriggerCollection>
            <mvvmInteractivity:EventTrigger EventName="RequestClose" EventSource="{Binding}">
                <mvvmInteractivity:EventTrigger.InvokeMethodAction>
                    <mvvmInteractivity:InvokeMethodAction Target="{Binding ElementName=_window}" MethodName="Close"/>
                </mvvmInteractivity:EventTrigger.InvokeMethodAction>
            </mvvmInteractivity:EventTrigger>
            <mvvmInteractivity:EventTrigger EventName="Closing" EventSource="{Binding ElementName=_window}">
                <mvvmInteractivity:EventTrigger.InvokeMethodAction>
                    <mvvmInteractivity:InvokeMethodAction Target="{Binding}" MethodName="Close"/>
                </mvvmInteractivity:EventTrigger.InvokeMethodAction>
            </mvvmInteractivity:EventTrigger>
        </mvvmInteractivity:TriggerCollection>
    </mvvmInteractivity:EventTrigger.Triggers>

    ...

</mvvmViews:WindowView>


А code-behind MainWindow становится таким:

public partial class MainWindow : WindowView
{
    public MainWindow()
    {
        InitializeComponent();

        string path = "Data/customers.xml";
        var viewModel = new MainWindowViewModel(path);

        DataContext = viewModel;
    }
}


Имеется, правда, сейчас одна проблема: если попытаться закрыть окно крестиком, то получим известный Exception "Cannot set Visibility to Visible or call Show, ShowDialog, Close, or WindowInteropHelper.EnsureHandle while a Window is closing". Возникает он, потому что мы вызываем Window.Close из обработчика события Window.Closing. Раньше, в code-behind, MainWindowViewModel.CloseCommand (что аналогично вызову публичного метода MainWindowViewModel.Close) дергалась в отдельным сообщением в очереди Dispatcher'а (Dispatcher.BeginInvoke). Чтобы повторить это в нашей ситуации, надо в InvokeMethodAction добавить параметр DispatcherAsync=true/false, со значением false по-умолчанию, немного откорректировать содержимое метода InvokeMethodAction.InvokeAction, ни у, наконец, добавить DispatcherAsync=true в соответствующий экземпляр InvokeMethodAction'а в хамле MainWindow:

public class InvokeMethodAction : Freezable
{
    ...

    public void InvokeAction(object state)
    {
        if (DispatcherAsync)
        {
            Dispatcher.BeginInvoke((Action)(() =>
            {
                InnerInvokeAction(state);
            }));
        }
        else
        {
            InnerInvokeAction(state);
        }
            
    }

    void InnerInvokeAction(object state)
    {
        Target.GetType().InvokeMember(MethodName,
            BindingFlags.Public | BindingFlags.Instance | BindingFlags.InvokeMethod,
            null, Target, null);
    }

    ...

    #region DispatcherAsyncProperty

    public static readonly DependencyProperty DispatcherAsyncProperty =
        DependencyProperty.Register("DispatcherAsync", typeof(bool), typeof(InvokeMethodAction),
        new UIPropertyMetadata(false));

    public bool DispatcherAsync
    {
        get { return (bool)GetValue(DispatcherAsyncProperty); }
        set { SetValue(DispatcherAsyncProperty, value); }
    }

    #endregion

    ...
}


<mvvmViews:WindowView
    ...
            <mvvmInteractivity:EventTrigger EventName="Closing" EventSource="{Binding ElementName=_window}">
                <mvvmInteractivity:EventTrigger.InvokeMethodAction>
                    <mvvmInteractivity:InvokeMethodAction Target="{Binding}" MethodName="Close" DispatcherAsync="True"/>
    ...


3.4. Теперь, когда с триггерами немного разобрались, вернемся к биндингу MainWindow.CanClose к MainWindowView.CanClose. Вспомнис, что CanClose у вида у нас определяется на уровне предка MainWindow — класса WindowView, который, по сути, является lookless-контролом. Очень хотелось бы не прописывать в каждом наследнике WindowView этот биндинг, т.к. их может быть много. Хотелось бы воспользоваться стилями и шаблонами, и наследовать уже готовый привязанный к предполагаемой модели вида View.

Для этого воспользуемся Generic.xaml — в статическом конструкторе WindowView пропишем:

public class WindowView : Window
{
    static WindowView()
    {
        DefaultStyleKeyProperty.OverrideMetadata(
            typeof(WindowView),
            new FrameworkPropertyMetadata(typeof(WindowView)));
    }

    ...


А в самом проекте библиотеки ServEn.MVVm создадим папочку Themes и в ней Dictionary WPF-ресурсов — файл Generic.xaml (не забываем про атрибут ThemeInfo):

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:mvvmInteractivity="clr-namespace:ServEn.MVVm.Interactivity"
    xmlns:mvvmViews="clr-namespace:ServEn.MVVm.Views">

    <Style TargetType="{x:Type mvvmViews:WindowView}" BasedOn="{StaticResource {x:Type Window}}">
        <Setter Property="CanClose" Value="{Binding CanClose}"/>
    </Style>

</ResourceDictionary>


Убираем в MainWindow.xaml CanClose={Binding CanClose} — теперь это будет браться из этого стиля.

3.5. Но было бы здорово еще внести и те триггеры в стиль WindowView. Тогда появляется возможность создавать базовые ViewModel и базовые View, через стили уже заточенные под конкретный ViewModel. Итак, попробуем убрать триггеры из MainWindow.xaml и перенести их в стиль Generic.xaml:

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:mvvmInteractivity="clr-namespace:ServEn.MVVm.Interactivity"
    xmlns:mvvmViews="clr-namespace:ServEn.MVVm.Views">

    <Style TargetType="{x:Type mvvmViews:WindowView}" BasedOn="{StaticResource {x:Type Window}}">
        <Setter Property="CanClose" Value="{Binding CanClose}"/>
        <Setter Property="mvvmInteractivity:EventTrigger.Triggers">
            <Setter.Value>
                <mvvmInteractivity:TriggerCollection>
                    <mvvmInteractivity:EventTrigger EventName="RequestClose" EventSource="{Binding}">
                        <mvvmInteractivity:EventTrigger.InvokeMethodAction>
                            <mvvmInteractivity:InvokeMethodAction Target="{Binding RelativeSource={RelativeSource Mode=FindAncestor,
                                AncestorType={x:Type mvvmViews:WindowView}}}" MethodName="Close"/>
                        </mvvmInteractivity:EventTrigger.InvokeMethodAction>
                    </mvvmInteractivity:EventTrigger>
                    <mvvmInteractivity:EventTrigger EventName="Closing" EventSource="{Binding RelativeSource={RelativeSource Mode=FindAncestor,
                                AncestorType={x:Type mvvmViews:WindowView}}}">
                        <mvvmInteractivity:EventTrigger.InvokeMethodAction>
                            <mvvmInteractivity:InvokeMethodAction Target="{Binding}" MethodName="Close" DispatcherAsync="True"/>
                        </mvvmInteractivity:EventTrigger.InvokeMethodAction>
                    </mvvmInteractivity:EventTrigger>
                </mvvmInteractivity:TriggerCollection>
            </Setter.Value>
        </Setter>
    </Style>

</ResourceDictionary>


Пробуем. Ура! Все работает! Но, как я сказал выше, если вы попытаетесь воспользоваться в стилях (и далее в шаблонах) триггерами из стандартных библиотек вроде Microsoft Expression Blend SDK 4, то получится облом. Проблема в том, как WPF применяет стили к своим элементам. Объекты стиля клонируются, а Freezable клонируется в двух вариантах — либо глубоком, либо простом — в зависимости от того, используется ли Binding в значениях его свойств. В результате при использовании подобным образом стандартных библиотек, получаются пустые коллекции TriggerAction'ов. В нашем же варианте мы используем FreezableCollection одного уровня — у нас одному Trigger'у может соответствовать только один TriggerAction. На самом деле надо, чтобы одному триггеру могла соответствовать коллекция TriggerAction'ов. Вот тут и возникают проблемы. Но я вроде бы нашел способ решения.

Есть еще одна очевидная недоработка в последнем вышеприведенном хамле. Для того, чтобы прибиндится к WindowView из глубин триггеров, приходится прибегать к Binding RelativeSource FindAncestor. Это не очень хорошо — весьма некомпактно и, возможно, где-то не очень хорошо скажется. выход — доработать Trigger/TriggerAction. В стандартных фреймворках это решено путем свойства AssociatedObject, которое спускается вниз по дереву триггеров, когда attach'ится верхнее Attached-свойство.

Вывод. Лень писать уже Кратко — все задуманное получилось, все готово для чистого MVVM. Перспектива полного разделения вида и его модели. Может быть кто-то скажет, что я написал здесь про велосипед. Ну, буду весьма благодарен, если пришлете мне ссылки на фреймворки, где это уже широко используется. В любом случае, никогда не вредно самому разобрать, как это все делается и почему было сделано.
Re[2]: Guide/Tutorial по WPF/MVVM
От: Fortnum  
Дата: 07.06.11 17:59
Оценка:
Вот проект, что получилось в итоге.
Re[2]: Guide/Tutorial по WPF/MVVM
От: Glas  
Дата: 08.06.11 06:27
Оценка:
Здравствуйте, Fortnum, Вы писали:

Может сейчас скажу что-то не очень умное, так как сам только разбираюсь с MVVM, но все эти танцы с бубном вокруг закрытия. Не проще ли забиндить к событию закрытия EventHandler в ModelView?
Re[3]: Guide/Tutorial по WPF/MVVM
От: Fortnum  
Дата: 08.06.11 08:08
Оценка:
Здравствуйте, Glas, Вы писали:

G>Может сейчас скажу что-то не очень умное, так как сам только разбираюсь с MVVM, но все эти танцы с бубном вокруг закрытия. Не проще ли забиндить к событию закрытия EventHandler в ModelView?


Предлагаемые триггеры — это и есть биндинг события к методу модели вида!

Если подробнее, то имеем два способа:

1. Если ViewModel ничего не знает про View:
1.1. У ModelView должен быть публичный метод, с сигнатурой как у события Closing
1.2. Где-то должно осуществиться создание делегата, т.е. подписка на это событие, а осуществиться при таком способе она может только во View:

public abstract class WorkspaceViewModel : ViewModelBase
{
    public void Close(object sender, CancelEventArgs e)
    {
        Close();
    }

    ...

}


public partial class MainWindow : WindowView
{
    public MainWindow()
    {
        ...

        Closing += viewModel.Close;
    }

    ...
}


То есть по-любому имеется необходимость использовать code-behind для обвязки модели вида.

Вместо того, чтобы создавать в модели вида метод с сигнатурой, завязанной на вид, не проще ли снять эту зависимость и использовать в том же code-behind анонимный метод, а подписку на событие осуществить через хамл:

public partial class MainWindow : WindowView
{
    void Window_Closing(object sender, CancelEventArgs e)
    {
        viewModel.Close();
    }
}


В результате приходим к code-behind и к избавлению от них через триггеры, что и описывалось.

2. Если модель вида имеет ссылку на вид (через интерфейс), то этот интерфейс может иметь событие Closing. Но в этом случае, все равно, так же как и при первом способе, когда модель вида не имеет ссылку на вид, — все так же придется в виде поднимать событие Closing. Исключением станет разве что, опять же, совпадение сигнатур метода у модели вида и публичного события Closing у уже умеющего это делать вида с сигнатурой события в интерфейсе

---

Короче, предлагаемые триггеры могут легко убирать такой импеданс (несоответствие) в сигнатурах событий и методов. Причем делается все это в хамле и при необходимости можно для этого дела написать визуальный редактор или скриптовый язык как в Caliburn. Предполагаю, что где-то это уже написано, или скоро будет написано. И тогда получается чистый MVVM
Re: «Pro Business Applications with Silverlight 4»
От: Qbit86 Кипр
Дата: 09.08.11 12:48
Оценка: 2 (1)
Здравствуйте, Fortnum, Вы писали:

F>Подсказажите известный компилируемый гайд/тьюториал по созданию более-менее сложного и работающего приложения для демонстрации WPF/MVVM?


Просмотрел главу, посвящённую MVVM, в книге Chris Anderson «Pro Business Applications with Silverlight 4». Мне очень понравилась, гораздо лучше всех статей по MVVM, которые я читал.
Глаза у меня добрые, но рубашка — смирительная!
Re: Оффтоп
От: Aviator  
Дата: 11.08.11 10:04
Оценка:
Здравствуйте, Fortnum, Вы писали:

F>Подсказажите известный компилируемый гайд/тьюториал по созданию более-менее сложного и работающего приложения для демонстрации WPF/MVVM?

Интересно, какой очередной паттерн будет пиарить MS когда внедрит свой обещанный UI на html в win8 .
Re[2]: Очередной паттерн
От: Qbit86 Кипр
Дата: 11.08.11 10:12
Оценка:
Здравствуйте, Aviator, Вы писали:

A>Интересно, какой очередной паттерн будет пиарить MS когда внедрит свой обещанный UI на html в win8 :shuffle:.


MVVM — это не «очередной паттерн», а вполне разумный архитектурный подход, существенно опирающийся на механизм привязок в UI-фреймворке (не обязательно WPF или Silverlight). Хоть его текущая формулировка (конкретизация старого паттерна Presentation Model) и исходит от Майкрософт, большую роль в его популяризации, afaik, сыграли Flash/Flex-программисты. Они его используют без всякой философии типа «рекомендовано Майкрософтом».
Глаза у меня добрые, но рубашка — смирительная!
Re[3]: Очередной паттерн
От: Aviator  
Дата: 11.08.11 10:36
Оценка:
Здравствуйте, Qbit86, Вы писали:

Q>Здравствуйте, Aviator, Вы писали:


A>>Интересно, какой очередной паттерн будет пиарить MS когда внедрит свой обещанный UI на html в win8 .


Q>MVVM — это не «очередной паттерн», а вполне разумный архитектурный подход, существенно опирающийся на механизм привязок в UI-фреймворке (не обязательно WPF или Silverlight). Хоть его текущая формулировка (конкретизация старого паттерна Presentation Model) и исходит от Майкрософт, большую роль в его популяризации, afaik, сыграли Flash/Flex-программисты. Они его используют без всякой философии типа «рекомендовано Майкрософтом».

Это вообще к чему ?
Re[4]: Очередной паттерн
От: Qbit86 Кипр
Дата: 11.08.11 10:55
Оценка:
Здравствуйте, Aviator, Вы писали:

A>Это вообще к чему :) ?


К тому, что исходное утверждение неявно предполагало, что 1) MVVM — сугубо майкрософтовский паттерн, 2) Майкрософт его пиарит. Оба утверждения не верны.
Глаза у меня добрые, но рубашка — смирительная!
Re[5]: Очередной паттерн
От: Aviator  
Дата: 11.08.11 12:18
Оценка:
Здравствуйте, Qbit86, Вы писали:

Q>Здравствуйте, Aviator, Вы писали:


A>>Это вообще к чему ?


Q>К тому, что исходное утверждение неявно предполагало, что 1) MVVM — сугубо майкрософтовский паттерн,

Это паттерн MS, то что он основан на application model и является её адаптпцией к инфраструктуре wpf не имеет значения в данном контексте

2) Майкрософт его пиарит. Оба утверждения не верны.
Майкрософт его действительно пиарит, скажете нет?
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.