The template is not loaded at the point of creation. It will be loaded once the ListViewItems
are loaded.
Since the GridViewColumn
is just the model for the actual ListViewItem
and not part of the visual tree (it doesn't even extend FrameworkElement
), you can't access the GridViewColumn
or GridViewColumn.CellTemplate
directly. The actual cell is placed inside a GridViewRowPresenter
of the ListViewItem
.
The solution is to iterate over all items, once the ListView
is completely loaded and all its items are displayed:
private void FillListView(DataTable table)
{
GridView grid = (GridView)lvMain.View;
foreach (DataColumn col in table.Columns)
{
DataTemplate cellTemplate = CreateTemplate(col.ColumnName);
var gridColumn = new GridViewColumn()
{
Header = col.ColumnName,
CellTemplate = cellTemplate
};
grid.Columns.Add(gridColumn);
}
lvMain.HorizontalContentAlignment = HorizontalAlignment.Stretch;
lvMain.ItemsSource = ((IListSource)table).GetList();
HandleGridViewColumns(lvMain);
}
private void HandleGridViewColumns(ListView listView)
{
foreach (var item in listView.Items)
{
DependencyObject itemContainer = listView.ItemContainerGenerator.ContainerFromItem(item);
// Each item corresponds to one row, which contains multiple cells
foreach (ContentPresenter cellContent in FindVisualChildElements<ContentPresenter>(itemContainer))
{
if (!cellContent.IsLoaded)
{
cellContent.Loaded += OnCellContentLoaded;
continue;
}
SubscribeToDockPanel(cellContent);
}
}
}
private void OnCellContentLoaded(object sender, RoutedEventArgs e)
{
SubscribeToDockPanel(sender as DependencyObject);
}
private void SubscribeToDockPanel(DependencyObject visualParent)
{
if (TryFindVisualChildElementByName(visualParent, "cellPanel", out FrameworkElement element))
{
var dockPanel = element as DockPanel;
// Handle DockPanel
}
}
private IEnumerable<TChild> FindVisualChildElements<TChild>(DependencyObject parent)
where TChild : DependencyObject
{
if (parent is Popup popup)
{
parent = popup.Child;
if (parent == null)
{
yield break;
}
}
for (var childIndex = 0; childIndex < VisualTreeHelper.GetChildrenCount(parent); childIndex++)
{
DependencyObject childElement = VisualTreeHelper.GetChild(parent, childIndex);
if (childElement is TChild child)
{
yield return child;
}
foreach (TChild childOfChildElement in FindVisualChildElement<TChild>(childElement))
{
yield return childOfChildElement;
}
}
}
private bool TryFindVisualChildElementByName(DependencyObject parent, string childElementName, out FrameworkElement resultElement)
{
resultElement = null;
if (parent is Popup popup)
{
parent = popup.Child;
if (parent == null)
{
return false;
}
}
for (var childIndex = 0; childIndex < VisualTreeHelper.GetChildrenCount(parent); childIndex++)
{
DependencyObject childElement = VisualTreeHelper.GetChild(parent, childIndex);
if (childElement is FrameworkElement uiElement && uiElement.Name.Equals(
childElementName,
StringComparison.OrdinalIgnoreCase))
{
resultElement = uiElement;
return true;
}
if (TryFindVisualChildElementByName(childElement, childElementName, out resultElement))
{
return true;
}
}
return false;
}
The above solution will generally work, but will fail in a scenario where UI virtualization is enabled (which is the default for the ListView
). The result of UI virtualization is, that items that are not realized, won't have a item container generated. ItemGenerator.ContainerFromItem
would return null
in this case, which means the template is not applied and its visual tree therefore not loaded and part of the application tree.
I may update the answer later, to show how to access the item's container template in an UI virtualization context.
But as your primary goal was to attach mouse event handlers to the DockPanel
, I recommend a different solution.
UIElement
events are Routed Events (which, according to best practice, come in tandem with an instance event wrapper).
Routed Events have the advantage, that they don't require the observer to subscribe to the instance which raises the event. This eliminates code complexity. Instance events introduce the necessity to handle instance life-cycles and their attached event handlers, as instances are created and removed dynamically from/to the visual tree (e.g. TabControl
, UI virtualization). Additionally Routed Events don't require any knowledge of the visual tree in order to handle them.
Routed Events work differently. The dependency property system will traverse the visual tree and invoke registered RoutedEventHandler
delegates for specific events.
The recommended solution is therefore to subscribe to the required Routed Event e.g. UIElement.PreviewLeftMouseButtonUp
.
Because Routed Events are realized by the framework's dependency property system, the listener must be a DependencyObject
.
The following example registers an event handler for the UIElement.PreviewLeftMouseButtonUp
Routed Event, which will only handle events, that originate from any child element of a DockPanel
named CellPanel
:
C#
public MainWindow()
{
AddHandler(UIElement.MouseLeftButtonUpEvent, new MouseButtonEventHandler(OnUIElementClicked));
}
XAML
<MainWindow MouseLeftButtonUp="OnUIElementClicked" />
MainWindow.xaml.cs
private void OnUIElementClicked(object sender, MouseButtonEventArgs e)
{
// Check if the event source is a child of the DockPanel named CellPanel
if (TryFindVisualParentElementByName(e.OriginalSource as DependencyObject, "CellPanel", out FrameworkElement element))
{
// Handle DockPanel clicked
}
}
private bool TryFindVisualParentElementByName(DependencyObject child, string elementName, out FrameworkElement resultElement)
{
resultElement = null;
if (child == null)
{
return false;
}
var parentElement = VisualTreeHelper.GetParent(child);
if (parentElement is FrameworkElement frameworkElement
&& frameworkElement.Name.Equals(elementName, StringComparison.OrdinalIgnoreCase))
{
resultElement = frameworkElement;
return true;
}
return TryFindVisualParentElementByName(parentElement, elementName, out resultElement);
}
Remarks
Since you usually want to do something UI related, when a UI event was raised, I recommend to handle Routed Events using an EventTrigger
. They can be defined in XAML, which makes the code more easier to read (as their markup syntax is simpler than the C# syntax) and eliminates the need to manually traverse the visual tree. If you need access to elements of a template, you should move the triggers inside it.
From within the template's scope, you can easily target any named element:
<DataTemplate>
<DataTemplate.Triggers>
<EventTrigger RoutedEvent="UIElement.MouseEnter"
SourceName="CellPanel">
<BeginStoryBoard>
<StoryBoard>
<DoubleAnimation Storyboard.TargetName="CellText"
Storyboard.TargetProperty="Opacity" ... />
</StoryBoard>
</BeginStoryBoard>
</EventTrigger>
</DataTemplate.Triggers>
<DockPanel x:Name="CellPanel">
<TextBlock x:Name="CellText" />
...
</DockPanel>
</DataTemplate>
If you need to do more complex operations, like selecting all text of a TextBox
on MouseEnter
, you should write an Attached Behavior using attached properties.