如何使用工厂来启用DataGrid的CanUserAddRows功能

20

我想使用DataGrid.CanUserAddRows = true功能。 不幸的是,它似乎只适用于具有默认构造函数的具体类。 我的业务对象集合不提供默认构造函数。

我正在寻找一种方法来注册一个工厂,该工厂知道如何为DataGrid创建对象。 我查看了DataGrid和ListCollectionView,但它们都不支持我的情况。

5个回答

30

问题:

"我正在寻找一种注册工厂的方法,该工厂知道如何为 DataGrid 创建对象"。 (因为我的业务对象集合没有提供默认构造函数。)

症状:

如果我们设置 DataGrid.CanUserAddRows = true,然后将一个没有默认构造函数的项集合绑定到 DataGrid,那么 DataGrid 就不会显示“新项目行”。

原因:

当将任何 WPF ItemControl 绑定到项集合时,WPF 会将集合包装在以下任一对象中:

  1. 当被绑定的集合是 BindingList<T> 时,使用 BindingListCollectionViewBindingListCollectionView 实现了 IEditableCollectionView 但未实现 IEditableCollectionViewAddNewItem

  2. 当被绑定的集合是其他类型时,使用 ListCollectionViewListCollectionView 实现了 IEditableCollectionViewAddNewItem(因此也实现了 IEditableCollectionView)。

对于选项 2),DataGrid 将创建新项的工作委托给 ListCollectionViewListCollectionView 在内部测试是否存在默认构造函数,并在不存在时禁用 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允许客户端附加回调函数,通过这个函数DataGrid可以请求一个默认的新项目,就像上面介绍的 BindingList<T>。这样一来,当需求产生新项时,客户端就可以第一时间创建一项。

但不幸的是,即使在.NET 4.5中,DataGrid也没有直接支持此功能。

尽管.NET 4.5似乎有一个之前不存在的'AddingNewItem'事件,但这只能让你知道正在添加一个新项。

解决方法:

  • 由同一程序集中的工具创建的业务对象:使用partial class

虽然这种情况很少见,但是想象一下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 ...);};
}

其他解决方案

  • 在现有的集合类型上实现IEditableCollectionViewAddNewItem。这可能需要大量工作。
  • 从ListCollectionView继承并覆盖功能。我尝试过一部分,但可能需要更多努力才能完成。

2
请注意,其他人报告BindingList不具有良好的可扩展性http://www.themissingdocs.net/wordpress/?p=465 - gap
很棒的答案。我不再使用ObservableCollection<T>,而是转而使用实际上可以完成相同工作的BindingList<T>,并在其构造函数中将AllowNew设置为true - Shimmy Weitzhandler

8
我已经找到了另一个解决此问题的方法。在我的情况下,我的对象需要使用工厂进行初始化,并且没有任何绕过这个问题的方法。我不能使用BindingList<T>,因为我的集合必须支持分组、排序和过滤,而BindingList<T>不支持。我通过使用DataGrid的AddingNewItem事件来解决了这个问题。这个几乎完全未记录的事件不仅告诉你正在添加一个新项,而且允许你选择要添加的项AddingNewItem在任何其他操作之前触发;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行的可见性。这可能被认为是某种错误。

1
我一直在苦苦挣扎着同样的问题,深入研究了https://referencesource.microsoft.com/#PresentationFramework/src/Framework/System/windows/Controls/DataGrid.cs.html,发现有一个CoerceCanUserAddRows,它查看的是CanAddNew而不是CanAddNewItem。我认为这应该被视为一个错误。 - sondergard

4
我查看了IEditableCollectionViewAddNewItem,它似乎可以添加这个功能。
根据MSDN的介绍,

IEditableCollectionViewAddNewItem接口使应用程序开发人员能够指定要添加到集合中的对象类型。此接口扩展了IEditableCollectionView,因此您可以在集合中添加、编辑和删除项目。IEditableCollectionViewAddNewItem添加了AddNewItem方法,该方法接受一个添加到集合中的对象。当集合和要添加的对象具有以下一个或多个特征时,此方法非常有用:

  • CollectionView中的对象是不同类型的。
  • 对象没有默认构造函数。
  • 对象已经存在。
  • 您想将null对象添加到集合中。
虽然在Bea Stollnitz博客中,您可以阅读以下内容:
  • 团队非常清楚不能在源没有默认构造函数时添加新项的限制。WPF 4.0 Beta 2引入了一个新功能,使我们更接近解决方案:引入包含AddNewItem方法的IEditableCollectionViewAddNewItem。您可以阅读有关此功能的MSDN文档。 MSDN中的示例显示了如何在创建自己的自定义UI以添加新项时使用它(使用ListBox显示数据和对话框输入新项)。从我所看到的,DataGrid尚未使用此方法(虽然很难100%确定,因为反编译器无法反编译4.0 Beta 2位)。
这个答案是2009年的,所以现在可能适用于DataGrid。

2
感谢您的出色回答。ListCollectionView类实现了IEditableCollectionViewAddNewItem接口。我通过Reflector查看了实现。Microsoft在这个类中进行了许多性能优化。我不想为自己实现此接口,只是想使用工厂方法。 - jbe
@jbe。我明白 :) 另外,关于IEditableCollectionViewAddNewItem的信息并不是很多,至少我没有找到。如果你找到了完成任务的方法,请务必更新。 - Fredrik Hedblad

0
我能建议的最简单的方法是,在没有默认构造函数的情况下为您的类提供包装器,其中将调用源类的构造函数。 例如,您有一个没有默认构造函数的类:
/// <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>

1
你甚至不需要一个包装器。你可以直接从现有的类继承并提供一个默认构造函数。 - Phil

0

我只是想提供一个使用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;
    }
}

然后使用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;
        }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接