Pull to refresh

Xamarin.Forms для WPF и UWP разработчиков

Reading time 15 min
Views 32K


Постараюсь коротко, но понятно, рассказать самое интересное о Xamarin. Самые основные концепты, которые необходимо знать UWP и WPF разработчикам, чтобы с места в карьер начать работать с Xamarin.Forms.

Занявшись разработкой на Xamarin я не бросил UWP, а просто расширил свой стек. UWP проекты — это один из типов проектов, поддерживаемых Xamarin.Forms. Мне показалось, что Xamarin ближе всего UWP разработчикам, так как он самый нативный и очень удобно тестировать приложения при работе с Visual Studio на Windows. Но как оказалось, тип проекта UWP не поддерживается Visual Studio для Mac. Так что Xamarin близок всем платформам.

Xamarin был создан той же командой, которая занималась разработкой Mono. Название было взято от вида обезьян Tamarin. Xamarin 2.0 вышел в начале 2013-ого года. И фактически с его выходом стало можно создавать приложения под iOS, Android и Windows на C#. Через год в 2014-ом вышел релиз Xamarin 3, а вместе с ним и Xamarin.Forms.

Есть два типа проектов, которые можно создать с помощью Xamarin — Xamarin.Forms и Native. В первом случае для всех платформ общий интерфейс создается с помощью XAML или C#, а во втором случае интерфейс создается отдельно для каждой из платформ. То есть у Xamarin.Forms еще и интерфейс пишется общим для всех платформ кодом (но возможны и правки для каждой из платформ в отдельности). Зато у Native приложений есть возможность использовать конструктор для создания графического интерфейса. А для того чтобы создать UI для Xamarin.Forms вам придется писать код вручную и каждый раз запускать отладку приложения, чтобы посмотреть на изменения во внешнем виде страницы.



Что выбрать? Стандартный совет: Xamarin.Forms отлично подходит для приложений с обычным дизайном. Если ваш клиент не капризен и не требует каких-то особых графических эффектов и если не требуется использовать специфичные для платформ API в больших объемах, то выбирайте Forms. Кроме того, если у вас опыт разработки на UWP или WPF, то XAML или C# интерфейс Xamarin.Forms будет вам знаком и понятен. В каком-то смысле Native наоборот ближе тем, кто раньше работал с нативными приложениями конкретной платформы.

Да, да. Вы не «ослышались», или точнее, не «очитались». В Xamarin.Forms у вас есть выбор создавать интерфейс на XAML или на C#. Графический редактор XAML пока что не поддерживается Visual Studio.

Если вы создаете контролы из C# кода, то при этом снижается читаемость и повышается сложность разработки. Зато код компилируется и становится чуть более производительным.

Может быть кому-то не знакомому с XAML проще создавать интерфейс на C#. Но как по мне XAML гораздо более удобен и читаем. После выхода XAML Standard он станет и более привычным.

Простейший пример. Вот такая страница MainPage.xaml будет создана у вас по умолчанию:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:App1"
             x:Class="App1.MainPage">

	<Label Text="Welcome to Xamarin Forms!" 
           VerticalOptions="Center" 
           HorizontalOptions="Center" />

</ContentPage>

Можно эту страницу удалить и создать класс MainPage.cs со следующим аналогичным содержимым:

using Xamarin.Forms;

namespace App1
{
    public class MainPage : ContentPage
    {
        public MainPage()
        {
            Label lblIntro = new Label
            {
                Text = "Welcome to Xamarin Forms!",
                VerticalOptions = LayoutOptions.Center,
                HorizontalOptions = LayoutOptions.Center,
            };
            Content = lblIntro;
        }
    }
}

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

using Xamarin.Forms.Xaml

и

[assembly: XamlCompilation(XamlCompilationOptions.Compile)]

где-либо в коде (не важно в каком файле — главное перед namespace)

Как это все работает


Xamarin с помощью платформы Mono связывает C# код с родным для платформ API. Сам .NET фреймворк включается в пакеты приложения. При этом неиспользуемые классы из него исключаются для того, чтобы уменьшить размер пакета. Получается что-то вроде портативной дистрибуции .NET Core.

Привязка классов C# к родным для платформ классам происходит при компиляции. И для iOS и для Android платформ привязка происходит одинаково, различие только в режиме компилятора.
Xamarin.Android использует just-in-time компиляцию для тонкой оптимизации производительности при компиляции в нативное Android APK.

Ahead-of-Time (AOT) компилятор компилирует Xamarin.iOS проекты сразу в нативный код ARM и получаемый в результате файл IPA тоже является нативным бинарником.
Для написания кода можно использовать языки C# и F#.

Для того чтобы разрабатывать и тестировать приложения желательно иметь физические девайсы. Для тестирования Android проектов есть возможность использовать не только реальные девайсы, но и различные эмуляторы. Настроить эмуляторы Android иногда занятие нетривиальное. Хотя, если у вас на компьютере присутствует Hyper-V, то вам повезло. В состав Visual Studio входит отличный эмулятор Visual Studio Emulator for Android, который требует Hyper-V, x64 и Windows PRO или Enterprise.

Если у вас нет Hyper-V, то вы все-равно можете настроить Google Android эмулятор. Его можно настроить и на процессорах Intel и на AMD, но не факт, что работать он будет быстро. Настройка эмулятора это отдельная тема.

Если вы разрабатываете приложение с помощью Visual Studio для Mac, то вы можете воспользоваться эмулятором iOS. Пользователи Visual Studio для Windows лишены возможности тестировать приложения с помощью эмулятора из-за ограничений iOS платформы. Для компиляции и тестирования Xamarin.iOS проекта необходимо реальное устройство.

На конференции Build 2017 был анонсирован Xamarin Live Player с помощью которого можно связать физическое iOS или Android устройство с Visual Studio и тестировать на реальных iOS и Android устройствах без установки гигабайт SDK. Уже доступно превью Xamarin Live Player.

Visual Studio вместе с Xamarin-ом и всеми SDK занимает довольно много места на диске. Если у вас HDD довольно объемный, то для вас это не будет проблемой. Visual Studio 2017 занимает поменьше места, чем 2015-ая, так что если есть выбор, то ставьте ее. В ближайшее время сэкономить место позволит установка Xamarin Live Player вместо SDK.

Небольшая шпаргалка по XAML элементам


Страницы / Pages


На iOS функцию страницы выполняет View Controller, на Windows Phone – Page, а на Android – Activity. Но если вы работаете в Xamarin.Forms приложении, то для вас это Page.

Основные типы страниц:

ContentPage — отображает единственный View (как правило контейнер/элемент компоновки)
MasterDetailPage — страница, отображающая две панели информации
NavigationPage — главную страницу приложения можно обернуть в NavigationPage. Например так: new NavigationPage(new MainPage()); Тогда в приложение добавится навигация.
TabbedPage — страница с несколькими закладками (Tab-ами)
CarouselPage — страница, «листы» которой можно пролистывать. Примерно, как в приложении «Фотографии» можно пролистывать фотографии с помощью жеста свайпа.



Еще есть TemplatedPage – это то, что лежит в основе ContentPage. Фактически благодаря TemplatedPage можно использовать ControlTemplate для ContentPage.

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

В файл App.xaml добавим следующий ControlTemplate:

   <Application.Resources>
        <ResourceDictionary>
            <ControlTemplate x:Key="MainPageTemplate">
                <StackLayout>
                    <Label Text="{TemplateBinding HeaderText}" FontSize="24" />
                    <ContentPresenter />
                </StackLayout>
            </ControlTemplate>
        </ResourceDictionary>
    </Application.Resources>

В MainPage.xaml добавим ControlTemplate="{StaticResource MainPageTemplate}"

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:App1"
             x:Class="App1.MainPage" ControlTemplate="{StaticResource MainPageTemplate}">

	<Label Text="Welcome to Xamarin Forms!" 
           VerticalOptions="Center" 
           HorizontalOptions="Center" />
</ContentPage>

Ну и раз у нас в ControlTemplate есть биндинг {TemplateBinding HeaderText}, то необходимо реализовать и его в MainPage.xaml.cs

  public static readonly BindableProperty HeaderTextProperty =
                BindableProperty.Create("HeaderText", typeof(string), typeof(MainPage), "Заголовок");
     public string HeaderText
     {
         get { return (string)GetValue(HeaderTextProperty); }
     }

Хотя даже этот пример должен быть понятен и близок WPF/UWP разработчикам.

Вместо OnNavigatedTo и OnNavigatedFrom у страниц Xamarin есть методы OnAppearing() и OnDisappearing().

Элементы компоновки / Layouts


Layouts как и большинство элементов управления наследуются от класса View. Давайте рассмотрим самые основные элементы компоновки:

StackLayout — располагает элементы по порядку горизонтально или вертикально
AbsoluteLayout — абсолютное позиционирование (как в WinForms)
RelativeLayout — что-то вроде AbsoluteLayout, но позиции задаются в процентах
Grid – таблица
ContentView – вариант View аналогичного ContentPage (т.е. содержащего в себе только один элемент)
ScrollView — элемент, содержимое которого можно прокручивать если оно не умещается
Frame — содержит в себе только один элемент с Padding отступом по умолчанию равным 20



И пару Layout можно рассматривать отдельно:

TemplatedView – очень похож на TemplatedPage, но в данном случае это View, а не Page.
ContentPresenter – менеджер компоновки для шаблонных вьюшек

Элементы управления


Контролы, которые есть в Xamarin.Forms. Следующие контролы тоже наследуются от View и не требуют особого представления UWP/WPF разработчикам:

Label, Button, Image, ProgressBar, Slider, SearchBar, DatePicker, TimePicker, WebView

А о следующих контролах я расскажу в двух словах:

Stepper — функция как и у slider, но интерфейс в виде двух кнопок (увеличивающих и уменьшающих значение)
Switch — переключатель. Выполняет функцию CheckBox
Entry — текстовое поле
Editor — многострочное текстовое поле
BoxView — прямоугольник, который можно закрасить каким-либо цветом
ActivityIndicator – отображает что что-то происходит (тот момент, когда приложение «задумалось»)

Кроме того, есть элементы для размещения в них различных коллекций: Picker, ListView, TableView.

И элементы для особых задач, название которых более-менее соответствует содержимому:
Map, OpenGLView.

Полный список элементов управления, унаследованных от View, доступен на следующей странице: Xamarin.Forms Views.

Cells


Последним типом элементов управления является тип, унаследованный от Cells. В качестве содержимого, находящегося в ячейках ListView, TableView могут выступать различные контролы с красноречивыми названиями: EntryCell, SwitchCell, TextCell, ImageCell.

Соответственно, значением ячейки таблицы может быть: поле ввода текста (EntryCell), переключатель (SwitchCell), текст (TextCell) или изображение (ImageCell).

В одной и той же таблице можно использовать различные типы ячеек. Ниже пример TableView, в качестве содержимого которого выступают текстовое поле и переключатель:

    <TableView>
        <TableRoot>
            <TableSection Title="Настройки">
                <SwitchCell Text="Уведомлять о новостях" />
                <EntryCell Text="Введите ваше имя" />
            </TableSection>
        </TableRoot>
    </TableView>

Кроме четырех уже упомянутых типов ячеек ListView и TableView вы можете задать ячейке какое-то свое содержимое с помощью ViewCell. Например, можно разместить внутри StackLayout с каким-то содержимым

    <TableSection Title="Getting Started">
        <ViewCell>
             <StackLayout Orientation="Horizontal" WidthRequest="300">
                 <Image Source="portrait.jpg" />
                 <Label Text="Сотрудник месяца" TextColor="#503026" />
             </StackLayout>
        </ViewCell>
   </TableSection>

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

Установленные значения для платформ устанавливаются равными их типам измерения размера. Для UWP это Effective pixels, для iOS — Points и для Android это Density-independent pixels. Атрибуты Width и Height доступны только для чтения и возвращают текущие размеры.

MessagingCenter


MessagingCenter позволяет различным компонентам приложения коммуницировать друг с другом.
Где это может быть использовано? Например, может быть необходимым прислать сообщение между двумя объектами Page или ViewModel. Или же может быть необходимо сообщить о завершенной фоновой загрузки.

Реальный пример того как это может быть реализовано. Где-либо в коде размещается следующая регистрация подписки на событие:

MessagingCenter.Subscribe<MainPage, string>(this, "SomeIdText", GetMessage);

Если после этого MainPage отправит сообщение с текстом SomeIdText, то будет вызван метод GetMessage вот с такой сигнатурой:

     private void GetMessage(object sender, string args)
     {
        // здесь можно что-то сделать
     }

или же можно использовать лямбда-выражение:

     MessagingCenter.Subscribe<MainPage, string>(this, "SomeIdText", (sender, arg) =>
     {
         // и здесь тоже можно что-то сделать
     });

Отправляется сообщение так:

MessagingCenter.Send<MainPage, string>(this, "SomeIdText", "значение, которое можно получить в args");

А отписка происходит с помощью следующей строчки:

MessagingCenter.Unsubscribe<MainPage, string>(this, "SomeIdText");

Полезные ссылки


Если вам мало контролов, то вы можете воспользоваться порталом Xamarin, откуда можно скачать какие-либо готовые платные или бесплатные компоненты.

Кроме того, можно устанавливать пакеты NuGet.

Какие-то популярные Open-Source компоненты можно найти на GitHub.

Сам открытый код Xamarin тоже можно найти на GitHub .

Отправить какой-то баг можно через BugZilla.

Shared Projects, PCL или .NET Standard


Вы можете хранить общий код или в общем проекте (Shared Project) или в общей библиотеке PCL.
В зависимости от того что вы выберете у вас будет определенные специфический нюансы в работе.

Shared Projects теоретически позволяют получить доступ к всем возможностям .NET Framework. Но не факт, что все их можно использовать в Xamarin проекте. Так что большие возможности Shared Projects обманчивы.

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

Никто не запрещает в одном решении иметь сразу и Shared Project и PCL.

Для того, чтобы в PCL использовать некоторые пространства имен иногда необходимо чтобы решение было приведено к .NET Standard.

Допустим, вы хотите использовать класс System.Security.Cryptography. Пока ваша библиотека PCL не будет приведена к .NET Standard у вас это не получится.

Впрочем, можно использовать криптографию в каждом из проектов по отдельности. Для UWP это можно сделать, используя пространство имен Windows.Security.Cryptography, а для Android и iOS — System.Security.Cryptography.

Минусом приведения к .NET Standard является то, что проекты Android будут в таком случае поддерживать только Android 7.0 и iOS 10.0 и выше. У библиотек PCL для стандартизации используются профили.

В целом использование в проектах PCL приносит больше удобств и возможностей. Для последующих проектов лучше использовать .NET Standard. Со временем библиотеки этого типа заменят PCL.

Как работать с API различных платформ


Особенности Shared Project


Если в качестве источника общего кода вы используете Shared Project, то вы можете использовать директивы компилятора:

#if, #elif, #else и #endif

Например:

#if __ANDROID__
// код, который использует возможности Android
#endif

Если вы используете директивы компилятора, то при рефакторинге средствами IDE заключенный в них код затронут не будет. Это относят к минусам общих проектов.

Можно создавать свою имплементацию какого-то класса в каждом из проектов по отдельности с помощью Class Mirroring.

Например, создать в каждом из проектов iOS, Android и UWP свою реализацию Alert. Например, для Android класс может быть таким:

internal class Alert
{
  internal static void Show(string title, string message)
  {
     new AlertDialog.Builder(Application.Context).SetTitle(title).SetMessage(message);
  }
}

Зачем это нужно? В данном примере можно создать свое окошко с сообщением с помощью нативных возможностей платформы.

Обратите внимание на предикат internal. Он в данном случае обязателен. Из общего проекта метод можно вызвать так:

   Alert.Show("Сообщение", "Привет Xamarin!");

DependencyService


С помощью этого функционала можно писать единый код для различных платформ и в PCL и в общих проектах. Функционал основан на паттерне Dependency Injection. Отсюда и название.

Делается это так. Создается какой-либо интерфейс общий для всех платформ (в общем проекте или PCL) и после, создается реализация этого интерфейса для каждой из платформ (в каждом из проектов платформы).

Приведу официальный пример Xamarin — Introduction to DependencyService, но только с изменениями для UWP.
В общем проекте/PCL создаем интерфейс:

    public interface ITextToSpeech
    {
        void Speak(string text);
    }

В проектах для каждой из платформ создаем реализацию. Для UWP необходимо в проекте создать файл TextToSpeechImplementation.cs со следующим кодом:

    public class TextToSpeechImplementation : ITextToSpeech
    {
        public TextToSpeechImplementation() { }

        public async void Speak(string text)
        {
            MediaElement ml = new MediaElement();
            SpeechSynthesizer synth = new SpeechSynthesizer();
            SpeechSynthesisStream stream = await synth.SynthesizeTextToStreamAsync(text);
            ml.SetSource(stream, stream.ContentType);
            ml.Play();
        }
    }

И нужно в любом файле проекта UWP перед namespace-ом зарегистрировать DependencyService:

[assembly: Xamarin.Forms.Dependency(typeof(AppName.UWP.TextToSpeechImplementation))]
namespace AppName.UWP

Для Android код будет таким:

        
        TextToSpeech _speaker;
        string _toSpeak;

        public TextToSpeechImplementation() { }

        public void OnInit([GeneratedEnum] OperationResult status)
        {
            if (status.Equals(OperationResult.Success))
            {
                var p = new Dictionary<string, string>();
                _speaker.Speak(_toSpeak, QueueMode.Flush, p);
            }
        }

        public async void Speak(string text)
        {
            var ctx = Forms.Context;
            _toSpeak = text;
            if (_speaker == null)
            {
                _speaker = new TextToSpeech(ctx, this);
            }
            else
            {
                var p = new Dictionary<string, string>();

                _speaker.Speak(_toSpeak, QueueMode.Flush, p);
            }
        }
    }

И нужно в любом файле проекта Droid перед namespace-ом зарегистрировать имплементацию:

[assembly: Xamarin.Forms.Dependency(typeof(TextToSpeechImplementation))]
namespace AppName.Droid

Код для iOS можете посмотреть на следующей страничке документации StackOverflow: Accessing native features with DependencyService.

Теперь можно в коде PCL или общего проекта вызывать реализацию интерфейса. Автоматически будет вызвана реализация для той платформы, которую вы используете на данный момент:

DependencyService.Get<ITextToSpeech>().Speak("Hello from Xamarin Forms");

Регистрация с помощью атрибута упрощает применение DI. Но можно применить Dependency Injection и самостоятельно. Например, так. Добавить в PCL класс:

    public static class TextToSpeech
    {
        public static ITextToSpeech Instance { get; set; }
    }

В каждом из проектов инициализировать экземпляр класса:

  TextToSpeech.Instance = new TextToSpeechImplementation();

И можно пользоваться из PCL:

  TextToSpeech.Instance.Speak("Hello! How are you?");

Класс Device


Этот класс позволяет:

— Определять текущий тип устройства с помощью Device.Idiom (Desktop, Tablet, Phone, Unsupported).
— Определять операционную систему с помощью Device.OS (Android, iOS, Windows, WinPhone).

Для задания различных параметров (очень часто различных размеров) платформам используется Device.RuntimePlatform. Его можно использовать в качестве условия

  if (Device.RuntimePlatform == Device.Android) Margin = new Thickness(0, 20, 0, 0);

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

Hint: Раньше можно было задавать значение с помощью Device.OnPlatform, но сейчас этот метод помечен как устаревший. Хотя стало можно из XAML использовать тэг с атрибутом Platform. Например:

<StackLayout>
  <StackLayout.Padding>
   <OnPlatform x:TypeArguments="Thickness">
     <On Platform="Android, WinPhone">0</On>
     <On Platform="iOS">0,20,0,0</On>
    </OnPlatform>
  </StackLayout.Padding>
</StackLayout>

Hint 2: Для получения информации о операционной системе и модели используются различные для платформ классы:

UIKit.UIDevice для iOS
Android.OS.Build для Android
Windows.Security.ExchangeActiveSyncProvisioning.EasClientDeviceInformation для Windows

Custom Renderers


В Xamarin Forms можно изменить внешний вид основного контрола с помощью декоратора в виде rendener. Для этого можно создать кастомный renderer – то есть отдельный внешний вид прорисовки контрола для каждой из платформ.

Рассмотрим на простейшем примере:

В Xamarin элемент управления со строкой ввода текста называется Entry. Сделаем так, чтобы внешний вид строки ввода текста немного отличался от стандартного. Для этого создаем в PCL или в Shared Project класс

public class MyEntry : Entry
{
}

Теперь в каждом из проектов можно создать экземпляр класса MyEntryRenderer, в котором необходимо override событие прорисовки OnElementChanged и в этом событии сперва прорисовать контрол с помощью base.OnElementChanged(e), а затем уже изменить его интерфейс в соответствии с необходимостью. Следующий класс – это пример того как в iOS приложении изменить цвет фона, элемента управления MyEntry:

using Xamarin.Forms.Platform.iOS;

[assembly: ExportRenderer (typeof(MyEntry), typeof(MyEntryRenderer))]
namespace YourAppName.iOS
{
    public class MyEntryRenderer : EntryRenderer
    {
        protected override void OnElementChanged (ElementChangedEventArgs<Entry> e)
        {
            base.OnElementChanged (e);

            if (Control != null) {
                Control.BackgroundColor = UIColor.FromRGB (204, 153, 255);
            }
        }
    }
}

Как вы можете заметить класс унаследован от EntryRenderer. Список всех классов, от которых можно наследоваться, доступен в англоязычной документации: Renderer Base Classes and Native Controls.

Для того, чтобы изменить внешний вид MyEntry для платформы Android можно взять тот же класс, но сделать небольшие изменения. В первую очередь имя платформы изменить с iOS на Android. Ну и затем уже заменить в событии OnElementChanged код интерфейса на специфичный для платформы.

   if (Control != null) {
      Control.SetBackgroundColor (global::Android.Graphics.Color.LightGreen);
   }

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

Кроме проверки на Control != null есть еще 2 проверки:

   if (e.OldElement != null) {
    // Отписаться от событий и очистить ресурсы (если необходимо) 
   }

и

   if (e.NewElement != null) {
    // Здесь можно настроить вид контрола и подписаться на события
   }

Если вам нужно только просто немного изменить дизайн, то вы можете по простому использовать Control != null

e.Old или e.NewElement — это элемент управления Xamarin.Forms, который прорисовывается с помощью renderer. Например Entry или Button.

Control – это версия элемента управления, которая используется текущей платформой. Можно сказать родной для платформы контрол (вы же помните, что Xamarin.Forms связывает свои классы с родными для платформ классами).

Например, для UWP родным аналогом Entry будет TextBox. Для iOS аналог Entry это UITextView, а для Android – EditText.

Или же взять контрол Xamarin.Forms под названием DatePicker. Для UWP его родным аналогом будет класс с таким же названием — DatePicker. На iOS будет прорисован контрол под названием UITextField, а на Android – EditText.

Даже в довольно объемной статье сложно раскрыть полностью тему разработки на Xamarin. Интерфейс Xamarin.Forms не особо далеко ушел от WPF и UWP. Поэтому знакомые классы C# позволяют начать разработку практически сразу. Да и разработчики платформы стараются сделать ее более привычной. К примеру, у приложений Xamarin довольно схожий с UWP life cycle.

Приложение точно также может переходить в состояние suspended (sleep). Для обработки можно использовать события: OnStart, OnSleep, OnResume.

Через какое-то время, после окончательного перехода на .NET Standard и XAML Standard разработчикам C# станет еще более комфортно работать с различными технологиями из стека языка.
Tags:
Hubs:
+11
Comments 6
Comments Comments 6

Articles