Windows Phone + Caliburn.Micro + Autofac

в 20:41, , рубрики: autofac, caliburn.micro, mvvm, windows phone 7, разработка под windows phone, метки: , , ,

Недавно я начал экспериментировать с разработкой под Windows Phone. У меня уже есть некоторый опыт разработки под WPF и Silverlight с использованием шаблона MVVM и IoC-контейнера, поэтому эта задача не представляет для меня большой трудности. Как выяснилось, мой любимый MVVM-фреймворк Caliburn.Micro (CM) и любимый IoC-контейнер Autofac поддерживают эту платформу. Я засучил рукава и начал формировать скелет приложения. CM активно использует IoC-контейнер для своей работы. Можно использовать либо встроенный контейнер (SimpleContainer), либо ряд других, для которых существуют адаптеры для CM. Есть такой адаптер и для Autofac – Caliburn.Micro.Autofac, который я уже использовал для WPF-приложений. Это замечательно, однако бочку мёда, как обычно, испортила ложка дёгтя – адаптер Autofac для CM 1.3.1 (текущая версия) под Windows Phone не работает. До недавнего времени он даже не компилировался, но его автор David Buksbaum недели три назад поправил это недочёт. Однако, так или иначе, приложения с его использованием не работают. Видимо David не разрабатывает сам приложений для Windows Phone и эта версия осталась недоработанной. Отказываться от любимой связки не хотелось и пришлось написать этот адаптер самостоятельно. Сегодня я представляю вашему вниманию эту реализацию.

Мне пришлось решить следующие проблемы:

  • Компоненты, реализующие IPhoneService и INavigationService, должны инстанцироваться в AutofacBootstrapper прежде, чем регистрировать в контейнере.
  • Отсутствовала реализация IPhoneContainer, что и вызвало основные трудности.


Окончательные варианты AutofacBootstrapper и AutofacPhoneContainer приведен ниже, я отослал их автору некоторое время назад, но пока он почему-то не интегрировал их в свой проект, там приведена попросту неработающая версия (по части сохранения данных в изолированное хранилище и состояние программы). Тестовый проект можно загрузить по этой ссылке.
Попутно стоит заметить, что я нашёл неприятный баг в текущей версии CM: значения полей, которые должны храниться в изолированном хранилище, сохраняются не во всех сценариях. Сборка CM в моём тестовом проекте уже избавлена от этого недостатка. Я создал баг-репорт на сайте CM с описанием этой проблемы. Будем надеяться, что скоро она будет исправлена.

AutofacBootstrapper

// 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
    }
}

AutofacBootstrapper

  
// 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

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js