如何将工厂用于 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 将新项目的创建委托给 ListCollectionViewListCollectionView 在内部测试默认构造函数是否存在,如果不存在则禁用 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&lt;T&gt;,而是切换到实际上做同样事情的BindingList&lt;T&gt;,并在其构造函数中将AllowNew设置为true【参考方案2】:

我找到了解决这个问题的另一种方法。在我的情况下,我的对象需要使用工厂进行初始化,实际上没有任何方法可以解决这个问题。

我不能使用BindingList&lt;T&gt;,因为我的集合必须支持分组、排序和过滤,而BindingList&lt;T&gt; 不支持。

我通过使用 DataGrid 的 AddingNewItem 事件解决了这个问题。这几乎是entirely undocumented event 不仅告诉您正在添加一个新项目,而且还告诉您allows lets you choose which item is being addedAddingNewItem 先触发; EventArgsNewItem 属性就是 null

即使您为事件提供处理程序,如果类没有默认构造函数,DataGrid 也将拒绝允许用户添加行。然而,奇怪的是(但幸运的是)如果你确实有一个,并且设置了 AddingNewItemEventArgsNewItem 属性,它将永远不会被调用。

如果您选择这样做,您可以使用[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 确实使用了AddNewItemCollectionListView.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 用于草图工厂模型

Angular with Jquery - 如何将 jQuery 包含到工厂中并在多个控制器之间使用/共享该工厂?

如何使用 azure 数据工厂下载 blob

如何在 Azure 数据工厂中执行 SQL 查询

创建型模式总结