Получить элементы из GridViewColumn или GridViewColumn.CellTemplate из GridView - PullRequest
0 голосов
/ 11 марта 2020

Я генерирую DataTemplate в коде, чтобы передать его в GridViewColumn из GridView из 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;
    }

Мне нужно подписаться на события мыши в DockPanel и не может сделать это через парсер, так как он не работает. Обходной путь, который я нашел, состоит в том, чтобы найти DockPanel по имени (которое является "cellPanel") и подписаться вручную. Как мне это сделать? Вот мой метод заполнения ListView столбцами и шаблонами:

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

Я, конечно, мог бы использовать метод TemplateFramework.FindName, но оба GridView и GridViewColumn не являются FrameworkElement с.

1 Ответ

1 голос
/ 11 марта 2020

Шаблон не загружен в момент создания. Он будет загружен после загрузки ListViewItems.

Поскольку GridViewColumn - это просто модель фактического ListViewItem, а не часть визуального дерева (оно даже не расширяется FrameworkElement ), вы не можете получить доступ к GridViewColumn или GridViewColumn.CellTemplate напрямую. Фактическая ячейка находится внутри GridViewRowPresenter из ListViewItem.

Решение состоит в том, чтобы перебрать все элементы, когда ListView полностью загружен и отображаются все его элементы :

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

Приведенное выше решение, как правило, будет работать, но не будет работать в сценарии, в котором включена виртуализация пользовательского интерфейса (по умолчанию для ListView). Результатом виртуализации пользовательского интерфейса является то, что не реализованные элементы не будут генерировать контейнер элементов. ItemGenerator.ContainerFromItem вернет null в этом случае, что означает, что шаблон не применяется, и поэтому его визуальное дерево не загружено и является частью дерева приложения.
Я могу обновить ответ позже, чтобы показать, как получить доступ к элементу. шаблон контейнера в контексте виртуализации пользовательского интерфейса.

Но поскольку вашей основной целью было присоединить обработчики событий мыши к DockPanel, я рекомендую другое решение.

UIElement события являются маршрутизируемыми событиями (которые, согласно лучшей практике, идут в тандеме с оберткой событий экземпляра).
Преимущество маршрутизируемых событий состоит в том, что они не требуют от подписчика подписки к экземпляру, который вызывает событие. Это устраняет сложность кода. События экземпляров вводят необходимость обработки жизненных циклов экземпляров и связанных с ними обработчиков событий, поскольку экземпляры создаются и удаляются динамически из / в визуальное дерево (например, TabControl, виртуализация пользовательского интерфейса). Кроме того, маршрутизируемые события не требуют каких-либо знаний о визуальном дереве для их обработки.

Маршрутизируемые события работают по-разному. Система свойств зависимостей будет проходить по визуальному дереву и вызывать зарегистрированных RoutedEventHandler делегатов для определенных c событий.
Поэтому рекомендуется подписаться на необходимое маршрутизируемое событие, например UIElement.PreviewLeftMouseButtonUp.

Поскольку маршрутизируемые события реализуются системой свойств зависимостей платформы, слушатель должен быть DependencyObject.

В следующем примере регистрируется обработчик события для маршрутизируемого события UIElement.PreviewLeftMouseButtonUp, который будет обрабатывать только события, которые происходят от любого дочернего элемента DockPanel с именем 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);
}

Замечания

Так как вы обычно хотите сделать что-то связанное с пользовательским интерфейсом, когда пользовательский интерфейс событие было возбуждено, я рекомендую обрабатывать маршрутизируемые события, используя EventTrigger. Они могут быть определены в XAML, что делает код более легким для чтения (поскольку их синтаксис разметки проще, чем синтаксис C#) и устраняет необходимость обхода визуального дерева вручную. Если вам нужен доступ к элементам шаблона, вы должны переместить триггеры внутри него.
Из области видимости шаблона вы можете легко настроить таргетинг на любой именованный элемент:

<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>

Если вам нужно сделать больше сложные операции, такие как выделение всего текста TextBox на MouseEnter, вы должны написать Attached Behavior , используя присоединенные свойства.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...