Мій досвід з MAUI. Створюємо прапорець з текстом за допомогою ContentView

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.

Усім привіт, мене звати Ігор Кравченко, я маю декілька років досвіду роботи з .NET MAUI та Xamarin Forms. У минулій статті ми розглядали, як в MAUI створити власний елемент керування на основі нативного. Сьогодні ми розглянемо, як створити власний компонент, використовуючи наявні елементи керування.

Зробимо це на прикладі прапорця CheckBox. Річ у тім, що стандартний прапорець від MAUI не має властивості Text. Тобто, щоб показати текст біля прапорця, потрібно написати щось таке:

<HorizontalStackLayout>
        <CheckBox IsChecked="False"/>
        <Label Text="CheckBox in StackLayout"
               VerticalTextAlignment="Center"/>
    </HorizontalStackLayout>

Або таке:

<Grid ColumnDefinitions="Auto, *">
        <CheckBox Grid.Column="0"
                  IsChecked="False"/>
        <Label Grid.Column="1"
               Text="CheckBox in Grid"
               VerticalTextAlignment="Center"/>
    </Grid>

В обох випадках прапорець і текст — це окремі компоненти, загорнуті в якийсь макет.

Якщо ви натиснете на текст, то прапорець не змінить свого стану, бо текст представлений елементом Label. А змушувати користувача цілитись точно у квадратик не хотілось би.

Тому доведеться додати розпізнавання жестів для Label.

<HorizontalStackLayout>
        <CheckBox x:Name="CheckBox"
                  IsChecked="False"/>
        <Label Text="CheckBox in StackLayout"
               VerticalTextAlignment="Center">
            <Label.GestureRecognizers>
                <TapGestureRecognizer Tapped="OnTextTapped"/>
            </Label.GestureRecognizers>
        </Label>
    </HorizontalStackLayout>

В коді сторінки для події натискання на текст буде приблизно такий код:

private void OnTextTapped(object sender, TappedEventArgs e) =>
        CheckBox.IsChecked = !CheckBox.IsChecked;

Якщо прив’язати за допомогою MVVM до прапорця якусь властивість, то звісно це буде працювати.

Але уявіть, що таких прапорців у вас на сторінці буде декілька. А якщо таких сторінок багато? Було б не дуже зручно кожен раз додавати StackLayout заради прапорця з текстом, а також створювати метод обробки події для кожного Label.

Можна зробити по-іншому. Ми можемо створити окремий компонент на основі ContentView, який матиме в собі вищезазначений код.

Для цього створимо новий ContentView з назвою TextCheckBox.

Тепер весь функціонал з текстом і прапорцем ми можемо реалізувати в одному місці. Візуальна частина:

<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MauiApp1.TextCheckBox">
    <HorizontalStackLayout>
        <CheckBox x:Name="CheckBox"
                  IsChecked="False"
                  CheckedChanged="OnCheckedChanged"/>
        <Label x:Name="Label"
               Text="CheckBox in StackLayout"
               VerticalTextAlignment="Center">
            <Label.GestureRecognizers>
                <TapGestureRecognizer Tapped="OnTextTapped"/>
            </Label.GestureRecognizers>
        </Label>
    </HorizontalStackLayout>
</ContentView>

Додамо дві основні властивості Text (напис біля прапорця) та IsChecked (стан прапорця). Потрібно реалізувати їх так, щоб можна було прив’язувати властивості з моделі (MVVM). Для цього використовуємо BindableProperty. В частині з кодом напишемо:

public static readonly BindableProperty TextProperty =
            BindableProperty.Create(nameof(Text), typeof(string), typeof(TextCheckBox), default, propertyChanged: OnTextChanged);
    public static readonly BindableProperty IsCheckedProperty =
            BindableProperty.Create(nameof(IsChecked), typeof(bool), typeof(TextCheckBox), default, propertyChanged: OnIsCheckedChanged);

    public string Text
    {
        get => (string)GetValue(TextProperty);
        set => SetValue(TextProperty, value);
    }

    public bool IsChecked
    {
        get => (bool)GetValue(IsCheckedProperty);
        set => SetValue(IsCheckedProperty, value);
    }

Далі додаємо методи, які викликаються автоматично при зміні цих властивостей.

private static void OnTextChanged(BindableObject bindable, object oldValue, object newValue)
    {
        if (!(bindable is TextCheckBox textCheckBox))
            return;
        if (!(newValue is string value))
            return;
        textCheckBox.Label.Text = value;
    }


    private static void OnIsCheckedChanged(BindableObject bindable, object oldValue, object newValue)
    {
        if (!(bindable is TextCheckBox textCheckBox))
            return;
        if (!(newValue is bool value))
            return;
        textCheckBox.CheckBox.IsChecked = value;
    }

Делегати цих методів ви могли побачити в методі Create для BindableProperty.

Коротке пояснення, що тут взагалі відбувається.

Наша ціль — зробити так, щоб до властивостей створеного елемента керування можна було прив’язувати властивості з моделі. В MAUI для цього використовується механізм Binding, який базується на інтерфейсі INotifyPropertyChanged. Цей інтерфейс ми маємо реалізувати на боці моделі.

А з боку елемента керування нам потрібно, щоб властивість базувалась на полі з типом BindableProperty. Значення властивості ми дістаємо з такого поля через метод GetValue та записуємо через метод SetValue.

Також при створенні BindableProperty ми маємо вказати назву властивості, її тип, тип елемента керування та значення за замовчуванням. Додатково можна вказати функції, які будуть викликані під час зміни властивості (ними ми і скористались).

Отже, коли змінюється властивість Text або IsChecked, ми вручну змінюємо відповідні властивості для Label та CheckBox. Саме ця робота виконується в методах OnTextChanged та OnIsCheckedChanged.

Також додаємо методи обробки подій, які ми вказали в xaml-частині: натискання на текст та зміна стану прапорця.

private void OnTextTapped(object sender, TappedEventArgs e) => IsChecked = !IsChecked;
private void OnCheckedChanged(object sender, CheckedChangedEventArgs e) => IsChecked = CheckBox.IsChecked;

В першому випадку змінюємо значення властивості на протилежне. А в другому — встановлюємо їй нове значення.

Ось повний код класу TextCheckBox:

public partial class TextCheckBox : ContentView
{
    public static readonly BindableProperty TextProperty =
            BindableProperty.Create(nameof(Text), typeof(string), typeof(TextCheckBox), default, propertyChanged: OnTextChanged);
    public static readonly BindableProperty IsCheckedProperty =
            BindableProperty.Create(nameof(IsChecked), typeof(bool), typeof(TextCheckBox), default, propertyChanged: OnIsCheckedChanged);

    public string Text
    {
        get => (string)GetValue(TextProperty);
        set => SetValue(TextProperty, value);
    }

    public bool IsChecked
    {
        get => (bool)GetValue(IsCheckedProperty);
        set => SetValue(IsCheckedProperty, value);
    }

    public TextCheckBox()
    {
        InitializeComponent();
    }

    private static void OnTextChanged(BindableObject bindable, object oldValue, object newValue)
    {
        if (!(bindable is TextCheckBox textCheckBox))
            return;
        if (!(newValue is string value))
            return;
        textCheckBox.Label.Text = value;
    }

    private static void OnIsCheckedChanged(BindableObject bindable, object oldValue, object newValue)
    {
        if (!(bindable is TextCheckBox textCheckBox))
            return;
        if (!(newValue is bool value))
            return;
        textCheckBox.CheckBox.IsChecked = value;
    }

    private void OnTextTapped(object sender, TappedEventArgs e) => IsChecked = !IsChecked;
    private void OnCheckedChanged(object sender, CheckedChangedEventArgs e) => IsChecked = CheckBox.IsChecked;
}

Тепер можна використовувати наш TextCheckBox.

Змінимо xaml-частину сторінки:

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:MauiApp1"
             x:Class="MauiApp1.MainPage"
             Title="Text">
    <VerticalStackLayout>
        <local:TextCheckBox IsChecked="{Binding IsHelloWorldOn, Mode=TwoWay}"
                            Text="I am CheckBox from ContentView"/>
        <Label Text="{Binding Status, Mode=OneWay}"/>
    </VerticalStackLayout>
</ContentPage>

Внизу я додав Label, щоб додатково показати стан властивості моделі.

Частину з кодом змінимо наступним чином:

private bool _isHelloWorldOn;

    public MainPage()
	{
		InitializeComponent();
        BindingContext = this;
	}

    public string Status => IsHelloWorldOn ? $"IsChecked: True" : $"IsChecked: False";

    public bool IsHelloWorldOn
    {
        get => _isHelloWorldOn;
        set
        {
            if (_isHelloWorldOn == value)
                return;
            _isHelloWorldOn = value;
            OnPropertyChanged(nameof(IsHelloWorldOn));
            OnPropertyChanged(nameof(Status));
        }
    }

Приблизно так властивість IsChecked буде виглядати в моделі.

В конструкторі ми встановили BindingContext = this, тобто контекстом з даними для сторінки буде слугувати вона сама.

Звісно, в даному випадку я зробив це для спрощення. В ідеалі, ми маємо використовувати MVVM, а контекстом має бути ViewModel.

Результат:

Натискаючи на прапорець або текст, змінюємо значення властивості IsHelloWorldOn.

Тепер нам непотрібно писати багато StackLayout або Grid з прапорцем і текстом всередині. Це все тепер спаковано в TextCheckBox, який займає два рядки в xaml.

Отже, якщо у вас в проєкті багато повторюваних елементів, ви можете запакувати їх в один за допомогою ContentView і використовувати без повторення однакового функціоналу.

Ось декілька кроків, щоб створити власний елемент керування, використовуючи готові елементи інтерфейсу:

  1. Створюємо ContentView, у візуальній частині якого додаємо потрібні елементи. Якщо потрібно, підписуємось на події цих внутрішніх елементів та встановлюємо їм назви за допомогою x:Name в xaml, щоб мати до них доступ з кодової частини.
  2. Додаємо потрібні властивості та BindableProperties для керування елементом інтерфейсу.
  3. Втілюємо потрібний функціонал у методах-делегатах.
  4. Використовуємо створений ContentView у будь-якому місці застосунку.

Але у нас є можливість додати текст до CheckBox іншим способом. Більш елегантним. Використовуючи нативні засоби.

Насправді, єдина причина, з якої в MAUI відсутня властивість Text для CheckBox з коробки, — це iOS. В нативному CheckBox для iOS відсутній текст.

Тому команда MAUI вирішила не напружуватись і додала CheckBox з мінімальним API, яке присутнє для всіх платформ. Така собі плата за універсальність.

Але ми можемо це виправити. За допомогою Handlers. І зробимо ми це в наступній статті.

Дякую за увагу.

👍ПодобаєтьсяСподобалось5
До обраногоВ обраному1
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

дякую автору за роботу)!
користуючись оказією запитаю©, чи є в мауі із коробки готові контроли для роботи з форматованим текстом? в впф і вінформах є RichTextBox, ще в впф є DocumentViewer, в принципі це все. і всі вони такі криві, що хочеться плакать.

перепрошую що втрутився, додам коментар в + до Flutter. В Flutter SDK є потужний річ текст який дозволяє додавати навіть віджети (вюхт) між текстом

приходь коли флутер навчиться в аддіни до поверпоїнту і іншого мікрософтофісу)

ось і вся справа, я не люблю продукцію мікрософта, а особливо сварюся коли постійно вилогінює з тімса :) слек так не робить :)

так ніхто не любить) але ж ми тут для того, щоб за гроші вирішувати проблеми наших користувачів і робити їхню роботу простішою і приємнішою)

до речі, в якості петпроекту пропоную перевірити велич флутер сдк на типовому реальному тексті від пересічного користувача.

береш сторінку красівого англійського тексту, можна навіть шекспіра якогось. вставляєш всередину чотириповерховий нумератор із різними стилями на кожному рівні. вставляєш фрагмент тексту в три колонки, в одну з них сунеш таблицю, в таблиці звичайно ж текст, нумератори, ітематори і кілька слів арабською. далі цю сторінку тексту розпізнаєш оцр-ом із підключеними німецьким і італійським словниками (без англійської). результат розпізнавання трохи редагуєш, хаотично додаєш декілька вертикальних табуляцій, section brake-ів, нерозривних пробілів і неюнікодних символів (скажімо, із шрифтів webdings та symbol) і кросс-посилання на слово, написане івритом. робиш із цього компоту ртф — і вуаля. результат повністю відповідає стандарту ртф, із ним можна працювати засобами ненависного мікрософту... але що це? чому ж не виходить його використати як начинку для контролів? як же так? чи флутер сдк зуміє;)?

можливо описаний спосіб отримання готового документу виглядає диким, але це _реальний_ приклад того, з чим насправді доводиться працювати.

Тут люди це обговорюють. github.com/...​net/maui/discussions/8087
Не схоже, щоб таке було в MAUI. Але MAUI для Windows використовує WinUI. А WinUI це така хитра надбудова над чимось ще, деталей вже не пам’ятаю. Якщо в цьому WinUI є RichTextBox, то ви зможете його використати в MAUI через Handler.

Щось таки є. learn.microsoft.com/...​gn/controls/rich-edit-box

дякую! цей rich edit box виглядає ідентично до контролу DocumentViewer, скоріш за все це він і є)

краще написати апку на найтіві java/kotlin або flutter ніж на цьому костилі

Костиль у чому? =)
Чим тоді Flutter не костиль?

Поясню свою думку. Потрібно це було відразу написати. Отже я вважаю, що:

1. Найкраще — це нативна розробка. Досвідчений Android або iOS девелопер знає багато нюансів фреймворка, його компонентів і взаємодії з ними, лайфсайклу, налаштування білд системи, оптимазації коду та аутпут файлів, оперування мовою програмування тощо. Для нього є зрозумілим динаміка, актуальність і напрям розвитку фреймворка. Наприклад Google має потужні гайдлайни щодо побудови вдалої/правильної клієнт-серверної (і не тільки) архітектури додатків. Для мене є зрозумілим що додатки на нейтіві будуть існувати завжди допоки існує ОС.

2. Flutter — безумовно крутий фреймворк особливо для клієнт-серверних додатків. Для ходіння в інтернет і парсання джейсонів — це ідеальне рішення. В його SDK вже неймовірна к-ть графічних Матіріал/Купертіно віджетів, які подекуди навіть на нейтіві реалізувати важче. Зрозуміла деревовидна структура написання коду, хот релоад, інспектор віджетів — це все топ. Свій двигунець для відмальовування графіки.

Я впевнений що додаток написаний рівними руками по перформенсу і всяких fps-ах виграє інші короспатформенні солюшини. Наприклад за 10-15хв можна запрототипити застосунок з Google картами чи OSM відразу на Android/iOS/Windows/MacOS i Web.
В мене особисто є багато запитань до 3rd-party бібліотек, якість яких не завжди найкраща (можливо тому, що їх пишуть не девелопери які «шарять» нейтів, деталі які я згадував вище, а саме флатеристи).

IMHO як AOSP/Android SE:
ReactNative/Xamarin/Ionic/апки на WebView i оте все — це пратично мертві штуки, які вистрілюють на кілька років до тих пір поки щось не зявиться краще (flutter наприклад). Хайлевел фреймворк — це завжди біль з лібами і резолванням їх версій, білд системою тощо.
+ купа враперів, бріджів, мостів, костилів щоб зробити чекбокс з текстом
+ мене особисто трігерить додатки які важать багато за рахунок *.so-шок які треба/не треба але напхані в APK/AAR-ки.
+ поріг входження в «Flutter» — думаю «трошки» швидший ніж в Xamarin чи ReactNative. Якось пробував швидко щось скомпілити на реакті а до того на за(кса)маріні — не вийшло бо нічого не понятно.

Не чув що якийсь крутий софт написаний на за(кса)маріні. Якщо такій апці 3-4 роки то — скоріш за все такий додаток вже переписали на нейтів чи флатер через «не можливість супортити такий продукт бо якась ліба вже не існує/підтримується».

Давайте візьмемо наприклад Інстаграм — він на реакт нейтіві, але виглядає він убого, купа багів.Чому він важить так багато? На скільки там оптимальний код?

Давайте спочатку.
Кросплатформові додатки можна поділити на такі (можливо, в деяких прикладах я помилився, виправте, якщо не так):
— ті, що використовують нативні компоненти (Xamarin/MAUI, React Native);
— ті, що використовують власний рендерінг (Flutter, Avalonia);
— гібридні — ті, що побудовані на WebView (PhoneGap, Ionic).
У кожного підходу є свої плюси та мінуси. Думаю, ви самі про них знаєте. Але розглянемо кожний.

Перший підхід.
Плюси: майже нативна швидкість, можливість використання нативних функцій.
Мінуси: потреба в створенні абстракцій над нативними компонентами та функціями.
Саме цей мінус багатьох відлякує. Я не знаю, як у React Native, але вам може здатись, що в MAUI невеликий вибір компонентів. Це не зовсім так. MAUI містить в собі стільки компонентів, скільки розробникам Microsoft не було лінь загорнути в абстрактні компоненти. Наприклад, у CheckBox відсутній текст, бо для iOS потрібно було трішки щось доробити. Або відсутній якийсь компонент з material. І багатьох лякає, що потрібно щось доробляти, щоб створити якийсь крутий дизайн. Але тут немає нічого страшного. Бо MAUI це обгортка над нативною платформою. Якщо вам чогось не вистачає, ви маєте усі інструменти, щоб це додати. І тут, до речі, ви можете отримати навички роботи з нативною платформою, використовуючи C#. Це взагалі круто для .NET-розробників.
Так, можливо, доведеться попрацювати над кастомізацією компонентів або над створенням нових, використовуючи нативні. Але такого коду у вас буде не більше 10%. Ви один раз напишете handler (або renderer у Xamarin) і будете це використовувати.

Другий підхід.
Плюси: власний рендерінг. Мінусів не знаю, бо не працював з такими технологіями.
Так, власний рендерінг дає якусь гнучкість. Але фактично потрібно усі компоненти створити з нуля. Тобто, сіли розробники Google і почали створювати кожний компонент. Хтось казав, що у них і Material, і Cupertino з коробки підтримуються. Але це ж треба було комусь намалювати. Тобто тут питання не у тому, що якась технологія хороша, а якась погана. А питання в тому, скільки ресурсів було використано, щоб створити усі компоненти з нуля за допомогою графічної бібліотеки (як зробили в google) або щоб зробити обгортки над нативними компонентами (як зробили Microsoft). А якщо вам потрібний і не купертіно, і не матеріал, то знову доведеться щось своє малювати?

Тобто, тут вже ви обираєте, що вам більше подобається. Мені, наприклад, не дуже подобається, як виглядає «верстка» в Dart. Але я не приходжу до Flutter-розробників і не кажу їм, що не бачу сенсу існування Flutter. Я так само міг би сказати, що Flutter це костиль, бо рендерить власним рушієм. Але я ж так не кажу, бо тоді усе можна буде назвати костилями.

Третій підхід.
Наскільки я розумію, він є трохи застарілим, бо всі намагаються від нього піти.
Плюсами мабуть є використання html. Але мінуси — це важкодоступність нативних функцій.

Отже, у кожного є свої плюси та мінуси. Звісно, що повністю нативні технології (Java, Kotlin, Objective C, Swift) швидші за кросплатформові. Але чи завжди нам потрібні переваги, які надають нативні технології? А якщо у нас звичайний інтернет-магазин або якийсь додаток для бізнесу зі звичайними переліками та формами, який не потребує ніякого особливого нативного функціоналу? А якщо у нас багато бізнес-логіки на клієнті? Це все доведеться писати на різних мовах, підтримувати. Тобто тут ми вже враховуємо не тільки наші вподобання, а також інфраструктуру нашої системи. Якщо у мене інфраструктура на .NET, то скільки усіляких моделей доведеться переписати на Kotlin та Swift або навіть на кросплатформовий Dart?

Чесно кажучи я не розумію для яких потреб кса(за)марін ще використовується. Поясніть, будь ласка, дуже цікаво почути вашу думку.

Підхід, що використовує Xamarin або MAUI не є поганим. Він просто інший. Кожний обирає технологію судячи із власних уподобань та інфраструктури власної системи.
Саме тому я й пишу статті, щоб люди змогли побачити MAUI в реальній роботі.
А Xamarin та MAUI використовувались мною для створення клієнтських додатків для облікової системи.
P. S. Мені не хотілося б, щоб коментарі перетворювались у MAUI vs Flutter vs React Native, Java vs .NET, Android vs iOS та інші суперечки.

І ще... вище лишили сніпет з кодом чекбокса, там є прикольний online-compiler — це теж плюс.

Рекомендую спробувати побавитись з анімаціями на Flutter, має сподобатись. Їх реалізація в рази легша і приємніша ніж на нейтіві. Це теж важливий аргумент.

IMHO:
Зробити фенсі-Figma UI на Flutter — значно простіше ніж на за(кса)маріні.
so-шка двигунця флатера важить, якщо не помиляюсь 6 чи 12МБ (для андроїд). Це означає, що в принципі додатки можна робити відносно не вел. розміру. Для мене ред флег коли апка важить якісь сотні мегабайтів. В мене питання що туди можна було напхати....

Чесно кажучи я не розумію для яких потреб кса(за)марін ще використовується. Поясніть, будь ласка, дуже цікаво почути вашу думку.

Вот вам еще пример с генерацией и распознаванием штрихкодов
docs.aspose.com/...​-maui-mobile-application

Підписатись на коментарі