1

I generate DataTemplate in code to pass it into a GridViewColumn of a GridView of a ListView:

    private static DataTemplate CreateTemplate(string sourceProperty)
    {
        string Xaml = "<DataTemplate xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\">" +
            "        <DataTemplate.Resources>" +
            "        <Style TargetType=\"DockPanel\">" +
            "            <Setter Property=\"HorizontalAlignment\" Value=\"Stretch\" />" +
            "            <Style.Triggers>" +
            "                <Trigger Property=\"IsMouseOver\" Value=\"True\">" +
            "                    <Setter Property=\"Background\" Value=\"Black\"/>" +
            "                </Trigger>" +
            "                <Trigger Property=\"IsMouseOver\" Value=\"False\">" +
            "                    <Setter Property=\"Background\" Value=\"White\"/>" +
            "                </Trigger>" +
            "            </Style.Triggers>" +
            "        </Style>" +
            "        </DataTemplate.Resources>" +
            "            <DockPanel x:Name=\"cellPanel\">" +
            "                <TextBlock Text=\"{Binding " + sourceProperty + "}\"/>" +
            "            </DockPanel>" +
            "    </DataTemplate>";

        return XamlReader.Parse(Xaml) as DataTemplate;
    }

I need to subscribe to mouse events of the DockPaneland cannot do it via parser, since it doesn't work. A workaround I found is to find the DockPanel by name (which is "cellPanel") and subscribe manually. How can I do it? Here is my method for fillinga ListView with columns and templates:

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

I could surely use TemplateFramework.FindName method, but both GridView and GridViewColumn are not FrameworkElements.

BionicCode
  • 1
  • 4
  • 28
  • 44
Arli Chokoev
  • 521
  • 6
  • 22
  • "A workaround I found is to use find the DockPanel by name (which is "cellPanel") and subscribe manually" - it is very unrelaible. I would try to 1. create behavior and include it temaple. or 2. in template convert event to command - Interactivity supports it (command provided via binding), or 3. use DynamicResource in template – ASh Mar 11 '20 at 10:33
  • I updated my answer to show how to use Routed Events. Instance events have the disadvantage, that you have to worry about instances to subscribe and unsubscribe to events, which is usually not required in an UI context. This is because the visual tree is very dynamic, as controls are added and removed at any time to improve performance (e.g. in an UI virtualization scenario). Using Routed Events also eliminates to deal with templates at all. A solution, which doesn't rely on the knowledge of the visual tree, is _always_ preferable. – BionicCode Mar 12 '20 at 00:51

1 Answers1

2

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.

BionicCode
  • 1
  • 4
  • 28
  • 44
  • "The solution is to iterate over all items, once the ListView is completely loaded and all its items are displayed" - I would rather have virtualization and option to add/remove items when needed – ASh Mar 11 '20 at 17:39
  • @ASh Yes, virtualization would does add complexity. Maybe the best solution, in order to keep things simple and robust but flexible, would be to take advantage of the event type. The mouse events, like almost all `UIElement` events, are Routed Events. Having an event handler that is just registered to the event, rather than to an instance, would be the recommended solution. I will add this to the answer. Thank you, for your comment. I didn't took virtualization into account. I was focused on how to access the panel. – BionicCode Mar 11 '20 at 20:22
  • @BionicCode The last solution you provided is very nice. Sure, I don't want to break virtualization of `ListView`'s `UIElement`s. But in case of routed events, how would you handle `MouseEnter` event on each cell? – Arli Chokoev Mar 12 '20 at 08:44
  • @ArliChokoev It depends on the scenario. If you want to do something UI related on `MouseEnter` I would use a `EventTrigger` defined in XAML, which I think would cover most situations. If you really need to dig into the cell's visual tree and need to use C#, you can follow the above example. What are you actually trying to achieve? Maybe the above method is not suitable. – BionicCode Mar 12 '20 at 10:53
  • @ArliChokoev As mentioned before, the preferred way is to use XAML and `EventTrigger` elements. If you have to modify a specific control inside the template e.g., the `TextBlock`, it would be wiser to move the trigger into this template instead of searching it later from some distant point in the visual tree (like the example does). – BionicCode Mar 12 '20 at 10:53
  • @ArliChokoev See the answer's remark section for more details. – BionicCode Mar 12 '20 at 11:15
  • @BionicCode I'm trying to bind a `DataTable` to a `ListView` with a `GridVIew` inside. THe number of columns is unknown, and that's why I create bindings with a `XamlReader`, iterating over columns. Additionally; I want to implement area/columns/cells selection, so I need to somehow handle `OnMouseEnter`, `OnMouseLeave` and probably some other mouse events in order to make selection more visual. However, thanks for remarks! – Arli Chokoev Mar 13 '20 at 08:10
  • @ArliChokoev I think you are overcomplicating things. If this is all you want to achieve, You should use the `DataGrid`. You can bind the `DataTable` directly to the `ItemsSource`. By default, it will auto-generate all columns for you, based on the `DataTable` column names. If you want to change the column naming or order, just subscribe to the `DataGrid.AutoGeneratingColumn` event ([example](https://learn.microsoft.com/en-us/dotnet/api/system.windows.controls.datagrid.autogeneratingcolumn?view=netframework-4.8#examples)). – BionicCode Mar 13 '20 at 09:08
  • @ArliChokoev For this, you don't need to create any templates in C#. I was already wondering why you are doing this. If you need a specialized column, choose one of the three build-in column types that derive from [DataGridColumn](https://learn.microsoft.com/en-us/dotnet/api/system.windows.controls.datagridcolumn?view=netframework-4.8) and also set it from `AutoGeneratingColumn` event by assigning an instance to the `DataGridAutoGeneratingColumnEventArgs.Column` property. – BionicCode Mar 13 '20 at 09:08