How to create custom control using Element and ElementHandler instead of View and ViewHandler?

Igor Kravchenko 281 Reputation points
2022-05-06T10:54:00.653+00:00

Full code here. Check Menu folder in Handlers folder.
I want to create a custom control Menu which is Android.Widget.PopupMenu on Android level. But I cannot use ViewHandler because PopupMenu inherits from Java.Lang.Object instead of Android.Views.View. So, I am creating control and it's handlers on Element level (using IElement, Element for virtual view and IElementHandler, ElementHandler for handler). Why method CreatePlatformElement in handler doesn't called? And handler is not creating?
In Xamarin Forms I have created this control using renderer inherited from class VisualElementRenderer<SharedMenu>. And all works fine there. But in MAUI nothing is happen.

Maui code:

VirtualView:

public interface IMenu : IElement
    {
        IList<MenuItem> Items { get; set; }
        bool IsOpened { get; set; }
    }

    [ContentProperty(nameof(Items))]
    public class Menu : Element, IMenu
    {
        public static readonly BindableProperty ItemsProperty =
            BindableProperty.Create(nameof(Items), typeof(IList<MenuItem>), typeof(Menu), new List<MenuItem>());

        public static readonly BindableProperty IsOpenedProperty =
            BindableProperty.Create(nameof(IsOpened), typeof(bool), typeof(Menu), default(bool));

        public static readonly BindableProperty ViewProperty = BindableProperty.Create(nameof(View), typeof(View),
           typeof(Menu), default(View), BindingMode.TwoWay);

        public IList<MenuItem> Items
        {
            get => (IList<MenuItem>)GetValue(ItemsProperty);
            set => SetValue(ItemsProperty, value);
        }

        public bool IsOpened
        {
            get => (bool)GetValue(IsOpenedProperty);
            set => SetValue(IsOpenedProperty, value);
        }

        public View View
        {
            get => (View)GetValue(ViewProperty);
            set => SetValue(ViewProperty, value);
        }

        public void SetSelectedIndex(int index)
        {
            MenuItem item = Items[index];
            if (item.Command != null && item.Command.CanExecute(item.CommandParameter))
                item.Command.Execute(item.CommandParameter);
        }
    }

Handler:

#if ANDROID
using PlatformView = Android.Widget.PopupMenu;
#elif NETSTANDARD || (NET6_0 && !IOS && !ANDROID && !TIZEN)
using PlatformView = System.Object;
#endif

public interface IMenuHandler : IElementHandler
    {
        new PlatformView PlatformView { get; }
    }

    public partial class MenuHandler : IMenuHandler
    {
        public static IPropertyMapper<IMenu, IMenuHandler> Mapper = new PropertyMapper<IMenu, IMenuHandler>()
        {

        };

        public MenuHandler(IPropertyMapper mapper, CommandMapper commandMapper = null) : base(mapper, commandMapper)
        {
        }

        public MenuHandler() : base(Mapper)
        {
        }
    }

public partial class MenuHandler : ElementHandler<Controls.Material.Menu, PopupMenu>
    {
        protected override PopupMenu CreatePlatformElement()
        {
            //didn't tested. I just need to this method be called.
            IElementHandler handler = VirtualView.Parent.Handler;
            return new PopupMenu(MainActivity.Instance, (Android.Views.View)(handler?.PlatformView));
        }

        protected override void ConnectHandler(PopupMenu platformView)
        {
            base.ConnectHandler(platformView);
            UpdateMenu();
        }

        private void UpdateMenu()
        {
            if (VirtualView.Items == null || VirtualView.Items.Count <= 0)
                return;
            for (int i = 0; i < VirtualView.Items.Count; i++)
            {
                MenuItem sharedItem = VirtualView.Items[i];
                PlatformView.Menu.Add(Android.Views.IMenu.None, i, i, sharedItem.Text);
                //await SetMenuIcon(i);
            }
            //if (Android.OS.Build.VERSION.SdkInt >= Android.OS.BuildVersionCodes.Q)
            //    PlatformView.SetForceShowIcon(true);
        }
    }

<controls:Menu x:Name="menu">
                <MenuItem Text="Item 1"/>
                <MenuItem Text="Item 2"/>
                <MenuItem Text="Item 3"/>
            </controls:Menu>

In Xamarin Forms I implemented this by the next way:

[ContentProperty(nameof(Items))]
    public class Menu : View
    {
        public static readonly BindableProperty ItemsProperty = BindableProperty.Create(nameof(Items), typeof(IList<MenuItem>),
           typeof(Menu), new List<MenuItem>(), BindingMode.TwoWay, propertyChanged: OnItemsChanged);

        public static readonly BindableProperty IsOpenedProperty = BindableProperty.Create(nameof(IsOpened), typeof(bool),
           typeof(Menu), default(bool), BindingMode.TwoWay, propertyChanged: OnIsOpenedChanged);

        public static readonly BindableProperty ViewProperty = BindableProperty.Create(nameof(View), typeof(View),
           typeof(Menu), default(View), BindingMode.TwoWay);

        public IList<MenuItem> Items
        {
            get => (IList<MenuItem>)GetValue(ItemsProperty);
            set => SetValue(ItemsProperty, value);
        }

        public bool IsOpened
        {
            get => (bool)GetValue(IsOpenedProperty);
            set => SetValue(IsOpenedProperty, value);
        }

        public View View
        {
            get => (View)GetValue(ViewProperty);
            set => SetValue(ViewProperty, value);
        }

        private static void OnItemsChanged(BindableObject bindable, object oldValue, object newValue)
        {
            if (!(bindable is Menu menu))
                return;
        }

        private static void OnIsOpenedChanged(BindableObject bindable, object oldValue, object newValue)
        {
            if (!(bindable is Menu menu))
                return;
        }

        public void SetSelectedIndex(int index)
        {
            MenuItem item = Items[index];
            if (item.Command != null && item.Command.CanExecute(item.CommandParameter))
                item.Command.Execute(item.CommandParameter);
        }

public class MenuRenderer : VisualElementRenderer<SharedMenu>
    {
        public PopupMenu Control { get; private set; }

        public MenuRenderer(Context context) : base(context)
        {
        }

        private void OnItemClicked(object sender, PopupMenu.MenuItemClickEventArgs e)
        {
            Element.SetSelectedIndex(e.Item.ItemId);
        }

        private void OnDismissed(object sender, PopupMenu.DismissEventArgs e)
        {
            Element.IsOpened = false;
        }

        private void CreateControl()
        {
            if (Element.View == null)
                return;
            IVisualElementRenderer renderer = Platform.GetRenderer(Element.View);
            Control = new PopupMenu(Context, renderer?.View);
        }

        private async void UpdateMenu()
        {
            if (Element.Items == null || Element.Items.Count <= 0)
                return;
            for (int i = 0; i < Element.Items.Count; i++)
            {
                MenuItem sharedItem = Element.Items[i];
                Control.Menu.Add(IMenu.None, i, i, sharedItem.Text);
                await SetMenuIcon(i);
            }
            if (Android.OS.Build.VERSION.SdkInt >= Android.OS.BuildVersionCodes.Q)
                Control.SetForceShowIcon(true);
        }

        private async Task SetMenuIcon(int index)
        {
            MenuItem sharedItem = Element.Items[index];
            if (sharedItem.IconImageSource == null)
                return;
            IMenuItem nativeItem = Control.Menu.GetItem(index);
            nativeItem.SetIcon(new BitmapDrawable(Context.Resources, await sharedItem.IconImageSource.GetBitmap(Context, new CancellationToken())));
        }

        private void UpdateIsOpen()
        {
            if (Element.IsOpened)
                Control.Show();
        }

        protected override void OnElementChanged(ElementChangedEventArgs<SharedMenu> e)
        {
            base.OnElementChanged(e);
            if (e.OldElement != null)
            {
                Control.MenuItemClick -= OnItemClicked;
                Control.DismissEvent -= OnDismissed;
                Control.Dispose();
                return;
            }
            if (e.NewElement != null)
            {
                if (Control == null)
                    CreateControl();
                Control.MenuItemClick += OnItemClicked;
                Control.DismissEvent += OnDismissed;
                UpdateMenu();
                UpdateIsOpen();
            }
        }

        protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            base.OnElementPropertyChanged(sender, e);
            if (e.PropertyName == nameof(Element.View))
                CreateControl();
            if (e.PropertyName == nameof(Element.Items))
                UpdateMenu();
            if (e.PropertyName == nameof(Element.IsOpened))
                UpdateIsOpen();
        }
    }

Note: in Xamarin Forms virtual view inherits from View. And I use renderer inherited from VisualElementRenderer. In MAUI I cannot inherit virtual view from View because it needs ViewHandler instead of ElementHandler. ViewHandler is blocked for me because Android.Widget.PopupMenu is not Android.View. But handler methods inherited from ElementHandler are not called.

Thanks.

P. S. I have tried to inherit virtual view from View and handler from ElementHandler but get Unable to convert TcuMaui.Controls.Material.Menu to Android.Views.View

.NET MAUI
.NET MAUI
A Microsoft open-source framework for building native device applications spanning mobile, tablet, and desktop.
2,861 questions
{count} votes

1 answer

Sort by: Most helpful
  1. Igor Kravchenko 281 Reputation points
    2022-05-19T19:17:26.807+00:00

    I have found a solution

    I have created class inherited from Android.View.View and add MenuPopup there.

    public class PopupMenuView : Android.Views.View
        {
            public PopupMenu Menu { get; }
    
            public PopupMenuView(Context context, Android.Views.View anchor) : base(context)
            {
                Menu = new PopupMenu(context, anchor);
            }
        }
    

    Now I can use ViewHandler:

    public partial class MenuHandler : ViewHandler<Controls.Material.IMenu, PopupMenuView>
    
    
    #if ANDROID
    using PlatformView = TcuMaui.Handlers.PopupMenuView;
    #elif NETSTANDARD || (NET6_0 && !IOS && !ANDROID && !TIZEN)
    using PlatformView = System.Object;
    #endif
    
    public interface IMenuHandler : IViewHandler
        {
            new PlatformView PlatformView { get; }
        }