В прошлой статье мы рассмотрели основы шаринга кода. Дополнить эту статью можно продемонстрированной на конференции Build возможностью шаринга между W8.1 и WP8.1. Этот подход очень хорошо описан здесь, поэтому сейчас мы не будем подробно останавливаться на Universal Apps.
В целом Microsoft радует шагами по унификации кода для обеих платформ, однако все же у нас остается наследие в виде Windows Phone 7. Кроме того, возможно, придется шарить код также и на десктоп, Android и т.д.
В этой статье мы рассмотрим один из наиболее часто используемых практических решений по шарингу кода.
Готовимся к шарингу кода
Следующие «инструменты» существенно упрощают написание кроссплатформенных приложений, но в виду того, что об этих подходах и паттернах написано немало статей/книг, мы не будем подробно останавливаться на этих пунктах.
Паттерн MVVM фактически стал чуть ли не обязательным стандартом качества при разработке приложений под WP, W8, WPF, Silverlight; и не смотря на то, что его совершенно не обязательно использовать, все же этот подход зачастую экономит массу времени и кода при создании сложных приложений.
Одно из основных заблуждений, с которым я очень часто сталкиваюсь, является мнение, что можно просто взять и использовать MVVM, а качество придет само. Или что этот паттерн заменит собой, к примеру, классическую трехзвенку. На самом деле, MVVM совершенно не отрицает выделение логики приложения и логики хранения данных. На деле все с точностью до наоборот: если писать всю логику приложения и логику хранения данных прямо в VM, то, по сути, мы получим тот же Code-Behind, только в VM, и приложение очень быстро засоряется и становится сложным для сопровождения.
Наиболее популярным фреймворком для WP и WinRT является, пожалуй, MVVM Light (), однако в своих приложениях я чаще всего предпочитаю использовать свой легковесный самописный фреймворк.
Inversion Of Control – если мы можем обойтись без MVVM при написании кроссплатформенных приложений, то, пожалуй, инверсия управления является обязательным инструментом. Даже в случае приложений для одной платформы IoC существенно упрощает разработку гибких, расширяемых и, соответственно, удобных для дальнейшего сопровождения приложений.
Основная идея использования IoC при разработке кроссплатформенных приложений — это обобщение особенностей для каждой платформы с помощью интерфейса и конкретной реализации для каждой платформы.
Есть множество готовых IoC-контейнеров, однако в своих приложениях я использую или самописный Service Locator (что, по мнению многих, является антипаттерном), а в части проектов использую легковесный фреймворк SimpleIoC (который, кстати, поставляется в комплекте с MVVM Light).
Рассмотрим пример с сохранением текста из предыдущей статьи, где мы разделили особенности сохранения текста с помощью директивы «#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, который представляет из себя наборы конкретных реализаций для решения самых разнообразных задач, таких как сохранение данных, получение местоположения пользователя, снимка с камеры и т.п.
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— как облегчить написание мобильных приложений под разные платформы. — отличная статья на тему использования Xamarin и MVVM
Использование паттерна MVVM при создании приложений для Windows Phone. В этой статье можно почерпнуть больше подробностей о MVVM и использовании в WP.
Windows Phone + Caliburn.Micro + Autofac. Еще одна статья о том, как настроить и использовать популярный 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