WPF 使用预定义的 DataTemplate 动态添加列

问题描述 投票:0回答:1

我在自定义 WPF UserControl 的资源区域中定义了一个

DataTemplate
,它为一堆“标志”呈现一堆彩色方块。 目标是向 GridView 动态添加共享相同
DataTemplate
的列,但对
ItemsSource={Binding Flags}
属性具有不同的绑定。
请注意,我不提前知道需要动态添加哪些列,否则我可以在 XAML 中定义它们。

<UserControl.Resources>
    <DataTemplate x:Key="FlagColumnTemplate">
        <ItemsControl ItemsSource="{Binding Flags}">
            ...
        </ItemsControl>
    </DataTemplate>
</UserControl.Resources>

<ListView>
    <ListView.View>
        <GridView x:Name="gridView">
             <GridViewColumn Header="Static Column 1" />
             <!-- Additional columns are added dynamically in code-behind -->
        </GridView>
    </ListView.View>
</ListView>

对于这个示例,我在代码后面动态地将新列添加到我的 GridView 中,如下所示(这是为了简化,实际上动态列将基于视图模型内的数据):

var flagDataTemplate = Resources["FlagColumnTemplate"] as DataTemplate;

var newColumn1 = new GridViewColumn() { Header = "Dynamic Column 1", CellTemplate = flagDataTemplate };
var newColumn2 = new GridViewColumn() { Header = "Dynamic Column 2", CellTemplate = flagDataTemplate };

gridView.Columns.Add(newColumn1);
gridView.Columns.Add(newColumn2);

// Somehow modify the "Flags" binding in the DataTemplate for each new column.

我的目标是设置

ItemsSource="{Binding Flags}"
绑定,以便每个新的动态列都有自己的
Flags
对象,而不是所有列都有相同的对象。

我尝试将

MultiBinding
与多值转换器配对,希望根据每列的
DataTemplate
的列标题设置项目源,但这不起作用。问题是,在实时视觉树中
GridViewColumn
不存在,因此无法通过
GridViewColumn
CellTemplate
内部访问
RelativeSource

在创建新列时,我也看不到如何修改

flagDataTemplate
内部的绑定,但也许我遗漏了一些东西......

.net wpf gridview data-binding binding
1个回答
0
投票

我理解您有理由坚持使用

GridView
。因此,我决定发布一个简单的解决方案,允许您动态更改
GridViewColumn.CellTemplate
中的任意数量的绑定。唯一的要求是您必须命名具有动态
Binding
值的元素。您还必须使用
GridViewDynamicColumn
,它扩展了正常的
GridViewColumn
。您可以混合使用这两种类型或互换它们,而不会破坏任何东西。
GridViewColumn
完全被忽略。

使用方法

对于要在

Binding
中替换的每个
DataTemplate
,您必须定义一个
BindingInfo
来指定新的
Binding
、作为绑定目标的元素的元素名称和目标属性:

<UserControl.Resources>
  <DataTemplate x:Key="FlagColumnTemplate">
    <ItemsControl x:Name="FlagItemsView" 
                  ItemsSource="{Binding Flags}">
            ...
    </ItemsControl>

    <TextBlock x:Name="Label" 
               Text="{Binding LabelValue}" />
  </DataTemplate>
</UserControl.Resources>

<!-- Example replaces the bindings on two elements contained within the above CellTemplate -->
<ListView x:Name="FlagsView">
  <ListView.ItemContainerStyle>
    <Style TargetType="ListViewItem">

    <!-- Register the attached behavior with each ListViewItem -->
    <Setter Property="local:GridViewHelper.IsDynamicCellContentEnabled"
            Value="True" />
    </Style>
  </ListView.ItemContainerStyle>

  <ListView.View>
    <GridView x:Name="gridView">
      <GridViewColumn Header="Static Column 1" />

      <!-- Override CellTemplate bindings at XAML level -->
      <GridViewDynamicColumn Header="Dynamic Column 1">
        <local:BindingInfo TargetName="FlagItemsView" 
                           Binding="{Binding OtherFlags}"
                           TargetProperty="{x:Static ItemsControl.ItemsSourceProperty}" />
        <local:BindingInfo TargetName="Label"
                           Binding="{Binding Id}"                            
                           TargetProperty="{x:Static TextBlock.TextProperty}" />
      </GridViewDynamicColumn>

      <!-- Additional columns are added dynamically in code-behind -->
    </GridView>
  </ListView.View>
</ListView>
/*** Override template bindings at C# level ***/

var flagDataTemplate = Resources["FlagColumnTemplate"] as DataTemplate;

var newColumn = new GridViewDynamicColumn() 
{ 
  Header = "Dynamic Column 2", 
  CellTemplate = flagDataTemplate 
};

// Assuming that the data items of ListView.ItemsSource are 
// of fictional type RowDataItem
var newBinding = new Binding(nameof(RowDataItem.OtherFlags));

var bindingInfo = new BindingInfo(binding: newBinding, targetName: "FlagItemsView", targetProperty: ItemsControl.ItemsSourceProperty);
column.BindingInfos.Add(bindingInfo);
gridView.Columns.Add(newColumn);

// Force ListView to reload all item containers. 
// Because ListView uses UI virtualization this will not be too expensive.
// This way we only have to care about the visible items.
// Each ListViewItem.Loaded event will trigger the update of the cell template bindings.
// This ensures that the realized item containers are always up to date without significantly impacting the overall performance.
this.FlagsView.Items.Refresh();

实施

GridViewHelper.cs
因为 ListView 使用 UI 虚拟化,所以基于行的单元格更新不会太昂贵。
这样我们就可以自动处理当前可见的项目容器(而不是总是迭代所有行)。 每个

ListViewItem.Loaded
事件都会触发单元模板绑定的更新。 这确保了实现的项目容器始终是最新的,而不会显着影响整体性能。
在这种情况下,强烈建议启用 UI 虚拟化。

public sealed class GridViewHelper : DependencyObject
{
  public static bool GetIsDynamicCellContentEnabled(DependencyObject attachingElement)
    => (bool)attachingElement.GetValue(IsDynamicCellContentEnabledProperty);

  public static void SetIsDynamicCellContentEnabled(DependencyObject attachingElement, bool value)
    => attachingElement.SetValue(IsDynamicCellContentEnabledProperty, value);

  public static readonly DependencyProperty IsDynamicCellContentEnabledProperty = DependencyProperty.RegisterAttached(
    "IsDynamicCellContentEnabled",
    typeof(bool),
    typeof(GridViewHelper),
    new PropertyMetadata(default(bool), OnIsDynamicCellContentEnabledChanged));

  private static void OnIsDynamicCellContentEnabledChanged(DependencyObject attachingElement, DependencyPropertyChangedEventArgs e)
  {
    if (attachingElement is not ListViewItem listViewItem)
    {
      throw new ArgumentException($"Only {typeof(ListViewItem).FullName} supported.", nameof(attachingElement));
    }

    bool isEnabled = (bool)e.NewValue;
    if (isEnabled)
    {
      listViewItem.AddHandler(FrameworkElement.LoadedEvent, new RoutedEventHandler(OnListViewItemLoaded));
    }
    else
    {
      listViewItem.RemoveHandler(FrameworkElement.LoadedEvent, new RoutedEventHandler(OnListViewItemLoaded));
    }
  }

  private static void OnListViewItemLoaded(object sender, RoutedEventArgs e)
  {
    var listViewItem = (ListViewItem)sender;

    if (!TryFindVisualChildElement(listViewItem, out GridViewRowPresenter gridViewRowPresenter))
    {
      return;
    }

    int childrenCount = VisualTreeHelper.GetChildrenCount(gridViewRowPresenter);
    for (int cellIndex = 0; cellIndex < childrenCount; cellIndex++)
    {
      if (gridViewRowPresenter.Columns[cellIndex] is not GridViewDynamicColumn column)
      {
        continue;
      }

      DependencyObject contentPresenter = VisualTreeHelper.GetChild(gridViewRowPresenter, cellIndex);

      foreach (BindingInfo bindingInfo in column.BindingInfos)
      {
        if (TryFindVisualChildElementByName(contentPresenter, bindingInfo.TargetName, out FrameworkElement bindingTarget))
        {
          BindingBase propertyBinding = bindingInfo.Binding;
          _ = bindingTarget?.SetBinding(bindingInfo.TargetProperty, propertyBinding);
        }
      }
    }
  }

#region Helper

  private static bool TryFindVisualChildElementByName<TChild>(
    DependencyObject parent,
    string childElementName,
    out TChild? resultElement) where TChild : FrameworkElement
  {
    resultElement = null;

    if (parent is System.Windows.Controls.Primitives.Popup popup)
    {
      parent = popup.Child;
      if (parent == null)
      {
        return false;
      }
    }

    for (int 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 as TChild;
        return true;
      }

      if (TryFindVisualChildElementByName(childElement, childElementName, out resultElement))
      {
        return true;
      }
    }

    return false;
  }

  private static bool TryFindVisualChildElement<TChild>(DependencyObject parent, out TChild? resultElement)
    where TChild : DependencyObject
  {
    resultElement = null;

    if (parent is System.Windows.Controls.Primitives.Popup popup)
    {
      parent = popup.Child;
      if (parent == null)
      {
        return false;
      }
    }

    for (int childIndex = 0; childIndex < VisualTreeHelper.GetChildrenCount(parent); childIndex++)
    {
      DependencyObject childElement = VisualTreeHelper.GetChild(parent, childIndex);
      if (childElement is TChild child)
      {
        resultElement = child;
        return true;
      }

      if (TryFindVisualChildElement(childElement, out resultElement))
      {
        return true;
      }
    }

    return false;
  }

#endregion Helper
}

GridViewDynamicColumn.cs
启用 XAML

BindingInfo
定义。

[ContentProperty(nameof(GridViewDynamicColumn.BindingInfos))]
public class GridViewDynamicColumn : GridViewColumn
{
  public BindingInfoCollection BindingInfos { get; } = new BindingInfoCollection();
}

BindingInfo.cs
保存要替换的绑定信息的数据结构。

public class BindingInfo
{
  public BindingInfo()
  {
  }

  public BindingInfo(BindingBase binding, string targetName, DependencyProperty targetProperty)
  {
    ArgumentNullException.ThrowIfNull(binding);
    ArgumentNullException.ThrowIfNullOrEmpty(targetName);
    ArgumentNullException.ThrowIfNull(targetProperty);

    this.Binding = binding;
    this.TargetName = targetName;
    this.TargetProperty = targetProperty;
  }

  public BindingBase Binding { get; set; }
  public string TargetName { get; set; }
  public DependencyProperty TargetProperty { get; set; }
}

BindingInfoCollection.cs
用于启用 XAML

BindingInfo
定义的集合。

public class BindingInfoCollection : List<BindingInfo>
{
  public BindingInfoCollection()
  {
  }

  public BindingInfoCollection(IEnumerable<BindingInfo> collection) : base(collection)
  {
  }

  public BindingInfoCollection(int capacity) : base(capacity)
  {
  }
}
© www.soinside.com 2019 - 2024. All rights reserved.