- PVSM.RU - https://www.pvsm.ru -
Недавно я начал экспериментировать с разработкой под Windows Phone. У меня уже есть некоторый опыт разработки под WPF и Silverlight с использованием шаблона MVVM [1] и IoC-контейнера, поэтому эта задача не представляет для меня большой трудности. Как выяснилось, мой любимый MVVM-фреймворк Caliburn.Micro [2] (CM) и любимый IoC-контейнер Autofac [3] поддерживают эту платформу. Я засучил рукава и начал формировать скелет приложения. CM активно использует IoC-контейнер для своей работы. Можно использовать либо встроенный контейнер (SimpleContainer), либо ряд других, для которых существуют адаптеры для CM. Есть такой адаптер и для Autofac – Caliburn.Micro.Autofac [4], который я уже использовал для WPF-приложений. Это замечательно, однако бочку мёда, как обычно, испортила ложка дёгтя – адаптер Autofac для CM 1.3.1 (текущая версия) под Windows Phone не работает. До недавнего времени он даже не компилировался, но его автор David Buksbaum [5] недели три назад поправил это недочёт. Однако, так или иначе, приложения с его использованием не работают. Видимо David не разрабатывает сам приложений для Windows Phone и эта версия осталась недоработанной. Отказываться от любимой связки не хотелось и пришлось написать этот адаптер самостоятельно. Сегодня я представляю вашему вниманию эту реализацию.
Мне пришлось решить следующие проблемы:
Окончательные варианты AutofacBootstrapper и AutofacPhoneContainer приведен ниже, я отослал их автору некоторое время назад, но пока он почему-то не интегрировал их в свой проект, там приведена попросту неработающая версия (по части сохранения данных в изолированное хранилище и состояние программы). Тестовый проект можно загрузить по этой ссылке [6].
Попутно стоит заметить, что я нашёл неприятный баг в текущей версии CM: значения полей, которые должны храниться в изолированном хранилище, сохраняются не во всех сценариях. Сборка CM в моём тестовом проекте уже избавлена от этого недостатка. Я создал баг-репорт [7] на сайте CM с описанием этой проблемы. Будем надеяться, что скоро она будет исправлена.
// Project: Caliburn.Micro.Autofac
// File name: AutofacBootstrapper.cs
// File GUID: C5C743E2-7AD1-496C-824A-9AEFBF5F8979
// Authors: David Buksbaum (david@buksbaum.us), Mike Eshva (mike@eshva.ru)
// Date of creation: 20.05.2012
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Windows.Controls;
using Autofac;
using Microsoft.Phone.Controls;
using IContainer = Autofac.IContainer;
namespace Caliburn.Micro.Autofac
{
/// <summary>
/// Autofac specific <see cref="PhoneBootstrapper"/> realization.
/// </summary>
public class AutofacBootstrapper : PhoneBootstrapper
{
#region Public properties
/// <summary>
/// Gets or sets flag should the namespace convention be enforced for type registration. The
/// default is true. For views, this would require a views namespace to end with Views For
/// view-models, this would require a view models namespace to end with ViewModels.
/// </summary>
/// <remarks>
/// Case is important as views would not match.
/// </remarks>
public bool EnforceNamespaceConvention { get; set; }
/// <summary>
/// Gets or sets flag should the view be treated as loaded when registering the
/// <see cref="INavigationService"/>.
/// </summary>
public bool TreatViewAsLoaded { get; set; }
/// <summary>
/// Gets or sets the base type required for a view model.
/// </summary>
public Type ViewModelBaseType { get; set; }
/// <summary>
/// Gets or sets method for creating the window manager.
/// </summary>
public Func<IWindowManager> CreateWindowManager { get; set; }
/// <summary>
/// Gets or sets method for creating the event aggregator.
/// </summary>
public Func<IEventAggregator> CreateEventAggregator { get; set; }
/// <summary>
/// Gets or sets method for creating the frame adapter.
/// </summary>
public Func<FrameAdapter> CreateFrameAdapter { get; set; }
/// <summary>
/// Gets or sets method for creating the phone application service adapter.
/// </summary>
public Func<PhoneApplicationServiceAdapter> CreatePhoneApplicationServiceAdapter { get; set; }
/// <summary>
/// Gets or sets method for creating the vibrate controller.
/// </summary>
public Func<IVibrateController> CreateVibrateController { get; set; }
/// <summary>
/// Gets or sets method for creating the sound effect player.
/// </summary>
public Func<ISoundEffectPlayer> CreateSoundEffectPlayer { get; set; }
#endregion
#region Protected interface
/// <summary>
/// Gets Autofac container instance.
/// </summary>
protected IContainer Container { get; private set; }
/// <summary>
/// Do not override this method. This is where the IoC container is configured.
/// </summary>
/// <exception cref="NullReferenceException">
/// Either CreateFrameAdapter or CreateWindowManager or CreateEventAggregator or
/// CreatePhoneApplicationServiceAdapter or CreateVibrateController or
/// CreateSoundEffectPlayer is null.
/// </exception>
protected override void Configure()
{
// allow base classes to change bootstrapper settings
ConfigureBootstrapper();
// validate settings
if (CreateFrameAdapter == null)
{
throw new NullReferenceException("CreateFrameAdapter is not specified.");
}
if (CreateWindowManager == null)
{
throw new NullReferenceException("CreateWindowManager is not specified.");
}
if (CreateEventAggregator == null)
{
throw new NullReferenceException("CreateEventAggregator is not specified.");
}
if (CreatePhoneApplicationServiceAdapter == null)
{
throw new NullReferenceException(
"CreatePhoneApplicationServiceAdapter is not specified.");
}
if (CreateVibrateController == null)
{
throw new NullReferenceException("CreateVibrateController is not specified.");
}
if (CreateSoundEffectPlayer == null)
{
throw new NullReferenceException("CreateSoundEffectPlayer is not specified.");
}
// Configure container.
ContainerBuilder lBuilder = new ContainerBuilder();
// Register phone services.
Assembly lCaliburnAssembly = typeof (IStorageMechanism).Assembly;
// Register IStorageMechanism implementors.
lBuilder.RegisterAssemblyTypes(lCaliburnAssembly)
.Where(
aType => typeof (IStorageMechanism).IsAssignableFrom(aType)
&& !aType.IsAbstract
&& !aType.IsInterface)
.As<IStorageMechanism>()
.InstancePerLifetimeScope();
// Register IStorageHandler implementors.
lBuilder.RegisterAssemblyTypes(AssemblySource.Instance.ToArray())
.Where(
aType => typeof (IStorageHandler).IsAssignableFrom(aType)
&& !aType.IsAbstract
&& !aType.IsInterface)
.As<IStorageHandler>()
.InstancePerLifetimeScope();
// Register view models.
lBuilder.RegisterAssemblyTypes(AssemblySource.Instance.ToArray())
// Must be a type with a name that ends with ViewModel.
.Where(aType => aType.Name.EndsWith("ViewModel"))
// Mmust be in a namespace ending with ViewModels.
.Where(
aType =>
!EnforceNamespaceConvention ||
(!string.IsNullOrEmpty(aType.Namespace) &&
aType.Namespace.EndsWith("ViewModels")))
// Must implement INotifyPropertyChanged (deriving from PropertyChangedBase will statisfy this).
.Where(aType => aType.GetInterface(ViewModelBaseType.Name, false) != null)
// Registered as self.
.AsSelf()
// Subscribe on Activated event for viewmodels to make storage mechanism work.
.OnActivated(aArgs => ActivateInstance(aArgs.Instance))
// Always create a new one.
.InstancePerDependency();
// Register views.
lBuilder.RegisterAssemblyTypes(AssemblySource.Instance.ToArray())
// Must be a type with a name that ends with View.
.Where(aType => aType.Name.EndsWith("View"))
// Must be in a namespace that ends in Views.
.Where(
aType =>
!EnforceNamespaceConvention ||
(!string.IsNullOrEmpty(aType.Namespace) &&
aType.Namespace.EndsWith("Views")))
// Registered as self.
.AsSelf()
// Always create a new one.
.InstancePerDependency();
// Register the singletons.
lBuilder.Register<IPhoneContainer>(
aContext => new AutofacPhoneContainer(aContext.Resolve<IComponentContext>()))
.SingleInstance();
lBuilder.RegisterInstance<INavigationService>(CreateFrameAdapter())
.SingleInstance();
PhoneApplicationServiceAdapter lPhoneService = CreatePhoneApplicationServiceAdapter();
lBuilder.RegisterInstance<IPhoneService>(lPhoneService)
.SingleInstance();
lBuilder.Register(aContext => CreateEventAggregator())
.SingleInstance();
lBuilder.Register(aContext => CreateWindowManager())
.SingleInstance();
lBuilder.Register(aContext => CreateVibrateController())
.SingleInstance();
lBuilder.Register(aContext => CreateSoundEffectPlayer())
.SingleInstance();
lBuilder.RegisterType<StorageCoordinator>().AsSelf()
.SingleInstance();
lBuilder.RegisterType<TaskController>().AsSelf()
.SingleInstance();
// Allow derived classes to add to the container.
ConfigureContainer(lBuilder);
// Build the container
Container = lBuilder.Build();
// Get the phone container instance.
PhoneContainer = (AutofacPhoneContainer) Container.Resolve<IPhoneContainer>();
// Start the storage coordinator.
StorageCoordinator lStorageCoordinator = Container.Resolve<StorageCoordinator>();
lStorageCoordinator.Start();
// Start the task controller.
TaskController lTaskController = Container.Resolve<TaskController>();
lTaskController.Start();
// Add custom conventions for the phone.
AddCustomConventions();
}
/// <summary>
/// Do not override unless you plan to full replace the logic. This is how the framework
/// retrieves services from the Autofac container.
/// </summary>
/// <param name="aService">
/// The service to locate.
/// </param>
/// <param name="aKey">
/// The key to locate.
/// </param>
/// <returns>
/// The located service.
/// </returns>
/// <exception cref="Exception">
/// Could not locate any instances of service.
/// </exception>
protected override object GetInstance(Type aService, string aKey)
{
object lInstance;
if (string.IsNullOrEmpty(aKey))
{
if (Container.TryResolve(aService, out lInstance))
{
return lInstance;
}
}
else
{
if (Container.TryResolveNamed(aKey, aService, out lInstance))
{
return lInstance;
}
}
throw new Exception(
string.Format(
"Could not locate any instances of service {0}.", aKey ?? aService.Name));
}
/// <summary>
/// Do not override unless you plan to full replace the logic. This is how the framework
/// retrieves services from the Autofac container.
/// </summary>
/// <param name="aService">
/// The service to locate.
/// </param>
/// <returns>
/// The located services.
/// </returns>
protected override IEnumerable<object> GetAllInstances(Type aService)
{
IEnumerable<object> lResult =
Container.Resolve(typeof (IEnumerable<>).MakeGenericType(aService)) as
IEnumerable<object>;
return lResult;
}
/// <summary>
/// Do not override unless you plan to full replace the logic. This is how the framework
/// retrieves services from the Autofac container.
/// </summary>
/// <param name="aInstance">
/// The instance to perform injection on.
/// </param>
protected override void BuildUp(object aInstance)
{
Container.InjectProperties(aInstance);
}
/// <summary>
/// Override to provide configuration prior to the Autofac configuration. You must call the base version BEFORE any
/// other statement or the behaviour is undefined.
/// Current Defaults:
/// EnforceNamespaceConvention = true
/// TreatViewAsLoaded = false
/// ViewModelBaseType = <see cref="System.ComponentModel.INotifyPropertyChanged"/>
/// CreateWindowManager = <see cref="Caliburn.Micro.WindowManager"/>
/// CreateEventAggregator = <see cref="Caliburn.Micro.EventAggregator"/>
/// CreateFrameAdapter = <see cref="Caliburn.Micro.FrameAdapter"/>
/// CreatePhoneApplicationServiceAdapter = <see cref="Caliburn.Micro.PhoneApplicationServiceAdapter"/>
/// CreateVibrateController = <see cref="Caliburn.Micro.SystemVibrateController"/>
/// CreateSoundEffectPlayer = <see cref="Caliburn.Micro.XnaSoundEffectPlayer"/>
/// </summary>
protected virtual void ConfigureBootstrapper()
{
// By default, enforce the namespace convention.
EnforceNamespaceConvention = true;
// By default, do not treat the view as loaded.
TreatViewAsLoaded = false;
// The default view model base type.
ViewModelBaseType = typeof (INotifyPropertyChanged);
// Default window manager.
CreateWindowManager = () => new WindowManager();
// Default event aggregator.
CreateEventAggregator = () => new EventAggregator();
// Default frame adapter.
CreateFrameAdapter = () => new FrameAdapter(RootFrame, TreatViewAsLoaded);
// Default phone application service adapter.
CreatePhoneApplicationServiceAdapter =
() => new PhoneApplicationServiceAdapter(RootFrame);
// Default vibrate controller.
CreateVibrateController = () => new SystemVibrateController();
// Default sound effect player.
CreateSoundEffectPlayer = () => new XnaSoundEffectPlayer();
}
/// <summary>
/// Override to include your own Autofac configuration after the framework has finished its
/// configuration, but before the container is created.
/// </summary>
/// <param name="aBuilder">
/// The Autofac configuration builder.
/// </param>
protected virtual void ConfigureContainer(ContainerBuilder aBuilder)
{
}
/// <summary>
/// Activates an instance in phone container. Derived types should call this method for
/// registering types that should support storage mechanism.
/// </summary>
/// <param name="aInstance">
/// Instance to activate.
/// </param>
/// <remarks>
/// Use this method as event handler on service registation in
/// <see cref="ConfigureContainer"/> method. ViewModels already registering with using it.
/// </remarks>
protected void ActivateInstance(object aInstance)
{
if (PhoneContainer == null)
{
return;
}
PhoneContainer.ActivateInstance(aInstance);
}
#endregion
#region Private properties
private AutofacPhoneContainer PhoneContainer { get; set; }
#endregion
#region Private methods
private static void AddCustomConventions()
{
ConventionManager.AddElementConvention<Pivot>(
ItemsControl.ItemsSourceProperty, "SelectedItem", "SelectionChanged").ApplyBinding =
(aViewModelType, aPath, aProperty, aElement, aConvention) =>
{
if (ConventionManager
.GetElementConvention(typeof (ItemsControl))
.ApplyBinding(aViewModelType, aPath, aProperty, aElement, aConvention))
{
ConventionManager
.ConfigureSelectedItem(
aElement, Pivot.SelectedItemProperty, aViewModelType, aPath);
ConventionManager
.ApplyHeaderTemplate(
aElement, Pivot.HeaderTemplateProperty, null, aViewModelType);
return true;
}
return false;
};
ConventionManager.AddElementConvention<Panorama>(
ItemsControl.ItemsSourceProperty, "SelectedItem", "SelectionChanged").ApplyBinding =
(aViewModelType, aPath, aProperty, aElement, aConvention) =>
{
if (ConventionManager
.GetElementConvention(typeof (ItemsControl))
.ApplyBinding(aViewModelType, aPath, aProperty, aElement, aConvention))
{
ConventionManager
.ConfigureSelectedItem(
aElement, Panorama.SelectedItemProperty, aViewModelType, aPath);
ConventionManager
.ApplyHeaderTemplate(
aElement, Panorama.HeaderTemplateProperty, null, aViewModelType);
return true;
}
return false;
};
}
#endregion
}
}
// Project: Caliburn.Micro.Autofac
// File name: AutofacBootstrapper.cs
// File GUID: CB807DF7-2D69-4B05-AC17-C3E191D7C182
// Authors: Mike Eshva (mike@eshva.ru)
// Date of creation: 20.05.2012
using System;
using System.IO.IsolatedStorage;
using Autofac;
namespace Caliburn.Micro.Autofac
{
/// <summary>
/// Component that allows storage system to communicate with Autofac IoC container.
/// </summary>
public class AutofacPhoneContainer : IPhoneContainer
{
#region Constructors
/// <summary>
/// Initialize new instance of component that allows storage system to communicate with
/// Autofac IoC container.
/// </summary>
/// <param name="aContext">
/// Autofac IoC context.
/// </param>
/// <exception cref="ArgumentNullException">
/// Autofac IoC context not specified.
/// </exception>
public AutofacPhoneContainer(IComponentContext aContext)
{
if (aContext == null)
{
throw new ArgumentNullException("aContext", "Autofac IoC context is not specified.");
}
Context = aContext;
}
#endregion
#region IPhoneContainer Members
/// <summary>
/// Registers the service as a singleton stored in the phone state.
/// </summary>
/// <param name="aService">
/// The service.
/// </param>
/// <param name="aPhoneStateKey">
/// The phone state key.
/// </param>
/// <param name="aImplementation">
/// The implementation.
/// </param>
public void RegisterWithPhoneService
(Type aService, string aPhoneStateKey, Type aImplementation)
{
string lObjectKey = aPhoneStateKey ?? aService.FullName;
IPhoneService lPhoneService = Context.Resolve<IPhoneService>();
if (!lPhoneService.State.ContainsKey(lObjectKey))
{
// There is no object with such a key in the phone state.
// Create a plain new object and store it the phone state.
object lEntireGraphObject = Context.Resolve(aService);
lPhoneService.State[lObjectKey] = lEntireGraphObject;
// Effectively replace service registration in
// Autofac-container (the last registration wins).
ContainerBuilder lBuilder = new ContainerBuilder();
lBuilder.RegisterType(aImplementation)
.As(aService)
.OnActivating(
// Replace an object build by the container with one
// from the phone state using OnActivating event.
aArgs =>
{
IPhoneService lInnerPhoneService =
aArgs.Context.Resolve<IPhoneService>();
if (lInnerPhoneService.State.ContainsKey(lObjectKey))
{
object lInstance = lInnerPhoneService.State[lObjectKey];
aArgs.ReplaceInstance(lInstance);
}
});
lBuilder.Update(Context.ComponentRegistry);
}
}
/// <summary>
/// Registers the service as a singleton stored in the app settings.
/// </summary>
/// <param name="aService">
/// The service.
/// </param>
/// <param name="aPhoneSettingsKey">
/// The app settings key.
/// </param>
/// <param name="aImplementation">
/// The implementation.
/// </param>
/// <exception cref="ArgumentNullException">
/// Service or implementation not specified.
/// </exception>
public void RegisterWithAppSettings
(Type aService, string aPhoneSettingsKey, Type aImplementation)
{
string lObjectKey = aPhoneSettingsKey ?? aService.FullName;
if (!IsolatedStorageSettings.ApplicationSettings.Contains(lObjectKey))
{
// There is no object with such a key in the application isolated storage.
// Probably it is the first time application executed.
// Create a plain new object and store it the application isolated storage.
object lEntireGraphObject = Context.Resolve(aImplementation);
IsolatedStorageSettings.ApplicationSettings[lObjectKey] = lEntireGraphObject;
}
// Effectively replace service registration in
// Autofac-container (the last registration wins).
ContainerBuilder lBuilder = new ContainerBuilder();
lBuilder.RegisterType(aImplementation)
.As(aService)
.OnActivating(
// Replace an object build by the container with one
// from the application isolated storage using OnActivating event.
aArgs =>
{
if (IsolatedStorageSettings.ApplicationSettings.Contains(lObjectKey))
{
object lInstance =
IsolatedStorageSettings.ApplicationSettings[lObjectKey];
aArgs.ReplaceInstance(lInstance);
}
});
lBuilder.Update(Context.ComponentRegistry);
}
/// <summary>
/// Occurs when a new instance is created.
/// </summary>
public event Action<object> Activated = delegate { };
#endregion
#region Assembly interface
/// <summary>
/// Activates <paramref name="aInstance"/>.
/// </summary>
/// <param name="aInstance">
/// Instance to activate.
/// </param>
internal void ActivateInstance(object aInstance)
{
Activated(aInstance);
}
#endregion
#region Private properties
private IComponentContext Context { get; set; }
#endregion
}
}
P.S. Сразу оговорюсь, что это запись из моего блога, и я знаю о правиле не делать перепечаток. Но дело в том, что мой блог не раскручен, а информация, которой я делюсь, на мой взгляд, может пригодиться многим. В качестве оправдания мне, надеюсь, будет засчитано то, что я не даю ссылки на свой блог.
Автор: Eshva
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/windows-phone-7/9293
Ссылки в тексте:
[1] MVVM: http://ru.wikipedia.org/wiki/Model-View-ViewModel
[2] Caliburn.Micro: http://caliburnmicro.codeplex.com/
[3] Autofac: http://code.google.com/p/autofac/
[4] Caliburn.Micro.Autofac: https://github.com/dbuksbaum/Caliburn.Micro.Autofac
[5] David Buksbaum: http://buksbaum.us/
[6] Тестовый проект можно загрузить по этой ссылке: http://mike.eshva.ru/Media/Default/files/code/Eshva.Caliburn.Micro.Autofac.zip
[7] баг-репорт: http://caliburnmicro.codeplex.com/workitem/232
Нажмите здесь для печати.