Hello,
I have native controls for iOS and Android that contain state and have methods. I would like to create a Xamarin.Forms View wrapper around these controls using a custom ViewRenderer. I would like to keep as much state in the native controls as possible, and simply use the XF View and ViewRenderer as a pass-through for method calls and fields.
The issue I have is that when a View is instantiated, a matching ViewRenderer is not created until the View is added to the UI tree. So this code works:
MyClassView v = new MyClassView();
myGrid.Children.Add(v); // renderer gets created behind the scenes here
v.DoAction1();
But not this code:
MyClassView v = new MyClassView();
v.DoAction1(); // no renderer yet, throws NullReferenceException
myGrid.Children.Add(v);
Here is the v1 implementation that exposes this problem:
namespace MyCustomNamespace
{
public class MyClassView : View, IDisposable
{
private IMyClassRenderer renderer;
public void DoAction1()
{
renderer.DoAction1();
}
internal void SetMyClassRenderer(IMyClassRenderer myClassRenderer)
{
this.renderer = myClassRenderer;
}
internal void TriggerLoadingStatusChanged(LoadingStatusChangedEventArgs e)
{
UpdateSomething();
}
}
}
using Internal = Com.My.Java.Namespace;
[assembly: ExportRenderer(typeof(MyClassView), typeof(Android.MyClassRenderer))]
namespace MyCustomNamespace.Android
{
public class MyClassRenderer : ViewRenderer<MyClassView, Internal.MyClass>, IMyClassRenderer
{
public void DoAction1()
{
Control.DoAction1();
}
protected override void OnElementChanged(ElementChangedEventArgs<MyClass> e)
{
base.OnElementChanged(e);
if (Control == null)
{
SetNativeControl(new Internal.MyClass(Context));
Element.SetMyClassRenderer(this);
}
if (e.OldElement != null)
{
Control.LoadingStatusChanged -= this.Control_LoadingStatusChanged;
}
if (e.NewElement != null)
{
Control.LoadingStatusChanged += this.Control_LoadingStatusChanged;
}
}
private void Control_LoadingStatusChanged(object sender, Internal.LoadingStatusChangedEventArgs args)
{
Element.TriggerLoadingStatusChanged(new LoadingStatusChangedEventArgs(args.Status));
}
}
}
public IMyClassRenderer
{
void DoAction1();
}
// JAVA CONTROL
public class MyClass extends FrameLayout
{
}
The solution I came up with is to create and set a renderer when in my View's constructor. This works, but gets messy when my View is added to the UI tree, and Xamarin creates another renderer. To preserve the state of my native control, I then grab the native control from the first renderer and give it to the second renderer, and have the first renderer unsubscribe to events form my native control.
Here is the v2 code for this solution:
namespace MyCustomNamespace
{
public class MyClassView : View, IDisposable
{
private IMyClassRenderer renderer;
internal object NativeControl;
public MyClassView()
{
this.renderer = DependencyService.Get<IRendererHelper>().CreateAndSetRenderer(this);
}
public void DoAction1()
{
renderer.DoAction1();
}
internal void SetMyClassRenderer(IMyClassRenderer myClassRenderer)
{
if (this.renderer != null)
{
this.renderer.UnhookEvents();
}
this.renderer = myClassRenderer;
}
internal void TriggerLoadingStatusChanged(LoadingStatusChangedEventArgs e)
{
UpdateSomething();
}
}
}
using Internal = Com.My.Java.Namespace;
[assembly: ExportRenderer(typeof(MyClass), typeof(MyClassRenderer))]
namespace MyCustomNamespace.Android
{
public class MyClassRenderer : ViewRenderer<MyClass, Internal.MyClass>, IMyClassRenderer
{
public void DoAction1()
{
Control.DoAction1();
}
protected override void OnElementChanged(ElementChangedEventArgs<MyClass> e)
{
base.OnElementChanged(e);
if (Control == null)
{
if (Element.NativeControl == null)
{
SetNativeControl(new Internal.MyClass(Context));
Element.SetMyClassRenderer(this);
Element.NativeControl = this.Control;
}
else
{
Internal.MyClass internalMyClass = Element.NativeControl as Internal.MyClass;
((MyClassRenderer)internalMyClass.Parent).RemoveView(internalMyClass);
SetNativeControl((Internal.MyClass)Element.NativeControl);
Element.SetMyClassRenderer(this);
}
}
if (e.OldElement != null)
{
Control.LoadingStatusChanged -= this.Control_LoadingStatusChanged;
}
if (e.NewElement != null)
{
Control.LoadingStatusChanged += this.Control_LoadingStatusChanged;
}
}
private void Control_LoadingStatusChanged(object sender, Internal.LoadingStatusChangedEventArgs args)
{
Element.TriggerLoadingStatusChanged(new LoadingStatusChangedEventArgs(args.Status));
}
public void UnhookEvents()
{
Control.LoadingStatusChanged -= this.Control_LoadingStatusChanged;
}
}
}
[assembly: Xamarin.Forms.Dependency (typeof (RendererHelperImplementation))]
namespace MyCustomNamespace.Android
{
internal class RendererHelperImplementation : IRendererHelper
{
public IMyClassRenderer CreateAndSetRenderer(View view)
{
var renderer = Platform.CreateRenderer(view);
Platform.SetRenderer(view, renderer);
return (IMyClassRenderer)renderer;
}
}
}
This seems to work, but boy oh boy does it feel inelegant. I also fear that some of the View <-> Renderer mapping that Xamarin normally takes care of will be incorrect due to my mucking around.
Does anyone know of a better solution to this problem?
Thanks,
Wyatt