Bagaimana cara mengikat DataGrid WPF ke sejumlah variabel kolom?

124

Aplikasi WPF saya menghasilkan kumpulan data yang mungkin memiliki jumlah kolom yang berbeda setiap saat. Termasuk dalam output adalah deskripsi setiap kolom yang akan digunakan untuk menerapkan format. Versi keluaran yang disederhanakan mungkin seperti ini:

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

Kelas ini ditetapkan sebagai DataContext pada WPF DataGrid tetapi saya benar-benar membuat kolom secara terprogram:

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

Apakah ada cara untuk mengganti kode ini dengan data binding di file XAML?

Kesalahan Umum
sumber

Jawaban:

127

Berikut adalah solusi untuk Binding Columns di datagrid. Karena properti Columns adalah ReadOnly, seperti yang diketahui semua orang, saya membuat Attached Property yang disebut BindableColumns yang memperbarui Kolom di DataGrid setiap kali koleksi berubah melalui acara CollectionChanged.

Jika kita memiliki Collection of DataGridColumn's

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

Kemudian kita bisa mengikat BindableColumns ke ColumnCollection seperti ini

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

The Attached Property 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
sumber
1
solusi bagus untuk pola MVVM
WPFKK
2
Solusi yang tepat! Mungkin Anda perlu melakukan beberapa hal lain di BindableColumnsPropertyChanged: 1. Periksa dataGrid untuk null sebelum mengaksesnya dan berikan pengecualian dengan penjelasan yang baik tentang mengikat hanya ke DataGrid. 2. Periksa e.OldValue untuk null dan berhenti berlangganan dari acara CollectionChanged untuk mencegah kebocoran memori. Hanya untuk meyakinkan Anda.
Mike Eshva
3
Anda mendaftarkan penangan peristiwa dengan CollectionChangedperistiwa koleksi kolom, namun Anda tidak pernah membatalkan pendaftarannya. Dengan cara itu, DataGridakan tetap hidup selama model tampilan ada, meskipun template kontrol yang berisi DataGriddi tempat pertama telah diganti sementara itu. Apakah ada cara yang dijamin untuk membatalkan registrasi event handler lagi saat DataGridtidak diperlukan lagi?
ATAU Mapper
1
@OR Mapper: Secara teoritis ada tetapi tidak berfungsi: WeakEventManager <ObservableCollection <DataGridColumn>, NotifyCollectionChangedEventArgs> .AddHandler (kolom, "CollectionChanged", (s, ne) => {switch ....});
juga
6
Ini bukan solusi. Alasan utamanya adalah Anda menggunakan kelas UI di ViewModel. Juga tidak akan berfungsi ketika Anda mencoba membuat beberapa pengalihan halaman. Saat beralih kembali ke halaman dengan datagrid seperti itu, Anda akan mendapatkan ekspektasi di baris dataGrid.Columns.Add(column)DataGridColumn dengan Header 'X' sudah ada di koleksi Kolom dari datagrid. DataGrids tidak dapat berbagi kolom dan tidak dapat berisi contoh kolom duplikat.
Ruslan F.
19

Saya telah melanjutkan penelitian saya dan belum menemukan cara yang masuk akal untuk melakukan ini. Properti Kolom di Datagrid bukanlah sesuatu yang bisa saya ikat, sebenarnya itu hanya baca.

Bryan menyarankan sesuatu untuk dilakukan dengan AutoGenerateColumns jadi saya melihatnya. Ini menggunakan refleksi .Net sederhana untuk melihat properti objek di ItemsSource dan menghasilkan kolom untuk masing-masing objek. Mungkin saya bisa menghasilkan tipe dengan cepat dengan properti untuk setiap kolom tetapi ini semakin keluar jalur.

Karena masalah ini sangat mudah ditemukan dalam kode, saya akan tetap menggunakan metode ekstensi sederhana yang saya panggil setiap kali konteks data diperbarui dengan kolom baru:

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);
Kesalahan Umum
sumber
1
Solusi yang dipilih dan diterima tertinggi bukanlah yang terbaik! Dua tahun kemudian jawabannya adalah: msmvps.com/blogs/deborahk/archive/2011/01/23/…
Mikhail
4
Tidak, tidak akan. Bukan tautan yang disediakan, karena hasil dari solusi itu sangat berbeda!
321X
2
Sepertinya solusi Mealek jauh lebih universal, dan berguna dalam situasi di mana penggunaan langsung kode C # bermasalah, misalnya di ControlTemplates.
EFraim
Tautan @ Mikhail rusak
LuckyLikey
3
ini tautannya: blogs.msmvps.com/deborahk/…
Mikhail
9

Saya telah menemukan artikel blog oleh Deborah Kurata dengan trik yang bagus bagaimana menunjukkan jumlah variabel kolom dalam datagrid:

Mengisi DataGrid dengan Kolom Dinamis di Aplikasi Silverlight menggunakan MVVM

Pada dasarnya, dia membuat DataGridTemplateColumndan menempatkan ItemsControldi dalam yang menampilkan banyak kolom.

Lukas Cenovsky
sumber
1
Sejauh ini hasilnya tidak sama dengan versi yang diprogram !!
321X
1
@ 321X: Bisakah Anda menjelaskan lebih lanjut tentang perbedaan yang diamati (dan juga menjelaskan apa yang Anda maksud dengan versi terprogram , karena semua solusi untuk ini sudah diprogram)?
ATAU Mapper
Tercantum
2
di sini adalah tautan blogs.msmvps.com/deborahk/…
Mikhail
Ini benar-benar luar biasa !!
Ravid Goldenberg
6

Saya berhasil memungkinkan untuk menambahkan kolom secara dinamis hanya dengan menggunakan sebaris kode seperti ini:

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

Mengenai pertanyaan, ini bukan solusi berbasis XAML (karena seperti yang disebutkan tidak ada cara yang masuk akal untuk melakukannya), juga bukan solusi yang akan beroperasi langsung dengan DataGrid.Columns. Ini benar-benar beroperasi dengan ItemsSource terikat DataGrid, yang mengimplementasikan ITypedList dan dengan demikian menyediakan metode kustom untuk pengambilan PropertyDescriptor. Di satu tempat dalam kode, Anda dapat menentukan "baris data" dan "kolom data" untuk kisi Anda.

Jika Anda ingin:

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

Anda bisa menggunakan misalnya:

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) 

dan grid Anda menggunakan pengikatan ke MyItemsCollection akan diisi dengan kolom yang sesuai. Kolom tersebut dapat dimodifikasi (baru ditambahkan atau yang sudah ada dihapus) pada waktu proses secara dinamis dan grid akan secara otomatis menyegarkan koleksi kolomnya.

DynamicPropertyDescriptor yang disebutkan di atas hanyalah peningkatan ke PropertyDescriptor biasa dan memberikan definisi kolom yang sangat diketik dengan beberapa opsi tambahan. DynamicDataGridSource sebaliknya akan bekerja dengan baik pada acara dengan PropertyDescriptor dasar.

doblak
sumber
3

Membuat versi jawaban yang diterima yang menangani berhenti berlangganan.

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);
    }
}
Mikhail Orlov
sumber
2

Anda dapat membuat kontrol pengguna dengan definisi kisi dan menentukan kontrol 'anak' dengan berbagai definisi kolom di xaml. Induk membutuhkan properti ketergantungan untuk kolom dan metode untuk memuat kolom:

Induk:


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

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

Dan akhirnya, bagian yang sulit adalah menemukan tempat untuk memanggil 'LoadGrid'.
Saya berjuang dengan ini tetapi membuat semuanya berfungsi dengan menelepon setelah InitalizeComponentdi konstruktor jendela saya (childGrid adalah x: nama di window.xaml):

childGrid.deGrid.LoadGrid();

Entri blog terkait

Andy
sumber
1

Anda mungkin dapat melakukan ini dengan AutoGenerateColumns dan DataTemplate. Saya tidak yakin apakah itu akan berhasil tanpa banyak pekerjaan, Anda harus bermain-main dengannya. Jujur saja jika Anda sudah memiliki solusi yang berfungsi, saya tidak akan membuat perubahan dulu kecuali ada alasan besar. Kontrol DataGrid menjadi sangat baik tetapi masih membutuhkan beberapa pekerjaan (dan saya memiliki banyak pembelajaran yang harus dilakukan) untuk dapat melakukan tugas-tugas dinamis seperti ini dengan mudah.

Bryan Anderson
sumber
Alasan saya adalah karena berasal dari ASP.Net. Saya baru mengetahui apa yang dapat dilakukan dengan pengikatan data yang layak dan saya tidak yakin di mana batasannya. Saya akan bermain dengan AutoGenerateColumns, terima kasih.
Kesalahan Umum
0

Ada contoh cara yang saya lakukan secara terprogram:

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
sumber