- PVSM.RU - https://www.pvsm.ru -
В прошлой статье [1] мы рассмотрели основы шаринга кода. Дополнить эту статью можно продемонстрированной на конференции Build возможностью шаринга между W8.1 и WP8.1. Этот подход очень хорошо описан здесь [2], поэтому сейчас мы не будем подробно останавливаться на Universal Apps.
В целом 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 с калькулятором, которая складывает два числа, может выглядеть следующим образом:
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 без изменений в каждой из платформ.
Зачастую приходится учитывать особенности каждой платформы. К примеру, для 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/
Нажмите здесь для печати.