XAML Binding to parent of data object

18,999

Solution 1

Here's the answer[1]. FindAncestor finds ancestors in the runtime XAML tree, not in arbitrary C# objects. It cannot walk up to the ItemClass instance from the member we're bound to. But we do know that somebody above us in the XAML tree bound us to that member, and he was bound to the ItemClass instance itself. So whoever that is, we find him, and then we've got the ItemClass.

So let's add debug tracing to the binding, and we'll see what the XAML situation looks like at runtime. No doubt there are other and probably smarter ways to do that, but I happen to know this one without any research.

First add this to the namespaces at the top of the XAML file:

xmlns:diag="clr-namespace:System.Diagnostics;assembly=WindowsBase"

...and then to the binding itself, add this:

diag:PresentationTraceSources.TraceLevel=High

Like so:

<TextBox Text="{Binding Value, Mode=TwoWay}"
     IsEnabled="{Binding Path=IsEditAllowed, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:ItemClass}, AncestorLevel=1}, diag:PresentationTraceSources.TraceLevel=High}"
/>

At runtime, when the TextEdit's IsEnabled property tries to get a value from the binding, the binding walks up through the XAML tree looking for an ancestor of the specified type. It keeps looking until it finds one or runs out of tree, and if we put tracing on it, it traces the type of everything it finds the whole way up. We've told it to look for garbage that it'll never find, so it will give us a trace of the type of every ancestor back to the root of the tree, leaf first and root last. I get 75 lines of ancestors in this case.

I did that, and found a few likely candidates. I checked each one, and the winner turned out to be dgx:GridCellContentPresenter, which has a RowData property. RowData has a lot of properties, and RowData.Row is the row's instance of ItemClass. dxg:GridCellContentPresenter belongs to the DevExpress grid library we're using; in another vendor's grid class, there would presumably be some equivalent.

Here's the working binding:

<TextBox Text="{Binding Value, Mode=TwoWay}"
    IsEnabled="{Binding Path=RowData.Row.IsEditAllowed, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type dxg:GridCellContentPresenter}, AncestorLevel=1}}"
/>

If DevExpress, the vendor, rewrites their GridControl class, we'll be in trouble. But that was true anyhow.

...

[1] Better answer, though it's too DevExpress specific to be of any real interest: The DataContext of the TextBox itself turns out to be dxg:EditGridCellData, which has a RowData property just like GridCellContentPresenter does. I can just use IsEnabled="{Binding Path=RowData.Row.IsEditAllowed}".

However, what I really wanted to do all along was not to present a grid full of stupid disabled textboxes, but rather to enable editing on certain rows in the grid. And the DevExpress grid lets you do that through the ShowingEditor event.

XAML:

<dxg:GridControl Name="grdItems">
    <dxg:GridControl.View>
        <dxg:TableView
            NavigationStyle="Cell"
            AllowEditing="True"
            ShowingEditor="grdItems_TableView_ShowingEditor"
            />
    </dxg:GridControl.View>
<!-- ... Much XAML ... -->
</dxg:GridControl Name="grdItems">

.cs:

private void grdItems_TableView_ShowingEditor(object sender, ShowingEditorEventArgs e)
{
    e.Cancel = !(e.Row as ItemClass).IsEditAllowed;
}

Solution 2

There are two potentially easier ways to set up this binding.

1) name the grid. Then your binding could look something like this (assuming dxg:GridControl has a property named "Items" and that you have assigned an instance of your ItemClass to that property):

<TextBox IsEnabled="{Binding Path=Items.IsEditAllowed, ElementName=MyGridControl />

2) use relative binding, but look for the GridControl rather than something nominally internal to the way GridControl works (that is, GridControlContentPresenter). This gets you away from the implementation details of GridControl, which are perhaps more likely to change in ways that break your application than are properties on GridControl itself.

<TextBox IsEnabled="{Binding Path=Items.IsEditAllowed, RelativeSource={RelativeSource AncestorType={x:Type dxg:GridControl}}}" />

You may also want to read up on the Visual Tree and the Logical Tree in WPF/xaml. The "Ancestor" in relative bindings refers to ancestors in the visual tree, that is, things like parent containers, and not to super- or base classes (as you've discovered, I think).

Share:
18,999
15ee8f99-57ff-4f92-890c-b56153
Author by

15ee8f99-57ff-4f92-890c-b56153

Updated on June 04, 2022

Comments

  • 15ee8f99-57ff-4f92-890c-b56153
    15ee8f99-57ff-4f92-890c-b56153 almost 2 years

    I have a grid column defined. The parent grid gets its items from an ObservableCollection of type ItemClass. ItemClass has two properties: String Foo, and bool IsEditAllowed.

    This column is bound to property Foo. There's a control template for editing the cell. I'd like to bind the ItemClass.IsEditAllowed property to the IsEnabled property of the TextBox in the template.

    The question is how to bind it. Can this be done? The XAML below gets me "Cannot find source for binding with reference" in the debug trace.

    The grid will let me bind the ItemClass itself to the field via some "custom" event thingy, and I can then bind to any of its properties. That's fine, but it seems kludgy. But if it's the only way, it's the only way.

    <dxg:GridColumn
                     Header="Foo Column"
                     FieldName="Foo">
        <dxg:GridColumn.EditTemplate>
            <ControlTemplate>
                <TextBox Text="{Binding Value, Mode=TwoWay}"
                         IsEnabled="{Binding Path=IsEditAllowed, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:ItemClass}, AncestorLevel=1}}" />
            </ControlTemplate>
        </dxg:GridColumn.EditTemplate>
    </dxg:GridColumn>
    
  • 15ee8f99-57ff-4f92-890c-b56153
    15ee8f99-57ff-4f92-890c-b56153 about 11 years
    The grid's Items property is bound to an ObservableCollection of many instances of ItemsClass. One instance of ItemsClass is displayed in each row of the grid. So in the cell template, I'd have to bind to Items[rowindex].IsEditAllowed, but where do I get rowindex from?
  • dtm
    dtm about 11 years
    Sorry, at first, I misunderstood that ItemsClass contained an ObservableCollection. The other potential out you have is DataContext. Most/all ItemControls set the DataContext on the item containers to be whatever the item is bound to from the source collection. Bindings, absent a source specification, use the DataContext as their source. Can you use something like Snoop to see what the DataContext is on your cells and/or rows?
  • 15ee8f99-57ff-4f92-890c-b56153
    15ee8f99-57ff-4f92-890c-b56153 about 11 years
    Yeah, i got that all settled. One option was to follow the visual tree. The easiest option, it turned out that the cell's DataContext object was a class that had the cell's value in a Value property, and also had the row object -- the ItemClass instance -- in a Row property. So it was a very simple matter of using that.