dotnet / MobileBlazorBindings

Experimental Mobile Blazor Bindings - Build native and hybrid mobile apps with Blazor
MIT License
1.2k stars 174 forks source link

Picker/Pattern for passing object references through the render tree builder #249

Closed lachlanwgordon closed 3 years ago

lachlanwgordon commented 3 years ago

There's currently no picker, but I want there to be.

The challenge to making a wrapper around the Xamarin Forms Picker is that it needs a List of items, but the RenderTreeBuilder can only pass strings and callbacks. I came across this when trying to work on CollectionView, but that's got other complications too.

I've found a solution, but I'm not sure if it's a good one, or if it's a hack that should be ignored.

I've added two methods to the AttributeBuilder, and a dictionary of keys and objects to back it

        public void AddAttributeReference(string name, object value)
        {
            var id = Guid.NewGuid().ToString();

            References.Add(id, value);

            _underlyingBuilder.AddAttribute(0, name, id);
        }

        public static object GetAttributeReference(string Id)
        {
            return References[Id];
        }

        static readonly Dictionary<string, object> References = new Dictionary<string, object>();

When building the razor frame, instead of putting the attribute value in the builder, it adds a GUID.

Then in the Picker's RenderAttribute(), I add the object, similar to you would with other attributes.

builder.AddAttributeReference(nameof(ItemsSource), ItemsSource);

And in the PickerHandler's ApplyAttribute() it uses the GUID it finds to get the object back.

Picker.ItemsSource = AttributesBuilder.GetAttributeReference((string)attributeValue) as IEnumerable;

There'll need to be a bit more thought around making this safe and preventing memory leaks, but that's the jist of it.

Do you think the is a good pattern? If so I'll open a PR. Otherwise do you have any suggestions of a better approach?

Dreamescaper commented 3 years ago

Can we provide this value as a callback (smth like _ => value)?

Dreamescaper commented 3 years ago

Another idea. Would it be possible to render all Xamarin controls as Components instead of Elements? Blazor does not apply any conversions for attributes for Components, so that would make AttributeHelper redundant altogether.

lachlanwgordon commented 3 years ago

Thanks for your ideas @Dreamescaper, you're probably right about the callback, I'll give that a shot and see how it turns out.

I'm not quite sure what you mean with using components instead of Elements, but you might be on to something. Hmm, We could make a component, which uses the SetParameter to pass around objects, but then inside that would we still need an element?

I don't think this is the right answer, but it's worth mentioning another approach would be to serialize and then deserialize the whole list of items, but that would get messy.

Dreamescaper commented 3 years ago

I was thinking whether it is possible not to use Blazor elements at all, and managing Xamarin elements at part of Blazor components lifecycle. But my Blazor internals knowledge is not enough to know whether it is possible and how hard would that be.

lachlanwgordon commented 3 years ago

@Dreamescaper, I think I've got what you're suggesting working. I've done a little more tinkering and have come up with something cool, that avoids using the dodgy dictionary by passing the list as a component parameter.

I now have a PickerElement. Which is wired up like any other MBB element but the end user(App Developer) won't use it, and there's no need to pass attributes through it.

public class PickerElement : View
    {
        public new XF.Picker NativeControl => base.NativeControl as XF.Picker;

        static PickerElement()
        {
            ElementHandlerRegistry.RegisterElementHandler<PickerElement>(renderer => new PickerHandler(renderer, new XF.Picker()));
        }
    }

And I have a PickerHandler, which is the same as the other handlers, but I'm not applying attributes.

public partial class PickerHandler : ViewHandler
    {
        public PickerHandler(NativeComponentRenderer renderer, XF.Picker pickerControl) : base(renderer, pickerControl)
        {
            PickerControl = pickerControl;
        }

        public XF.Picker PickerControl { get; }
    }

And I have a new type of class that I've just called Picker, which inherits from ComponenetBase directly and wraps the PickerElement. It has [Parameter]s for any attributes that need to be passed through to the native control.

public partial class Picker : ComponentBase
    {
        protected override void BuildRenderTree(RenderTreeBuilder builder)
        {
            builder.OpenComponent<PickerElement>(0);
            builder.AddComponentReferenceCapture(1, (value) => {
                ThePicker = (PickerElement)value;
            }
            );
            builder.CloseComponent();
        }

        public PickerElement ThePicker { get; set; }

        [Parameter]
        public IList ItemsSource { get; set; }

        protected override void OnAfterRender(bool firstRender)
        {
            if (ItemsSource != null)
            {
                ThePicker.NativeControl.ItemsSource = ItemsSource;
            }
        }
    }

I don't really like doing that work inside the OnAfterRender() because a user can overide it, but the NativeControl doesn't exist yet at the time of OnParametersSet()

I have a feeling that the Picker and Picker Element can actually be combined into one class, but Razor is still confusing to me.

Thoughts?

Dreamescaper commented 3 years ago

Btw, seems like approach with putting value as a callback is already used in MBB, see https://github.com/xamarin/MobileBlazorBindings/blob/master/src/Microsoft.MobileBlazorBindings/Elements/AttributeHelper.ImageSource.cs .

lachlanwgordon commented 3 years ago

Thanks for that, it's much cleaner than my other two approaches. PR's on it's way.

PI2R commented 3 years ago

Hi guys, I was looking for a drop down list and was about to try learning how to wrap one myself when I found this thread. Is there any luck this would answer my need and be available soon? Thanks

lachlanwgordon commented 3 years ago

Hi @PI2R , I ran into a couple of issues with the two way binding of SelectedItem so it's not ready yet, but I don't think its anything I can't work out. I'll get back onto it and let you know when it's ready.

I'm using the approach with CallbackAttribute helpers Dreamscaper suggested and it works well.

PI2R commented 3 years ago

HI @lachlanwgordon, Thanks for the update,. If I can be of any support let me know. I would gladly help and learn how to help more on other issues or developments as well. I like where this project is going/leading and would even dream about an even more unified UI markup for desktop/mobile apps and web ;-)

lachlanwgordon commented 3 years ago

Thanks @PI2R, there's a PR open #275 for this. Hopefully we'll have it merged in soon and you can give it a go.

I agree, this is an exciting project!

Eilon commented 3 years ago

This is now merged, thank you @lachlanwgordon for the Picker!