Статьи

Портирование реального приложения Win8 на WP8

Прошло много времени с тех пор, как я в последний раз работал над переносом Comic Cloud с Windows 8 на Windows Phone. Если вы все еще помните, цель состояла в том, чтобы максимизировать повторное использование кода, используя PCL, где это возможно.

Часть 3 будет последней в этой серии, в настоящее время у меня полнофункциональное приложение для Магазина Windows и приложение для Windows Phone 8, которое может перемещаться по страницам и отправлять поисковый запрос в API с использованием уровня общих служб. Теоретически все разделяется между двумя платформами, кроме представлений, что имеет смысл. Но все еще требовалось много усилий, чтобы заставить его работать.

PCL улучшается

Microsoft усердно работает над тем, чтобы в PCL было как можно больше библиотек. Во второй части этой серии я уже упоминал переносимый HttpClient, и эта библиотека наконец-то дала нам единый способ выполнения HTTP-запросов на нескольких платформах. Между частью 2 и этой частью Microsoft выпустила PCL-версию своего Azure Mobile Services SDK (будьте осторожны! В этом есть серьезные изменения, если вы переходите с SDK для конкретной платформы).

Изменения в моем проекте

Я решил пока не использовать PCL-версию WAMS, потому что в ней есть критические изменения, и это не помогает мне избавиться от некоторых проектов, специфичных для платформы, так что реального применения там пока нет.

Чего я хотел добиться для демонстрации, так это чтобы на телефоне работала функция поиска. Функция поиска в приложении Магазина Windows использует BlockingCollection (ссылка MSDN)Это потокобезопасная коллекция, то есть я могу безопасно предварительно выбирать данные из одного потока при загрузке данных в другой поток. Вся моя служба поиска полагается на этот класс (кстати, это реализация шаблона потребитель / производитель), единственная проблема: в Windows Phone нет класса BlockingCollection. Таким образом, я могу либо абстрагировать сервис поиска, либо полностью изменить его, либо реализовать свою собственную версию BlockingCollection. Последний вариант показался мне самым трудным, поэтому я выбрал его. Я не совсем уверен, получил ли я точно такую ​​же функциональность реальной коллекции BlockingCollection (в ней отсутствуют некоторые методы и свойства, я реализовал только то, что мне было нужно для моего приложения), но вот она

    public class BlockingCollection<T> : Queue<T>
    {
        private readonly object _locker = new object();
        private readonly Queue<T> _itemQ;
        private bool _canAddItems;
     
        public BlockingCollection()
        {
            _itemQ = new Queue<T>();
            _canAddItems = true;
        }
     
        public void EnqueueItem(T item)
        {
            lock (_locker)
            {
                _itemQ.Enqueue(item); // We must pulse because we're
                Monitor.Pulse(_locker); // changing a blocking condition.
            }
        }
     
        public bool TryTake(out T item, int millisecondsTimeout, CancellationToken cancellationToken)
        {
            cancellationToken.ThrowIfCancellationRequested();
     
            if (_canAddItems)
            {
                lock (this)
                {
                    try
                    {
                        item = Dequeue();
                        return true;
                    }
                    catch (Exception)
                    {
                        item = default(T);
                        return false;
                    }
                }
            }
     
            item = default(T);
            return false;
        }
     
        public bool TryAdd(T item, int millisecondsTimeout, CancellationToken cancellationToken)
        {
            cancellationToken.ThrowIfCancellationRequested();
     
            if (_canAddItems)
            {
                lock (this)
                {
                    try
                    {
                        Enqueue(item);
                        return true;
                    }
                    catch (Exception)
                    {
                        return false;
                    }
                }
            }
     
            return false;
        }
     
        public void CompleteAdding()
        {
            _canAddItems = false;
        }
    }

Это в основном очередь с некоторыми заявлениями о блокировке, она работает для меня, но я не несу ответственности за любые несчастные случаи, которые могут произойти Glimlach

Обмен ViewModels

Все мои view-модели находятся в библиотеке PCL, удалось заставить это работать в первой части. ViewModelLocator нельзя сделать переносимым, поскольку некоторые операторы using отличаются, и для версии WP8 могут потребоваться некоторые другие классы, чем для версии win8. Я решил добавить Windows Store ViewModelLocator в качестве ссылки в проект Windows Phone 8, добавив некоторые директивы препроцессора, чтобы он работал как шарм (я сделал этот звук простым, но потребовалось некоторое время, чтобы сделать его правильным).

    using ComicDB.Framework;
    using ComicDB.SDKBroker;
    using ComicDB.View;
    using GalaSoft.MvvmLight;
    using GalaSoft.MvvmLight.Ioc;
    using Microsoft.Practices.ServiceLocation;
     
    #if !WINDOWS_PHONE
    using ComicDB.Framework.WinRT;
    using ComicDB.SDKBroker.WinRT;
    #else
    using ComicDB.Framework.WP8;
    using ComicDB.SDKBroker.WP8;
    #endif
     
    namespace ComicDB.ViewModel
    {
        public class ViewModelLocator
        {
            public ViewModelLocator()
            {
                ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);
     
                if (ViewModelBase.IsInDesignModeStatic)
                {
                    // Create design time view services and models
                    //SimpleIoc.Default.Register<IDataService, DesignDataService>();
                }
                else
                {
                    // Create run time view services and models
    #if !WINDOWS_PHONE
                    SimpleIoc.Default.Register<ComicDB.Framework.Interface.INavigationService, ComicDB.Framework.WinRT.NavigationService>();
    #else
                    SimpleIoc.Default.Register<ComicDB.Framework.Interface.INavigationService, ComicDB.Framework.WP8.NavigationService>();
    #endif
                    SimpleIoc.Default.Register<IService, Service>();
                    SimpleIoc.Default.Register<IMessageApi, MessageApi>();
                    SimpleIoc.Default.Register<IFrameworkApi, FrameworkApi>();
                    SimpleIoc.Default.Register<IDispatcher, Dispatcher>();
                    SimpleIoc.Default.Register<INetworkApi, NetworkApi>();
                }
     
                //register views
    #if !WINDOWS_PHONE
                SimpleIoc.Default.Register<IMainPage, MainPage>();
                SimpleIoc.Default.Register<IVolumeDetailPage, VolumeDetailPage>();
                SimpleIoc.Default.Register<ICharacterDetailPage, CharacterDetailPage>();
                SimpleIoc.Default.Register<ICollectionPage, CollectionPage>();
                SimpleIoc.Default.Register<IDetailPage, DetailPage>();
                SimpleIoc.Default.Register<IIssueDetailPage, IssueDetailPage>();
                SimpleIoc.Default.Register<ILocationDetailPage, LocationDetailPage>();
                SimpleIoc.Default.Register<INewsFeedPage, NewsFeedPage>();
                SimpleIoc.Default.Register<IPersonDetailPage, PersonDetailPage>();
                SimpleIoc.Default.Register<IStoryArcDetailPage, StoryArcDetailPage>();
                SimpleIoc.Default.Register<ITeamDetailPage, TeamDetailPage>();
    #endif
                //register viewmodels
                SimpleIoc.Default.Register<MainViewModel>();
                SimpleIoc.Default.Register<VolumeDetailViewModel>(true);
                SimpleIoc.Default.Register<CharacterDetailViewModel>(true);
                SimpleIoc.Default.Register<TeamDetailViewModel>(true);
                SimpleIoc.Default.Register<IssueDetailViewModel>(true);
                SimpleIoc.Default.Register<SearchViewModel>();
                SimpleIoc.Default.Register<DetailViewModel>(true);
                SimpleIoc.Default.Register<StoryArcDetailViewModel>(true);
                SimpleIoc.Default.Register<LocationDetailViewModel>(true);
                SimpleIoc.Default.Register<PersonDetailViewModel>(true);
                SimpleIoc.Default.Register<CollectionViewModel>();
                SimpleIoc.Default.Register<NewsFeedViewModel>(true);
            }
     
            public MainViewModel Main
            {
                get
                {
                    return ServiceLocator.Current.GetInstance<MainViewModel>();
                }
            }
            //... all other VM properties follow here, left out for demo purpose

Директивы препроцессора делают класс немного грязным, но он выполняет свою работу.

В этот момент приложение WP8 запустилось и показало мне главную страницу, а mainviewmodel — ее текстовый текст. Теперь я хотел добавить панель приложений с кнопкой поиска, там было несколько проблем:

  • панель приложений по умолчанию не привязывается (решается с помощью Cimbalino)
  • mainviewmodel не имеет команды для перехода на страницу поиска, поскольку в Магазине Windows используется очарование поиска

Я решил взять быстрое и грязное решение, поэтому добавил обычную панель приложений с кнопкой и оператором навигации в коде позади. SearchPage имеет SearchViewModel как текстовый текст. В Магазине Windows было нормальным, чтобы свойство SearchText немедленно содержало значение, поскольку оно пришло из чудо-кнопки поиска, а не в WP8. Небольшое изменение для модели представления, чтобы она не запускала свою функцию поиска, когда SearchText пуст или равен нулю. Это был результат после всей моей тяжелой работы

 

Миссия выполнена!

Вывод

PCL еще предстоит пройти долгий путь, но он улучшается, и в некоторых случаях он уже может быть очень полезным (например, для совместного использования классов моделей на разных платформах).

Однако я бы посоветовал не идти на максимальное повторное использование кода, все это звучит великолепно, но реальность совсем иная. Мне пришлось принять много решений, изменить архитектуру и даже добавить недостающие классы (например, BlockingCollection).

Мой совет, если вы хотите создать мультиплатформенное приложение: используйте PCL для совместного использования вашей модели, возможно, даже небольшую инфраструктуру с вспомогательными классами, но создайте пользовательскую реализацию сервисных слоев и моделей представления для каждой платформы, это избавит вас от многих хлопот и наверное даже время. Если вы решите пойти на максимальное повторное использование кода, убедитесь, что вы действительно думаете об этом при разработке своей архитектуры, убедитесь, что каждая мелочь имеет абстракцию лучше, чем один интерфейс, слишком много, чем необходимость переписывать класс.

Вот сравнительный скриншот между решением до и после добавления проекта WP8 и рефакторинга всего.