Jak powiązać WPF DataGrid ze zmienną liczbą kolumn?

124

Moja aplikacja WPF generuje zestawy danych, które mogą mieć za każdym razem inną liczbę kolumn. Dane wyjściowe zawierają opis każdej kolumny, która zostanie użyta do zastosowania formatowania. Uproszczona wersja wyniku może wyglądać następująco:

class Data
{
    IList<ColumnDescription> ColumnDescriptions { get; set; }
    string[][] Rows { get; set; }
}

Ta klasa jest ustawiana jako DataContext na WPF DataGrid, ale faktycznie tworzę kolumny programowo:

for (int i = 0; i < data.ColumnDescriptions.Count; i++)
{
    dataGrid.Columns.Add(new DataGridTextColumn
    {
        Header = data.ColumnDescriptions[i].Name,
        Binding = new Binding(string.Format("[{0}]", i))
    });
}

Czy istnieje sposób zastąpienia tego kodu powiązaniami danych w pliku XAML zamiast tego?

Błąd ogólny
źródło

Odpowiedzi:

127

Oto obejście dla Binding Columns w DataGrid. Ponieważ właściwość Columns jest ReadOnly, jak wszyscy zauważyli, utworzyłem Attached Property o nazwie BindableColumns, która aktualizuje kolumny w DataGrid za każdym razem, gdy kolekcja zmienia się za pośrednictwem zdarzenia CollectionChanged.

Jeśli mamy tę kolekcję DataGridColumn

public ObservableCollection<DataGridColumn> ColumnCollection
{
    get;
    private set;
}

Następnie możemy powiązać BindableColumns z ColumnCollection w ten sposób

<DataGrid Name="dataGrid"
          local:DataGridColumnsBehavior.BindableColumns="{Binding ColumnCollection}"
          AutoGenerateColumns="False"
          ...>

Dołączona właściwość BindableColumns

public class DataGridColumnsBehavior
{
    public static readonly DependencyProperty BindableColumnsProperty =
        DependencyProperty.RegisterAttached("BindableColumns",
                                            typeof(ObservableCollection<DataGridColumn>),
                                            typeof(DataGridColumnsBehavior),
                                            new UIPropertyMetadata(null, BindableColumnsPropertyChanged));
    private static void BindableColumnsPropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
    {
        DataGrid dataGrid = source as DataGrid;
        ObservableCollection<DataGridColumn> columns = e.NewValue as ObservableCollection<DataGridColumn>;
        dataGrid.Columns.Clear();
        if (columns == null)
        {
            return;
        }
        foreach (DataGridColumn column in columns)
        {
            dataGrid.Columns.Add(column);
        }
        columns.CollectionChanged += (sender, e2) =>
        {
            NotifyCollectionChangedEventArgs ne = e2 as NotifyCollectionChangedEventArgs;
            if (ne.Action == NotifyCollectionChangedAction.Reset)
            {
                dataGrid.Columns.Clear();
                foreach (DataGridColumn column in ne.NewItems)
                {
                    dataGrid.Columns.Add(column);
                }
            }
            else if (ne.Action == NotifyCollectionChangedAction.Add)
            {
                foreach (DataGridColumn column in ne.NewItems)
                {
                    dataGrid.Columns.Add(column);
                }
            }
            else if (ne.Action == NotifyCollectionChangedAction.Move)
            {
                dataGrid.Columns.Move(ne.OldStartingIndex, ne.NewStartingIndex);
            }
            else if (ne.Action == NotifyCollectionChangedAction.Remove)
            {
                foreach (DataGridColumn column in ne.OldItems)
                {
                    dataGrid.Columns.Remove(column);
                }
            }
            else if (ne.Action == NotifyCollectionChangedAction.Replace)
            {
                dataGrid.Columns[ne.NewStartingIndex] = ne.NewItems[0] as DataGridColumn;
            }
        };
    }
    public static void SetBindableColumns(DependencyObject element, ObservableCollection<DataGridColumn> value)
    {
        element.SetValue(BindableColumnsProperty, value);
    }
    public static ObservableCollection<DataGridColumn> GetBindableColumns(DependencyObject element)
    {
        return (ObservableCollection<DataGridColumn>)element.GetValue(BindableColumnsProperty);
    }
}
Fredrik Hedblad
źródło
1
fajne rozwiązanie dla wzorca MVVM
WPFKK
2
Idealne rozwiązanie! Prawdopodobnie musisz zrobić kilka innych rzeczy w BindableColumnsPropertyChanged: 1. Sprawdź, czy dataGrid ma wartość null przed uzyskaniem do niej dostępu i zgłoś wyjątek z dobrym wyjaśnieniem dotyczącym wiązania tylko z DataGrid. 2. Sprawdź, czy w e.OldValue nie ma wartości null, i anuluj subskrypcję zdarzenia CollectionChanged, aby zapobiec wyciekom pamięci. Tylko dla twojego przekonania.
Mike Eshva
3
Rejestrujesz procedurę obsługi zdarzeń ze CollectionChangedzdarzeniem kolekcji kolumn, jednak nigdy jej nie wyrejestrujesz. W ten sposób DataGridzostanie utrzymany przy życiu tak długo, jak istnieje model widoku, nawet jeśli szablon kontrolny, który zawierał DataGridw pierwszej kolejności, został w międzyczasie zastąpiony. Czy istnieje gwarantowany sposób ponownego wyrejestrowania tego programu obsługi zdarzeń, gdy DataGridnie jest on już wymagany?
LUB Mapper
1
@OR Mapper: Teoretycznie jest, ale nie działa: WeakEventManager <ObservableCollection <DataGridColumn>, NotifyCollectionChangedEventArgs> .AddHandler (columns, "CollectionChanged", (s, ne) => {switch ....});
też
6
Nie jest to najlepsze rozwiązanie. Głównym powodem jest to, że używasz klas interfejsu użytkownika w ViewModel. Nie będzie również działać, gdy spróbujesz utworzyć przełączanie stron. Przełączając się z powrotem na stronę z takim datagrid, otrzymasz oczekiwanie w wierszu dataGrid.Columns.Add(column)DataGridColumn z nagłówkiem „X” już istnieje w kolekcji Columns elementu DataGrid. DataGrids nie może współużytkować kolumn i nie może zawierać zduplikowanych wystąpień kolumn.
Ruslan F.
19

Kontynuowałem moje badania i nie znalazłem żadnego rozsądnego sposobu, aby to zrobić. Właściwość Columns w DataGrid nie jest czymś, z czym mogę się powiązać, w rzeczywistości jest tylko do odczytu.

Bryan zasugerował, że można coś zrobić z AutoGenerateColumns, więc rzuciłem okiem. Używa prostej refleksji .Net, aby przyjrzeć się właściwościom obiektów w ItemsSource i generuje kolumnę dla każdego z nich. Być może mógłbym wygenerować typ w locie z właściwością dla każdej kolumny, ale to jest zbyteczne.

Ponieważ ten problem jest tak łatwy do rozwiązania w kodzie, będę trzymać się prostej metody rozszerzenia, którą wywołuję za każdym razem, gdy kontekst danych zostanie zaktualizowany o nowe kolumny:

public static void GenerateColumns(this DataGrid dataGrid, IEnumerable<ColumnSchema> columns)
{
    dataGrid.Columns.Clear();

    int index = 0;
    foreach (var column in columns)
    {
        dataGrid.Columns.Add(new DataGridTextColumn
        {
            Header = column.Name,
            Binding = new Binding(string.Format("[{0}]", index++))
        });
    }
}

// E.g. myGrid.GenerateColumns(schema);
Błąd ogólny
źródło
1
Rozwiązanie z największą liczbą głosów i zaakceptowane nie jest najlepszym rozwiązaniem! Dwa lata później odpowiedź brzmiałaby: msmvps.com/blogs/deborahk/archive/2011/01/23/…
Michaił
4
Nie, nie byłoby. I tak nie podany link, bo wynik takiego rozwiązania jest zupełnie inny!
321X
2
Wydaje się, że rozwiązanie Mealek jest znacznie bardziej uniwersalne i jest przydatne w sytuacjach, gdy bezpośrednie użycie kodu C # jest problematyczne, np. W ControlTemplates.
EFraim
Link do @Mikhail uszkodzony
LuckyLikey
3
tutaj jest link: blogs.msmvps.com/deborahk/ ...
Michaił
9

Znalazłem artykuł na blogu Deborah Kurata z niezłą sztuczką pokazującą zmienną liczbę kolumn w DataGrid:

Wypełnianie DataGrid z dynamicznymi kolumnami w aplikacji Silverlight przy użyciu MVVM

Zasadniczo tworzy DataGridTemplateColumni umieszcza ItemsControlwewnątrz, który wyświetla wiele kolumn.

Lukas Cenovsky
źródło
1
To zdecydowanie nie ten sam wynik, co wersja zaprogramowana !!
321X
1
@ 321X: Czy mógłbyś wyjaśnić, jakie są zaobserwowane różnice (a także sprecyzować, co masz na myśli przez wersję programowaną , ponieważ wszystkie rozwiązania tego problemu są zaprogramowane)?
LUB Mapper
Jest tam napisane „Nie znaleziono strony”
Jeson Martajaya
2
tutaj jest link blogs.msmvps.com/deborahk/ ...
Michaił
To jest po prostu niesamowite !!
Ravid Goldenberg
6

Udało mi się umożliwić dynamiczne dodawanie kolumny za pomocą zaledwie linii kodu takiego:

MyItemsCollection.AddPropertyDescriptor(
    new DynamicPropertyDescriptor<User, int>("Age", x => x.Age));

Wracając do pytania, nie jest to rozwiązanie oparte na XAML (ponieważ jak wspomniano, nie ma na to rozsądnego sposobu), ani też nie jest to rozwiązanie, które działałoby bezpośrednio z DataGrid.Columns. W rzeczywistości działa z komponentem ItemsSource powiązanym z DataGrid, który implementuje ITypedList i jako taki zapewnia niestandardowe metody pobierania PropertyDescriptor. W jednym miejscu w kodzie możesz zdefiniować „wiersze danych” i „kolumny danych” dla swojej siatki.

Jeśli chciałbyś:

IList<string> ColumnNames { get; set; }
//dict.key is column name, dict.value is value
Dictionary<string, string> Rows { get; set; }

możesz użyć na przykład:

var descriptors= new List<PropertyDescriptor>();
//retrieve column name from preprepared list or retrieve from one of the items in dictionary
foreach(var columnName in ColumnNames)
    descriptors.Add(new DynamicPropertyDescriptor<Dictionary, string>(ColumnName, x => x[columnName]))
MyItemsCollection = new DynamicDataGridSource(Rows, descriptors) 

a Twoja siatka przy użyciu powiązania z MyItemsCollection zostanie wypełniona odpowiednimi kolumnami. Te kolumny można dynamicznie modyfikować (nowe dodane lub istniejące usunięte) w czasie wykonywania, a grid automatycznie odświeży swoją kolekcję kolumn.

Wspomniany powyżej DynamicPropertyDescriptor to tylko uaktualnienie do zwykłego PropertyDescriptor i zapewnia definicję kolumn o silnym typie z kilkoma dodatkowymi opcjami. W przeciwnym razie DynamicDataGridSource działałoby po prostu dobrze zdarzenie z podstawowym PropertyDescriptor.

doblak
źródło
3

Stworzono wersję zaakceptowanej odpowiedzi, która obsługuje anulowanie subskrypcji.

public class DataGridColumnsBehavior
{
    public static readonly DependencyProperty BindableColumnsProperty =
        DependencyProperty.RegisterAttached("BindableColumns",
                                            typeof(ObservableCollection<DataGridColumn>),
                                            typeof(DataGridColumnsBehavior),
                                            new UIPropertyMetadata(null, BindableColumnsPropertyChanged));

    /// <summary>Collection to store collection change handlers - to be able to unsubscribe later.</summary>
    private static readonly Dictionary<DataGrid, NotifyCollectionChangedEventHandler> _handlers;

    static DataGridColumnsBehavior()
    {
        _handlers = new Dictionary<DataGrid, NotifyCollectionChangedEventHandler>();
    }

    private static void BindableColumnsPropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
    {
        DataGrid dataGrid = source as DataGrid;

        ObservableCollection<DataGridColumn> oldColumns = e.OldValue as ObservableCollection<DataGridColumn>;
        if (oldColumns != null)
        {
            // Remove all columns.
            dataGrid.Columns.Clear();

            // Unsubscribe from old collection.
            NotifyCollectionChangedEventHandler h;
            if (_handlers.TryGetValue(dataGrid, out h))
            {
                oldColumns.CollectionChanged -= h;
                _handlers.Remove(dataGrid);
            }
        }

        ObservableCollection<DataGridColumn> newColumns = e.NewValue as ObservableCollection<DataGridColumn>;
        dataGrid.Columns.Clear();
        if (newColumns != null)
        {
            // Add columns from this source.
            foreach (DataGridColumn column in newColumns)
                dataGrid.Columns.Add(column);

            // Subscribe to future changes.
            NotifyCollectionChangedEventHandler h = (_, ne) => OnCollectionChanged(ne, dataGrid);
            _handlers[dataGrid] = h;
            newColumns.CollectionChanged += h;
        }
    }

    static void OnCollectionChanged(NotifyCollectionChangedEventArgs ne, DataGrid dataGrid)
    {
        switch (ne.Action)
        {
            case NotifyCollectionChangedAction.Reset:
                dataGrid.Columns.Clear();
                foreach (DataGridColumn column in ne.NewItems)
                    dataGrid.Columns.Add(column);
                break;
            case NotifyCollectionChangedAction.Add:
                foreach (DataGridColumn column in ne.NewItems)
                    dataGrid.Columns.Add(column);
                break;
            case NotifyCollectionChangedAction.Move:
                dataGrid.Columns.Move(ne.OldStartingIndex, ne.NewStartingIndex);
                break;
            case NotifyCollectionChangedAction.Remove:
                foreach (DataGridColumn column in ne.OldItems)
                    dataGrid.Columns.Remove(column);
                break;
            case NotifyCollectionChangedAction.Replace:
                dataGrid.Columns[ne.NewStartingIndex] = ne.NewItems[0] as DataGridColumn;
                break;
        }
    }

    public static void SetBindableColumns(DependencyObject element, ObservableCollection<DataGridColumn> value)
    {
        element.SetValue(BindableColumnsProperty, value);
    }

    public static ObservableCollection<DataGridColumn> GetBindableColumns(DependencyObject element)
    {
        return (ObservableCollection<DataGridColumn>)element.GetValue(BindableColumnsProperty);
    }
}
Michaił Orłow
źródło
2

Możesz utworzyć kontrolę użytkownika z definicją siatki i zdefiniować kontrolki „podrzędne” z różnymi definicjami kolumn w XAML. Element nadrzędny potrzebuje właściwości zależności dla kolumn i metody ładowania kolumn:

Rodzic:


public ObservableCollection<DataGridColumn> gridColumns
{
  get
  {
    return (ObservableCollection<DataGridColumn>)GetValue(ColumnsProperty);
  }
  set
  {
    SetValue(ColumnsProperty, value);
  }
}
public static readonly DependencyProperty ColumnsProperty =
  DependencyProperty.Register("gridColumns",
  typeof(ObservableCollection<DataGridColumn>),
  typeof(parentControl),
  new PropertyMetadata(new ObservableCollection<DataGridColumn>()));

public void LoadGrid()
{
  if (gridColumns.Count > 0)
    myGrid.Columns.Clear();

  foreach (DataGridColumn c in gridColumns)
  {
    myGrid.Columns.Add(c);
  }
}

Dziecko Xaml:


<local:parentControl x:Name="deGrid">           
  <local:parentControl.gridColumns>
    <toolkit:DataGridTextColumn Width="Auto" Header="1" Binding="{Binding Path=.}" />
    <toolkit:DataGridTextColumn Width="Auto" Header="2" Binding="{Binding Path=.}" />
  </local:parentControl.gridColumns>  
</local:parentControl>

I wreszcie, najtrudniejsza część polega na znalezieniu, gdzie wywołać „LoadGrid”. Zmagam
się z tym, ale coś działało, wywołując after InitalizeComponentw moim konstruktorze okna (childGrid to x: name in window.xaml):

childGrid.deGrid.LoadGrid();

Powiązany wpis na blogu

Andy
źródło
1

Możesz to zrobić za pomocą AutoGenerateColumns i DataTemplate. Nie jestem pewien, czy to działałoby bez dużego nakładu pracy, musiałbyś się tym bawić. Szczerze mówiąc, jeśli masz już działające rozwiązanie, nie wprowadziłbym jeszcze zmiany, chyba że jest duży powód. Formant DataGrid staje się bardzo dobry, ale nadal wymaga trochę pracy (i zostało mi dużo do zrobienia), aby móc łatwo wykonywać dynamiczne zadania, takie jak ta.

Bryan Anderson
źródło
Powodem jest to, że pochodząc z ASP.Net jestem nowy w tym, co można zrobić z przyzwoitym wiązaniem danych i nie jestem pewien, gdzie są to ograniczenia. Bawię się z AutoGenerateColumns, dzięki.
Błąd ogólny
0

Oto przykład sposobu, w jaki robię to programowo:

public partial class UserControlWithComboBoxColumnDataGrid : UserControl
{
    private Dictionary<int, string> _Dictionary;
    private ObservableCollection<MyItem> _MyItems;
    public UserControlWithComboBoxColumnDataGrid() {
      _Dictionary = new Dictionary<int, string>();
      _Dictionary.Add(1,"A");
      _Dictionary.Add(2,"B");
      _MyItems = new ObservableCollection<MyItem>();
      dataGridMyItems.AutoGeneratingColumn += DataGridMyItems_AutoGeneratingColumn;
      dataGridMyItems.ItemsSource = _MyItems;

    }
private void DataGridMyItems_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
        {
            var desc = e.PropertyDescriptor as PropertyDescriptor;
            var att = desc.Attributes[typeof(ColumnNameAttribute)] as ColumnNameAttribute;
            if (att != null)
            {
                if (att.Name == "My Combobox Item") {
                    var comboBoxColumn =  new DataGridComboBoxColumn {
                        DisplayMemberPath = "Value",
                        SelectedValuePath = "Key",
                        ItemsSource = _ApprovalTypes,
                        SelectedValueBinding =  new Binding( "Bazinga"),   
                    };
                    e.Column = comboBoxColumn;
                }

            }
        }

}
public class MyItem {
    public string Name{get;set;}
    [ColumnName("My Combobox Item")]
    public int Bazinga {get;set;}
}

  public class ColumnNameAttribute : Attribute
    {
        public string Name { get; set; }
        public ColumnNameAttribute(string name) { Name = name; }
}
David Soler
źródło