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

Теги : Unity, .NET, MVVM, WPF, IoC-контейнер