Проблема:
«Я ищу способ зарегистрировать фабрику, которая знает, как создавать объекты для DataGrid». (Поскольку моя коллекция бизнес-объектов не предоставляет конструктор по умолчанию.)
Симптомы:
Если мы установим DataGrid.CanUserAddRows = true
и затем свяжем коллекцию элементов с DataGrid, где у элемента нет конструктора по умолчанию, тогда DataGrid не отображает «новую строку элемента».
Причины:
Когда набор элементов привязан к любому элементу WPF ItemControl, 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. Не требуется грязного переноса свойств или копирования значений.
Решение: (ура нашел один)
- Использование
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 и переопределять функциональность. Я был частично успешным, пытаясь это, вероятно, можно сделать с большим количеством усилий.