views:

281

answers:

3

I have a WPF application which runs as a excel plugin, it has its visual tree like so

  • Excel
    • ElementHost
      • WPF UserControl
        • WPF ribbon bar control

Now any controls sitting on the WPF ribbon bar control are not enabled when the plugin is loaded within excel. See error below

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')

If I nest the ribbon bar control in a standalone Window(outside excel) it works fine.

Is there a way to intercept the FindAncestor call for a Window and wire it to something else.? Note that I cannot change the above binding as it isn't my control.

A: 

When using the control in Excel there is no Window in the ancestry, however, perhaps you can use Snoop to find where the binding is defined, then during run-time, find the dependency object (by type) and change its property's binding expression?

Danny Varod
I have already found the control that is having the problematic from the error message given above. The control is Ribbon. But I dont know which dependency property of the control is having this failing binding expression.
Pradeep
Use Snoop, also re-read the binding error message in the output window - you should find the dependency property name there.
Danny Varod
as mentioned in the error message the dependency property name is not available. Also Snoop is unable to snoop managed content when hosted in excel.
Pradeep
A: 

Another option would be to add a custom control that inherits from Window as an ancestor, then bind that to the Excel control.

Danny Varod
This doesnt work as all the controls have ElementHost as the parent and a Window cannot be added to a ElementHost.
Pradeep
A: 

The most direct answer

FindAncestor is processed internally by WPF and will search up the visual tree as far as it can before going anywhere else. Only when it reaches a Visual that has no visual parent will it search elsewhere, and this depends on what it reached. For example, if it hits a FrameworkContentElement it can go to the document's container. Unfortunately if the top of the visual tree is a ElementHost, it will stop, so there is no way to reroute the call.

This means that your simplest option is to replace the binding. Fortunately this is not very difficult.

How to automatically replace a binding

Here is a simple method I wrote a while back that searches through a visual tree and replaces bindings as directed by an updateFunction. If the updateFunction returns a different binding than it is passed, the binding is updated.

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);
  }
}

To illustrate how this works, here is how you could write a method that replaces a specific AncestorType in all RelativeSource FindAncestor instances, as follows:

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,
    });
}

Note that only commonly-used properties are copied over to the new binding.

The ReplaceFindAncestorVisualType method could be used something like this:

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

In your case this generic replace technique won't work: It will be looking for an IsActive property on your ElementHost, which does not exist. So you probably need to change more than just the RelativeSource. This means your actual code will be more like this:

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
    });
};

Note that the above code assumes any FindAncestor:Window binding is the one we are looking for. More conditions can be added as needed in the conditional.

Alternative solution

There is another, completely different, solution available: It is possible to actually host the content in a borderless Window and add custom code to keep this window positioned over the ElementHost so it appears to be within the other window. This is trickier than it sounds since you have to deal with things such as ActiveWindow, ForegroundWindow, Z Order, Minimized state, keyboard focus, etc. But if your needs are very simple this can be a reasonable solution.

Ray Burns