Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Error when using TransformToAncestor: "The specified Visual is not an ancestor of this Visual."

Tags:

wpf

I'm trying to get the offset of a control relative to the top of its window, but I'm running into trouble when using the TransformToAncestor method of the control. Note: this code is in a value converter which will convert from a control to its relative Y position in relation to the window.

public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
    var ctrl = (Control) value;
    var win = Window.GetWindow(ctrl);
    var transform = ctrl.TransformToAncestor(win); // Exception thrown here.
    var pt = transform.Transform(new Point(0, 0));
    return pt.Y;
}

The call to Window.GetWindow works just fine, and returns the correct window object inside which the control resides.

Am I misunderstanding what WPF thinks of as an "ancestor"? I would think that given the result of GetWindow, that window would be an ancestor of the control. Are there certain nesting patters that would cause the line of ancestry to be cut off at a certain point?

UPDATE:

It looks like this may be a timing issue. When I tried calling the TransformToAncestor method inside an event handler rather than the value converter, it worked just fine. It seems that the value converter must be running as certain elements are instantiated before the ancestry relationship is established.

Not sure how to get around this, since I'm trying to use the MVVM pattern (and thusly don't really want to use event handlers, and would rather not have System.Windows stuff in my ViewModel).

like image 853
Brian Sullivan Avatar asked May 14 '10 20:05

Brian Sullivan


1 Answers

The converter is being called while the visual tree is still being assembled, thus your Visual is not yet a descendant of the Window.

You want to do the conversion once your visual tree is already built. This is done by registering a Dispatcher callback using Dispatcher.BeginInvoke(DispatcherPriority.Render, ...) and doing your work inside the callback.

This cannot be used with a converter, however, because a converter must immediately return the converted value. The simple workaround is to use an attached property instead of a converter. Here's how:

Assume your binding is like this:

<SomeObject Abc="{Binding Xyz, Converter={x:Static my:Converter.Instance}}" />

Create a DependencyObject subclass "Whatever" containing an attached property "AbcControl", whose PropertyChangedCallback does the conversion and modifies the "Abc" property:

public class AttachedProperties : DependencyObject
{
  public Control GetAbcControl(...
  public void SetAbcControl(...
  ... AbcControlProperty = RegisterAttached("AbcControl", typeof(Control), typeof(AttachedProperties), new PropertyMetadata
  {
    PropertyChangedCallback = (obj, e) =>
    {
      var ctrl = (Control)e.NewValue;
      Dispatcher.BeginInvoke(DispatcherPriority.Render, new Action(() =>
      {
        var win = Window.GetWindow(ctrl);   
        var transform = ctrl.TransformToAncestor(win); // Exception thrown here.   
        var pt = transform.Transform(new Point(0, 0));   
        obj.SetValue(AbcProperty, pt.Y);
      });
    }
  });
}

Now you can write:

<SomeObject AbcControl="{Binding Xyz}" />

Which will set the Abc property to the transformed Y value.

There are many variations possible on this general idea.

like image 100
Ray Burns Avatar answered Nov 14 '22 10:11

Ray Burns