Как использовать factory для DataGrid.CanUserAddRows = true

Я хотел бы использовать функцию DataGrid.CanUserAddRows = true. К сожалению, он работает только с конкретными классами, которые имеют конструктор по умолчанию. Моя коллекция бизнес-объектов не предоставляет конструктор по умолчанию.

Я ищу способ зарегистрировать factory, который знает, как создавать объекты для DataGrid. Я взглянул на DataGrid и ListCollectionView, но ни один из них, похоже, не поддерживает мой сценарий.

Ответы

Ответ 1

Проблема:

"Я ищу способ зарегистрировать factory, который знает, как создавать объекты для DataGrid". (Поскольку моя коллекция бизнес-объектов не предоставляет конструктор по умолчанию.)

Симптомы:

Если мы установим DataGrid.CanUserAddRows = true, а затем привяжем коллекцию элементов к DataGrid, где элемент не имеет конструктора по умолчанию, тогда DataGrid не показывает строку "новый элемент".

Причины:

Когда коллекция элементов привязана к любому элементу ItemControl WPF, WPF обертывает коллекцию либо:

  • a BindingListCollectionView, когда привязка коллекции является BindingList<T>. BindingListCollectionView реализует IEditableCollectionView, но не реализует IEditableCollectionViewAddNewItem.

  • a 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);
  }
}

Non-решения:

  • Поддержка 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 для проецирования между списками этих объектов, а затем привязать к MyBusinessObject.BusinessObject в DataGrid. Нет грязной упаковки свойств или копирования требуемых значений.

Решение: (hurray found one)

  • Используйте 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

Я нашел другое решение этой проблемы. В моем случае мои объекты должны быть инициализированы с помощью factory, и нет никакого способа обойти это.

Я не мог использовать BindingList<T>, потому что моя коллекция должна поддерживать группировку, сортировку и фильтрацию, которые BindingList<T> не поддерживает.

Я решил проблему, используя событие DataGrid AddingNewItem. Это почти полностью недокументированное событие не только сообщает вам, что добавляется новый элемент, но также позволяет вам выбирать, какой элемент будет добавлен. AddingNewItem срабатывает прежде всего; свойство NewItem EventArgs просто null.

Даже если вы предоставите обработчик для события, DataGrid откажется позволить пользователю добавлять строки, если класс не имеет конструктора по умолчанию. Однако, странно (но, к счастью), если у вас его есть, и установите свойство NewItem AddingNewItemEventArgs, он никогда не будет вызван.

Если вы решите сделать это, вы можете использовать атрибуты, такие как [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;
  }
}

Таким образом, это не объясняет, почему нам еще нужно иметь конструктор (даже если он является фиктивным), чтобы появилась опция добавления строки. Я считаю, что ответ кроется в некотором коде, который определяет видимость строки NewItemPlaceholder, используя CanAddNew, а не CanAddNewItem. Это может считаться некоторой ошибкой.

Ответ 3

Я просмотрел IEditableCollectionViewAddNewItem и, похоже, добавляет эту функциональность.

Из MSDN

IEditableCollectionViewAddNewItem интерфейс позволяет применять разработчикам указать, какой тип объект для добавления в коллекцию. Эта расширяет интерфейс IEditableCollectionView, так что вы можете добавлять, редактировать и удалять элементы в коллекция. Добавлен IEditableCollectionViewAddNewItem метод AddNewItem, который принимает объект, который добавляется к коллекция. Этот метод полезен, когда сбор и объекты, которые вы хотите добавить, есть один или несколько из следующие характеристики:

  • Объекты в CollectionView являются разными типами.
  • Объекты не имеют конструктора по умолчанию.
  • Объект уже существует.
  • Вы хотите добавить нулевой объект в коллекцию.

Хотя в блоге Bea Stollnitz, вы можете прочитать следующее

  • Ограничение невозможности добавления нового элемента, когда источник не имеет конструктор по умолчанию очень хорошо понимаемый командой. WPF 4.0 Beta 2 имеет новую функцию, которая приносит нам шаг ближе к решению: представление о IEditableCollectionViewAddNewItem содержащий метод AddNewItem. Вы может прочитать документацию MSDN о эта особенность. Образец в MSDN показывает как использовать его при создании собственных пользовательский интерфейс, чтобы добавить новый элемент (используя ListBox для отображения данных и для ввода нового элемента). Из того, что я могу сказать, DataGrid doesnt однако использовать этот метод хотя (хотя его немного сложно быть на 100% уверенным потому что Reflector не декомпилирует 4.0 Beta 2 бита).

Этот ответ с 2009 года, возможно, он теперь можно использовать для DataGrid

Ответ 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;
}

Codebehind. В вашей модели ViewModel вам необходимо создать коллекцию обложек для вашей исходной коллекции, которая будет обрабатывать добавление/удаление элементов в datagrid.

    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. В моей ситуации бизнес-объекты содержались в IEntitySet в переносимом проекте (Silverlight), который не поддерживал 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;
        }