2010-12-19 15 views
15

Chciałbym skorzystać z funkcji DataGrid.CanUserAddRows = true. Niestety wydaje się, że działa tylko z konkretnymi klasami, które mają domyślny konstruktor. Moja kolekcja obiektów biznesowych nie udostępnia domyślnego konstruktora.Jak używać fabryki dla DataGrid.CanUserAddRows = true

Szukam sposobu na zarejestrowanie fabryki, która wie, jak tworzyć obiekty dla DataGrid. Spojrzałem na DataGrid i ListCollectionView, ale żaden z nich nie obsługuje mojego scenariusza.

Odpowiedz

4

Spojrzałem na IEditableCollectionViewAddNewItem i wydaje się, że dodałem tę funkcjonalność.

Od MSDN

IEditableCollectionViewAddNewItem interfejs umożliwia aplikacja deweloperom określić, jaki rodzaj obiektu dodać do kolekcji. Ten interfejs rozszerza się o IEditableCollectionView, dzięki czemu można dodawać, edytować i usuwać elementy z kolekcji . IEditableCollectionViewAddNewItem dodaje metodę AddNewItem, która pobiera obiekt dodany do kolekcji . Metoda ta jest przydatna, gdy gromadzenie i obiekty, które chcesz dodać jeden lub więcej z następujących cech:

  • Obiekty w CollectionView różne rodzaje.
  • Obiekty nie mają domyślnego konstruktora.
  • Obiekt już istnieje.
  • Chcesz dodać obiekt zerowy do kolekcji.

Chociaż na Bea Stollnitz blog, można przeczytać następujące

  • Ograniczenie nie jest w stanie dodać nowy element, gdy źródło nie ma domyślnego konstruktora jest bardzo dobrze zrozumiałe drużyna. WPF 4.0 Beta 2 ma nową funkcję, która prowadzi nas krok bliżej o rozwiązanie: wprowadzenie IEditableCollectionViewAddNewItem zawierającej metodę AddNewItem . Możesz można przeczytać w dokumentacji MSDN o tej funkcji. Przykład w MSDN pokazuje , jak z niego korzystać podczas tworzenia własnego niestandardowego interfejsu użytkownika , aby dodać nowy element (przy użyciu bloku listy , aby wyświetlić dane i okno dialogowe , aby wprowadzić nowy element). Z tego, co mogę powiedzieć, DataGrid nie korzysta już z (chociaż jest trochę trudno być w 100% pewnym , ponieważ Reflector nie dekompiluje 4,0 Beta 2 bitów).

Ta odpowiedź jest od 2009 roku, więc może to jest wykorzystywane do DataGrid teraz

+2

Dzięki za wspaniałą odpowiedź. Klasa ListCollectionView implementuje interfejs IEditableCollectionViewAddNewItem. Spojrzałem na implementację za pośrednictwem Reflectora.Microsoft wykonał wiele optymalizacji wydajności w tej klasie. Nie chcę zaimplementować tego interfejsu dla samej tylko metody fabrycznej. – jbe

+0

@jbe. Rozumiem, że :) Ponadto, nie było wiele informacji na temat IEditableCollectionViewAddNewItem, co najmniej nie udało mi się znaleźć. Pamiętaj, aby zaktualizować, jeśli znajdziesz sposób na wykonanie zadania. –

0

Najprostszym sposobem mógłbym zasugerować, aby zapewnić otoki dla klasy bez konstruktora domyślnego, w którym konstruktor klasy źródłowej zostanie wywołana . Na przykład masz tę klasę bez domyślnego konstruktora:

/// <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; } 
} 

Napisz otoki dla niego:

/// <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; 
} 

kodzie. W swoim ViewModelu należy utworzyć kolekcję wrapper dla kolekcji źródłowej, która będzie obsługiwać dodawanie/usuwanie elementów w 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; } 
} 

I wreszcie, kod 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

Nie potrzebujesz nawet opakowania. Możesz po prostu dziedziczyć z istniejącej klasy i zapewnić domyślny konstruktor. – Phil

27

Problem:

"szukam sposobu zarejestrowania zakładu, który wie, jak tworzyć obiekty do siatki danych" . (Ponieważ mój zbiór obiektów biznesowych nie zapewnia konstruktora domyślnego.)

Objawy:

Jeśli ustawimy DataGrid.CanUserAddRows = true a następnie powiązać zbiór elementów do DataGrid gdzie pozycja nie posiada domyślne konstruktor, a następnie DataGrid nie wyświetla "nowego wiersza pozycji".

Przyczyny:

Kiedy zbiór elementów jest związany z dowolnym WPF ItemControl, WPF owija w kolekcji albo:

  1. BindingListCollectionView gdy zbiór wiążąc to BindingList<T>. BindingListCollectionView implementuje IEditableCollectionView, ale nie implementuje IEditableCollectionViewAddNewItem.

  2. a ListCollectionView, gdy kolekcja jest związana, jakakolwiek inna kolekcja. ListCollectionView implementuje IEditableCollectionViewAddNewItem (i tym samym IEditableCollectionView).

Dla opcji 2) DataGrid deleguje tworzenie nowych pozycji do ListCollectionView. ListCollectionView Wewnętrznie testuje istnienie domyślnego konstruktora i wyłącza AddNew, jeśli taki nie istnieje. Oto odpowiedni kod z ListCollectionView przy użyciu 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; 
    } 
} 

Wydaje się, że nie istnieje łatwy sposób na zastąpienie tego zachowania.

Dla opcji 1) sytuacja jest o wiele lepsza. DataGrid deleguje tworzenie nowych elementów do BindingListView, który z kolei deleguje na BindingList. BindingList<T> sprawdza także istnienie domyślnego konstruktora, ale na szczęście BindingList<T> pozwala również klientowi ustawić właściwość AllowNew i dołączyć procedurę obsługi zdarzeń do dostarczania nowego elementu.Zobacz rozwiązanie później, ale tutaj jest odpowiedni kod w 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-rozwiązań:

  • Wsparcie przez DataGrid (niedostępne)

Byłoby rozsądnie oczekiwać DataGrid, aby umożliwić klientowi dołączenie wywołania zwrotnego, za pomocą którego DataGrid zażąda domyślnego nowego elementu, tak jak powyżej BindingList<T>. Dałoby to klientowi pierwsze pęknięcie przy tworzeniu nowego przedmiotu, gdy jest on wymagany.

Niestety, nie jest to obsługiwane bezpośrednio z DataGrid, nawet w .NET 4.5.

. Wydaje się, że .NET 4.5 ma nowe wydarzenie "AddingNewItem", które nie było wcześniej dostępne, ale to tylko pozwala na dodanie nowego przedmiotu.

arounds pracy:

  • firm obiekt utworzony przez narzędzie w tym samym zespole: stosować częściowe klasa

Ten scenariusz wydaje się bardzo mało prawdopodobne, ale wyobrażam sobie, że Entity Framework stworzony swoich klas encji bez domyślnego konstruktora (mało prawdopodobne, ponieważ nie można ich było serializować), moglibyśmy po prostu utworzyć klasę częściową z domyślnym konstruktorem. Problem rozwiązany.

  • Obiekt biznesowy znajduje się w innym zespole i nie jest zaplombowany: utwórz supertyp obiektu biznesowego.

Tutaj możemy dziedziczyć z typu obiektu biznesowego i dodawać domyślny konstruktor.

To początkowo wydawało się dobrym pomysłem, ale na drugi rzut oka może to wymagać więcej pracy, niż to konieczne, ponieważ musimy skopiować dane generowane przez warstwę biznesową do naszej superplikowej wersji obiektu biznesowego.

Musielibyśmy kod jak

class MyBusinessObject : BusinessObject 
{ 
    public MyBusinessObject(BusinessObject bo){ ... copy properties of bo } 
    public MyBusinessObject(){} 
} 

A potem jakiś LINQ do projektu pomiędzy listami tych obiektów.

  • obiektu biznesowego jest w innym zespole i jest uszczelniony (lub nie): hermetyzacji obiektu biznesowego.

Jest to o wiele łatwiejsze

class MyBusinessObject 
{ 
    public BusinessObject{ get; private set; } 

    public MyBusinessObject(BusinessObject bo){ BusinessObject = bo; } 
    public MyBusinessObject(){} 
} 

Teraz wszystko, co musimy zrobić, to użyć trochę LINQ do projektu pomiędzy listami tych obiektów, a następnie wiążą się MyBusinessObject.BusinessObject w DataGrid. Nie jest wymagane zawiłe pakowanie właściwości lub kopiowanie wartości.

Rozwiązanie: (hurra znaleźć jeden)

  • Zastosowanie BindingList<T>

Jeśli owinąć naszą kolekcję obiektów biznesowych w BindingList<BusinessObject> a następnie powiązać DataGrid do tego, z kilkoma linie kodu nasz problem został rozwiązany, a DataGrid odpowiednio wyświetli nowy wiersz pozycji.

public void BindData() 
{ 
    var list = new BindingList<BusinessObject>(GetBusinessObjects()); 
    list.AllowNew = true; 
    list.AddingNew += (sender, e) => 
     {e.NewObject = new BusinessObject(... some default params ...);}; 
} 

Inne rozwiązania

  • wdrożyć IEditableCollectionViewAddNewItem na szczycie istniejącego typu kolekcji. Prawdopodobnie dużo pracy.
  • dziedziczy z ListCollectionView i zastępuje funkcjonalność. Częściowo mi się to udało, prawdopodobnie można to zrobić przy większym wysiłku.
+1

Uwaga: inne osoby zgłaszające BindingList nie skalują się dobrze http://www.themissingdocs.net/wordpress/?p=465 – gap

7

Znalazłem inne rozwiązanie tego problemu. W moim przypadku, moje obiekty muszą zostać zainicjalizowane za pomocą fabryki i nie ma na to żadnego sposobu, aby to obejść.

Nie mogłem użyć BindingList<T>, ponieważ moja kolekcja musi obsługiwać grupowanie, sortowanie i filtrowanie, które nie jest obsługiwane przez BindingList<T>.

Rozwiązałem problem za pomocą zdarzenia DataGrid AddingNewItem. To prawie entirely undocumented event informuje nie tylko o dodaniu nowego przedmiotu, ale także o allows lets you choose which item is being added. AddingNewItem pożary przed czymkolwiek innym; właściwość NewItem z EventArgs jest po prostu null.

Nawet jeśli udostępni się procedurę obsługi zdarzenia, DataGrid nie pozwoli użytkownikowi dodać wierszy, jeśli klasa nie ma domyślnego konstruktora. Jednakże, dziwacznie (ale na szczęście), jeśli go masz i ustawisz właściwość NewItem na AddingNewItemEventArgs, nigdy nie zostanie wywołana.

Jeśli zdecydujesz się to zrobić, możesz użyć atrybutów, takich jak [Obsolete("Error", true)] i [EditorBrowsable(EditorBrowsableState.Never)], aby upewnić się, że nikt nigdy nie wywoła konstruktora. Można również wysłać wyjątek od konstruktora body. Dekodowanie kontrolki pozwala nam zobaczyć, co się tam dzieje.

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; 
} 

Jak widzimy, w wersji 4.5, DataGrid rzeczywiście skorzystać z AddNewItem. Zawartość CollectionListView.CanAddNewItem są po prostu:

public bool CanAddNewItem 
{ 
    get 
    { 
    if (!this.IsEditingItem) 
     return !this.SourceList.IsFixedSize; 
    else 
     return false; 
    } 
} 

Więc to nie wyjaśnia, dlaczego wciąż musimy mieć konstruktor (nawet jeśli jest to obojętne) w celu opcję Dodaj wiersz do stawienia się. Wierzę, że odpowiedź leży w pewnym kodzie, który określa widoczność wiersza NewItemPlaceholder przy użyciu CanAddNew zamiast CanAddNewItem. Można to uznać za rodzaj błędu.