Мій досвід з MAUI. Створюємо діалог за допомогою Handlers

Усі статті, обговорення, новини про Mobile — в одному місці. Підписуйтеся на телеграм-канал!

Усім привіт, мене звати Ігор Кравченко, я маю декілька років досвіду роботи з .NET MAUI та Xamarin Forms. Але у цій статті не розповідатиму про те, що таке MAUI і його основи. Зроблю це іншим разом. Я перейду одразу до справи: сьогодні ми розглянемо, як створювати власні компоненти на основі нативних компонентів платформ.

Введення

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

До речі, є група ентузіастів, яка для MAUI робить набір елементів, що теж малюються за допомогою графіки. Тобто, хлопці вирішили спробувати Flutter-підхід у MAUI і начебто щось зробили. Якщо ж розглянути Apache Cordova, то ця технологія малює інтерфейс у web-застосунку за допомогою html, а потім маскується під нативний застосунок.

У кожного підходу є свої плюси та мінуси. Мене влаштовує те, як це зроблено в MAUI, і зараз я покажу, як з цим працювати.

Основи

В MAUI було введено поняття Handler. Це клас, який пов’язує віртуальний елемент інтерфейсу (той, що використовується в xaml) і спеціалізований елемент платформи. Handler є аналогом Renderer у Xamarin.Forms.

Коротке пояснення, як це працює.

Є загальнодоступний клас Button, який використовується в XAML. Для нього під кожну платформу є свій клас Handler, в якому відбувається взаємодія з нативною кнопкою. Ось і все.

Я полюбляю Material Design, тому розглянемо приклад створення вікна діалогу у стилі Material. Але лише для Android, бо на iOS у мене не було часу =)

Також в цей діалог ми зможемо додавати власний вміст (кнопки, написи та інше). Щоб створити свій елемент інтерфейсу, потрібно створити загальнодоступний клас (наприклад, Dialog) з інтерфейсом (IDialog), де описати властивості, притаманні цьому елементу керування.

Створений інтерфейс (IDialog) має наслідувати інтерфейс IView. А клас (Dialog) має реалізувати цей інтерфейс (щоб не реалізовувати властивості інтерфейса IView власноруч, наслідуємо Dialog від View). Тобто виходить паралельна ієрархія класів та інтерфейсів.

Ось наш інтерфейс.

public interface IDialog : IView
    {
        string Title { get; set; }
        string Accept { get; set; }
        string Cancel { get; set; }
        string Neutral { get; set; }
        View Content { get; set; }
        void SendAcceptClicked();
        void SendCancelClicked();
        void SendNeutralClicked();
        void SendDismissed();
        void SendAppeared();
        void SendDisappeared();
        void Show();
        void Close();
    }

Тепер потрібно реалізувати його у класі Dialog.

Дуже примітивний огляд механізму Binding

Маленьке відхилення від теми. В MAUI ми активно використовуємо BindableProperty. Це статична властивість, яка дозволяє використовувати прив’язку даних. Binding та MVVM це основи MAUI, про які я детально розповім наступного разу. Зараз лише обмежусь швидким оглядом.

Якщо коротко, то в XAML ми можемо прив’язати дані до елементу керування за допомогою спеціального механізму. Розглянемо це на прикладі стандартного поля вводу Entry. Ми можемо прив’язати дані до елементу керування наступним чином: <Entry Text=”{Binding Name}”/>в XAML або entry.SetBinding(Entry.TextProperty, "Name"); в коді (далі я буду наводити приклади лише з XAML). Це означає, що при введенні тексту в поле властивість буде змінюватись автоматично.

Але для того, щоб воно працювало, потрібно дещо зробити як на боці даних, так і на боці елементу керування. З боку даних у нас має бути клас, що реалізує інтерфейс INotifyPropertyChanged (в патерні проектування MVVM такий клас називається ViewModel). Зазвичай реалізація інтерфейсу у 90% випадках виглядає якось так:

readonly WeakEventManager _propertyChangedEventManager = new WeakEventManager();
        public event PropertyChangedEventHandler PropertyChanged
        {
            add => _propertyChangedEventManager.AddEventHandler(value);
            remove => _propertyChangedEventManager.RemoveEventHandler(value);
        }
        protected bool SetProperty<T>(ref T backingStore, T value, [CallerMemberName] string propertyName = "", Action onChanged = null)
        {
            if (EqualityComparer<T>.Default.Equals(backingStore, value))
                return false;
            backingStore = value;
            onChanged?.Invoke();
            OnPropertyChanged(propertyName);
            return true;
        }
        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = "") =>
            _propertyChangedEventManager.HandleEvent(this, new PropertyChangedEventArgs(propertyName), nameof(PropertyChanged));

Якщо з боку даних нам потрібно реалізувати інтерфейс INotifyPropertyChanged, то з боку елементу керування ми маємо властивість Text типу string, яка обгортає іншу властивість TextProperty типу BindableProperty.

public static readonly BindableProperty TextProperty = BindableProperty.Create(nameof(Text), typeof(string), typeof(IEntry), default);
public string Text
        {
            get => (string)GetValue(TextProperty);
            set => SetValue(TextProperty, value);
        }

Саме через присутність цієї BindableProperty наш XAML дозволить зробити прив’язку.

Отже, повернімось до нашого діалогу.

Віртуальний елемент керування

Створимо клас Dialog, який втілює функціонал з інтерфейсу IDialog.

[ContentProperty(nameof(Content))]
    public class Dialog : View, IDialog
    {
        public static readonly BindableProperty ContentProperty =
            BindableProperty.Create(nameof(Content), typeof(View), typeof(IDialog), default(View), propertyChanged: OnContentChanged);
        public static readonly BindableProperty TitleProperty =
            BindableProperty.Create(nameof(Title), typeof(string), typeof(IDialog), default);
        public static readonly BindableProperty AcceptProperty =
            BindableProperty.Create(nameof(Accept), typeof(string), typeof(IDialog), default);
        public static readonly BindableProperty CancelProperty =
            BindableProperty.Create(nameof(Cancel), typeof(string), typeof(IDialog), default);
        public static readonly BindableProperty NeutralProperty =
            BindableProperty.Create(nameof(Neutral), typeof(string), typeof(IDialog), default);
        
public View Content
        {
            get => (View)GetValue(ContentProperty);
            set => SetValue(ContentProperty, value);
        }
        public string Title
        {
            get => (string)GetValue(TitleProperty);
            set => SetValue(TitleProperty, value);
        }
        public string Accept
        {
            get => (string)GetValue(AcceptProperty);
            set => SetValue(AcceptProperty, value);
        }
        public string Cancel
        {
            get => (string)GetValue(CancelProperty);
            set => SetValue(CancelProperty, value);
        }
        public string Neutral
        {
            get => (string)GetValue(NeutralProperty);
            set => SetValue(NeutralProperty, value);
        }
        
static void OnContentChanged(BindableObject bindable, object oldValue, object newValue)
        {
            if (bindable is Dialog dialog)
                dialog.OnBindingContextChanged();
        }
        protected override void OnBindingContextChanged()
        {
            base.OnBindingContextChanged();
            if (Content != null)
                SetInheritedBindingContext(Content, BindingContext);
        }
        
public event EventHandler AcceptClicked;
        public event EventHandler CancelClicked;
        public event EventHandler NeutralClicked;
        public event EventHandler Dismissed;
        public event EventHandler Appeared;
        public event EventHandler Dissapeared;
        
public void SendAcceptClicked() => AcceptClicked?.Invoke(this, EventArgs.Empty);
        public void SendCancelClicked() => CancelClicked?.Invoke(this, EventArgs.Empty);
        public void SendNeutralClicked() => NeutralClicked?.Invoke(this, EventArgs.Empty);
        public void SendDismissed() => Dismissed?.Invoke(this, EventArgs.Empty);
        public void SendAppeared()
        {
            OnAppearing();
            Appeared?.Invoke(this, EventArgs.Empty);
        }
        public void SendDisappeared()
        {
            OnDissapearing();
            Dissapeared?.Invoke(this, EventArgs.Empty);
        }
        public void Show()
        {
            this.ToHandler(App.Current.MainPage.Handler.MauiContext);        
            Handler?.Invoke(nameof(IDialog.Show));
        }
        protected TaskCompletionSource ShowTaskCompletionSource { get; private set; }
        public Task ShowAsync()
        {
            ShowTaskCompletionSource = new TaskCompletionSource();
            Show();
            return ShowTaskCompletionSource.Task;
        }
        public void Close()
        {
            Handler?.Invoke(nameof(IDialog.Close));
        }
        protected virtual void OnAppearing()
        {
        }
        protected virtual void OnDissapearing()
        {
            ShowTaskCompletionSource?.SetResult();
        }
    }

З приводи структури. Цей клас та інтерфейс я розмістив у теці Controls в корені проєкту. Саме клас Dialog ми будемо використовувати у нашому проєкті. В ньому ми описали логіку роботи діалогу лише на рівні даних. Робота з нативною частиною буде виконуватись у handlers. Код для різних платформ я зазвичай розділяю за допомогою назви файлу. Можна це також робити за назвами тек. Детальніше тут.

Створення загального DialogHandler

Я створив теку Handlers, у якій для кожного елементу керування матиму свою теку з handlers. Створимо загальний клас-handler та інтерфейс для нього.

#if IOS || MACCATALYST
using PlatformView = UIKit.UIView;
#elif ANDROID
using PlatformView = AndriyCo.Tcu.MauiClient.Platforms.Android.AlertDialogView;
#elif WINDOWS
using PlatformView = Microsoft.UI.Xaml.Controls.Border;
#elif (NETSTANDARD || !PLATFORM) || (NET6_0_OR_GREATER && !IOS && !ANDROID)
using PlatformView = System.Object;
#endif

namespace AndriyCo.Tcu.MauiClient.Handlers
{
    public interface IDialogHandler : IViewHandler
    {
        new IDialog VirtualView { get; }
        new PlatformView PlatformView { get; }
    }
    
public partial class DialogHandler : IDialogHandler
    {
        public static IPropertyMapper<IDialog, IDialogHandler> Mapper = new PropertyMapper<IDialog, IDialogHandler>()
        {
            [nameof(IDialog.Title)] = MapTitle,
            [nameof(IDialog.Accept)] = MapAccept,
            [nameof(IDialog.Cancel)] = MapCancel,
            [nameof(IDialog.Neutral)] = MapNeutral,
            [nameof(IDialog.Content)] = MapContent,
        };
        
public static CommandMapper<IDialog, IDialogHandler> CommandMapper = new CommandMapper<IDialog, IDialogHandler>(ViewCommandMapper)
        {
            [nameof(IDialog.Show)] = MapShow,
            [nameof(IDialog.Close)] = MapClose
        };
        public DialogHandler() : base(Mapper, CommandMapper)
        {
        }
    }
}

Розгляньмо все детальніше. Вгорі визначаємо тип PlatformView. Це і є нативний елемент керування, різний для кожної платформи. Для iOS та Windows я ще не робив реалізацію діалогу (не встиг), тому поки що звертайте увагу лише на другу та останню умови. Якщо це Android, то використовуємо клас AlertDialogView, який я створив раніше. Розглянемо його трохи ближче.

Оскільки це елемент, що стосується лише платформи Android, то я розмістив його у теці Platforms/Android.

Ось, як він виглядає. Я на всяк випадок буду вказувати деякі using, щоб ви не шукали і не гадали, який namespace потрібно використати.

using Android.Content;
using AndroidX.AppCompat.App;
using Google.Android.Material.Dialog;
namespace AndriyCo.Tcu.MauiClient.Platforms.Android
{
    //Это нужно убрать
    public class AlertDialogView : global::Android.Views.View
    {
        public MaterialAlertDialogBuilder Builder { get; }
        public AlertDialogView(Context context) : base(context)
        {
            Builder = new MaterialAlertDialogBuilder(context);
        }
    }
}

Коментар «Это нужно убрать» та необхідність створення цього класу я поясню трохи пізніше.

Повернемось до нашого DialogHandler. Вгорі в останній умові вказано, що якщо це бібліотека або одна з платформ, то використовуємо нативний компонент як System.Object (найбазовіший клас для .NET-типів). Навіщо це потрібно, я насправді не знаю. Але воно має бути і працює.

Далі декларуємо інтерфейс з двома властивостями:

new IDialog VirtualView { get; }
new PlatformView PlatformView { get; }

Перше — це наш віртуальний елемент керування, який ми визначили від самого початку. Насправді, не обов’язково взагалі створювати інтерфейс. Але для гнучкості і краси ми чогось так робимо.

Друге — це нативний елемент керування, який ми тільки що визначили вгорі.

Зверніть увагу, що обидві властивості ми визначаємо через new. Бо, як можна здогадатись, базовий інтерфейс IViewHandler вже має ці властивості з базовішими типами (VirtualView це Microsoft.Maui.IView, а PlatformView це System.Object).

Далі ми створюємо сам клас DialogHandler, який наслідує інтерфейс IDialogHandler. Зверніть увагу на partial. Нам це потрібно, щоб реалізувати такий клас для кожної платформи.

У цьому класі ми визначили ще дві статичні властивості: Mapper та CommandMapper. Саме за допомогою цих штук ми з’єднуємо властивості та методи віртуального елементу керування (в даному випадку IDialog) з властивостями та методами нативного елементу керування (наприклад, AlertDialogView для Android).

[nameof(IDialog.Title)] = MapTitle

Але поки що ми лише декларували це «з’єднання». Щоб воно працювало, потрібно створити реалізацію цих map-методів (MapTitle, MapAccept, та інші). Якщо ви зараз додали цей клас у свій проєкт, то вам насипе купу помилок, бо ці методи у вас поки що відсутні.

Нижче знаходяться конструктори для handler, які за необхідністю MAUI викликатиме власноруч. До речі, перший конструктор здається не є обов’язковим. Чого я його додав, вже не пам’ятаю.

Тепер нам потрібно додати власне реалізацію цих map-методів для кожної платформи.

Створення DialogHandler.Android для взаємодії з нативним діалогом від Android

Додамо файл DialogHandler.Android.cs.

using AndroidX.AppCompat.App;
using Microsoft.Maui.Handlers;
using Microsoft.Maui.Platform;
public partial class DialogHandler : ViewHandler<IDialog, AlertDialogView>
    {
        protected override AlertDialogView CreatePlatformView() =>
            new AlertDialogView(Context);
        private AlertDialog AlertDialog { get; set; }

        protected override void DisconnectHandler(AlertDialogView platformView)
        {
            base.DisconnectHandler(platformView);
            AlertDialog.CancelEvent -= OnCancel;
            AlertDialog.ShowEvent -= OnShown;
            AlertDialog.DismissEvent -= OnDismissed;
        }
        private void OnCreated()
        {
            AlertDialog.CancelEvent += OnCancel;
            AlertDialog.ShowEvent += OnShown;
            AlertDialog.DismissEvent += OnDismissed;
        }
        
private void OnDismissed(object sender, EventArgs e) => VirtualView.SendDisappeared();
        private void OnShown(object sender, EventArgs e) => VirtualView.SendAppeared();
        private void OnCancel(object sender, EventArgs e) => VirtualView.SendDismissed();
        
public static void MapTitle(IDialogHandler handler, IDialog virtualView) =>
            handler.PlatformView.Builder.SetTitle(virtualView.Title);
        public static void MapAccept(IDialogHandler handler, IDialog virtualView) =>
            handler.PlatformView.Builder.SetPositiveButton(virtualView.Accept, (sender, args) => virtualView.SendAcceptClicked());
        public static void MapCancel(IDialogHandler handler, IDialog virtualView) =>
            handler.PlatformView.Builder.SetNegativeButton(virtualView.Cancel, (sender, args) => virtualView.SendCancelClicked());
        public static void MapNeutral(IDialogHandler handler, IDialog virtualView) =>
            handler.PlatformView.Builder.SetNeutralButton(virtualView.Neutral, (sender, args) => virtualView.SendNeutralClicked());
        
public static void MapContent(IDialogHandler handler, IDialog virtualView)
        {
            if (handler.VirtualView.Content == null)
                return;
            handler.PlatformView.Builder.SetView(virtualView.Content.ToPlatform(handler.MauiContext));
        }
        public static void MapShow(IDialogHandler handler, IDialog virtualView, object args)
        {
            if (!(handler is DialogHandler platformHandler))
                return;
            platformHandler.AlertDialog = handler.PlatformView.Builder.Create();
            platformHandler.OnCreated();
            platformHandler.AlertDialog.Show();
        }
        public static void MapClose(IDialogHandler handler, IDialog virtualView, object args)
        {
            if (!(handler is DialogHandler platformHandler))
                return;
            platformHandler.AlertDialog.Cancel();
        }
    }

Ми додали partial клас з реалізацією функцій для платформи Android. Він є спадкоємцем від ViewHandler<IDialog, AlertDialogView>. Як бачимо, цей базовий клас є узагальненим і приймає два параметри: тип віртуального елементу керування та тип нативного елементу керування. Таким чином, у нас є загальний DialogHandler, в якому ми лише декларуємо відображення властивостей з віртуального елементу у властивості нативного елементу. А також є платформовий DialogHandler, в якому виконується саме реалізація цього відображення для якоїсь з платформ.

А тепер давайте повернемось до класу AlertDialogView, який ми створили раніше для Android-реалізації.

Насправді, це просто обгортка над класом-builder для створення діалогу. Так як в Android не можна створити діалог з повітря (або можна, але builder не дарма ж придумали), то я маю використати MaterialAlertDialogBuilder. Але є одна проблемка. Builder не є спадкоємцем від Android.Views.View, тому я не можу його використовувати в DialogHandler, який є спадкоємцем ViewHandler. Бо цей базовий клас потребує другий параметр як Android.Views.View. Тому і довелось створити клас-обгортку, що наслідує від Android.Views.View.

Але. Пам’ятаєте той смішний коментар, що потрібно прибрати цей клас? Я вже не впевнений на 100%, чому його залишив, але, здається, я хотів наслідувати DialogHandler від ElementHandler замість ViewHandler. А також використати інтерфейс IElementHandler замість IViewHandler. Бо Element не потребує нативного View (тобто, Android.Views.View). Тоді не довелось би створювати цей клас-обгортку. Правда, я пам’ятаю, що колись намагався так зробити, але стикнувся з однією проблемою. Мені навіть на форумі не змогли допомогти. Та про це я розповім у іншій статті.

Повернімось до нашого DialogHandler.Android.

protected override AlertDialogView CreatePlatformView() => new AlertDialogView(Context);

Це реалізація абстрактного методу зі створення нативного елементу керування. Нічого особливого в даному випадку.

Далі йде метод DisconnectHandler:

protected override void DisconnectHandler(AlertDialogView platformView)
        {
            base.DisconnectHandler(platformView);
            AlertDialog.CancelEvent -= OnCancel;
            AlertDialog.ShowEvent -= OnShown;
            AlertDialog.DismissEvent -= OnDismissed;
        }

У ньому ми маємо очистити використані ресурси. Дуже часто тут відбувається відписка від подій. Зазвичай, поруч з цим методом реалізується схожий метод ConnectHandler, в якому ми підписуємось на усілякі події. Але через те, що діалог для Android не схожий на інші елементи керування і має трохи інший спосіб створення, то зручніше було не використовувати метод ConnectHandler, а зробити приватний метод OnCreated, який ми будемо викликати власноруч, і в якому відбувається підписка на необхідні події.

private void OnCreated()
        {
            AlertDialog.CancelEvent += OnCancel;
            AlertDialog.ShowEvent += OnShown;
            AlertDialog.DismissEvent += OnDismissed;
        }

А ось приклад реалізації map-методу.

public static void MapTitle(IDialogHandler handler, IDialog virtualView) => handler.PlatformView.Builder.SetTitle(virtualView.Title);

Усе дуже просто: метод статичний, тому туди передаються усі необхідні параметри. Перший параметр — це сам handler, другий — це віртуальний елемент керування. У тілі методу ми просто встановлюємо потрібний текст для заголовку діалогу.

Як я писав на початку, я хочу наповнювати діалог іншими елементами інтерфейсу (кнопками, списками...). Зверніть увагу на цей метод:

public static void MapContent(IDialogHandler handler, IDialog virtualView)
        {
            if (handler.VirtualView.Content == null)
                return;
            handler.PlatformView.Builder.SetView(virtualView.Content.ToPlatform(handler.MauiContext));
        }

Щоб встановити власний контент для діалогу, ми викликаємо метод SetView у builder`a, конвертуючи віртуальний контент типу Microsoft.Maui.Controls.View у Android.Views.View за допомогою методу ToPlatform, передаючи специфічну властивість MauiContext. Таким чином ми конвертуємо MAUI-контент в нативний.

Ще зверніть увагу на метод MapShow.

public static void MapShow(IDialogHandler handler, IDialog virtualView, object args)

Ми його декларували у CommandMapper. Але для того, щоб цей метод «почув», що його викликають із загального коду (у віртуальному Dialog), ми повинні дещо зробити.

У віртуальному Dialog ми додали наступний метод, всередині якого через «мапінг» ми викликаємо нативний метод:

public void Close() => Handler?.Invoke(nameof(IDialog.Close));

«Мапінг» для властивостей не потребує ніяких додаткових викликів, бо вони і так розуміють зміни за допомогою механізму Binding.

Реєстрація та використання

Тепер єдине, що залишилось, — це зареєструвати наш handler у класі MauiProgram:

public static MauiApp CreateMauiApp()
    {
        MauiAppBuilder builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<App>()
            .UseMauiCommunityToolkit()
            .ConfigureMauiHandlers(Handlers =>
            {
                Handlers.AddHandler(typeof(Dialog), typeof(DialogHandler));
            });
        return builder.Build();
    }

Ну все. Тепер ми готові використовувати наш діалог.

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

Спочатку я створив звичайний ContentView.

Можна також ContentPage, це не важливо, бо ми все одно використаємо Dialog в якості базового класу. Нам лише потрібна база для XAML. Відредагуємо XAML-частину:

<?xml version="1.0" encoding="utf-8" ?>
<controls:Dialog xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
                xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                 xmlns:icons="clr-namespace:AndriyCo.Tcu.MauiClient.FontIcons"
                 <!--В controls вкажіть простір імен вашого Dialog-->
                 xmlns:controls="clr-namespace:AndriyCo.Tcu.MauiClient.Controls"
                 xmlns:properties="clr-namespace:AndriyCo.Tcu.MauiClient.Properties"
                 xmlns:viewModel="clr-namespace:AndriyCo.Tcu.MauiClient.ViewModels.List"
                 x:Class="AndriyCo.Tcu.MauiClient.Views.ApproveHistoryDialog"
                 x:DataType="viewModel:ApproveRecordsViewModel"
                 Title="{Static properties:Translator.ApproveJournal}"
                 Accept="{Static properties:Translator.Close}">
    <controls:Dialog.BindingContext>
        <viewModel:ApproveRecordsViewModel x:Name="viewModel"
                                           UseGrouping="True"/>
    </controls:Dialog.BindingContext>
    <Grid Padding="15, 10, 15, 0">
        <RefreshView IsRefreshing="{Binding IsBusy, Mode=TwoWay}"
                     Command="{Binding LoadCommand, Mode=OneWay}">
            <CollectionView IsGrouped="True"
                            ItemsSource="{Binding GroupedItems, Mode=OneWay}">
                <CollectionView.EmptyView>
                    <!--Тут я вказав, що показувати, коли в переліку відсутні елементи-->
                </CollectionView.EmptyView>
                <CollectionView.GroupHeaderTemplate>
                    <!--Тут я вказав шаблон для заголовку групи (в даному випадку це дата)-->
                </CollectionView.GroupHeaderTemplate>
                <CollectionView.ItemTemplate>
                    <!--Тут я вказав шаблон для елементу-->
                </CollectionView.ItemTemplate>
            </CollectionView>
        </RefreshView>
    </Grid>
</controls:Dialog>

Тут ми вказуємо, що наш клас наслідує controls:Dialog. Це той самий віртуальний елемент керування Dialog, що наслідує інтерфейс IDialog.

C#-частина виглядає наступним чином:

public partial class ApproveHistoryDialog : Dialog
{
    public ApproveRecordsViewModel ViewModel => (ApproveRecordsViewModel)BindingContext;
    public ApproveHistoryDialog()
    {
        InitializeComponent();
    }
    protected override void OnAppearing()
    {
        base.OnAppearing();
        //За допомогою цього методу ми змушуємо RefreshView зробити виклик команди для завантаження даних.
        ViewModel.SetBusy(false);
    }
}

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

ApproveHistoryDialog dialog = new ApproveHistoryDialog();
dialog.Show();

Всередині методу Show можна побачити наступне:

this.ToHandler(App.Current.MainPage.Handler.MauiContext);

Оскільки ми хочемо мати можливість створити діалог з будь-якої точки коду, нам потрібно створити handler власноруч. Тому ми і викликаємо метод ToHandler, який робить усе потрібне для створення handler. Для звичайних елементів керування (кнопка, напис, список) нам потрібно лише декларувати їх в XAML, а інше все MAUI зробить власноруч.

Ось, як виглядає наш діалог:

Якщо ви помітили, я в XAML не вказував явно властивість Content, а просто в тілі написав те, що я хочу встановити для властивості Content. Таку змогу нам дає атрибут [ContentProperty(nameof(Content))], який ми вказали для класу Dialog.

Як бачите, створити власний елемент керування, оснований на нативному, не складно. Потрібно лише декілька кроків:

  1. Створити віртуальний елемент керування з набором необхідних властивостей.
  2. Створити загальний клас-handler з переліком map-методів.
  3. Створити клас-handler для потрібних платформ з реалізацією абстрактного методу CreatePlatformView та map-методів для властивостей. Також бажано використовувати методи ConnectHandler та DisconnectHandler.
  4. Додати створений віртуальний елемент керування до XAML.

Сподіваюсь, я не сильно ускладнив свою розповідь. Бо важко зберегти баланс між детальністю та складністю. Пишіть, ставте питання, буду радий поділитись досвідом.

Чекайте на нові статті. Веселого програмування!

👍ПодобаєтьсяСподобалось3
До обраногоВ обраному3
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

Забуть о Xamarin/MAUI переходи на React Native, там не нужна вся эта фигня)))

Можливо, вам буде цікаво прочитати відповідь у сусідній темі
dou.ua/...​3481/?from=fptech#2629139

MAUI — дуже забагована платформа, іноді прості речі крашаться просто так!
React Native — взагалі немає потреби писати нативні рендери, бо все і так працює як потрібно з коробки!

Як бачите, створити власний елемент керування, оснований на нативному, не складно. Потрібно лише декілька кроків:

Створити віртуальний елемент керування з набором необхідних властивостей.
Створити загальний клас-handler з переліком map-методів.
Створити клас-handler для потрібних платформ з реалізацією абстрактного методу CreatePlatformView та map-методів для властивостей. Також бажано використовувати методи ConnectHandler та DisconnectHandler.
Додати створений віртуальний елемент керування до XAML.

Мiй досвiд на Flutter — 31 рядок це контент, а дiалог — 1 виклик фн)
api.flutter.dev/...​tml#material.showDialog.2

Це добре, що Flutter підтримують Material 3 діалог з коробки.
Бо в MAUI стандартний діалог з Material Design 2. Насправді, для MAUI є CommunityToolkit з готовим діалогом. Він не material, але туди можна вбудувати будь-який вміст і зробити йому будь-який дизайн.
До речі, я бачу у документації, що можна додати текст:
content: const Text(’A dialog is a type of modal window that\n’
’appears in front of app content to\n’
’provide critical information, or prompt\n’
’for a decision to be made.’),
А як додати власний вміст? Кнопки, написи, переліки?

как то действительно так и есть, смотришь на реакты всякие и так все просто там делается, а на шарпе простыни кода надо делать

Разве это простыни? Так, разминочка.
А если серьёзно, то нужно один раз напрячься и реализовать такой диалог. А дальше просто создаёшь свои диалоги, как в последнем xaml и всё.
Тем более, обычный диалог c текстом в MAUI присутствует из коробки. И есть диалог с настраиваемым контентом от CommunityToolkit.
Здесь же я захотел свой диалог, максимально основанный на нативном material-диалоге. Да, некоторых вещей в MAUI из коробки не хватает. Но зато они представляют хорошую базу, чтоб реализовать многие вещи самому. Для бизнес-приложений с .NET-инфраструктурой MAUI просто идеальный вариант.

Покажіть, будь ласка, як у Flutter додати власний вміст в діалог. Мені цікаво.

Там же все есть в редакторе
с 38ой по 64 строчку

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