WPF 用户控件(UserControl)继承

在WPF项目中,你是否遇到过希望编写一个UserControl继承自另一个UserControl的场景?
比如下面:DerivedUserControl继承自BaseUserControl

UserControl继承另一个UserControl

步骤:

1、新建一个UserControl命名为 BaseUserControl

<UserControl
    x:Class="MultiLibUserControl.BaseUserControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:local="clr-namespace:MultiLibUserControl"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    d:DesignHeight="450"
    d:DesignWidth="800"
    mc:Ignorable="d">
    <Grid>
        <!--  say  -->
        <Button
            x:Name="showTimeBtn"
            Click="showTimeBtn_Click"
            Content="show time" />
    </Grid>
</UserControl>

后台代码

using System;
using System.Windows;
using System.Windows.Controls;

namespace MultiLibUserControl
{
    public partial class BaseUserControl : UserControl
    {
        public BaseUserControl()
        {
            InitializeComponent();
        }

        private void showTimeBtn_Click(object sender, RoutedEventArgs e)
        {
            MessageBox.Show($"{DateTime.Now}");
        }
    }
}

2、再新建一个UserControl命名为 DerivedUserControl,派生类改为 BaseUserControl

<local:BaseUserControl
    x:Class="MultiLibUserControl.DerivedUserControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:local="clr-namespace:MultiLibUserControl"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    d:DesignHeight="450"
    d:DesignWidth="800"
    mc:Ignorable="d">
    <!--<Grid>
        <TextBlock x:Name="infoText" Text="hahahah" />
    </Grid>-->
</local:BaseUserControl>

后台代码

namespace MultiLibUserControl
{
    public partial class DerivedUserControl : BaseUserControl
    {
        public DerivedUserControl()
        {
            InitializeComponent();
        }
    }
}

编译报错:

错误1:
2>xxx\DerivedUserControl.g.cs(50,21,50,40): warning CS0108: “DerivedUserControl.InitializeComponent()”隐藏继承的成员“BaseUserControl.InitializeComponent()”。如果是有意隐藏,请使用关键字 new。

错误2:
2>xxxx: error MC6017: “MultiLibUserControl.BaseUserControl”不能是 XAML 文件的根,因为它是使用 XAML 定义的。 行 2 位置 5.

分析原因:

不允许“使用 XAML 定义”的类为另一XAML文件的根元素,我们就避开这个限制;两种办法:要么不让基类带“XAML 定义”;要么不让派生类带“XAML 定义”,就避开了上面限制。

解决方案:

方式1: 删掉基类的XAML文件,将基类改成纯C#类。
using System.Windows;
using System.Windows.Controls;

namespace MultiLibUserControl
{
    public partial class BaseUserControl : UserControl
    {
        public BaseUserControl()
        {
            System.Uri resourceLocater = new System.Uri("/MultiLibUserControl;component/BaseUserControl.xaml", System.UriKind.Relative);
            var component = Application.LoadComponent(resourceLocater);
            var ctrl = component as UserControl;//已知xaml文件是一个以UserControl为根的文件,所以预期得到的对象就是UserControl类型对象
            this.Content = ctrl;//将 UserControl 类型对象赋值给 BaseUserControl 的 Content 属性-->将来BaseUserControl的外观就来自BaseUserControl.xaml里面了
            if (ctrl != null)
            {
                btn = ctrl.FindName("SayHelloBtn") as Button;//通过FindName方法,从UserControl对象取出来 子元素, 维护成字段,添加事件处理程序
                if (btn != null)
                {
                    btn.Click -= Btn_Click;
                    btn.Click += Btn_Click;
                }
            }
        }

       protected  Button btn;


        private void Btn_Click(object sender, RoutedEventArgs e)
        {
            MessageBox.Show("hello");
        }
    }
}

xaml文件
注意这个xaml文件中不能有x:Class和路由事件属性设置,否则会编译报错:

# 错误:
1>xxxx: error MC6024: “UserControl”根元素需要 x:Class 特性来支持 XAML 文件中的事件处理程序。请移除 Click 事件的事件处理程序,或将 x:Class 特性添加到根元素。
<UserControl
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:local="clr-namespace:WpfControlLibrary1"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    d:DesignHeight="450"
    d:DesignWidth="800"
    mc:Ignorable="d">
    <Grid>
        <Button x:Name="SayHelloBtn" Content="SayHello" />
    </Grid>
</UserControl>

因为基类的.xaml文件要去掉x:Class-->这样做之后,WPF不会自动生成.g.i.cs文件(自动生成的代码就没了如:InitializeComponent方法就不会生成)-->故BaseUserControl.cs文件中,构造函数就调用不了InitializeComponent方法;
最终:xaml文件中的布局代码中的 对象、事件处理程序 ,需要自己处理。

方式2: 删掉派生类的XAML文件,将派生类改成纯C#类。
namespace MultiLibUserControl
{
    internal class CustomDerivedUserControl : BaseUserControl
    {
    }
}

用户控件派生类于基类不在一个项目

复现步骤

在项目A中新建一个继承自项目B中的BaseUserControl(UserControl类型)的c#类,如下:
基类所在的项目B

<UserControl
    x:Class="WpfControlLibraryB.BaseUserControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:local="clr-namespace:WpfControlLibrary1"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    d:DesignHeight="450"
    d:DesignWidth="800"
    mc:Ignorable="d">
    <Grid>
        <Button
            x:Name="SayHelloBtn"
            Click="SayHelloBtn_Click"
            Content="SayHello" />
    </Grid>
</UserControl>

后台代码

using System.Windows;
using System.Windows.Controls;

namespace WpfControlLibraryB
{
    public partial class BaseUserControl: UserControl
    {
        public BaseUserControl()
        {
            InitializeComponent();
        }

        private void SayHelloBtn_Click(object sender, RoutedEventArgs e)
        {
            MessageBox.Show("hello");
        }
    }
}

派生类所在的项目A

using WpfControlLibraryB;

namespace WpfControlLibraryA
{
    internal class CustomDerivedUserControl : BaseUserControl
    {
    }
}

运行报错

未经处理的异常:  System.Windows.Markup.XamlParseException: “对类型“WpfControlLibraryA.CustomDerivedUserControl”的构造函数执行符合指定的绑定约束的调用时引发了异常。”,xxx。 
---> System.Exception: 组件“WpfControlLibraryA.CustomDerivedUserControl”不具有由 URI“/WpfControlLibraryB;component/baseUserControl.xaml”识别的资源。

分析原因

顺着异常堆栈,看了一下报错发生在下面代码最后面的部分


[System.Diagnostics.DebuggerNonUserCodeAttribute()]
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "4.0.0.0")]
public void InitializeComponent() {
    if (_contentLoaded) {
        return;
    }
    _contentLoaded = true;
    System.Uri resourceLocater = new System.Uri("/WpfControlLibraryB;component/baseUserControl.xaml", System.UriKind.Relative);

    System.Windows.Application.LoadComponent(this, resourceLocater);
    
}

[SecurityCritical]
public static void LoadComponent(object component, Uri resourceLocator)
{
    Uri uri = new Uri(BaseUriHelper.PackAppBaseUri, resourceLocator);
    ParserContext parserContext = new ParserContext();
    parserContext.BaseUri = uri;
    bool flag = true;
    Stream stream = null;
    ........此处省去很多行.........

    //这里WPF框架层判断了程序集是否一致 不一致抛出异常
    if (!(stream is IStreamInfo streamInfo) || streamInfo.Assembly != component.GetType().Assembly)
    {
        throw new Exception(SR.Get("UriNotMatchWithRootType", component.GetType(), resourceLocator));
    }

    XamlReader.LoadBaml(stream, parserContext, component, flag);
}

最终发现,“自定义的 UserControl 用户控件不能跨程序集继承”,这是框架限制。

解决方案

从F12得到的源码中,得知 XAML文件的加载是由InitializeComponent进而调用System.Windows.Application.LoadComponent实现的;
我们把System.Windows.Application.LoadComponent方法的源码拷出来,把程序集一致性判断逻辑删掉,封装成扩展方法LoadViewFromUri
此扩展方法来自 the-component-does-not-have-a-resource-identified-by-the-uri
最后不在基类构造函数中调用InitializeComponent,而是调用自己封装的方法来加载XAML文件,来解决

  • 工具方法
//
public static class Extensions
{
    public static void LoadViewFromUri(this FrameworkElement userControl, string baseUri)
    {
        try
        {
            var resourceLocater = new Uri(baseUri, UriKind.Relative);
            var exprCa = (PackagePart)typeof(Application).GetMethod("GetResourceOrContentPart",
                BindingFlags.NonPublic | BindingFlags.Static)
                .Invoke(null, new object[] { resourceLocater });
            var stream = exprCa.GetStream();
            var uri = new Uri((Uri)typeof(BaseUriHelper).GetProperty("PackAppBaseUri",
                BindingFlags.Static | BindingFlags.NonPublic)
                .GetValue(null, null), resourceLocater);
            var parserContext = new ParserContext
            {
                BaseUri = uri
            };
            typeof(XamlReader).GetMethod("LoadBaml", BindingFlags.NonPublic
                | BindingFlags.Static)
                .Invoke(null, new object[] { stream, parserContext, userControl, true });
        }
        catch
        {
            throw;
        }
    }
}
  • 基类构造函数修改
using System.Windows;
using System.Windows.Controls;

namespace WpfControlLibraryB
{
    public partial class BaseUserControl: UserControl
    {
        public BaseUserControl()
        {
            //InitializeComponent();
            this.LoadViewFromUri("/WpfControlLibraryB;component/baseUserControl.xaml");
        }

        //....
    }
}

通过以上方法解决。


思考一下下面为什么会死循环:

using System.Windows;
using System.Windows.Controls;

namespace WpfControlLibraryB
{
    public partial class BaseUserControl: UserControl
    {
        public BaseUserControl()
        {
            //InitializeComponent();
            // this.LoadViewFromUri("/WpfControlLibraryB;component/baseUserControl.xaml");
            System.Uri resourceLocater = new System.Uri("/WpfControlLibrary1;component/BaseUserControl.xaml", System.UriKind.Relative);
            var component = Application.LoadComponent(resourceLocater);
            var ctrl = component as UserControl;
            this.Content = ctrl;
        }

        //....
    }
}

以上,记录了一下用户控件继承工作中踩的坑🕳;其实 自定义控件库 解决:UI复用、逻辑复用才是最合适的途径,本文就不再展开叙述了。

参考:

wpf自定义控件或窗体继承问题,继承之后出现不能是 XAML 文件的根
WPF 自定义泛型用户控件后跨程序集继承用户控件的解决方案
dotnet 读 WPF 源代码笔记 为什么自定义的 UserControl 用户控件不能跨程序集继承
XAML UserControl的继承

posted @ 2026-01-21 17:37  BigBosscyb  阅读(1)  评论(0)    收藏  举报