Как использовать фабрику для DataGrid.CanUserAddRows = true - PullRequest
18 голосов
/ 19 декабря 2010

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

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

Ответы [ 5 ]

28 голосов
/ 26 марта 2012

Проблема:

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

Симптомы:

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

Причины:

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

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

  2. 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. Не требуется грязного переноса свойств или копирования значений.

Решение: (ура нашел один)

  • Использование 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 и переопределять функциональность. Я был частично успешным, пытаясь это, вероятно, можно сделать с большим количеством усилий.
8 голосов
/ 13 сентября 2013

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

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

Я решил проблему с помощью события AddingNewItem DataGrid.Это почти полностью недокументированное событие не только сообщает вам, что добавляется новый элемент, но также позволяет вам выбирать, какой элемент добавляется .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.Это можно считать некоторой ошибкой.

4 голосов
/ 20 декабря 2010

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

Из MSDN

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

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

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

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

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

0 голосов
/ 01 февраля 2019

Я просто хотел предоставить альтернативное решение для использования 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;
        }
0 голосов
/ 23 марта 2012

Самый простой способ, которым я мог бы предложить предоставить оболочку для вашего класса без конструктора по умолчанию, в котором будет вызываться конструктор для исходного класса. Например, у вас есть этот класс без конструктора по умолчанию:

/// <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 вам нужно создать коллекцию оберток для вашей исходной коллекции, которая будет обрабатывать добавление / удаление элементов в сетке данных.

    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>
...