不借助Prism导航实现WPF UserControl构造函数注入
我用Prism的Navigation功能时,它能正常把服务注入视图或视图模型的构造函数,实现INavigationAware接口也能接收参数,整体运行没问题。但有时候Prism的松耦合太“过”了,为了让UserControl真正可复用,我经常需要在XAML里直接绑定属性,而不是只靠NavigationParameter传参,同时还想保留Prism默认的构造注入服务的能力。
我想要的控件具备这些特性:
- 是带DependencyProperties的常规UserControl
- 能直接在XAML中布局
- 支持绑定操作
- 不需要专用视图模型
- 把自身设为DataContext
这部分实现不难,但怎么实现依赖注入找不到非hack的办法。比如我有个带单个依赖属性的简单Widget控件:
namespace MyApp.Controls; public partial class Widget: UserControl { Widget() // 没有任何服务注入 { InitializeComponent(); Scene.DataContext = this; } public static readonly DependencyProperty WidgetLabelProperty = DependencyProperty.Register(nameof(WidgetLabel), typeof(string), typeof(Widget), new PropertyMetadata(string.Empty)); public string WidgetLabel { get => (string)GetValue(WidgetLabelProperty); set => SetValue(WidgetLabelProperty, value); } }
因为WPF会直接构造这个控件,没法像Prism控件那样给构造函数加参数:
Widget(IMyCustomService customService) // **** 这样行不通 *** { InitializeComponent(); Scene.DataContext = this; }
所以如果要访问服务,我想到的唯一办法是加个DependencyProperty当“后门”:
public static readonly DependencyProperty CustomServiceProperty = DependencyProperty.Register(nameof(CustomService), typeof(ICustomService), typeof(Widget), new PropertyMetadata(null)); public ICustomService? CustomService { get => GetValue(CustomServiceProperty) as ICustomService; set => SetValue(CustomServiceProperty , value); }
但这种方式允许用户把这个属性设为null,有风险。有没有更合理的非hack实现方式?
方法1:在控件初始化时直接从Prism容器获取服务
Prism的容器是全局可访问的(通过PrismApplicationBase.Current.Container),你可以在控件构造函数或Loaded事件里直接解析所需服务,既不用暴露公共属性,也能通过容器的注册保证服务不为null:
namespace MyApp.Controls; public partial class Widget: UserControl { private readonly IMyCustomService _customService; Widget() { InitializeComponent(); Scene.DataContext = this; // 从容器解析服务,未注册会直接抛出异常,避免null风险 _customService = PrismApplicationBase.Current.Container.Resolve<IMyCustomService>(); } // 你的依赖属性代码... }
这种方式的优势是控件自主获取服务,无需外部传入,且异常能提前暴露未注册的问题,不会出现运行时null的情况。唯一的耦合是控件依赖Prism容器,这在Prism项目中属于可接受的场景。
方法2:通过附加属性实现隐式注入
创建一个附加属性,专门负责给目标控件注入指定服务,同时在回调逻辑里确保服务不为null:
public static class ServiceInjection { public static readonly DependencyProperty InjectCustomServiceProperty = DependencyProperty.RegisterAttached( "InjectCustomService", typeof(bool), typeof(ServiceInjection), new PropertyMetadata(false, OnInjectCustomServiceChanged)); public static bool GetInjectCustomService(DependencyObject obj) { return (bool)obj.GetValue(InjectCustomServiceProperty); } public static void SetInjectCustomService(DependencyObject obj, bool value) { obj.SetValue(InjectCustomServiceProperty, value); } private static void OnInjectCustomServiceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if ((bool)e.NewValue && d is Widget widget) { var service = PrismApplicationBase.Current.Container.Resolve<IMyCustomService>(); // 给控件私有字段赋值,不暴露公共属性 widget.SetCustomService(service); } } }
然后在Widget中添加私有方法接收服务:
private IMyCustomService _customService; private void SetCustomService(IMyCustomService service) { _customService = service ?? throw new ArgumentNullException(nameof(service)); }
XAML中只需添加附加属性即可完成注入:
<controls:Widget WidgetLabel="测试" local:ServiceInjection.InjectCustomService="True"/>
这种方式既保持了控件的封装性(服务是私有字段),又避免了用户手动设置服务的可能,完全由附加属性自动完成注入,同时通过异常保证服务不为null。
方法3:自定义控件工厂(适合批量场景)
如果有大量同类控件需要注入服务,可以自定义一个基于Prism容器的控件工厂,通过XAML扩展标记来引用控件实例。这种方式适合复杂场景,能统一管理控件的创建与注入逻辑。
内容的提问来源于stack exchange,提问作者Joe

