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 и при вводе имени в одном из окон результат будет отображаться во всех.