Подсказажите известный компилируемый гайд/тьюториал по созданию более-менее сложного и работающего приложения для демонстрации WPF/MVVM?
Здравствуйте, 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. Перспектива полного разделения вида и его модели. Может быть кто-то скажет, что я написал здесь про велосипед. Ну, буду весьма благодарен, если пришлете мне ссылки на фреймворки, где это уже широко используется. В любом случае, никогда не вредно самому разобрать, как это все делается и почему было сделано.