Паттерн MVVM. Часть 1.

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



Паттерн MVP и Unity

MVP – Model View Presenter – паттерн организации PL (presentation layer – уровень представления).

MVP применяется при создании десктопных интерфейсов. Выделяют три комопнента: есть модель – группа классов, которые отдают данные или получают команды, представление – форма обладающая состоянием и некоторым поведением. Презентер создают для отделения бизнес-логики от деталей GUI-фреймворка. В отличие от MVC в MVP представление определяет презентер, а не наоборот.

MVP обычно строится вокруг существующих GUI-фреймворков. На практике существуют две принципиально различные различные реализации паттерна – Supervising Controller и Passive View.
В первом случае логика помещается в обработчики событий button_click, а сами обработчики помещаются в отдельный класс. Для полной изоляции презентера от деталей представления надо писать достаточно много врапперов\адаптеров.
Во втором случае создается пара интерфейсов для общения между представлением и презентером. При совершении какого-либо действия представление напрямую обращается к презентеру, тот выполняет некоторый код и вызывает установку свойств представления. Passive View способствует максимальному перемещению кода в в презентер, что облегчает тестирование.

Создание MVP в WinForms с помощью Unity.

Создаем новое winforms приложение и грохаем оттуда форму.

Для начала определим служебные интерфейсы.

//Маркерный интерфейс представления
public interface IView
{
}

//Интрефейс презентера
public interface IPresenter<T> where T:IView
{
    T View { get; set; }
}

//Базовый класс для презентера
public abstract class BasePresenter<T> :IPresenter<T> where T:IView
{
    public T View { get; set; }
}

Класс “бизнес-логики” будем использовать тот же, что и в предыдущем посте.

public interface ISayHelloService
{
    string SayHello(string name);
}
 
public class SayHelloSerivce : ISayHelloService
{
    public string SayHello(string name)
    {
        return "Привет, " + name;
    }
}

Теперь определим рабочие интерфейсы.

public interface ISayHelloPresenter : IPresenter<ISayHelloView>
{
    void SayHello();
}

public interface ISayHelloView: IView
{
    string GetInputText();
    void SetOutputText(string text);
}
Для перезнтера код будет тривиальный.
public class SayHelloPresenter : BasePresenter<ISayHelloView>, ISayHelloPresenter
{
    ISayHelloService _service;

    public SayHelloPresenter(ISayHelloService service)
    {
        this._service = service;
    }

    public void SayHello()
    {
        this.View.SetOutputText(_service.SayHello(this.View.GetInputText()));
    }
}
Теперь нарисуем простую формочку:

Form1

В коде напишем следеющее:

public partial class Form1 : Form, ISayHelloView
{
    ISayHelloPresenter _presenter;

    public Form1(ISayHelloPresenter presenter)
    {
        InitializeComponent();
        _presenter = presenter;
        //Циклическая зависимость
        _presenter.View = this; 
    }

    public string GetInputText()
    {
        return textBox1.Text;
    }

    public void SetOutputText(string text)
    {
        textBox2.Text = text;
    }

    private void button1_Click(object sender, EventArgs e)
    {
        _presenter.SayHello();
    }
}

Циклическая зависимость в Passive View не позволяет с помощью контейнера пропихнуть все зависимости. Поэтому передача презентеру ссылки на представление делается в коде view.

Теперь чтобы увязать это вместе надо создать и сконфигурировать контейнер. Сделаем это прямо в Program.cs.

static void Main()
{
    var container = new UnityContainer();
    container
        .RegisterType<ISayHelloService, SayHelloSerivce>()
        .RegisterType<ISayHelloPresenter, SayHelloPresenter>()
        .RegisterType<ISayHelloView, Form1>()
        ;
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);
    Application.Run((Form)container.Resolve<ISayHelloView>());
}

Вот и все.



Unity + ASP.NET MVC

Недавно на хабре появилась статья о Unity. В одном из комментов предложили связать ASP.NET MVC и Unity.

А вот есть прикольная задачка уже не для начинающих — связать ASP.NET MVC с Unity. Требований в целом три:

— обеспечить связывание инфраструктуры контроллеров MVC с инъектором
— модульность
— уникальность контейнера в разрезе сессии

Я бы расписал все, но блин, не успеваю вообще ничего, кроме работы. А вам в рамках обучающих методик будет полезно, имхо.

хабраюзер acerv

Я напишу серию постов о применении Unity в паттернах уровня представления.

Сначала о том что такое MVC.

MVC – Model View Controller – паттерн организации PL (presentation layer – уровень представления). Целью этого паттерна, как и многих других, служит отделение модели (логики работы программы) от представления (средств отображения информации). В итоге применение такого паттерна должно приводить к улучшению тестируемости кода.

В современном виде MVC применяется в вебе. Выглядит так: есть модель – группа классов, которые отдают данные или получают команды, есть различные представления этих данных – HTML, сделанные с помощью какого-либо шаблонизатора, JSON, SOAP, XML или еще какой – либо. Для того чтобы передавать данные от модели к представления вводят контроллер.

Все MVC фреймворки проектируются так, чтобы управление приходило сразу на контроллер. Контроллер вызывает методы модели, если нужно формирует данные для передачи представлению (эти данные называют Presentation Entity, но термин неустоявшийся), и выбирает представление для этих данных.

ASP.NET MVC – фреймворк для ASP.NET, реализующий паттерн MVC.
Контроллеры в ASP.NET MVC – классы, унаследованные от класса Controller, содержащие несколько методов - “действий”  (actions). По умолчанию действия отображаются на различные urlы см помощью механизма раутинга (System.Web.Routing).
Каждое действие возвращает ActionResult, который может вызывать генерацию HTML, сериализацию данных в JSON итд. Можно писать свои ActionResult.

Применение Unity в ASP.NET MVC.

Инфраструктура ASP.NET MVC получает имя контроллера из параметров запроса, формируемых механизмом раутинга, потом вызывает класс ControllerFactory, который по имени возвращает экземпляр контроллера.

Сначала создадим новое asp.net mvc приложение. Оно уже включает в себя большой функционал, его трогать не будем.

Чтобы подключить Unity к механизму создания контроллеров надо написать свой класс ControllerFactory.  По умолчанию используется DefaultControllerFactory, нам надо изменить в нем один метод, который создает объект контроллера.

public class UnityControllerFactory: DefaultControllerFactory
{
    IUnityContainer _container;

    public UnityControllerFactory(IUnityContainer container)
    {
        this._container = container;
    }

    protected override IController GetControllerInstance(Type controllerType)
    {
        if (controllerType == null)
        {
            return null;
        }

        return (IController)_container.Resolve(controllerType);
    }
}

Теперь в Global.asax добавим следующий код.

protected void Application_Start()
{
    var container = new UnityContainer();
    RegisterControllerFactory(container);
    //...
}

private static void RegisterControllerFactory(IUnityContainer container)
{
    var factory = new UnityControllerFactory(container);
    ControllerBuilder.Current.SetControllerFactory(factory);
}

Жмем F5 и все работает! В принципе для успешного применения unity в asp.net mvc этого достаточно.

Собственно инъекция зависимостей.

Буду использовать один класс “бизнес-логики” во всех примерах.

public interface ISayHelloService
{
    string SayHello(string name);
}

public class SayHelloSerivce : ISayHelloService
{
    public string SayHello(string name)
    {
        return "Привет, " + name;
    }
}

Надеюсь комментарии не нужны.

Снова дополним global.asax.

protected void Application_Start()
{
    var container = new UnityContainer();
    RegisterTypes(container);
    RegisterControllerFactory(container);
    //...
}

private static void RegisterTypes(IUnityContainer container)
{
    container
        .RegisterType<ISayHelloService, SayHelloService>();
}

Теперь у нас будут подпихиваться зависимости, осталось создать контроллер куда они будут подпихиваться.

public class SayHelloController : Controller
{
    ISayHelloService _service;

    public SayHelloController(ISayHelloService service)
    {
        this._service = service;
    }

    [AcceptVerbs(HttpVerbs.Get)]
    public ActionResult Index()
    {
        return View();
    }

    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult Index(string name)
    {
	//Преобразование к object нужно чтобы строка 
	//не была принята за имя View
        return View((object)_service.SayHello(name));
    }
}

Создаем View с типом string, и помещаем в MainContent незамысловатый код:

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
<%using (Html.BeginForm()){%>
    Введите имя <%=Html.TextBox("name")%>
    <br />      
    <input type="submit" value="Сказать привет" />
<%} %>

<%if (!string.IsNullOrEmpty(Model)){%>    
    <h3><%=Html.Encode(Model)%></h3>      
<%} %>
</asp:Content>

Теперь жмем F5, переходим о адресу http://localhost:<порт>/SayHello и смотрим как работает.



MEF

MEF – Managed Extensibility Framework – новая библиотека для создания композитных приложений. То есть приложений, которые собираются из отдельных частей.

Скачать его можно по адресу http://mef.codeplex.com/, версия на момент написания поста – Preview 5. На базе MEF построена Visual Studio 2010.

MEF по функциональности похожа на IoC-контейнеры, но авторы не стремились повторить функциональность существующих контейнеров. В MEF есть несколько уникальных фич, которых нету в  IoC-контейнерах.

Сразу примеры, снова поиск фильмов. Возьмем основной код из поста про Unity.

// Фильм
public class Movie
{
    public string Title { get; set; }
    public string Director { get; set; }
    public int Year { get; set; }
}

/// <summary>
/// Интерфейс репозитария
/// </summary>
public interface IMovieRepository
{
    IQueryable<Movie> GetMovies();
}

// Сервис поиска фильмов
[Export]
public class MovieFinder
{
    IMovieRepository _repository;

    [ImportingConstructor]
    public MovieFinder(IMovieRepository repository)
    {
        _repository = repository;
    }

    public IEnumerable<Movie> FindByTitle(string q)
    {
        return _repository
                .GetMovies()
                .Where(m => m.Title.Contains(q));
    }
}

// Заглушка для репозитария
[Export(typeof(IMovieRepository))]
public class InMemoryMovieRepository : IMovieRepository
{
    public IQueryable<Movie> GetMovies()
    {
        return new[]   
        {   
            new Movie   
            {   
                Title = "Гарри Поттер и узник Азкабана",   
                Director = "Альфонсо Куарон",   
                Year = 2004   
            },   
            new Movie   
            {   
                Title = "Звездные войны: Эпизод 2 - Атака клонов",   
                Director = "Джордж Лукас",   
                Year = 2002   
            },   
            new Movie   
            {   
                Title = "Властелин колец: Братство кольца",   
                Director = "Питер Джексон",   
                Year = 2001   
            },   
        }.AsQueryable();
    }
}  

В коде атрибутами размечены экспортируемые классы и импортирующий конструктор.

Теперь код, который это все использует.

static void Main(string[] args)
{
    //Создание каталога "частей"
    var catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly());

    //Создание контейнера
    var container = new CompositionContainer(catalog);

    //Получение экземпляра
    var finder = container.GetExportedObject<MovieFinder>();
}

В текущем виде мало отличается от примера с Unity.

На самом деле в MEF гораздо более широкие возможности работы с импортами и экспортами. Экспортировать можно не только классы, но и методы, и значения свойств. Импортировать можно не только через конструктор, но и через свойства и даже через приватные поля (но это не будет работать в частично доверенном окружении).

Упростим пример, класс Movie и Main останутся без изменений.

[Export]
public class MovieFinder
{
    [Import]
    Func<IQueryable<Movie>> _getMovies = null;

    public IEnumerable<Movie> FindByTitle(string q)
    {
        return _getMovies().Where(m => m.Title.Contains(q));
    }
}

public class InMemoryMovieRepository
{
    [Export(typeof(Func<IQueryable<Movie>>))]
    public IQueryable<Movie> GetMovies()
    {
        return new[]   
        {   
            //...
        }.AsQueryable();
    }
}

Также важной фичей MEF является возможность поиска частей в сборках в каталоге на диске, а также мониторинг изменений этих сборок и перезагрузка частей. Кроме того есть API для пересборки частей при изменении.

Но об этом в другой раз.



Нельзя построить коммунизм на ESB

SOA с помощью IoC – это практически коммунизм. В точности как гласит лозунг “от каждого по возможностям, каждому - по потребностям”. Потребности – зависимости классов, возможности – реализуемые интерфейсы, а ЦК КПСС – IoC-контейнер.

В архитектуре с шиной сообщений (ESB) сервисы не общаются друг с другом, а только с шиной, живут в изоляции. Это не коммунизм, а тирания.