我想使用DataGrid.CanUserAddRows = true功能。 不幸的是,它似乎只适用于具有默认构造函数的具体类。 我的业务对象集合不提供默认构造函数。
我正在寻找一种方法来注册一个工厂,该工厂知道如何为DataGrid创建对象。 我查看了DataGrid和ListCollectionView,但它们都不支持我的情况。
我想使用DataGrid.CanUserAddRows = true功能。 不幸的是,它似乎只适用于具有默认构造函数的具体类。 我的业务对象集合不提供默认构造函数。
我正在寻找一种方法来注册一个工厂,该工厂知道如何为DataGrid创建对象。 我查看了DataGrid和ListCollectionView,但它们都不支持我的情况。
问题:
"我正在寻找一种注册工厂的方法,该工厂知道如何为 DataGrid 创建对象"。 (因为我的业务对象集合没有提供默认构造函数。)
症状:
如果我们设置 DataGrid.CanUserAddRows = true
,然后将一个没有默认构造函数的项集合绑定到 DataGrid,那么 DataGrid 就不会显示“新项目行”。
原因:
当将任何 WPF ItemControl 绑定到项集合时,WPF 会将集合包装在以下任一对象中:
当被绑定的集合是 BindingList<T>
时,使用 BindingListCollectionView。 BindingListCollectionView
实现了 IEditableCollectionView 但未实现 IEditableCollectionViewAddNewItem
。
当被绑定的集合是其他类型时,使用 ListCollectionView。 ListCollectionView
实现了 IEditableCollectionViewAddNewItem(因此也实现了 IEditableCollectionView
)。
对于选项 2),DataGrid 将创建新项的工作委托给 ListCollectionView
。 ListCollectionView
在内部测试是否存在默认构造函数,并在不存在时禁用 AddNew
。以下是使用 DotPeek 查看的 ListCollectionView 中相关代码。
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,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可以请求一个默认的新项目,就像上面介绍的 BindingList<T>
。这样一来,当需求产生新项时,客户端就可以第一时间创建一项。
但不幸的是,即使在.NET 4.5中,DataGrid也没有直接支持此功能。
尽管.NET 4.5似乎有一个之前不存在的'AddingNewItem'事件,但这只能让你知道正在添加一个新项。
解决方法:
虽然这种情况很少见,但是想象一下Entity Framework创建了没有默认构造函数的实体类(不太可能,因为它们无法序列化),那么我们只需创建一个带有默认构造函数的partial class即可解决问题。
在这种情况下,我们可以从业务对象类型继承并添加默认构造函数。
起初看起来这是个好主意,但仔细一想,这可能需要比必要更多的工作,因为我们需要将业务层生成的数据复制到业务对象的超类型中。
我们需要这样的代码:
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来在这些对象列表之间进行投影,然后绑定到MyBusinessObject.BusinessObject
在DataGrid中。无需包装属性或复制值。
解决方案:(赞,找到了一个)
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 ...);};
}
其他解决方案
BindingList<T>
,因为我的集合必须支持分组、排序和过滤,而BindingList<T>
不支持。我通过使用DataGrid的AddingNewItem
事件来解决了这个问题。这个几乎完全未记录的事件不仅告诉你正在添加一个新项,而且允许你选择要添加的项。AddingNewItem
在任何其他操作之前触发;EventArgs
的NewItem
属性只是null
。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
行的可见性。这可能被认为是某种错误。虽然在Bea Stollnitz博客中,您可以阅读以下内容:IEditableCollectionViewAddNewItem接口使应用程序开发人员能够指定要添加到集合中的对象类型。此接口扩展了IEditableCollectionView,因此您可以在集合中添加、编辑和删除项目。IEditableCollectionViewAddNewItem添加了AddNewItem方法,该方法接受一个添加到集合中的对象。当集合和要添加的对象具有以下一个或多个特征时,此方法非常有用:
- CollectionView中的对象是不同类型的。
- 对象没有默认构造函数。
- 对象已经存在。
- 您想将null对象添加到集合中。
这个答案是2009年的,所以现在可能适用于DataGrid。
- 团队非常清楚不能在源没有默认构造函数时添加新项的限制。WPF 4.0 Beta 2引入了一个新功能,使我们更接近解决方案:引入包含AddNewItem方法的IEditableCollectionViewAddNewItem。您可以阅读有关此功能的MSDN文档。 MSDN中的示例显示了如何在创建自己的自定义UI以添加新项时使用它(使用ListBox显示数据和对话框输入新项)。从我所看到的,DataGrid尚未使用此方法(虽然很难100%确定,因为反编译器无法反编译4.0 Beta 2位)。
/// <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; }
}
<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>
我只是想提供一个使用BindingList的替代方案。在我的情况下,业务对象保存在可移植项目(Silverlight)中的IEntitySet中,该项目不支持IBindingList。
解决方案首先是子类化网格,并覆盖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;
}
}
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;
}
ObservableCollection<T>
,而是转而使用实际上可以完成相同工作的BindingList<T>
,并在其构造函数中将AllowNew
设置为true
。 - Shimmy Weitzhandler