Статьи

Silverlight, MV-VM … и IoC — часть 3

Пришло время продолжить эту серию, на данный момент мы знаем основные принципы MVVM и у нас есть набор базовых классов и интерфейсов, которые мы можем использовать. Нам нужно привести его в действие и посмотреть, как мы можем реализовать View и ViewModel и как мы можем связать их вместе с контейнером IoC.

Примечание : все образцы, которые я публикую здесь, напрямую получены для «тестового проекта», который я использую для всех моих семинаров и экспериментов на основе Silverlight. Я опубликовал его в Codeplex, и я перехожу к нему из своей частной Subversion … Я постараюсь восстановить историю проверок, следуя тем же основным шагам, которые были у меня в моем личном репозитории.

Вы можете найти проект по этой ссылке: http://dnmmusicstoresl.codeplex.com/ . Представленный здесь код ссылается на Changeset 41379, это был прямой перенос из проекта Silverlight 3 (на этом этапе не использовались ни службы RIA, ни пользовательское поведение, ни командный механизм).

Сценарий: мы создаем бэк-офис для магазина музыкального магазина, у них есть список альбомов в нашем магазине, и мы хотим создать форму, которая позволит нам выполнять простые запросы к данным, которые мы предоставляем как само собой разумеющееся. со стороны всего этого (у нас есть веб-сервис WCF, на который мы можем сослаться и который предоставит функцию SearchAlbum (string), которая будет фильтровать данные).

Цель: создать ViewModel, который предоставляет данные и некоторые функции, которые View может связать и использовать, создать View, который может использовать ViewModel, а затем сконфигурировать наш контейнер IoC для разрешения дерева объектов, которое нам нужно, чтобы показать Форма для пользователя.

Модель : на этом этапе я держал вещи простыми (новых технологий, таких как RIA-сервисы, пока нет): «на стороне сервера». У меня есть простой класс AlbumSummary, который будет содержать всю информацию, необходимую для отображения, «на стороне клиента», на которую мы ссылаемся к веб-службе, и мы используем прокси-классы, которые он будет сгенерирован для нас.

/// <summary>
/// class returned by the search it contains some informations about the albums
/// </summary>
[DataContract]
public class AlbumSummary : DomainObject<int>
{
[DataMember]
public virtual string Title { get; set; }

[DataMember]
public virtual string Author { get; set; }

[DataMember]
public virtual string Label { get; set; }

[DataMember]
public virtual string Genre { get; set; }

[DataMember]
public virtual DateTime PublicationDate { get; set; }

/// <summary>
/// will contain the url (if we are in a web environment) or the path (if desktop appplication) of the image
/// this will be usually computed using the album id
/// </summary>
[DataMember]
public virtual string Image { get; set; }
}

Теперь мы сталкиваемся с нашей первой архитектурной проблемой: при создании View и ViewModel нам нужно помнить, что мы собираемся использовать их с контейнером IoC, поэтому нам нужно заботиться о зависимостях и порядке создания объектов. , Мы должны решить, какой «главный» объект мы собираемся разрешить, у нас есть два подхода:

  • Разрешите ViewModel : сначала вы запрашиваете ViewModel, ему потребуется обязательная зависимость (внедрение конструктора) для View, иначе вы ничего не сможете показать пользователю или вам нужно полагаться на внешние методы, чтобы приклеить ViewModel к View, который вы выбрали отображения; представление будет иметь необязательную зависимость от ViewModel.
  • Разрешить View : вы запрашиваете кулак View, для него потребуется обязательная зависимость от ViewModel, в противном случае у вас есть только пустая серия элементов управления, с которыми вы не можете взаимодействовать (поведение в ViewModel). ViewModel будет иметь необязательную зависимость от View.

Очевидно, что у вас не может быть инъекции конструктора для View и ViewModel из-за проблемы с циклической ссылкой; поэтому вам нужно выбрать один из этих двух подходов и, возможно, полагаться на некоторые расширения контейнера IoC, чтобы автоматически вводить необязательные ссылки (мы увидим реализацию средства Castle Windsor позже).

В демонстрационном приложении я предпочел следовать второму подходу, потому что ViewModel должен иметь возможность работать правильно даже без присоединенного View (просто для еще большего тестирования), плюс я просто подумал, что он может работать лучше с некоторыми экспериментами, которые я провел с Silverlight Navigation Framework (см. Мой предыдущий пост: Silverlight Navigation Framework: разрешите страницы с помощью контейнера IoC ), и должно быть немного проще настроить несколько представлений, работающих с одной и той же моделью представления.

ViewModel : я начал определять интерфейс, он должен иметь коллекцию для хранения данных и метод, который будет использоваться для поиска (веб-служба принимает строку и выполняет поиск в различных полях сущности Album):

public interface ISearchViewModel : IViewModel
{
new ISearchView View { get; }

/// <summary>
/// Collection that will be used to expose the result of the search operation
/// </summary>
PagedCollectionView SearchResults { get; set; }

/// <summary>
/// performs an asyncronous search throught the webservice and returns the results filling in the SearchResults collection
/// </summary>
/// <param name="query"></param>
void PerformSearch(string query);
}

И его реализация:

/// <summary>
/// glues the interface with the data model, exposes the data that will be used by the view
/// </summary>
public class SearchViewModel : ViewModel, ISearchViewModel
{
public SearchViewModel()
{
SearchResults = new PagedCollectionView(_internalSearchResult);
}

public new ISearchView View { get { return (this as IViewModel).View as ISearchView; } }

public ILogger Logger { get; set; }

/// <summary>
/// collection that holds the actual data
/// </summary>
private readonly ObservableCollection<AlbumSummary> _internalSearchResult = new ObservableCollection<AlbumSummary>();

/// <summary>
/// object that will be used by the binding to expose the collection of data resulting from the search
/// </summary>
public PagedCollectionView SearchResults { get; set; }

/// <summary>
/// performs an asyncronous search throught the webservice and returns the results filling in the SearchResults collection
/// </summary>
/// <param name="query"></param>
public void PerformSearch(string query)
{
IsBusy = true;
MusicStoreServiceClient service = ServiceHelper.GetServiceClient(); ;
service.SearchAlbumsCompleted += (sender, e) =>
{
if (e.Error == null)
{
_internalSearchResult.Clear();
foreach (var result in e.Result)
_internalSearchResult.Add(result);
Logger.Info("VM - Search Completed");
IsBusy = false;
}
else
{
Logger.ErrorFormat(e.Error, "VM - Error performing search with query: {0}", query);
IsBusy = false;
throw e.Error;
}
};
Logger.InfoFormat("VM - Performing search with query: {0}", query);
service.SearchAlbumsAsync(query);
}

}

Представление : я пропущу XAML, который просто определяет текстовое поле для поиска, две кнопки для выполнения операции поиска и добавления нового альбома в бэк-офис, сетку для отображения результатов вместе с пейджером (вы можете получите этот код из репозитория), и я покажу вам код файла. 

public interface ISearchView : IView
{
new ISearchViewModel ViewModel { get; }
}
public partial class Search : Page, ISearchView
{
protected Search()
{
InitializeComponent();

Loaded += Search_Loaded;
}

public Search(ISearchViewModel viewModel)
: this()
{
(this as IView).ViewModel = viewModel;
}

Чтобы иметь возможность привязать элементы управления к свойствам ViewModel, нам нужно установить его как DataContext элемента управления (это делается в конструкторе, поскольку свойство ViewModel реализовано в виде оболочки над DataContext); Теперь вы можете напрямую связать со свойствами, предоставляемыми ViewModel.

Здесь я не использую декларативный подход (который объявляет ViewModel как ресурс в XAML представления и привязывается к нему), потому что я хочу иметь возможность внедрить его в представление с использованием контейнера IoC.

Это в основном то, как вы были вынуждены делать MVVM в Silverlight 3 без использования какого-либо пользовательского поведения или командной структуры, вы просто подключались к событиям, вызванным элементами управления, и перенаправляли вызовы в функции ViewModel.

Прямо сейчас у нас есть все фрагменты мозаики, и мы можем настроить контейнер IoC (обернутый локатором службы в моем примере, потому что мне нравится тестировать несколько IoC-структур). На самом деле в примере используется перенос Castle Windsor на Silverlight 4, поэтому мы будем использовать его синтаксис для настройки битов (не стесняйтесь заменить его на вашу любимую систему IoC).

При запуске приложения мы можем вызвать функцию начальной загрузки, которая просто настраивает контейнер:

private void Bootstrap()
{
WindsorContainer Container = new WindsorContainer();

Container.Kernel.AddFacility("ViewModelInitializationFacility", new ViewModelInitializationFacility());

Container.Kernel.AddFacility("LoggingFacility",
new LoggingFacility(new LoggerConfig() { Level = LoggerLevel.Debug, AppendersNames = new[] { "DefAppender" } }));
Container.Register(
Component.For<IAppender>().ImplementedBy<BrowserConsoleAppender>().Named("DefAppender"),
Component.For<IInterceptor>().ImplementedBy<LoggerInterceptor>().Named("LoggerInterceptor")
);

// Features registration
Container.Register(
// application infrastructure and shell configuration
Component.For<IPageProvider>().ImplementedBy<PageProvider>(),
Component.For<INavigationContentLoader>().ImplementedBy<IocNavigationContentLoader>(),
Component.For<MainPage>(),

// search Feature
Component.For<ISearchViewModel>().ImplementedBy<SearchViewModel>().LifeStyle.Custom<Structura.Castle.Windsor.Lifecycle.SingletonLifestyleManager>(),
Component.For<ISearchView>().ImplementedBy<Search>().Named("Search"),

// album management feature
Component.For<IAlbumViewModel>().ImplementedBy<AlbumViewModel>().Interceptors(InterceptorReference.ForKey("LoggerInterceptor")).Last,
Component.For<IAlbumView>().ImplementedBy<Album>().Named("Album")
);

ServiceLocator = new CastleWindsorServiceLocator(Container);
}

Сначала мы создаем контейнер и инициализируем его двумя средствами, одним для ведения журнала (см. Два моих предыдущих поста о Castle WIndsor и входе в Silverlight) и средством, которое мы используем, чтобы «внедрить» необязательную зависимость View в ViewModel сразу после создания View. :

public class ViewModelInitializationFacility : AbstractFacility
{
protected override void Init()
{
this.Kernel.ComponentCreated += new ComponentInstanceDelegate(Kernel_ComponentCreated);
}

void Kernel_ComponentCreated(ComponentModel model, object instance)
{
// we configure the ViewModel injecting the optinal dependency to the view
IView view = instance as IView;
if (view != null)
{
view.ViewModel.View = view;
}
}
}

Обычай Singleton Lifecycle вы видите здесь обходной путь для Castle.Windsor ошибки (см http://support.castleproject.org/projects/IOC/issues/view/IOC-ISSUE-192 ) , и она будет удалена , как только фиксированный ,

Теперь, чтобы иметь полностью сконфигурированный и пригодный для использования объект View, мы можем просто попросить контейнер разрешить объект по имени или по его типу.

Что мы получили: представление и ViewModel полностью отделены, и они общаются только с использованием интерфейсов, что означает, что мы можем поменять любой из них для целей тестирования или просто потому, что у нас есть лучшая реализация или, возможно, из-за требований к брендингу или скинингу; все хранится внутри контейнера, который становится центральным элементом в архитектуре вашего приложения (поскольку он также отвечает за жизненный цикл объектов).

Мы, однако, далеки от «чистой» реализации шаблона, у нас есть много кода в представлении, в то время как шаблон требует (если возможно) полностью декларативного подхода; Следующий шаг покажет вам, как перейти к «лучшей» реализации, используя новую инфраструктуру команд Silverlight 4 по умолчанию (мы также увидим некоторые приятные вещи, которые вы можете сделать с помощью AOP).

Полный исходный код доступен на CodePlex .