Android自定义ContentPageRenderer触发TargetInvocationException求助
一、先解决TargetInvocationException异常问题
你提到注释掉OnElementChanged里的代码还是会触发异常,那大概率不是你写的逻辑导致的,而是渲染器的基类初始化或者注册环节出了问题。我之前处理过类似的Xamarin.Forms渲染器异常,给你几个具体的排查方向:
检查渲染器注册的类型引用是否正确:
你的ExportRenderer特性里,typeof(CropDetailPage)指向共享项目的页面,要确认这个类型的命名空间有没有被完整引用。比如如果共享项目的命名空间是MyApp.Shared,那特性里应该写typeof(MyApp.Shared.CropDetailPage),如果漏了命名空间,会导致反射找不到目标类型,直接抛出这个异常。确认
PageRenderer的继承是否正确:
如果你用的是较新版本的Xamarin.Forms,要确保继承的是Xamarin.Forms.Platform.Android.PageRenderer,而不是不小心引用了Android原生的同名类(虽然概率低,但也有可能踩坑)。错误的基类继承会直接导致初始化失败。检查依赖版本兼容性:
一定要保证共享项目和Android项目的Xamarin.Forms版本完全一致,版本不匹配是这类反射异常的高发原因。另外,你代码里用的Android.Support.V7.Widget.Toolbar已经被弃用了,现在推荐改用AndroidX.AppCompat.Widget.Toolbar,旧的Support库和新版本Forms兼容度很差,这也可能是触发异常的元凶。
二、关于移动Toolbar按钮的方式是否正确?
直接通过bar.GetChildAt(0).SetX(10)修改子View坐标的方式非常不可靠,主要有两个问题:
- Toolbar的子View结构不是固定的——不同Android版本、不同Forms版本下,Toolbar的子View顺序可能完全不一样,你以为的第0个是按钮,实际可能是标题容器或者其他控件;
- 硬编码的坐标在屏幕旋转、不同分辨率设备上会直接出现布局错乱,适配性极差。
正确的实现方式(两种可选)
方法1:自定义Toolbar布局(灵活度高)
如果需要定制复杂的Toolbar布局,推荐直接替换默认Toolbar:
- 在Android项目的
Resources/layout文件夹下创建CustomToolbar.axml:
<?xml version="1.0" encoding="utf-8"?> <androidx.appcompat.widget.Toolbar xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="@color/azure"> <!-- 左侧放置返回按钮 --> <Button android:id="@+id/back_btn" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Back" android:textColor="@android:color/white"/> <!-- 中间放置标题 --> <TextView android:id="@+id/toolbar_title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:textColor="@android:color/white" android:textSize="18sp"/> </androidx.appcompat.widget.Toolbar>
- 在渲染器中替换默认Toolbar并绑定Command:
[assembly: ExportRenderer(typeof(CropDetailPage), typeof(CropDetailPageRenderer))] namespace MyApp.Droid { public class CropDetailPageRenderer : PageRenderer { public CropDetailPageRenderer(Context context) : base(context) { } protected override void OnElementChanged(ElementChangedEventArgs<Page> e) { base.OnElementChanged(e); if (e.OldElement != null || Element == null) return; // 加载自定义Toolbar布局 var toolbarLayout = LayoutInflater.From(Context).Inflate(Resource.Layout.CustomToolbar, this, false); var toolbar = toolbarLayout.FindViewById<AndroidX.AppCompat.Widget.Toolbar>(Resource.Id.toolbar); var backBtn = toolbarLayout.FindViewById<Button>(Resource.Id.back_btn); var titleTv = toolbarLayout.FindViewById<TextView>(Resource.Id.toolbar_title); // 设置页面标题 titleTv.Text = Element.Title; // 绑定返回按钮的Command if (Element is CropDetailPage page && page.BindingContext is CropDetailViewModel vm) { backBtn.Click += (s, args) => vm.GoBackCommand.Execute(null); } // 将自定义Toolbar添加到页面 AddView(toolbarLayout); } protected override void OnLayout(bool changed, int l, int t, int r, int b) { base.OnLayout(changed, l, t, r, b); // 确保自定义Toolbar占满顶部区域 var toolbar = GetChildAt(0); toolbar.Layout(0, 0, r - l, (int)Context.Resources.GetDimension(Resource.Dimension.abc_action_bar_default_height_material)); } } }
方法2:利用Toolbar的NavigationIcon(简单快捷)
如果只是需要一个左侧返回按钮,直接用Toolbar自带的NavigationIcon更简单:
protected override void OnElementChanged(ElementChangedEventArgs<Page> e) { base.OnElementChanged(e); if (e.OldElement != null || Element == null) return; var toolbar = FindViewById<AndroidX.AppCompat.Widget.Toolbar>(Resource.Id.toolbar); if (toolbar != null) { // 设置左侧返回图标(替换成你的自定义图标) toolbar.NavigationIcon = Context.GetDrawable(Resource.Drawable.ic_back_white); // 绑定点击事件到ViewModel的Command toolbar.SetNavigationOnClickListener(v => { if (Element is CropDetailPage page) { (page.BindingContext as CropDetailViewModel)?.GoBackCommand.Execute(null); } }); // 移除原来的ToolbarItem(避免重复显示) Element.ToolbarItems.Clear(); } }
总结解决思路
- 先解决异常:从渲染器注册的类型引用、基类继承、依赖版本兼容性这三个核心方向排查,确保渲染器能被正确初始化;
- 替换按钮移动方案:放弃硬改子View坐标的方式,改用自定义Toolbar布局或者NavigationIcon的方案,既能保证功能实现,又能提升兼容性和稳定性。
内容的提问来源于stack exchange,提问作者Greggz

