Как привязать WPF DataGrid к переменному количеству столбцов?

Мое приложение WPF генерирует наборы данных, которые каждый раз могут иметь разное количество столбцов. В вывод включено описание каждого столбца, который будет использоваться для применения форматирования. Упрощенная версия вывода может выглядеть примерно так:

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

Этот класс установлен как DataContext в WPF DataGrid, но на самом деле я создаю столбцы программно:

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

Есть ли способ заменить этот код привязками данных в файле XAML?

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
124
0
115 666
8
Перейти к ответу Данный вопрос помечен как решенный

Ответы 8

Возможно, вы сможете сделать это с помощью AutoGenerateColumns и DataTemplate. Я не уверен, что это будет работать без большого количества работы, вам придется поиграть с этим. Честно говоря, если у вас уже есть работающее решение, я бы пока не стал вносить изменения, если нет серьезной причины. Элемент управления DataGrid становится очень хорошим, но он все еще нуждается в некоторой доработке (а мне еще предстоит много учиться), чтобы иметь возможность легко выполнять такие динамические задачи.

Моя причина в том, что из ASP.Net я новичок в том, что можно сделать с приличным связыванием данных, и я не уверен, где его ограничения. Я поиграю с AutoGenerateColumns, спасибо.

Generic Error 26.11.2008 23:37

Я продолжил свои исследования и не нашел разумного способа сделать это. Свойство Columns в DataGrid не является чем-то, с чем я могу связываться, на самом деле оно только для чтения.

Брайан предположил, что что-то можно сделать с помощью AutoGenerateColumns, поэтому я посмотрел. Он использует простое отражение .Net для просмотра свойств объектов в ItemsSource и генерирует столбец для каждого из них. Возможно, я мог бы «на лету» сгенерировать тип со свойством для каждого столбца, но это уже не так.

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

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

Решение, получившее наибольшее количество голосов и принятое, не является лучшим! Два года спустя ответ будет: msmvps.com/blogs/deborahk/archive/2011/01/23/…

Mikhail 18.04.2011 22:08

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

321X 10.08.2011 23:59

Похоже, что решение Mealek гораздо более универсально и полезно в ситуациях, когда прямое использование кода C# проблематично, например. в ControlTemplates.

EFraim 07.12.2011 22:59

вот ссылка: blogs.msmvps.com/deborahk/…

Mikhail 16.09.2015 16:20

Вы можете создать пользовательский элемент управления с определением сетки и определить «дочерние» элементы управления с различными определениями столбцов в xaml. Родителю требуется свойство зависимости для столбцов и метод загрузки столбцов:

Родитель:


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

Дочерний 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>

И, наконец, самая сложная часть - найти, где вызвать LoadGrid. Я борюсь с этим, но получил работу, вызвав после InitalizeComponent в моем конструкторе окна (childGrid - это x: name в window.xaml):

childGrid.deGrid.LoadGrid();

Связанная запись в блоге

Ответ принят как подходящий

Вот обходной путь для привязки столбцов в DataGrid. Поскольку свойство Columns имеет значение ReadOnly, как все заметили, я создал прикрепленное свойство под названием BindableColumns, которое обновляет столбцы в DataGrid каждый раз, когда коллекция изменяется посредством события CollectionChanged.

Если у нас есть эта коллекция DataGridColumn's

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

Затем мы можем привязать BindableColumns к ColumnCollection следующим образом

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

Прикрепленное свойство 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);
    }
}

Согласен, мне больше всего нравится это решение.

Jaime 27.11.2012 23:45

хорошее решение для шаблона MVVM

WPFKK 18.04.2013 18:17

Прекрасное решение! Вероятно, вам нужно сделать еще несколько вещей в BindableColumnsPropertyChanged: 1. Проверить dataGrid на null перед доступом к нему и выбросить исключение с подробным объяснением привязки только к DataGrid. 2. Проверьте e.OldValue на null и откажитесь от подписки на событие CollectionChanged, чтобы предотвратить утечку памяти. Просто для вашего убеждения.

Mike Eshva 23.10.2013 17:01

Это то, что я искал. Лучший ответ для добавления динамических столбцов в сетку данных

Nikhil Chavan 28.12.2013 10:14

Вы регистрируете обработчик событий в событии CollectionChanged коллекции столбцов, но никогда не отменяете его регистрацию. Таким образом, DataGrid будет оставаться в живых до тех пор, пока существует модель представления, даже если шаблон управления, который изначально содержал DataGrid, был заменен тем временем. Есть ли какой-либо гарантированный способ отменить регистрацию этого обработчика событий снова, когда DataGrid больше не требуется?

O. R. Mapper 29.12.2013 04:06

@O. Р. Мэппер: Теоретически есть, но не работает: WeakEventManager <ObservableCollection <DataGridColumn>, NotifyCollectionChangedEventArgs> .AddHandler (columns, "CollectionChanged", (s, ne) => {switch ....});

too 22.07.2014 14:09

Это не лучшее решение. Основная причина в том, что вы используете классы пользовательского интерфейса в ViewModel. Также это не сработает, когда вы попытаетесь создать какое-либо переключение страниц. При переключении обратно на страницу с такой сеткой данных вы получите ожидание в строке dataGrid.Columns.Add(column) DataGridColumn с заголовком «X», уже существующей в коллекции Columns DataGrid. DataGrids не может совместно использовать столбцы и не может содержать повторяющиеся экземпляры столбцов.

Ruslan F. 06.10.2015 18:12

@RuslanF. Чтобы справиться с коммутационным обменом, часть foreach (DataGridColumn column in columns) { dataGrid.Columns.Add(column); } с foreach (var column in columns) { var dataGridOwnerProperty = column.GetType().GetProperty("DataGridOwner", BindingFlags.Instance | BindingFlags.NonPublic); if ( dataGridOwnerProperty != null) dataGridOwnerProperty.SetValue(column, null); dataGrid.Columns.Add(column); } Sry не может заставить работать разрыв строки

Rune Andersen 31.10.2017 12:11

Я также пытаюсь выполнить привязку к свойству «выбранного» столбца из контекстного меню ячейки. Как я могу это сделать при таком подходе?

Matthew S 12.07.2018 22:48

Код неправильно обрабатывает вставки. все столбцы, которые вы вставляете в коллекцию привязок данных, вставляются как последний столбец. Чтобы исправить это, измените обработку NotifyCollectionChangedAction.Add на else if (ne.Action == NotifyCollectionChangedAction.Add) { var index = ne.NewStartingIndex; foreach (DataGridColumn column in ne.NewItems) { dataGrid.Columns.Insert(index++, column); } }

stambikk 09.10.2019 22:37

Чтобы получить полный пример, не имеющий проблемы с несколькими заголовками, см. Также этот ответ: stackoverflow.com/a/57478201/3142379 Проблема возникла у меня при использовании UserControl, содержащего DataGrid с BindableColumns в нескольких других UserControls.

Jessica 09.02.2021 10:09

Я нашел статью в блоге Деборы Курата с хорошим трюком, как показать переменное количество столбцов в DataGrid:

Заполнение DataGrid динамическими столбцами в приложении Silverlight с помощью MVVM

По сути, она создает DataGridTemplateColumn и помещает внутрь ItemsControl, который отображает несколько столбцов.

Это далеко не тот результат, что и у запрограммированной версии !!

321X 10.08.2011 23:58

@ 321X: Не могли бы вы подробнее рассказать о наблюдаемых различиях (а также указать, что вы имеете в виду под запрограммированная версия, поскольку все решения этого вопроса запрограммированы), пожалуйста?

O. R. Mapper 29.12.2013 04:09

Он говорит: "Страница не найдена"

Jeson Martajaya 17.04.2015 05:38

вот ссылка blogs.msmvps.com/deborahk/…

Mikhail 16.09.2015 16:22

Это просто потрясающе !!

Ravid Goldenberg 29.06.2016 14:20

Используется это решение, но не удается отобразить заголовки.

MaTTo 01.02.2017 23:16

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

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

Что касается вопроса, это не решение на основе XAML (поскольку, как уже упоминалось, нет разумного способа сделать это), и это не решение, которое будет работать напрямую с DataGrid.Columns. Фактически он работает с ItemsSource, привязанным к DataGrid, который реализует ITypedList и, как таковой, предоставляет настраиваемые методы для получения PropertyDescriptor. В одном месте кода вы можете определить «строки данных» и «столбцы данных» для своей сетки.

Если бы у вас были:

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

вы можете использовать, например:

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) 

и ваша сетка, использующая привязку к MyItemsCollection, будет заполнена соответствующими столбцами. Эти столбцы можно изменять (добавлять новые или удалять существующие) во время выполнения динамически, а сетка автоматически обновляет коллекцию столбцов.

Упомянутый выше DynamicPropertyDescriptor является просто обновлением обычного PropertyDescriptor и обеспечивает определение строго типизированных столбцов с некоторыми дополнительными параметрами. В противном случае DynamicDataGridSource отлично работал бы с базовым PropertyDescriptor.

Сделал версию принятого ответа, обрабатывающую отмену подписки.

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

Вот пример того, как я делаю это программно:

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

Другие вопросы по теме