簡體   English   中英

從 GridViewColumn 或 GridViewColumn.CellTemplate 的 GridView 獲取項目

[英]Get items from GridViewColumn or GridViewColumn.CellTemplate of GridView

我在代碼中生成DataTemplate以將其傳遞到ListViewGridViewColumnGridView中:

    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鼠標事件並且不能通過解析器來完成,因為它不起作用。 我發現的一種解決方法是按名稱(即“cellPanel”)查找DockPanel並手動訂閱。 我該怎么做? 這是我用列和模板填充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方法,但GridViewGridViewColumn都不是FrameworkElement s。

模板在創建時未加載。 一旦加載了ListViewItems它就會被加載。

由於GridViewColumn只是實際ListViewItem的模型,而不是可視化樹的一部分(它甚至不擴展FrameworkElement ),因此您無法直接訪問GridViewColumnGridViewColumn.CellTemplate 實際單元格放置在ListViewItemGridViewRowPresenter內。

解決方案是遍歷所有項目,一旦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;
}

上述解決方案通常可以工作,但在啟用 UI 虛擬化(這是ListView的默認設置)的情況下會失敗。 UI 虛擬化的結果是,未實現的項目不會生成項目容器。 在這種情況下, ItemGenerator.ContainerFromItem將返回null ,這意味着未應用模板,因此它的可視化樹未加載並且是應用程序樹的一部分。
我稍后可能會更新答案,以展示如何在 UI 虛擬化上下文中訪問項目的容器模板。

但由於您的主要目標是將鼠標事件處理程序附加到DockPanel ,我建議使用不同的解決方案。

UIElement事件是路由事件(根據最佳實踐,它與實例事件包裝器一起出現)。
路由事件的優點是它們不需要觀察者訂閱引發事件的實例。 這消除了代碼復雜性。 實例事件引入了處理實例生命周期及其附加事件處理程序的必要性,因為實例是在可視化樹中動態創建和刪除的(例如TabControl 、UI 虛擬化)。 此外,路由事件不需要任何可視化樹的知識來處理它們。

路由事件的工作方式不同。 依賴屬性系統將遍歷可視化樹並為特定事件調用已注冊的RoutedEventHandler委托。
因此,推薦的解決方案是訂閱所需的路由事件,例如UIElement.PreviewLeftMouseButtonUp

因為路由事件是由框架的依賴屬性系統實現的,所以監聽器必須是一個DependencyObject

以下示例為UIElement.PreviewLeftMouseButtonUp路由事件注冊了一個事件處理程序,該事件將僅處理源自名為CellPanelDockPanel CellPanel元素的CellPanel

C#

public MainWindow()
{
  AddHandler(UIElement.MouseLeftButtonUpEvent, new MouseButtonEventHandler(OnUIElementClicked));
}

XAML

<MainWindow MouseLeftButtonUp="OnUIElementClicked" />

主窗口.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);
}

評論

由於您通常想做一些與 UI 相關的事情,因此在引發 UI 事件時,我建議使用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>

如果您需要執行更復雜的操作,例如在MouseEnter上選擇TextBox所有文本,您應該使用附加屬性編寫一個附加行為

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM