Як я прив’яжу WPF DataGrid до змінної кількості стовпців?


124

Мій додаток 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?

Відповіді:


127

Ось вирішення для прив'язки стовпців у DataGrid. Оскільки властивість стовпців є ReadOnly, як всі помічали, я створив додане властивість під назвою BindableColumns, яке оновлює стовпці в DataGrid кожного разу, коли колекція змінюється через події CollectionChanged.

Якщо у нас є ця колекція DataGridColumn

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

Тоді ми можемо прив’язати BindableColumns до ColumnCollection так

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

Стовпці доданих властивостей

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

1
приємне рішення для моделі MVVM
WPFKK

2
Ідеальне рішення! Ймовірно, вам потрібно зробити ще кілька речей у BindableColumnsPropertyChanged: 1. Перевірте dataGrid на null перед тим, як отримати доступ до нього, та викиньте виняток із хорошим поясненням щодо прив’язки лише до DataGrid. 2. Перевірте e.OldValue на наявність нуля та скасуйте підписку на подію CollectionChanged, щоб запобігти витоку пам'яті. Тільки для переконання.
Майк Ешва

3
Ви реєструєте обробника події разом із CollectionChangedподією колекції стовпців, однак ви ніколи не скасовуєте її. Таким чином, DataGridзаповіт зберігатиметься до тих пір, поки існує модель перегляду, навіть якщо DataGridтим часом замінено шаблон управління, який містив перше місце. Чи є якийсь гарантований спосіб повторної реєстрації цього обробника подій, коли DataGridбільше не потрібно?
АБО Mapper

1
@OR Mapper: Теоретично є, але він не працює: WeakEventManager <ObservableCollection <DataGridColumn>, NotifyCollectionChangedEventArgs> .AddHandler (стовпці, "CollectionChanged", (s, ne) => {switch ....});
теж

6
Це не просто рішення. Основна причина полягає в тому, що ви використовуєте класи UI у ViewModel. Крім того, це не спрацює, якщо ви спробуєте створити деяку переключення сторінок. При переході на сторінку з такою сіткою даних ви отримаєте очікування у рядку dataGrid.Columns.Add(column)DataGridColumn з заголовком "X", який вже існує в колекції стовпців DataGrid. DataGrids не можуть спільно використовувати стовпці і не можуть містити повторювані екземпляри стовпців.
Руслан Ф.

19

Я продовжував свої дослідження і не знайшов жодного розумного способу зробити це. Властивість стовпців на DataGrid - це не те, з чим я можу пов'язувати, насправді це лише читання.

Брайан запропонував щось зробити з AutoGenerateColumns, щоб я подивився. Він використовує просте відображення .Net для перегляду властивостей об'єктів у ItemSource та створює стовпчик для кожного з них. Можливо, я міг би генерувати тип на льоту з властивістю для кожного стовпця, але це стає далеко не трасою.

Оскільки ця проблема так легко вирішується в коді, я буду дотримуватися простого методу розширення, який я закликаю, коли контекст даних оновлюється новими стовпцями:

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

1
Найвище прийняте та прийняте рішення - не найкраще! Через два роки відповідь буде: msmvps.com/blogs/deborahk/archive/2011/01/23/…
Михайло

4
Ні, не було б. Не надане посилання все одно, тому що результат цього рішення зовсім інший!
321X

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

Перервано посилання @Mikhail
LuckyLikey

3
ось посилання: blogs.msmvps.com/deborahk/…
Михайло

9

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

Популяція DataGrid з динамічними стовпцями в додатку Silverlight за допомогою MVVM

В основному, вона створює DataGridTemplateColumnі розміщує ItemsControlвсередині, що відображає кілька стовпців.


1
Це далеко не такий результат, як запрограмована версія !!
321X

1
@ 321X: Чи можете ви, будь ласка, пояснити, що таке спостережувані відмінності (а також вкажіть, що ви маєте на увазі під запрограмованою версією , оскільки всі рішення для цього запрограмовані)?
АБО Mapper

У ньому написано "Сторінку не знайдено"
Jeson Martajaya

2
ось посилання blogs.msmvps.com/deborahk/…
Михайло

Це нічого не дивовижне !!
Ravid Goldenberg

6

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

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

Що стосується питання, це не рішення на основі XAML (оскільки, як було сказано, немає розумного способу зробити це), не є рішенням, яке б працювало безпосередньо з DataGrid.Columns. Він фактично працює з DataSrid-зв'язаним ItemSource, який реалізує 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.


3

Зробив версію прийнятої відповіді, яка обробляє підписку.

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

2

Ви можете створити контролера користувача з визначенням сітки та визначити „дочірні” елементи керування із різноманітними визначеннями стовпців у 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: ім'я в window.xaml):

childGrid.deGrid.LoadGrid();

Пов’язаний запис у блозі


1

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


Моя причина полягає в тому, що я надходжу від ASP.Net Я новачок у тому, що можна зробити за допомогою пристойної прив'язки даних, і я не впевнений, де це обмеження. Я буду грати з AutoGenerateColumn, дякую.
Загальна помилка

0

Є зразок того, як я роблю програмно:

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; }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.