- PVSM.RU - https://www.pvsm.ru -

Шаринг кода между WP, Win8. Часть 2

В прошлой статье [1] мы рассмотрели основы шаринга кода. Дополнить эту статью можно продемонстрированной на конференции Build возможностью шаринга между W8.1 и WP8.1. Этот подход очень хорошо описан здесь [2], поэтому сейчас мы не будем подробно останавливаться на Universal Apps.
Шаринг кода между WP, Win8. Часть 2
В целом Microsoft радует шагами по унификации кода для обеих платформ, однако все же у нас остается наследие в виде Windows Phone 7. Кроме того, возможно, придется шарить код также и на десктоп, Android и т.д.

В этой статье мы рассмотрим один из наиболее часто используемых практических решений по шарингу кода.

Готовимся к шарингу кода

Следующие «инструменты» существенно упрощают написание кроссплатформенных приложений, но в виду того, что об этих подходах и паттернах написано немало статей/книг, мы не будем подробно останавливаться на этих пунктах.

Паттерн MVVM [3] фактически стал чуть ли не обязательным стандартом качества при разработке приложений под WP, W8, WPF, Silverlight; и не смотря на то, что его совершенно не обязательно использовать, все же этот подход зачастую экономит массу времени и кода при создании сложных приложений.

Одно из основных заблуждений, с которым я очень часто сталкиваюсь, является мнение, что можно просто взять и использовать MVVM, а качество придет само. Или что этот паттерн заменит собой, к примеру, классическую трехзвенку. На самом деле, MVVM совершенно не отрицает выделение логики приложения и логики хранения данных. На деле все с точностью до наоборот: если писать всю логику приложения и логику хранения данных прямо в VM, то, по сути, мы получим тот же Code-Behind, только в VM, и приложение очень быстро засоряется и становится сложным для сопровождения.

Наиболее популярным фреймворком для WP и WinRT является, пожалуй, MVVM Light [4] (), однако в своих приложениях я чаще всего предпочитаю использовать свой легковесный самописный фреймворк.

Inversion Of Control [5] – если мы можем обойтись без MVVM при написании кроссплатформенных приложений, то, пожалуй, инверсия управления является обязательным инструментом. Даже в случае приложений для одной платформы IoC существенно упрощает разработку гибких, расширяемых и, соответственно, удобных для дальнейшего сопровождения приложений.

Основная идея использования IoC при разработке кроссплатформенных приложений — это обобщение особенностей для каждой платформы с помощью интерфейса и конкретной реализации для каждой платформы.

Есть множество готовых IoC-контейнеров, однако в своих приложениях я использую или самописный Service Locator (что, по мнению многих, является антипаттерном), а в части проектов использую легковесный фреймворк SimpleIoC (который, кстати, поставляется в комплекте с MVVM Light).

Рассмотрим пример с сохранением текста из предыдущей статьи [1], где мы разделили особенности сохранения текста с помощью директивы «#if WP7». Можно было реализовать этот пример несколько иначе. К примеру, если этот метод находится в некоем классе с DataLayer, то наш код мог бы выглядеть следующим образом:

В проекте с общим кодом (к примеру, Portable Library, о котором у нас речь пойдет ниже) можем выделить общий для сохранения текста интерфейс:

public interface IDataLayer
{
    Task SaveText(string text);
}

Этот интерфейс мы можем использовать в нашем слое логики. Например, так:

public class LogicLayer : ILogicLayer
{
    private readonly IDataLayer dataLayer;

    public LogicLayer(IDataLayer dataLayer)
    {
        this.dataLayer = dataLayer;
    }

    public void SomeAction()
    {
        dataLayer.SaveText("myText");
    }
}

Как видите, слой логики ничего не знает о том, каким способом и где именно будет сохраняться текст.
Соответственно, реализация сохранения текста для WP7/WP8 может выглядеть следующим образом:

public class DataLayerWP : IDataLayer
{
    public async Task SaveText(string text)
    {
        using (var local = IsolatedStorageFile.GetUserStoreForApplication())
        {
            using (var stream = local.CreateFile("DataFile.txt"))
            {
                using (var streamWriter = new StreamWriter(stream))
                {
                    streamWriter.Write(text);
                }
            }
        }
    }
}

И, соответственно, для WP8/W8 может быть следующий код:

public class DataLayerWinRT : IDataLayer
{
    public async Task SaveText(string text)
    {
        var fileBytes = Encoding.UTF8.GetBytes(text);
        var local = ApplicationData.Current.LocalFolder;
        var file = await local.CreateFileAsync("DataFile.txt", CreationCollisionOption.ReplaceExisting);
        using (var s = await file.OpenStreamForWriteAsync())
        {
            s.Write(fileBytes, 0, fileBytes.Length);
        }
    }
}

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

Все, что теперь осталось, это зарегистрировать в каждом из проектов конкретную реализацию. Если взять пример SimpleIoC, то регистрация может выглядеть следующим образом (в нашем случае для WP7):

SimpleIoc.Default.Register<IDataLayer,DataLayerWP>();
и в проекте с WP8:
SimpleIoc.Default.Register<IDataLayer,DataLayerWinRT>();

Таким образом, мы можем разделить практически все: постраничную навигацию, получение данных с сенсоров, открытие html-страницы на клиенте и т.д. и т.п.

Для тех, кто планирует активно использовать Xamarin, могу порекомендовать Xamarin mobile api [6], который представляет из себя наборы конкретных реализаций для решения самых разнообразных задач, таких как сохранение данных, получение местоположения пользователя, снимка с камеры и т.п.

Portable Library. Начиная с VS2012 мы получили возможность использовать новый тип проектов – Portable Library (PL). Строго говоря, мы получили эту возможность еще и с VS2010, но в качестве отдельно устанавливаемого расширения. PL позволяет создавать приложения с общим кодом, и основная «фишка» этого типа проекта заключается в том, что PL автоматически использует только те возможности языка, которые являются общими для выбранных типов проектов. Это накладывает свои ограничения и особенности использования этого инструмента. К примеру, вы не можете использовать XAML в PL. Тем не менее, для приложений со сложной логикой PL принесет огромную экономию времени и сил.
Пожалуй, отдельно стоит отметить проблему использования атрибута CallerMember, который не поддерживается PL и который позволяет в BaseViewModel выделить следующий общий для всех VM метод:

public class BaseViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    [NotifyPropertyChangedInvocator]
    protected virtual void OnPropertyChanged([CallerMemberName]string propertyName=null)
    {
        var handler = PropertyChanged;
        if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
    }
}

Что позволяет писать вместо:

public string Name
{
    get { return name; }
    set
    {
        name = value;
        OnPropertyChanged("Name");
    }
}

Альтернативную запись без указания поля:

public string Name
{
    get { return name; }
    set
    {
        name = value;
        OnPropertyChanged();
    }
}

Что соответственно, существенно, упрощает сопровождение приложения.

Для того, чтобы добавить в PL поддержку этого атрибута, достаточно вручную объявить его:
CallerMemberNameAttribute.cs

namespace System.Runtime.CompilerServices
{
    // Summary:
    //     Allows you to obtain the method or property name of the caller to the method.
    [AttributeUsage(AttributeTargets.Parameter, Inherited = false)]
    sealed class CallerMemberNameAttribute : Attribute { }

    // Summary:
    //     Allows you to obtain the line number in the source file at which the method
    //     is called.
    [AttributeUsage(AttributeTargets.Parameter, Inherited = false)]
    public sealed class CallerLineNumberAttribute : Attribute { }

    // Summary:
    //     Allows you to obtain the full path of the source file that contains the caller.
    //     This is the file path at the time of compile.
    [AttributeUsage(AttributeTargets.Parameter, Inherited = false)]
    public sealed class CallerFilePathAttribute : Attribute { }
}

Настройка и подключение

Примеров использования MVVM и IoC даже на Хабре было немало, приведу здесь несколько ссылок вместо того, чтобы раздувать статью:
Xamarin + PCL + MVVM— как облегчить написание мобильных приложений под разные платформы. [7] — отличная статья на тему использования Xamarin и MVVM

Использование паттерна MVVM при создании приложений для Windows Phone [8]. В этой статье можно почерпнуть больше подробностей о MVVM и использовании в WP.

Windows Phone + Caliburn.Micro + Autofac [9]. Еще одна статья о том, как настроить и использовать популярный MVVM-фреймворк Caliburn.Micro, и не менее популярный при построении веб- и десктоп-приложений IoC-контейнер Autofac.

Конечно это далеко не окончательный список и на просторах хабра и интернета можно найти не менее интересные статьи на эту тему.

Далее мы можем рассмотреть особенности написания кроссплатформенных приложений с использованием этих инструментов.

Общая VM

Первое очевидное решение, которое приходит в голову, это использование общего VM для каждого типа проекта. В достаточно простых проектах мы можем именно так и делать.

К примеру, VM с калькулятором, которая складывает два числа, может выглядеть следующим образом:

public class MainViewModel : BaseViewModel
{
    private int valueA;
    public int ValueA
    {
        get { return valueA; }
        set
        {
            valueA = value;
            OnPropertyChanged();
        }
    }

    private int valueB;
    public int ValueB
    {
        get { return valueB; }
        set
        {
            valueB = value;
            OnPropertyChanged();
        }
    }

    public ICommand CalculateCommand
    {
        get
        {
            return new ActionCommand(CalculateResult);
        }
    }

    private void CalculateResult()
    {
        Result = ValueA + ValueB;
    }

    private int result;
    public int Result
    {
        get { return result; }
        private set
        {
            result = value;
            OnPropertyChanged();
        }
    }
}

И мы можем использовать эту VM без изменений в каждой из платформ.

Детализация VM для каждой из платформ

Зачастую приходится учитывать особенности каждой платформы. К примеру, для Win8, где у нас большой экран, возникла задача выводить результат также и прописью, а для WP было решено выводить прописью лишь результат меньше 100.

Первое и, обычно, неправильное решение, которое сразу же хочется использовать, может выглядеть следующим образом:

private int result;
public int Result
{
    get { return result; }
    private set
    {
        result = value;
        OnPropertyChanged();
        OnPropertyChanged("ResultTextWP");
        OnPropertyChanged("ResultTextWinRT");
    }
}

public string ResultTextWP
{
    get
    {
        if (result < 100)
            return Result.ToString();
        return NumberUtil.ToText(Result);
    }
}

public string ResultTextWinRT
{
    get
    {
        return NumberUtil.ToText(Result);
    }
}

Где мы можем использовать каждое из полей для конкретной платформы. Вместо этого мы можем использовать наследование в качестве кастомизации VM, т.е. в PL в классе MainViewModel можем объявить одно виртуальное поле ResultText:

private int result;
public int Result
{
    get { return result; }
    private set
    {
        result = value;
        OnPropertyChanged();
        OnPropertyChanged("ResultText");
    }
}

public virtual string ResultText
{
    get
    {
        throw new NotImplementedException();
    }
}

Где мы можем заменить throw new NotImplementedException() к примеру, на return NumberUtil.ToText(Result), если не хотим обязать каждого наследника явно переопределять это поле для использования

Теперь мы можем унаследовать MainViewModel в каждом проекте и переопределить его свойство:

public class MainViewModelWinRT : MainViewModel
{
    public override string ResultText
    {
        get { return NumberUtil.ToText(Result); }
    }
}

И для WP:


public class MainViewModelWP : MainViewModel
{
    public override string ResultText
    {
        get
 	 {
     if (result < 100)
  	        return Result.ToString();
   	     return NumberUtil.ToText(Result);
 }
    }
}

Конечно же, в IoC-контейнере мы должны зарегистрировать вместо общего MainViewModel конкретную реализацию для каждой платформы.

Если же какое-либо свойство VM нам нужно ТОЛЬКО на одной платформе, — само собой, определять его в базовом классе нет никакой надобности, — то мы можем указать это свойство только для конкретной платформы. Например, вдруг понадобилось показывать дату в UI только для W8:

public class MainViewModelWinRT : MainViewModel
{
    public override string ResultText
    {
        get { return NumberUtil.ToText(Result); }
    }

    public string Date
    {
        get
        {
            return DateTime.Now.ToString("yy-mm-dd");
        }
    }
}

Резюме

На первый взгляд, по сравнению с предыдущей статьей, мы получаем огромное количество ненужной головной боли с MVVM, IoC и т.п., в то время как можем просто линковать файлы и разделять фичи директивами #if.

К сожалению, пример с калькулятором показывает не преимущества, а лишь получение значительно большего количества кода для такого простого приложения.

На практике, можно получить намного больше преимуществ при построении средних и больших приложений со сложной логикой, за счет того, что наш код занимает существенно большую часть «служебного» кода. Кроме того, код приложения становится менее связанным и существенно облегчается сопровождение приложения. К сожалению, «за бортом» осталось еще много пунктов, которые я не стал включать в эту статью, чтобы не засорять основные моменты шаринга кода. Если вы сообщите в комментариях, какие проблемы/темы/решения Вам наиболее интересны, то могу дополнить эту тему еще одной статьей.

Автор: Atreides07

Источник [10]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/news/59104

Ссылки в тексте:

[1] прошлой статье: http://habrahabr.ru/company/mailru/blog/217691/

[2] здесь: http://habrahabr.ru/company/microsoft/blog/218441/

[3] Паттерн MVVM: http://ru.wikipedia.org/wiki/Model-View-ViewModel

[4] MVVM Light: https://mvvmlight.codeplex.com/

[5] Inversion Of Control: http://ru.wikipedia.org/wiki/%D0%98%D0%BD%D0%B2%D0%B5%D1%80%D1%81%D0%B8%D1%8F_%D1%83%D0%BF%D1%80%D0%B0%D0%B2%D0%BB%D0%B5%D0%BD%D0%B8%D1%8F

[6] Xamarin mobile api: https://xamarin.com/mobileapi

[7] Xamarin + PCL + MVVM— как облегчить написание мобильных приложений под разные платформы. : http://habrahabr.ru/post/182354/

[8] Использование паттерна MVVM при создании приложений для Windows Phone: http://habrahabr.ru/post/137541/

[9] Windows Phone + Caliburn.Micro + Autofac: http://habrahabr.ru/post/145583/

[10] Источник: http://habrahabr.ru/post/219779/