Master-detail: How to fetch a control from a template inside the "detail" ContentControl?

Viewed 102

I have a ListView (on the 'master' side) whose selection drives a ContentControl's Content property (on the 'detail' side). The ContentControl's visual tree comes from either of two DataTemplate resources that use DataType to choose which detail view to render based on what is selected in the ListView. That part works fine.

The part I'm struggling with is that there is a particular control inside (one of) the templates that I need to obtain a reference to whenever it changes (e.g. the template selected changes or the ListView selection changes such that the instance of the control is recreated.)

In my ListView.SelectionChanged event handler, I find the ContentControl has not yet been updated with its new visual tree, so initially it's empty on the first selection, and for subsequent selections its visual tree matches the old selection instead of the new one. I've tried delaying my code by scheduling on the Dispatcher with a priority as low as DispatcherPriority.Loaded, which works for the first selection but on subsequent selections my code still runs before the visual tree is updated.

Is there a better event I should be hooking to run whenever the ContentControl's visual tree is changed to reflect a changed data-bound value to its Content property?

Extra info: the reason I need to reach into the expanded DataTemplate is that I need to effectively set my view model's IList SelectedItems property to a DataGrid control's SelectedItems property. Since DataGrid.SelectedItems is not a dependency property, I have to do this manually in code.

1 Answers

The fix required a combination of techniques. For the first selection that populates the visual tree, I needed to handle ContentControl.OnApplyTemplate() which is only a virtual method rather than an event. I derived from it and exposed it as an event:

public class ContentControlWithEvents : ContentControl
{
    public event EventHandler? TemplateApplied;

    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();
        this.TemplateApplied?.Invoke(this, EventArgs.Empty);
    }
}

In the XAML I used the above class rather than ContentControl:

<local:ContentControlWithEvents
   Content="{Binding SelectedAccount}"
   x:Name="BankingSelectedAccountPresenter" 
   TemplateApplied="BankingSelectedAccountPresenter_TemplateApplied" />

Then I handle the event like this:

void BankingSelectedAccountPresenter_TemplateApplied(object sender, EventArgs e) => this.UpdateSelectedTransactions();

private void UpdateSelectedTransactions()
{
    if (this.MyListView.SelectedItem?.GetType() is Type type)
    {
        DataTemplateKey key = new(type);
        var accountTemplate = (DataTemplate?)this.FindResource(key);
        Assumes.NotNull(accountTemplate);
        if (VisualTreeHelper.GetChildrenCount(this.BankingSelectedAccountPresenter) > 0)
        {
            ContentPresenter? presenter = VisualTreeHelper.GetChild(this.BankingSelectedAccountPresenter, 0) as ContentPresenter;
            Assumes.NotNull(presenter);
            presenter.ApplyTemplate();
            var transactionDataGrid = (DataGrid?)accountTemplate.FindName("TransactionDataGrid", presenter);
            this.ViewModel.Document.SelectedTransactions = transactionDataGrid?.SelectedItems;
        }
    }
}

Note the GetChildrenCount check that avoids an exception thrown from GetChild later if there are no children yet. We'll need that for later.

The TemplateApplied event is raised only once -- when the ContentControl is first given its ContentPresenter child. We still the UpdateSelectedTransactions method to run when the ListView in the 'master' part of the view changes selection:

void BankingPanelAccountList_SelectionChanged(object sender, SelectionChangedEventArgs e) => this.UpdateSelectedTransactions();

On initial startup, SelectionChanged is raised first, and we skip this one with the GetChildrenCount check. Then TemplateApplied is raised and we use the current selection to find the right template and search for the control we need. Later when the selection changes, the first event is raised again and re-triggers our logic.

The last trick is we must call ContentPresenter.ApplyTemplate() to force the template selection to be updated before we search for the child control. Without that, this code may still run before the template is updated based on the type of item selected in the ListView.

Related