Wednesday, March 25, 2015

Caliburn.Micro & Simple IoC контейнер

Здравствуйте, уважаемые читатели. Буквально месяц я не писал статьи в своем блоге, чему было несколько причин. Среди причин, которые хоть немного могут меня оправдать, – плотное изучение английского (поставил за цель ежедневное чтение литературы в оригинале и выучить за февраль 500 новых слов, что успешно выполнил). Плюс за этот месяц начал писать статью по паттерну UnitOfWork, которая требует много времени. 
Надеюсь, что материал сегодняшней статьи вам пригодится при использовании Caliburn.Micro для ваших проектов, и что вам не приходилось (и не придется) наступать на те "грабли", с которыми столкнулся я.
Наверняка вам приходилось слышать о таких понятиях, как IoC контейнер, внедрение зависимостей DI. Сегодня мы поговорим о том, как реализовано простое управление внедрением зависимостей с помощью SimpleContainer в Caliburn Micro. Предполагаю, вам приходилось сталкиваться с таким паттерном, как Service Locator, в таком случае, вы понимаете, как работают DI контейнеры. Основное отличие ServiceLocator от самых простых DI контейнеров заключается в том, что ServiceLocator может быть представлен в виде статического класса с набором статических методов или в виде интерфейса. Ниже вы можете посмотреть реализацию простого IoC контейнера по ссылке. Для примера взглянем на часть реализации SimpleContainer с фреймворка Caliburn.Micro. Мы не будем детально и долго останавливаться на разборе этого контейнера, поскольку он очень простой, и вы сами можете посмотреть его реализацию, например, через ILSpy.
public class SimpleContainer
{
    private class ContainerEntry : List<Func<SimpleContainerobject>>
    {
        public string Key;
        public Type Service;
    }

    private class FactoryFactory<T>
    {
        public Func<T> Create(SimpleContainer container)
        {
            return () => (T) ((object) container.GetInstance(typeof (T), null));
        }
    }

    private static readonly Type delegateType = typeof (Delegate);
    private static readonly Type enumerableType = typeof (IEnumerable);
    private readonly List<SimpleContainer.ContainerEntry> entries;

    /// <summary>
    ///   Occurs when a new instance is created.
    /// </summary>
    public event Action<object> Activated = delegate(object param0)
    {
    };
}
Для хранения связывания интерфейсов с их реализацией у нас есть коллекция entries. Все остальное – дело техники. Например, если нам нужно зарегистрировать интерфейс как Singleton, нам достаточно посмотреть, есть ли уже зарегистрированный ранее такой тип; если есть, то просто возвратить его реализацию, иначе зарегистрировать этот тип данных.
public void RegisterSingleton(Type service, string key, Type implementation)
{
    object singleton = null;
    RegisterHandler(service, key, container =>
    {
        object value;
        if ((value = singleton) == null)
        {
            value = (singleton = container.BuildInstance(implementation));
        }
        return value;
    });
}
/// <summary>
///   Registers a custom handler for serving requests from the container.
/// </summary>
/// <param name="service">The service.</param>
/// <param name="key">The key.</param>
/// <param name="handler">The handler.</param>
public void RegisterHandler(Type service, string key, Func<SimpleContainerobject> handler)
{
    this.GetOrCreateEntry(service, key).Add(handler);
}

private ContainerEntry GetOrCreateEntry(Type service, string key)
{
    ContainerEntry containerEntry = GetEntry(service, key);
    if (containerEntry == null)
    {
        containerEntry = new ContainerEntry
        {
            Service = service,
            Key = key
        };
        entries.Add(containerEntry);
    }
    return containerEntry;
}
private ContainerEntry GetEntry(Type service, string key)
{
    if (service == null)
    {
        return entries.FirstOrDefault(x => x.Key == key);
    }
    if (key == null)
    {
        ContainerEntry entry;
        if ((entry = this.entries.FirstOrDefault(x => x.Service == service && x.Key == null)) == null)
        {
            entry = this.entries.FirstOrDefault(x => x.Service == service);
        }
        return entry;
    }
    return this.entries.FirstOrDefault(x => x.Service == service && x.Key == key);
}
Как видите, ничего страшного в данном коде нет. Он очень простой. Думаю, на этом теории достаточно. Пора перейти к практике по использованию данного IoC контейнера. Я решил использовать старенький пример, на котором демонстрировал использование разных типов контейнеров, как AutofacNinject, Unity и других. Это пример по созданию электронной библиотеки, в которой можно посмотреть технические книги по программированию.
Для начала создадим новый проект  WPF Application и дадим ему имя CaliburnMicroSimpleIoC.
Следующим делом установим Caliburn.Micro фреймворк с помощью NuGet Package Manager для нашего тестового проекта. 
Затем создаем классическую структуру для проекта, который использует паттерн MVVM для работы.

Примечание. Чтобы Caliburn.Micro мог нормально связывать вашу модель представления с самим представлением, они должны быть в одном пространстве имен. Учитывайте этот факт сразу, чтобы у вас не возникало вопросов, почему не работает связывание для вашего примера.
Если посмотреть на примеры, которые можно скачать на github и которые объясняют, как работает Caliburn.Micro, то в большинстве примеров у нас представление лежит в той же папке, что и модель представления. Если вам удобен такой вариант, то вы можете его использовать. В моем примере я использую такую разбивку, наверное, больше по привычке, которую пришил к себе из-за постоянного использования паттерна MVVM в WPF
Рекомендую вам начать немного от обратного и перейти к созданию моделей, которые будут использоваться в нашем примере. Обычно в Caliburn.Micro практикуется подход ViewModel-First. в котором работа начинается с модели представления. Мы нарушим его лишь слегка, поскольку для нашей модели представления будут нужны уже готовые модели.
Начнем с интерфейса, который будет отображать информацию об электронной книге. Назовём этот интерфейс IBook и посмотрим на его реализацию.
public interface IBook
{
    string Author { getset; }
    string Title { getset; }
    DateTime Year { getset; }
    string SN { getset; }
    int Count { getset; }
}
Имплементация данного интерфейса приведена в классе Book ниже.
public class Book : PropertyChangedBaseIBook
{
    private string _author;

    public string Author
    {
        get { return _author; }
        set
        {
            _author = value;
            NotifyOfPropertyChange(() => Author);
        }
    }

    private string _title;

    public string Title
    {
        get { return _title; }
        set
        {
            _title = value;
            NotifyOfPropertyChange(() => Title);
        }
    }

    private DateTime _year;

    public DateTime Year
    {
        get { return _year; }
        set
        {
            _year = value;
            NotifyOfPropertyChange(() => Year);
        }
    }

    private string _sn;

    public string SN
    {
        get { return _sn; }
        set
        {
            _sn = value;
            NotifyOfPropertyChange(() => SN);
        }
    }

    private int _count;

    public int Count
    {
        get { return _count; }
        set
        {
            _count = value;
            NotifyOfPropertyChange(() => Count);
        }
    }
}
На данном этапе у вас не должно возникнуть каких-либо проблем. Как минимум, ваш проект должен с легкостью компилироваться.
Следующим этапом нам необходимо добавить интерфейс IBookService, задачей которого будет предоставление интерфейса для работы с книгами (отложенная подгрузка, поиск и т.д.).
public interface IBookService
{
    void GetData(Action<BindableCollection<IBook>, Exception> callback);
    IBook FindBook(IBook findBook);
    void CreateNewBook();
    void RemoveBook(IBook book);
}
Теперь посмотрим на имплементацию этого интерфейса в классе BookService.
public class BookService : IBookService
{
    #region Variable
    private BindableCollection<IBook> _books;
    #endregion

    #region Constructor
    public BookService()
    {
        _books = new BindableCollection<IBook>();
        _books.Add(new Book { Author = "Jon Skeet", Title = "C# in Depth", Count = 3, SN = "ISBN: 9781617291340", Year = new DateTime(2013, 9, 10) });
        _books.Add(new Book { Author = "Martin Fowler", Title = "Refactoring: Improving the Design of Existing Code", Count = 2, SN = "ISBN-10: 0201485672", Year = new DateTime(1999, 7, 8) });
        _books.Add(new Book { Author = "Jeffrey Richter", Title = "CLR via C# (Developer Reference)", Count = 5, SN = "ISBN-10: 0735667454", Year = new DateTime(2012, 12, 4) });
    }
    #endregion

    #region Public Methods
    public void GetData(Action<BindableCollection<IBook>, Exception> callback)
    {
        callback(_books, null);
    }

    public IBook FindBook(IBook findBook)
    {
        if (findBook == null)
            return null;

        return _books.FirstOrDefault(book => book.Author == findBook.Author
                                    && book.Title == findBook.Title
                                    && book.SN == findBook.SN
                                    && book.Year == findBook.Year);
    }

    public void CreateNewBook()
    {
        _books.Add(new Book { Author = "Test1", Title = "Test1", Count = 5, SN = "ISBN-10: 0735667454", Year = DateTime.Now });
    }

    public void RemoveBook(IBook book)
    {
        if (book == null)
            return;

        _books.Remove(book);
    }
    #endregion
}
Для теста данные добавлены вручную. Но в реальных приложениях эти данные могут быть загружены с БД, получены через WCF сервис или любыми другими способами.
Теперь нам нужно сделать несколько шагов, чтобы запустить пустой проект и посмотреть, что он работает. Если вы до этого не работали с Caliburn.Micro, вы можете почитать статью "Начало работы с Caliburn Micro", чтобы получить необходимые начальные навыки. Мы же будем быстро рассказывать о том что нужно сделать дальше. Итак, перенесем наше главное окно MainWindow в папку Views и переименуем его на ShellView. Следующим делом добавим пустую модель представления, которую назовем ShellViewModel по правилам связывания модели представления с представлением, которое по умолчанию существует в Caliburn. Если вы не знаете, как происходит такое связывание, обратитесь к ссылке, которая приведена вначале этого абзаца. Реализация данной модели представления пока будет иметь следующий вид:
public class ShellViewModel : PropertyChangedBase
{
}
Остальное наполнение мы добавим немого позже. Осталось дело за загрузчиком. Добавим новый класс ShellBootstrapper, который наследуем от класса BootstrapperBase.
public class ShellBootstrapper : BootstrapperBase
{
    public ShellBootstrapper()
        : base(true)
    {
        Initialize();
    }

    protected override void OnStartup(object sender, StartupEventArgs e)
    {
        DisplayRootViewFor<ShellViewModel>();
    }
}
Осталось сделать загрузку нашего загрузчика при старте нашего приложения. Для этого изменим немного наш App.xaml файл, как приведено ниже.
<Application x:Class="CaliburnMicroSimpleIoC.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:caliburnMicroSimpleIoC="clr-namespace:CaliburnMicroSimpleIoC">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary>
                    <caliburnMicroSimpleIoC:ShellBootstrapper x:Key="Bootstrapper" />
                </ResourceDictionary>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</Application>
Если вы не забыли, что для модели представления и для представления нужно указать одно и то же пространство имен, то вы уже сможете увидеть пустое окно. Если же вы забыли это сделать, то увидите окно с сообщением о том, что не удалось обнаружить вашу модель ShellViewModel (вы можете загрузить рабочий пример, который будет приведен в конце статьи).
Пришло время усложнить нашу задачу и добавить использование созданных ранее моделей в нашем приложении. Для этого нам нужно все это настроить в нашем классе ShellBootstrapper. Для того чтобы работать с SimpleContainer, нам необходимо переопределить как минимум три метода: GetInstance, GetAllInstances и BuildUp, как показано ниже.
private readonly SimpleContainer _container = new SimpleContainer();

protected override object GetInstance(Type serviceType, string key)
{
    var instance = _container.GetInstance(serviceType, key);
    return instance;
}

protected override IEnumerable<object> GetAllInstances(Type serviceType)
{
    return _container.GetAllInstances(serviceType);
}

protected override void BuildUp(object instance)
{
    _container.BuildUp(instance);
}
Но для нашего примера этого, к сожалению, будет недостаточно. Наши интерфейсы не будут автоматически связаны  с реализацией. Вторая проблема заключается в том, что если мы используем SimpleContainer для управления зависимостями, нам необходимо явно указать использование WindowManager, который будет использоваться в нашем приложении. Это становится обязательным условием.
Для конфигурирования DI  в нашем приложении нужно переопределить метод Configure.
 protected override void Configure()
{
    _container.Singleton<IBookServiceBookService>();
    _container.Singleton<IWindowManagerWindowManager>();
    _container.PerRequest<IBookBook>();
    _container.PerRequest<ShellViewModel>();
}
Ниже приведена полная реализация класса ShellBootstrapper.
public class ShellBootstrapper : BootstrapperBase
{
    private readonly SimpleContainer _container = new SimpleContainer();

    public ShellBootstrapper()
    {
        Initialize();
    }

    protected override void OnStartup(object sender, StartupEventArgs e)
    {
        DisplayRootViewFor<ShellViewModel>();
    }

    protected override object GetInstance(Type serviceType, string key)
    {
        var instance = _container.GetInstance(serviceType, key);
        return instance;
    }

    protected override IEnumerable<object> GetAllInstances(Type serviceType)
    {
        return _container.GetAllInstances(serviceType);
    }

    protected override void BuildUp(object instance)
    {
        _container.BuildUp(instance);
    }

    protected override void Configure()
    {
        _container.Singleton<IBookServiceBookService>();
        _container.Singleton<IWindowManagerWindowManager>();
        _container.PerRequest<IBookBook>();
        _container.PerRequest<ShellViewModel>();
    }
}
Теперь самое время приступить к реализации самой модели представления ShellViewModel. Ниже приведена реализация данной модели представления с добавлением двух методов, с помощью которых мы можем добавлять и удалять книги. А также интерфейс IBookService, который нам позволит загружать нужную информацию о книгах.
public class ShellViewModel : PropertyChangedBase
{
    private readonly IBookService _bookDataService;

    public ShellViewModel(IBookService dataService)
    {
        _bookDataService = dataService;

        _bookDataService.GetData(
            (items, error) =>
            {
                Books = items;
            });
    }

    #region Public Properties

    public BindableCollection<IBook> Books { getset; }

    private IBook _selectedBook;
    public IBook SelectedBook
    {
        get { return _selectedBook; }
        set
        {
            if (_selectedBook != value)
            {
                _selectedBook = value
                NotifyOfPropertyChange();
                NotifyOfPropertyChange(() => CanRemoveBook);
            }
        }
    }

    #endregion

    #region Command

    public void AddNewBook()
    {
        _bookDataService.CreateNewBook();
    }

    public void RemoveBook()
    {
        Books.Remove(SelectedBook);
    }

    public bool CanRemoveBook
    {
        get
        {
            if (SelectedBook == null)
                return false;
            var findBook = _bookDataService.FindBook(SelectedBook);
            if (findBook == null)
                return false;

            return true;
        }
    }
    #endregion
}
Остались буквально последние штрихи: реализация самого представления. Небольшое напоминание для тех, кто забыл, что в Caliburn.Micro есть такая замечательная особенность, как связывание свойств модели представления с контролами в модели представления через имя x:Name, которое должно совпадать с именем свойства в VM. Еще один интересный нюанс, который касается нашего свойства SelectedBook
private IBook _selectedBook;
public IBook SelectedBook
{
    get { return _selectedBook; }
    set
    {
        if (_selectedBook != value)
        {
            _selectedBook = value
            NotifyOfPropertyChange();
            NotifyOfPropertyChange(() => CanRemoveBook);
        }
    }
}
Приставка Selected позволяет нам сразу связать наш элемент с выбранной книгой, например, в ListBox, ComboBox и других контролах, которые поддерживают выбор элементов. Раньше для того чтобы такое поведение заработало, нам  нужно было написать код, подобный коду ниже.
<ListView x:Name ="library" Grid.Column ="0" ItemsSource="{Binding Books}" SelectedItem="{Binding SelectedBook}">
Сейчас же нам не нужно явно прописывать связку с SelectedBook, за нас это автоматически сделает Caliburn.Micro. Теперь пришло время перейти к самой сложной части: к реализации UI (ShellView).
<Window x:Class="CaliburnMicroSimpleIoC.ShellView"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:cal="http://www.caliburnproject.org"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Grid.Resources>
            <DataTemplate x:Key="BookDataTemplate">
                <WrapPanel cal:Bind.Model="{Binding}">
                    <TextBlock Text="Author: " />
                    <TextBlock x:Name="Author" FontWeight="Bold" />
                    <TextBlock Text=", " />
                    <TextBlock Text="Caption: " />
                    <TextBlock x:Name="Title" FontWeight="Bold" />
                    <TextBlock Text="Count: " />
                    <TextBlock x:Name="Count" FontWeight="Bold" />
                </WrapPanel>
            </DataTemplate>
        </Grid.Resources>
        
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <ListView x:Name ="Books" Grid.Column ="0" ItemTemplate="{DynamicResource BookDataTemplate}" />
        <Grid Grid.Column="1">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>
            <Grid Grid.Row="0" DataContext="{Binding SelectedBook}" cal:Bind.Model="{Binding}">
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="Auto" />
                </Grid.RowDefinitions>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="*" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>
                <Label Grid.Row="0" Grid.Column="0" Content="Author" />
                <TextBox x:Name="Author" Grid.Row="0" Grid.Column="1" />
                <Label Grid.Row="1" Grid.Column="0" Content="Title" />
                <TextBox x:Name="Title" Grid.Row="1" Grid.Column="1" />
                <Label Grid.Row="2" Grid.Column="0" Content="Year" />
                <DatePicker x:Name="Year" Grid.Row="2" Grid.Column="1" />
                <Label Grid.Row="3" Grid.Column="0" Content="Serial Number" />
                <TextBox x:Name="SN" Grid.Row="3" Grid.Column="1"  />
                <Label Grid.Row="4" Grid.Column="0" Content="Count" />
                <TextBox x:Name="Count" Grid.Row="4" Grid.Column="1" />
            </Grid>

            <StackPanel Grid.Row="1" Orientation="Horizontal">
                <Button Content="Add new book" Margin="3"  cal:Message.Attach="[Event Click] = [Action AddNewBook()]" />
                <Button Content="Remove book" Margin="3" cal:Message.Attach="RemoveBook()"/>
            </StackPanel>
        </Grid>
    </Grid>
</Window>
Вот пример, как у меня получилось сделать привязку с помощью Caliburn.Micro, оставив только в одном месте установку DataContext.
<Grid Grid.Row="0" DataContext="{Binding SelectedBook}" cal:Bind.Model="{Binding}">
Было бы интересно узнать, как этот финт можно было бы провернуть без использования установки явно DataContext. Результат того, что получилось, можно посмотреть на рисунке ниже.
Возможно, стоило как-то поиграть с View.Model свойством, но у меня это не особо получилось.
Итоги
В статье мы рассмотрели использование SimpleContainer, который идет в Caliburn.Micro, для управления зависимостями. Надеюсь, что пример поможет вам справиться с трудностями, если такие у вас возникнут.
Источники


No comments:

Post a Comment