3

我有一个作为 excel 插件运行的 WPF 应用程序,它的可视化树像这样

  • Excel
    • 元素主机
      • WPF 用户控件
        • WPF 功能区栏控件

现在,当插件在 excel 中加载时,位于 WPF 功能区栏控件上的任何控件都不会启用。请参阅下面的错误

System.Windows.Data Error: 4 : Cannot find source for binding with 
reference 'RelativeSource FindAncestor, AncestorType='System.Windows.Window', AncestorLevel='1''. BindingExpression:Path=IsActive; DataItem=null; target element 
is 'Ribbon' (Name=''); target property is 'NoTarget' (type 'Object')

如果我将功能区栏控件嵌套在独立窗口(excel 外部)中,它工作正常。

有没有办法拦截 FindAncestor 对 Window 的调用并将其连接到其他东西。?请注意,我无法更改上述绑定,因为它不是我的控制。

4

3 回答 3

2

最直接的答案

FindAncestor 由 WPF 在内部进行处理,并且会在到达其他任何地方之前尽可能地搜索可视化树。只有当它到达一个没有视觉父级的 Visual 时,它才会在其他地方搜索,这取决于它到达了什么。例如,如果它命中一个 FrameworkContentElement,它可以转到文档的容器。不幸的是,如果可视化树的顶部是 ElementHost,它将停止,因此无法重新路由调用。

这意味着您最简单的选择是替换绑定。幸运的是,这不是很困难。

如何自动替换绑定

这是我不久前编写的一个简单方法,它搜索可视化树并按照 updateFunction 的指示替换绑定。如果 updateFunction 返回的绑定与传递的绑定不同,则更新绑定。

static void UpdateBindings(Visual visual, Func<Binding, Binding> updateFunction)
{
  if(visual==null) return;
  for(int i=0; i<VisualTreeHelper.GetChildrenCount(visual); i++)
    UpdateBindings(VisualTreeHelper.GetChild(visual, i) as Visual, updateFunction);
  for(var enumerator = visual.GetLocalValueEnumerator(); enumerator.MoveNext(); )
  {
    var property = enumerator.Current.Property;
    var binding = BindingOperations.GetBinding(visual, property);
    if(binding==null) continue;
    var newBinding = updateFunction(binding);
    if(newBinding!=binding)
      BindingOperations.SetBinding(visual, property, newBinding);
  }
}

为了说明这是如何工作的,您可以编写一个方法来替换所有 RelativeSource FindAncestor 实例中的特定 AncestorType,如下所示:

static void ReplaceFindAncestorType(Visual visual, Type fromType, Type toType)
{
  UpdateBindings(visual, binding =>
    binding.RelativeSource.Mode != RelativeSourceMode.FindAncestor ? binding :
    binding.RelativeSource.AncestorType != fromType ? binding :
    new Binding
    {
      RelativeSource = new RelativeSource(
        RelativeSourceMode.FindAncestor,
        toType,
        binding.RelativeSource.AncestorLevel),
      Path = binding.Path,
      Mode = binding.Mode,
      Converter = binding.Converter,
      StringFormat = binding.StringFormat,
      UpdateSourceTrigger = binding.UpdateSourceTrigger,
    });
}

请注意,仅将常用属性复制到新绑定中。

ReplaceFindAncestorVisualType 方法可以这样使用:

elementHost.LayoutUpdated += (obj, e) =>
{
  ReplaceFindAncestorType(elementHost, typeof(Window), typeof(ElementHost);
};

在您的情况下,这种通用替换技术将不起作用:它将在您的 ElementHost 上查找 IsActive 属性,该属性不存在。所以你可能需要改变的不仅仅是RelativeSource。这意味着您的实际代码将更像这样:

elementHost.LayoutUpdated += (obj, e) =>
{
  UpdateBindings(elementHost, binding =>
    binding.RelativeSource.AncestorType != typeof(Window) ? binding :
    new Binding
    {
      Source = ultimateContainingWindowOrOtherObjectHavingIsActiveProperty,
      Path = new PropertyPath("IsActive"), // Put property name here
    });
};

请注意,上面的代码假定任何 FindAncestor:Window 绑定都是我们正在寻找的绑定。条件中可以根据需要添加更多条件。

替代解决方案

还有另一种完全不同的解决方案:可以将内容实际托管在无边框窗口中,并添加自定义代码以使该窗口位于 ElementHost 上方,使其看起来位于另一个窗口中。这比听起来要棘手,因为您必须处理诸如 ActiveWindow、ForegroundWindow、Z 顺序、最小化状态、键盘焦点等。但如果您的需求非常简单,这可能是一个合理的解决方案。

于 2010-04-07T04:59:53.237 回答
0

在 Excel 中使用控件时,祖先中没有 Window,但是,也许您可​​以使用Snoop查找定义绑定的位置,然后在运行时找到依赖对象(按类型)并更改其属性的绑定表达式?

于 2010-04-04T22:33:45.377 回答
0

另一种选择是添加一个继承自 Window 作为祖先的自定义控件,然后将其绑定到 Excel 控件。

于 2010-04-04T22:44:05.117 回答