MVVM – Model – View – ViewModel – паттерн организации PL (presentation layer – уровень представления).
Паттерн MVVM применяется при создании приложений с помощью WPF и Silverlight. Этот паттерн был придуман архитектором этих самых WPF и Silverlight - John Gossman (его блог). Паттерн MVVM применяется в Expression Blend.
Идеологически MVVM похож на Presentation Model описанный небезызвестным Фаулером, но MVVM сильно опирается на возможности WPF.
Основная особенность MVVM заключается в том, что все поведение выносится из представления (view) в модель представления (view model). Связывание представления и модели представления осуществляется декларативными байндингами в XAML разметке. Это позволяет тестировать все детали интерфейса не используя сложных инструментальных средств.
Я сначала хотел кратко описать применение MVVM и Unity для построения PL, но понял что одного поста для описания возможностей MVVM очень мало.
В WPF для передачи данных между объектами и визуальными элементами используются байндинги (binding – привязка) в простонародии биндинги. Передача может быть как однонаправленная, так и двунаправленная. Работают байндинги с помощью зависимых свойств (DependencyProperty) или интерфейса INotifyPropertyChanged. Передача управляющих воздействий от визуальных элементов осуществляется с помощью команд, реализующих интерфейс ICommand.
Для начала надоевший уже пример SayHello.
Как всегда используется супер-сложный класс бизнес логики:
public interface ISayHelloService
{
string SayHello(string name);
}
public class SayHelloSerivce : ISayHelloService
{
public string SayHello(string name)
{
return "Привет, " + name;
}
}
Теперь определение класса команды, которая состоит из пары делегатов
public class DelegateCommand : ICommand
{
Func<object, bool> _canExecute;
Action<object> _execute;
//Конструктор
public DelegateCommand(Func<object, bool> canExecute, Action<object> execute)
{
this._canExecute = canExecute;
this._execute = execute;
}
//Проверка доступности команды
public bool CanExecute(object parameter)
{
return this._canExecute(parameter);
}
//Выполнение команды
public void Execute(object parameter)
{
this._execute(parameter);
}
//Служебное событие
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested += value; }
}
}
Теперь напишем нашу модель представления.
public class ViewModel: INotifyPropertyChanged
{
//Имя
public string Name { get; set; }
//Текст приветствия
public string HelloText { get; set; }
//Команда
public ICommand SayHelloCommand
{
get
{
return _sayHelloCommand;
}
}
ISayHelloService _service;
ICommand _sayHelloCommand;
//Конструктор
public ViewModel(ISayHelloService service)
{
this._service = service;
//Создаем команду
this._sayHelloCommand = new DelegateCommand(
o => CanExecuteHello(),
o => ExecuteHello());
}
private void ExecuteHello()
{
this.HelloText = _service.SayHello(this.Name);
OnPropertyChanged("HelloText");
}
private bool CanExecuteHello()
{
return !string.IsNullOrEmpty(this.Name);
}
//Для поддержка байндинга
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null)
{
handler(this,
new PropertyChangedEventArgs(propertyName));
}
}
#endregion
}
Получилось слегка многословно по причине того, что пример искусственный.
Дело за разметкой:
<StackPanel>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="75" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Text="Введите имя"/>
<TextBox Text="{Binding Name}" Grid.Column="1"/>
</Grid>
<TextBox Text="{Binding HelloText}"/>
<Button Content="Сказать привет" Command="{Binding SayHelloCommand}"/>
</StackPanel>
И немного изменим констрктор View:
public Window1(ViewModel model)
{
InitializeComponent();
DataContext = model;
}
В App.xaml уберем атрибут StartupUri, и добавим обработчик события Startup, в котором напишем следующий код:
var container = new UnityContainer();
container
.RegisterType<ViewModel>()
.RegisterType<ISayHelloService, SayHelloSerivce>();
var window = container.Resolve<Window1>();
window.Show();
Можно нажать F5 и смотреть что получилось.
Теперь воспользуемся фичами WPF.
Изменим код ViewModel.
public class ViewModel : INotifyPropertyChanged
{
//Имя
public string Name
{
get { return this._name; }
set
{
this._name = value;
OnPropertyChanged("Name");
OnPropertyChanged("HelloText");
}
}
//Текст приветствия
public string HelloText
{
get
{
return _service.SayHello(this.Name);
}
}
string _name;
ISayHelloService _service;
//Конструктор
public ViewModel(ISayHelloService service)
{
this._service = service;
}
//Для поддержка байндинга
#region INotifyPropertyChanged Members
//Без изменений
#endregion
}
В разметке View уберем кнопку и поставим Mode=OneWay для байндинга второго текстбокса.
Кроме этого слега изменим App.xml.cs
var container = new UnityContainer();
container
.RegisterType<ViewModel>(new ContainerControlledLifetimeManager())
.RegisterType<ISayHelloService, SayHelloSerivce>();
container.Resolve<Window1>().Show();
container.Resolve<Window1>().Show();
Два созданных окна будут разделять одну ViewModel и при вводе имени в одном из окон результат будет отображаться во всех.
