Мій досвід з MAUI. Створюємо прапорець з текстом за допомогою ContentView
Усім привіт, мене звати Ігор Кравченко, я маю декілька років досвіду роботи з .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 і використовувати без повторення однакового функціоналу.
Ось декілька кроків, щоб створити власний елемент керування, використовуючи готові елементи інтерфейсу:
- Створюємо
ContentView
, у візуальній частині якого додаємо потрібні елементи. Якщо потрібно, підписуємось на події цих внутрішніх елементів та встановлюємо їм назви за допомогоюx:Name
в xaml, щоб мати до них доступ з кодової частини. - Додаємо потрібні властивості та
BindableProperties
для керування елементом інтерфейсу. - Втілюємо потрібний функціонал у методах-делегатах.
- Використовуємо створений
ContentView
у будь-якому місці застосунку.
Але у нас є можливість додати текст до CheckBox
іншим способом. Більш елегантним. Використовуючи нативні засоби.
Насправді, єдина причина, з якої в MAUI відсутня властивість Text
для CheckBox
з коробки, — це iOS. В нативному CheckBox
для iOS відсутній текст.
Тому команда MAUI вирішила не напружуватись і додала CheckBox
з мінімальним API, яке присутнє для всіх платформ. Така собі плата за універсальність.
Але ми можемо це виправити. За допомогою Handlers. І зробимо ми це в наступній статті.
Дякую за увагу.
20 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів