Skip to content

⚠️文章由GPT5撰写,注意辨别内容真实性。 这是因为你在使用控件模板(ControlTemplate)时,模板内部的元素(比如 Button、ContentPresenter 等)并不直接拥有你自定义的附加属性(如 helpers:NavItemIconHelper.UnselectedIcon)。这些属性通常是定义在控件本身(也就是模板的宿主/父级)上的。

绑定到父属性的原因:

  • TemplatedParent 指的是应用模板的控件实例(比如你的自定义控件或控件派生类)。
  • 模板内部的元素需要通过 RelativeSource={RelativeSource TemplatedParent} 才能访问到宿主控件上的属性(包括附加属性)。
  • 如果直接写 {Binding Path=UnselectedIcon},绑定会在模板内部元素自身上查找该属性,通常找不到。
  • 使用附加属性时,语法是 Path=(命名空间:属性名),比如 Path=(helpers:NavItemIconHelper.UnselectedIcon)

举例说明:

假设你有如下控件模板:

xml
<ControlTemplate TargetType="local:MyNavItem">
    <ContentPresenter
        Content="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=(helpers:NavItemIconHelper.UnselectedIcon)}"/>
</ControlTemplate>

这里的 ContentPresenter 通过 TemplatedParent 绑定,才能拿到 MyNavItem 上的 UnselectedIcon 附加属性。

总结:

  • 模板内部元素要访问宿主控件的属性(包括附加属性),必须用 RelativeSource={RelativeSource TemplatedParent}
  • 这样才能正确获取到父控件(即模板应用者)上的数据。

我是 GitHub Copilot.

最可能的错误源和改法在 Styles/NavigationView.xaml 的两个模板里:

  • 症状 0x80004005/E_INVALIDARG 常见原因:
    • 用 TemplateBinding 绑定附加属性(SelectedIcon/UnselectedIcon/StaticIconVisibility)导致解析/取值失败;
    • 或复用同一个 UIElement 实例作为多个项/多个位置的内容(UIElement 不能同时在两个父级中)。

必须做的两步绑定(两跳):

  1. 在 NavigationViewItem 的模板里,把附加属性“从项传给 Presenter” 位置:MainNavigationViewItemStyle -> ControlTemplate -> NavigationViewItemPresenter 元素 把旧的 TemplateBinding 全部换成 Binding 到 TemplatedParent 的附加属性路径:

    • helpers:NavItemIconHelper.SelectedIcon="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=(helpers:NavItemIconHelper.SelectedIcon)}"
    • helpers:NavItemIconHelper.UnselectedIcon="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=(helpers:NavItemIconHelper.UnselectedIcon)}"
    • helpers:NavItemIconHelper.StaticIconVisibility="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=(helpers:NavItemIconHelper.StaticIconVisibility)}"
  2. 在 NavigationViewItemPresenter 的模板里,真正“取值显示” 位置:MUX_NavigationViewItemPresenterStyleWhenOnLeftPane_Main -> ControlTemplate

    • 给 UnselectedIcon/SelectedIcon 两个 ContentPresenter 设置 Content 绑定: Content="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=(helpers:NavItemIconHelper.UnselectedIcon)}" Content="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=(helpers:NavItemIconHelper.SelectedIcon)}"
    • 给 UnselectedIconBox/SelectedIconBox 的 Visibility 绑定: Visibility="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=(helpers:NavItemIconHelper.StaticIconVisibility)}"

注意:

  • 附加属性路径要用括号语法 (helpers:NavItemIconHelper.Xxx);不要用 TemplateBinding 绑定附加属性。
  • XAML 顶部 xmlns:helpers 必须是 using:MicrosoftDocsGallery.Helpers。
  • 在 NavigationViewItem 上自己赋值 SelectedIcon/UnselectedIcon,且不要复用同一个 UIElement 实例;用内联元素或 IconSourceElement: helpers:NavItemIconHelper.SelectedIcon</helpers:NavItemIconHelper.SelectedIcon>

按上面两处改完,清理重建。若仍报错,多半是复用了 UIElement 实例,改用 IconSourceElement 即可。

简明解释:

  • 这句是“普通 Binding”到模板父级(TemplatedParent)上的“附加属性”的语法。 Content="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=(helpers:NavItemIconHelper.UnselectedIcon)}"
    • RelativeSource={RelativeSource TemplatedParent}:目标是模板父级(即 NavigationViewItem 实例)。
    • Path=(helpers:NavItemIconHelper.UnselectedIcon):用“括号”语法访问附加依赖属性。附加属性路径必须加括号。

和 TemplateBinding 的区别:

  • TemplateBinding 是针对模板父级普通属性的“轻量优化绑定”,语法简单但功能有限; Content="{TemplateBinding Icon}" 仅适用于非附加属性,不能灵活处理复杂路径。
  • 普通 Binding 支持 RelativeSource、附加属性、Fallback、转换器等,更通用;对附加属性要用括号路径。 Content="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Icon}" 等价于 TemplateBinding,但能做更多事。
  • 在 WinUI 3 里,用 TemplateBinding 绑定附加属性常失败;用 Binding+TemplatedParent+(附加属性路径)才稳。

对你这案例要用:

  • 正确绑定附加属性 Content="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=(helpers:NavItemIconHelper.UnselectedIcon)}" Content="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=(helpers:NavItemIconHelper.SelectedIcon)}" Visibility="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=(helpers:NavItemIconHelper.StaticIconVisibility)}"

记忆点:

  • 附加属性路径必须用括号。
  • 绑定到模板父级用 RelativeSource=TemplatedParent。
  • 模板里尽量不要用 TemplateBinding 访问附加属性。

我是 GitHub Copilot.

结论:你贴的 AIDevGallery 代码不会导致 SelectedIcon 被识别,因为它根本没用你的附加属性;你当前项目里 SelectedIcon 无法识别/不生效的原因在这几处用法差异和绑定缺失。

必须同时满足的三件事:

  • 命名空间匹配

    • 在使用样式/模板的 XAML 顶部声明的 xmlns 必须与运行时类完全一致。
    • 你的 helper 运行时类型是 MicrosoftDocsGallery.Helpers.NavItemIconHelper(IDL 已确认)。
    • 所以要用 xmlns:helpers="using:MicrosoftDocsGallery.Helpers"。如果你把别的项目的 xmlns(如 using:AIDevGallery.Helpers)原样抄来,就会报 “property not found”。
  • 模板里的绑定方式要正确

    • 不要用 TemplateBinding 绑附加属性,WinUI 3 下不稳定/易解析失败。
    • 用 Binding + RelativeSource=TemplatedParent 的附加属性语法:
      • 在 NavigationViewItemPresenter 的模板中:
        • UnselectedIcon 的 Content 需要绑定: Content="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=(helpers:NavItemIconHelper.UnselectedIcon)}"
        • SelectedIcon 的 Content 需要绑定: Content="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=(helpers:NavItemIconHelper.SelectedIcon)}"
        • 两个 Viewbox 的 Visibility 需要绑定: Visibility="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=(helpers:NavItemIconHelper.StaticIconVisibility)}"
    • 你当前的 Styles/NavigationView.xaml 里,这三处 Content/Visibility 绑定是空的或仍用 TemplateBinding,必须补上,否则永远拿不到值。
  • 每个 NavigationViewItem 上自己赋值

    • 在实际项上设置附加属性,否则值就是 null:
      • 最安全:内联元素
      • 或用 IconSourceElement(避免复用同一 UIElement 实例)

常见导致 “not found”/E_INVALIDARG 的坑:

  • xmlns 指到错误的命名空间(最常见)。确认每个使用文件的 xmlns:helpers 是 using:MicrosoftDocsGallery.Helpers。
  • 用 TemplateBinding 绑定附加属性。改成 Binding RelativeSource=TemplatedParent + (helpers:...)。
  • 在样式里把附加属性从 NavigationViewItem 转发到 Presenter,但 Presenter 的模板里没取值(你现在就是这样,ContentPresenter 没 Content 绑定)。
  • 复用同一个 UIElement StaticResource 到多个位置/多个项,运行时报 0x80004005/0x80070057。改用内联或 IconSourceElement。

最后检查清单:

  • IDL 与实现一致(SelectedIconProperty/UnselectedIconProperty + Get/Set 返回 Object/IInspectable)。
  • C++ 注册类型用 IInspectable,且只注册一次(懒加载)。
  • Styles/NavigationView.xaml 引入了 xmlns:helpers=using:MicrosoftDocsGallery.Helpers,并按上面的三处绑定修改。
  • App.xaml 合并了该样式资源字典,样式确实被应用。

把它分成“谁赋值、赋了什么、模板怎么用”三个例子说清楚:

一、谁赋值(你来赋)

  • 你在每个 NavigationViewItem 上给附加属性赋值。
  • 例子:内联元素(不会复用实例,最安全)
xml
  <NavigationViewItem Content="首页" helpers:NavItemIconHelper.StaticIconVisibility="Visible">
    <helpers:NavItemIconHelper.UnselectedIcon>
      <FontIcon Glyph="&#xE10F;"/> <!-- 未选中图标 -->
    </helpers:NavItemIconHelper.UnselectedIcon>
    <helpers:NavItemIconHelper.SelectedIcon>
      <FontIcon Glyph="&#xE80F;"/> <!-- 选中图标 -->
    </helpers:NavItemIconHelper.SelectedIcon>
  </NavigationViewItem>
  • 例子:用 IconSource 可复用资源(不会复用 UIElement 实例,安全)
xml
  <Page.Resources>
    <FontIconSource x:Key="HomeOutlineSrc" Glyph="&#xE10F;"/>
    <FontIconSource x:Key="HomeFilledSrc"  Glyph="&#xE80F;"/>
  </Page.Resources>
  <NavigationViewItem Content="首页" helpers:NavItemIconHelper.StaticIconVisibility="Visible">
    <helpers:NavItemIconHelper.UnselectedIcon>
      <IconSourceElement IconSource="{StaticResource HomeOutlineSrc}"/>
    </helpers:NavItemIconHelper.UnselectedIcon>
    <helpers:NavItemIconHelper.SelectedIcon>
      <IconSourceElement IconSource="{StaticResource HomeFilledSrc}"/>
    </helpers:NavItemIconHelper.SelectedIcon>
  </NavigationViewItem>

二、赋了什么(就是一个对象)

  • 附加属性 SelectedIcon/UnselectedIcon 的值就是“一个要显示的对象”,通常是 UIElement(FontIcon/PathIcon/IconSourceElement 等)。
  • 属性类型在代码里是 IInspectable(object),和 ContentPresenter.Content 完全兼容。

三、模板怎么用(模板把它显示出来)

  • 模板里有一个元素 x:Name="SelectedIcon"(只是一个 ContentPresenter 的名字,和附加属性重名但不是一回事)。
  • 它需要这样绑定,才能把你赋的值拿来显示:
    xml
    Content="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=(helpers:NavItemIconHelper.SelectedIcon)}"
    同理,未选中:
    xml
    Content="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=(helpers:NavItemIconHelper.UnselectedIcon)}"
  • 视觉状态把 SelectedIconBox/UnselectedIconBox 的 Opacity 在选中/未选中间切换,实现两套图标的显示。

容易踩的坑

  • 不要把同一个 UIElement 实例放到多个地方(或多个项)显示,会报 E_INVALIDARG/0x80004005。用内联元素或 IconSourceElement。
  • 绑定用 Binding+TemplatedParent 的附加属性语法;TemplateBinding 对附加属性不稳定。
  • 你当前样式末尾的 MUX_NavigationViewItemPresenterStyleWhenOnLeftPane_Main 里,SelectedIcon/UnselectedIcon 的 ContentPresenter 没有 Content 绑定,需要按上面的绑定补上,才会显示你的图标。