如何将工厂用于 DataGrid.CanUserAddRows = true
Posted
技术标签:
【中文标题】如何将工厂用于 DataGrid.CanUserAddRows = true【英文标题】:How to use a factory for DataGrid.CanUserAddRows = true 【发布时间】:2011-05-27 22:08:33 【问题描述】:我想使用 DataGrid.CanUserAddRows = true 功能。不幸的是,它似乎只适用于具有默认构造函数的具体类。我的业务对象集合不提供默认构造函数。
我正在寻找一种方法来注册一个知道如何为 DataGrid 创建对象的工厂。我查看了 DataGrid 和 ListCollectionView,但它们似乎都不支持我的方案。
【问题讨论】:
【参考方案1】:问题:
“我正在寻找一种方法来注册一个知道如何为 DataGrid 创建对象的工厂”。 (因为我的业务对象集合没有提供默认构造函数。)
症状:
如果我们设置 DataGrid.CanUserAddRows = true
,然后将一组项目绑定到 DataGrid,其中项目没有默认构造函数,则 DataGrid 不会显示“新项目行”。
原因:
当项目集合绑定到任何 WPF ItemControl 时,WPF 将集合包装在以下任一中:
当被绑定的集合是 BindingList<T>
时,BindingListCollectionView。 BindingListCollectionView
实现了IEditableCollectionView,但没有实现IEditableCollectionViewAddNewItem
。
ListCollectionView 当被绑定的集合是任何其他集合时。 ListCollectionView
实现了IEditableCollectionViewAddNewItem(因此实现了IEditableCollectionView
)。
对于选项 2),DataGrid 将新项目的创建委托给 ListCollectionView
。 ListCollectionView
在内部测试默认构造函数是否存在,如果不存在则禁用 AddNew
。这是 ListCollectionView 中使用 DotPeek 的相关代码。
public bool CanAddNewItem (method from IEditableCollectionView)
get
if (!this.IsEditingItem)
return !this.SourceList.IsFixedSize;
else
return false;
bool CanConstructItem
private get
if (!this._isItemConstructorValid)
this.EnsureItemConstructor();
return this._itemConstructor != (ConstructorInfo) null;
似乎没有一种简单的方法可以覆盖此行为。
对于选项 1),情况要好得多。 DataGrid 将新项目的创建委托给 BindingListView,后者又委托给BindingList。 BindingList<T>
还检查是否存在默认构造函数,但幸运的是 BindingList<T>
还允许客户端设置 AllowNew 属性并附加一个事件处理程序以提供新项目。稍后查看解决方案,但这是BindingList<T>
中的相关代码
public bool AllowNew
get
if (this.userSetAllowNew || this.allowNew)
return this.allowNew;
else
return this.AddingNewHandled;
set
bool allowNew = this.AllowNew;
this.userSetAllowNew = true;
this.allowNew = value;
if (allowNew == value)
return;
this.FireListChanged(ListChangedType.Reset, -1);
非解决方案:
DataGrid 支持(不可用)期望 DataGrid 允许客户端附加一个回调是合理的,DataGrid 将通过该回调请求一个默认的新项目,就像上面的BindingList<T>
。这将使客户在需要时第一次创建新项目。
不幸的是,DataGrid 不直接支持这一点,即使在 .NET 4.5 中也是如此。
.NET 4.5 似乎确实有一个以前不可用的新事件“AddingNewItem”,但这只是让您知道正在添加一个新项目。
解决方法:
由同一程序集中的工具创建的业务对象:使用分部类这种情况看起来不太可能,但是想象一下 Entity Framework 创建它的实体类时没有默认构造函数(不太可能,因为它们不能被序列化),那么我们可以简单地创建一个带有默认构造函数的部分类。问题解决了。
业务对象在另一个程序集中,并且未密封:创建业务对象的超类型。这里我们可以继承业务对象类型并添加一个默认构造函数。
这最初似乎是个好主意,但转念一想,这可能需要比必要更多的工作,因为我们需要将业务层生成的数据复制到业务对象的超类型版本中。
我们需要类似的代码
class MyBusinessObject : BusinessObject
public MyBusinessObject(BusinessObject bo) ... copy properties of bo
public MyBusinessObject()
然后一些 LINQ 在这些对象的列表之间进行投影。
业务对象位于另一个程序集中,并且已密封(或未密封):封装业务对象。这更容易
class MyBusinessObject
public BusinessObject get; private set;
public MyBusinessObject(BusinessObject bo) BusinessObject = bo;
public MyBusinessObject()
现在我们需要做的就是使用一些 LINQ 在这些对象的列表之间进行投影,然后绑定到 DataGrid 中的MyBusinessObject.BusinessObject
。不需要杂乱的属性包装或值复制。
解决办法:(万岁找到了)
使用BindingList<T>
如果我们将业务对象集合包装在 BindingList<BusinessObject>
中,然后将 DataGrid 绑定到它,只需几行代码,我们的问题就解决了,DataGrid 将适当地显示一个新的项目行。
public void BindData()
var list = new BindingList<BusinessObject>( GetBusinessObjects() );
list.AllowNew = true;
list.AddingNew += (sender, e) =>
e.NewObject = new BusinessObject(... some default params ...);;
其他解决方案
在现有集合类型之上实现 IEditableCollectionViewAddNewItem。可能需要做很多工作。 从 ListCollectionView 继承并覆盖功能。我在尝试这个方面取得了部分成功,可能需要更多的努力才能完成。【讨论】:
请注意其他人报告的 BindingList 无法很好地扩展themissingdocs.net/wordpress/?p=465 很好的答案。我没有使用ObservableCollection<T>
,而是切换到实际上做同样事情的BindingList<T>
,并在其构造函数中将AllowNew
设置为true
。【参考方案2】:
我找到了解决这个问题的另一种方法。在我的情况下,我的对象需要使用工厂进行初始化,实际上没有任何方法可以解决这个问题。
我不能使用BindingList<T>
,因为我的集合必须支持分组、排序和过滤,而BindingList<T>
不支持。
我通过使用 DataGrid 的 AddingNewItem
事件解决了这个问题。这几乎是entirely undocumented event 不仅告诉您正在添加一个新项目,而且还告诉您allows lets you choose which item is being added。 AddingNewItem
先触发; EventArgs
的 NewItem
属性就是 null
。
即使您为事件提供处理程序,如果类没有默认构造函数,DataGrid 也将拒绝允许用户添加行。然而,奇怪的是(但幸运的是)如果你确实有一个,并且设置了 AddingNewItemEventArgs
的 NewItem
属性,它将永远不会被调用。
如果您选择这样做,您可以使用[Obsolete("Error", true)]
和[EditorBrowsable(EditorBrowsableState.Never)]
等属性来确保没有人调用构造函数。你也可以让构造函数体抛出异常
反编译控件可以让我们看到里面发生了什么。
private object AddNewItem()
this.UpdateNewItemPlaceholder(true);
object newItem1 = (object) null;
IEditableCollectionViewAddNewItem collectionViewAddNewItem = (IEditableCollectionViewAddNewItem) this.Items;
if (collectionViewAddNewItem.CanAddNewItem)
AddingNewItemEventArgs e = new AddingNewItemEventArgs();
this.OnAddingNewItem(e);
newItem1 = e.NewItem;
object newItem2 = newItem1 != null ? collectionViewAddNewItem.AddNewItem(newItem1) : this.EditableItems.AddNew();
if (newItem2 != null)
this.OnInitializingNewItem(new InitializingNewItemEventArgs(newItem2));
CommandManager.InvalidateRequerySuggested();
return newItem2;
正如我们所见,在版本4.5
中,DataGrid 确实使用了AddNewItem
。 CollectionListView.CanAddNewItem
的内容很简单:
public bool CanAddNewItem
get
if (!this.IsEditingItem)
return !this.SourceList.IsFixedSize;
else
return false;
所以这并不能解释为什么我们仍然需要一个构造函数(即使它是一个虚拟的)才能出现添加行选项。我相信答案在于使用CanAddNew
而不是CanAddNewItem
确定NewItemPlaceholder
行的可见性的一些代码。这可能被认为是某种错误。
【讨论】:
我一直在为完全相同的问题苦苦挣扎,并且正在通过referencesource.microsoft.com/#PresentationFramework/src/… 进行挖掘,发现有一个 CoerceCanUserAddRows 查看 CanAddNew 而不是 CanAddNewItem。我同意这应该被视为一个错误。【参考方案3】:我查看了IEditableCollectionViewAddNewItem,它似乎正在添加此功能。
来自 MSDN
IEditableCollectionViewAddNewItem 接口启用应用程序 开发人员指定什么类型 要添加到集合中的对象。这 接口扩展 IEditableCollectionView,所以你可以 添加、编辑和删除项目 收藏。 IEditableCollectionViewAddNewItem 添加 AddNewItem 方法,它需要一个 添加到 收藏。这种方法在以下情况下很有用 你的收藏和对象 想要添加有一个或多个 以下特点:
CollectionView 中的对象是不同的类型。 对象没有默认构造函数。 对象已存在。 您想向集合中添加一个空对象。
虽然在Bea Stollnitz blog,你可以阅读以下内容
来源无时无法添加新项目的限制 默认构造函数很好 被团队理解。 WPF 4.0 测试版 2 有一个新功能给我们带来了 离解决方案更近了一步: 介绍 IEditableCollectionViewAddNewItem 包含 AddNewItem 方法。你 可以阅读有关的 MSDN 文档 这项特征。 MSDN 中的示例显示 创建自己的时如何使用它 自定义 UI 添加新项目(使用 ListBox 显示数据和一个 对话框输入新项目)。 据我所知,DataGrid 没有 但是使用这种方法(虽然 100% 确定有点困难 因为 Reflector 不反编译 4.0 Beta 2 位)。
这个答案来自 2009 年,所以它现在可能可用于 DataGrid
【讨论】:
感谢您的出色回答。 ListCollectionView 类实现了 IEditableCollectionViewAddNewItem 接口。我通过反射器查看了实现。微软在这门课上做了很多性能优化。我不想仅仅为了使用工厂方法而为自己实现这个接口。 @jbe。我明白 :) 另外,关于 IEditableCollectionViewAddNewItem 的信息并不多,至少我找不到。如果您找到完成任务的方法,请务必更新【参考方案4】:我可以建议为您的类提供包装器的最简单方法,而无需默认构造函数,其中将调用源类的构造函数。 例如,你有这个没有默认构造函数的类:
/// <summary>
/// Complicate class without default constructor.
/// </summary>
public class ComplicateClass
public ComplicateClass(string name, string surname)
Name = name;
Surname = surname;
public string Name get; set;
public string Surname get; set;
为它写一个包装器:
/// <summary>
/// Wrapper for complicated class.
/// </summary>
public class ComplicateClassWraper
public ComplicateClassWraper()
_item = new ComplicateClass("def_name", "def_surname");
public ComplicateClassWraper(ComplicateClass item)
_item = item;
public ComplicateClass GetItem() return _item;
public string Name
get return _item.Name;
set _item.Name = value;
public string Surname
get return _item.Surname;
set _item.Surname = value;
ComplicateClass _item;
代码隐藏。 在您的 ViewModel 中,您需要为源集合创建包装器集合,它将处理数据网格中的项目添加/删除。
public MainWindow()
// Prepare collection with complicated objects.
_sourceCollection = new List<ComplicateClass>();
_sourceCollection.Add(new ComplicateClass("a1", "b1"));
_sourceCollection.Add(new ComplicateClass("a2", "b2"));
// Do wrapper collection.
WrappedSourceCollection = new ObservableCollection<ComplicateClassWraper>();
foreach (var item in _sourceCollection)
WrappedSourceCollection.Add(new ComplicateClassWraper(item));
// Each time new item was added to grid need add it to source collection.
// Same on delete.
WrappedSourceCollection.CollectionChanged += new NotifyCollectionChangedEventHandler(Items_CollectionChanged);
InitializeComponent();
DataContext = this;
void Items_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
if (e.Action == NotifyCollectionChangedAction.Add)
foreach (ComplicateClassWraper wrapper in e.NewItems)
_sourceCollection.Add(wrapper.GetItem());
else if (e.Action == NotifyCollectionChangedAction.Remove)
foreach (ComplicateClassWraper wrapper in e.OldItems)
_sourceCollection.Remove(wrapper.GetItem());
private List<ComplicateClass> _sourceCollection;
public ObservableCollection<ComplicateClassWraper> WrappedSourceCollection get; set;
最后,XAML 代码:
<DataGrid CanUserAddRows="True" AutoGenerateColumns="False"
ItemsSource="Binding Path=Items">
<DataGrid.Columns>
<DataGridTextColumn Header="Name" Binding="Binding Path=Name"/>
<DataGridTextColumn Header="SecondName" Binding="Binding Path=Surname"/>
</DataGrid.Columns>
</DataGrid>
【讨论】:
你甚至不需要包装器。您可以从现有类继承并提供默认构造函数。【参考方案5】:我只是想提供一个替代解决方案来使用 BindingList。在我的情况下,业务对象保存在不支持 IBindingList 的可移植项目 (Silverlight) 中的 IEntitySet 中。
解决方案首先是对网格进行子类化,并覆盖 CanUserAddRows 的强制回调以使用 IEditableCollectionViewAddNewItem:
public class DataGridEx : DataGrid
static DataGridEx()
CanUserAddRowsProperty.OverrideMetadata(typeof(DataGridEx), new FrameworkPropertyMetadata(true, null, CoerceCanUserAddRows));
private static object CoerceCanUserAddRows(DependencyObject sender, object newValue)
var dataGrid = (DataGrid)sender;
var canAddValue= (bool)newValue;
if (canAddValue)
if (dataGrid.IsReadOnly || !dataGrid.IsEnabled)
return false;
if (dataGrid.Items is IEditableCollectionViewAddNewItem v && v.CanAddNewItem == false)
// The view does not support inserting new items
return false;
return canAddValue;
然后使用 AddingNewItem 事件来创建项目:
dataGrid.AddingNewItem += (sender, args) => args.NewItem = new BusinessObject(args);
如果你关心细节,这就是它首先成为问题的原因。框架中的强制回调如下所示:
private static bool OnCoerceCanUserAddOrDeleteRows(DataGrid dataGrid, bool baseValue, bool canUserAddRowsProperty)
// Only when the base value is true do we need to validate that the user
// can actually add or delete rows.
if (baseValue)
if (dataGrid.IsReadOnly || !dataGrid.IsEnabled)
// Read-only/disabled DataGrids cannot be modified.
return false;
else
if ((canUserAddRowsProperty && !dataGrid.EditableItems.CanAddNew) ||
(!canUserAddRowsProperty && !dataGrid.EditableItems.CanRemove))
// The collection view does not allow the add or delete action
return false;
return baseValue;
你看到它是如何获得 IEditableCollectionView.CanAddNew 的了吗?这意味着它仅在视图可以插入并构造项目时启用添加。有趣的是,当我们想要添加一个新项目时,它会检查 IEditableCollectionViewAddNewItem.CanAddNewItem,它只询问视图是否支持插入新项目(而不是创建):
object newItem = null;
IEditableCollectionViewAddNewItem ani = (IEditableCollectionViewAddNewItem)Items;
if (ani.CanAddNewItem)
AddingNewItemEventArgs e = new AddingNewItemEventArgs();
OnAddingNewItem(e);
newItem = e.NewItem;
【讨论】:
以上是关于如何将工厂用于 DataGrid.CanUserAddRows = true的主要内容,如果未能解决你的问题,请参考以下文章
如何从数据流中获取特定数据以用于 Azure 数据工厂中的其他活动
A-frame 将 zip 包转换为 glb 用于草图工厂模型