我在自定义 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
内部的绑定,但也许我遗漏了一些东西......
我理解您有理由坚持使用
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
事件都会触发单元模板绑定的更新。
这确保了实现的项目容器始终是最新的,而不会显着影响整体性能。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)
{
}
}