我有一个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')


如果我将功能区控件嵌套在独立的Window(excel外部)中,则可以正常工作。

有没有一种方法可以拦截对Window的FindAncestor调用并将其连接到其他对象。请注意,我无法更改上述绑定,因为它不是我的控制。

最佳答案

最直接的答案

WPF在内部对FindAncestor进行处理,它将在查找其他任何地方之前尽可能地查找可视树。只有到达了没有视觉父级的视觉对象时,它才会在其他位置搜索,这取决于它到达的对象。例如,如果它命中了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 Order,Minimized状态,键盘焦点等问题。但是,如果您的需求非常简单,那么这可以是一个合理的解决方案。

关于wpf - 拦截RelativeSource FindAncestor,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/2402280/

10-09 22:04