question

IgorKravchenko-7896 avatar image
0 Votes"
IgorKravchenko-7896 asked LeonLu-MSFT commented

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

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


dotnet-maui
· 6
5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

I copy your code in my demo, I get several errors, can your share a demo about it? And Is PlatformView.Menu.Add(Android.Views.IMenu.None, i, i, sharedItem.Text); line no errors?

0 Votes 0 ·

I have no errors here.
Here is project


0 Votes 0 ·
LeonLu-MSFT avatar image LeonLu-MSFT IgorKravchenko-7896 ·

I think you can create an interface, implementing IView like this thread: https://github.com/dotnet/maui/wiki/Porting-Custom-Renderers-to-Handlers.

0 Votes 0 ·
Show more comments

1 Answer

IgorKravchenko-7896 avatar image
0 Votes"
IgorKravchenko-7896 answered LeonLu-MSFT commented

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; }
     }

· 1
5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

Thanks for your sharing.

0 Votes 0 ·